@snipcodeit/mgw 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,375 @@
1
+ ---
2
+ name: mgw:next
3
+ description: Show next unblocked issue — what to work on now, based on declared dependencies
4
+ argument-hint: ""
5
+ allowed-tools:
6
+ - Bash
7
+ - Read
8
+ - AskUserQuestion
9
+ ---
10
+
11
+ <objective>
12
+ Surface the next unblocked issue across the current milestone based on dependency
13
+ declarations. Local-first: reads project.json for fast answer, then does a quick
14
+ `gh` API check to verify the issue is still open.
15
+
16
+ This is a read-only command — it does NOT modify state, run pipelines, or create
17
+ worktrees. After displaying the brief, it offers to run `/mgw:run` for the
18
+ recommended issue.
19
+ </objective>
20
+
21
+ <execution_context>
22
+ @~/.claude/commands/mgw/workflows/state.md
23
+ @~/.claude/commands/mgw/workflows/github.md
24
+ </execution_context>
25
+
26
+ <process>
27
+
28
+ <step name="load_state">
29
+ **Load project.json and validate:**
30
+
31
+ ```bash
32
+ REPO_ROOT=$(git rev-parse --show-toplevel)
33
+ MGW_DIR="${REPO_ROOT}/.mgw"
34
+
35
+ if [ ! -f "${MGW_DIR}/project.json" ]; then
36
+ echo "No project initialized. Run /mgw:project first."
37
+ exit 1
38
+ fi
39
+
40
+ PROJECT_JSON=$(cat "${MGW_DIR}/project.json")
41
+
42
+ # Resolve active milestone index using state resolution (supports both schema versions)
43
+ ACTIVE_IDX=$(node -e "
44
+ const { loadProjectState, resolveActiveMilestoneIndex } = require('./lib/state.cjs');
45
+ const state = loadProjectState();
46
+ console.log(resolveActiveMilestoneIndex(state));
47
+ ")
48
+
49
+ # Get milestone data
50
+ MILESTONE_DATA=$(echo "$PROJECT_JSON" | python3 -c "
51
+ import json,sys
52
+ p = json.load(sys.stdin)
53
+ idx = ${ACTIVE_IDX}
54
+ if idx < 0 or idx >= len(p['milestones']):
55
+ print(json.dumps({'error': 'No more milestones'}))
56
+ sys.exit(0)
57
+ m = p['milestones'][idx]
58
+ print(json.dumps(m))
59
+ ")
60
+
61
+ MILESTONE_NAME=$(echo "$MILESTONE_DATA" | python3 -c "import json,sys; print(json.load(sys.stdin)['name'])")
62
+ ISSUES_JSON=$(echo "$MILESTONE_DATA" | python3 -c "import json,sys; print(json.dumps(json.load(sys.stdin)['issues']))")
63
+ TOTAL_ISSUES=$(echo "$ISSUES_JSON" | python3 -c "import json,sys; print(len(json.load(sys.stdin)))")
64
+ ```
65
+ </step>
66
+
67
+ <step name="resolve_dependencies">
68
+ **Compute dependency graph and ordered issue list:**
69
+
70
+ ```bash
71
+ DEPENDENCY_RESULT=$(echo "$ISSUES_JSON" | python3 -c "
72
+ import json, sys
73
+ from collections import defaultdict
74
+
75
+ issues = json.load(sys.stdin)
76
+
77
+ # Build slug-to-issue and number-to-issue mappings
78
+ slug_to_issue = {}
79
+ num_to_issue = {}
80
+ slug_to_num = {}
81
+ for issue in issues:
82
+ title = issue.get('title', '')
83
+ slug = title.lower().replace(' ', '-')[:40]
84
+ slug_to_issue[slug] = issue
85
+ num_to_issue[issue['github_number']] = issue
86
+ slug_to_num[slug] = issue['github_number']
87
+
88
+ # Build forward and reverse dependency maps
89
+ # forward: issue -> what it depends on
90
+ # reverse: issue -> what it unblocks
91
+ forward_deps = {} # slug -> [blocking_slugs]
92
+ reverse_deps = defaultdict(list) # slug -> [dependent_slugs]
93
+
94
+ for issue in issues:
95
+ title = issue.get('title', '')
96
+ slug = title.lower().replace(' ', '-')[:40]
97
+ deps = issue.get('depends_on_slugs', [])
98
+ forward_deps[slug] = deps
99
+ for dep_slug in deps:
100
+ if dep_slug in slug_to_issue:
101
+ reverse_deps[dep_slug].append(slug)
102
+
103
+ # Find unblocked issues:
104
+ # - pipeline_stage == 'new' (not done, not failed, not in-progress)
105
+ # - ALL depends_on issues have pipeline_stage == 'done'
106
+ unblocked = []
107
+ blocked = []
108
+ done_count = 0
109
+ failed_issues = []
110
+
111
+ for issue in issues:
112
+ title = issue.get('title', '')
113
+ slug = title.lower().replace(' ', '-')[:40]
114
+ stage = issue.get('pipeline_stage', 'new')
115
+
116
+ if stage == 'done':
117
+ done_count += 1
118
+ continue
119
+ if stage == 'failed':
120
+ failed_issues.append(issue)
121
+ continue
122
+ if stage not in ('new',):
123
+ continue # in-progress, skip for now
124
+
125
+ # Check if all dependencies are done
126
+ deps = issue.get('depends_on_slugs', [])
127
+ all_deps_done = True
128
+ blocking_info = []
129
+ for dep_slug in deps:
130
+ if dep_slug in slug_to_issue:
131
+ dep_issue = slug_to_issue[dep_slug]
132
+ if dep_issue.get('pipeline_stage') != 'done':
133
+ all_deps_done = False
134
+ blocking_info.append({
135
+ 'number': dep_issue['github_number'],
136
+ 'title': dep_issue['title'],
137
+ 'stage': dep_issue.get('pipeline_stage', 'new')
138
+ })
139
+
140
+ if all_deps_done:
141
+ # Compute what this issue unblocks
142
+ unblocks = []
143
+ for dep_slug in reverse_deps.get(slug, []):
144
+ if dep_slug in slug_to_issue:
145
+ dep_issue = slug_to_issue[dep_slug]
146
+ unblocks.append({
147
+ 'number': dep_issue['github_number'],
148
+ 'title': dep_issue['title']
149
+ })
150
+
151
+ # Compute resolved dependencies
152
+ resolved_deps = []
153
+ for dep_slug in deps:
154
+ if dep_slug in slug_to_issue:
155
+ dep_issue = slug_to_issue[dep_slug]
156
+ resolved_deps.append({
157
+ 'number': dep_issue['github_number'],
158
+ 'title': dep_issue['title']
159
+ })
160
+
161
+ unblocked.append({
162
+ 'issue': issue,
163
+ 'unblocks': unblocks,
164
+ 'resolved_deps': resolved_deps
165
+ })
166
+ else:
167
+ blocked.append({
168
+ 'issue': issue,
169
+ 'blocked_by': blocking_info
170
+ })
171
+
172
+ # Sort unblocked by phase_number (first = recommended)
173
+ unblocked.sort(key=lambda x: x['issue'].get('phase_number', 999))
174
+
175
+ result = {
176
+ 'unblocked': unblocked,
177
+ 'blocked': blocked,
178
+ 'done_count': done_count,
179
+ 'total': len(issues),
180
+ 'failed': [{'number': i['github_number'], 'title': i['title']} for i in failed_issues]
181
+ }
182
+ print(json.dumps(result, indent=2))
183
+ ")
184
+ ```
185
+ </step>
186
+
187
+ <step name="handle_nothing_unblocked">
188
+ **If no issues are unblocked:**
189
+
190
+ Check if ALL issues are done:
191
+ ```bash
192
+ ALL_DONE=$(echo "$DEPENDENCY_RESULT" | python3 -c "
193
+ import json,sys
194
+ r = json.load(sys.stdin)
195
+ print('true' if r['done_count'] == r['total'] else 'false')
196
+ ")
197
+ ```
198
+
199
+ If all done:
200
+ ```
201
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
202
+ MGW ► ALL DONE
203
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
204
+
205
+ All ${TOTAL_ISSUES} issues in milestone "${MILESTONE_NAME}" are complete.
206
+
207
+ Run /mgw:milestone to finalize (close milestone, create release).
208
+ ```
209
+
210
+ If not all done (some are blocked/failed):
211
+ ```
212
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
213
+ MGW ► BLOCKED
214
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
215
+
216
+ No unblocked issues in milestone "${MILESTONE_NAME}".
217
+
218
+ | Issue | Blocked By | Blocker Status |
219
+ |-------|-----------|----------------|
220
+ | #N title | #M title | ◆ In progress |
221
+ | #K title | #J title | ✗ Failed |
222
+
223
+ ${failed_advice}
224
+
225
+ Progress: ${done_count}/${total} complete
226
+ ```
227
+
228
+ Where `${failed_advice}` is: "Resolve #J to unblock #K." (specific actionable advice for each failed blocker).
229
+ </step>
230
+
231
+ <step name="verify_live">
232
+ **Quick GitHub verification for recommended issue:**
233
+
234
+ ```bash
235
+ RECOMMENDED=$(echo "$DEPENDENCY_RESULT" | python3 -c "
236
+ import json,sys
237
+ r = json.load(sys.stdin)
238
+ if r['unblocked']:
239
+ print(json.dumps(r['unblocked'][0]))
240
+ else:
241
+ print('null')
242
+ ")
243
+ ```
244
+
245
+ If recommended issue exists:
246
+ ```bash
247
+ REC_NUMBER=$(echo "$RECOMMENDED" | python3 -c "import json,sys; print(json.load(sys.stdin)['issue']['github_number'])")
248
+
249
+ # Quick GitHub check — verify issue is still open
250
+ GH_CHECK=$(gh issue view ${REC_NUMBER} --json state,title,labels -q '{state: .state, title: .title}' 2>/dev/null)
251
+
252
+ if [ -n "$GH_CHECK" ]; then
253
+ GH_STATE=$(echo "$GH_CHECK" | python3 -c "import json,sys; print(json.load(sys.stdin)['state'])")
254
+ if [ "$GH_STATE" != "OPEN" ]; then
255
+ # Issue was closed externally — skip to next unblocked
256
+ echo "Issue #${REC_NUMBER} is ${GH_STATE} on GitHub. Checking next..."
257
+ # Remove from unblocked list, try next
258
+ fi
259
+ VERIFIED="(verified)"
260
+ else
261
+ VERIFIED="(GitHub state unverified)"
262
+ fi
263
+ ```
264
+ </step>
265
+
266
+ <step name="display_brief">
267
+ **Display full brief for recommended issue:**
268
+
269
+ ```bash
270
+ REC_ISSUE=$(echo "$RECOMMENDED" | python3 -c "import json,sys; r=json.load(sys.stdin); print(json.dumps(r['issue']))")
271
+ REC_TITLE=$(echo "$REC_ISSUE" | python3 -c "import json,sys; print(json.load(sys.stdin)['title'])")
272
+ REC_GSD_ROUTE=$(echo "$REC_ISSUE" | python3 -c "import json,sys; print(json.load(sys.stdin).get('gsd_route','plan-phase'))")
273
+ REC_LABELS=$(echo "$REC_ISSUE" | python3 -c "import json,sys; print(', '.join(json.load(sys.stdin).get('labels',[])))")
274
+ REC_PHASE=$(echo "$REC_ISSUE" | python3 -c "import json,sys; print(json.load(sys.stdin).get('phase_number',''))")
275
+ REC_PHASE_NAME=$(echo "$REC_ISSUE" | python3 -c "import json,sys; print(json.load(sys.stdin).get('phase_name',''))")
276
+
277
+ DONE_COUNT=$(echo "$DEPENDENCY_RESULT" | python3 -c "import json,sys; print(json.load(sys.stdin)['done_count'])")
278
+ ```
279
+
280
+ Display:
281
+ ```
282
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
283
+ MGW ► NEXT UP
284
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
285
+
286
+ Issue: #${REC_NUMBER} — ${REC_TITLE} ${VERIFIED}
287
+ GSD Route: ${REC_GSD_ROUTE}
288
+ Phase: ${REC_PHASE}: ${REC_PHASE_NAME}
289
+ Labels: ${REC_LABELS}
290
+ Milestone: ${MILESTONE_NAME} (${DONE_COUNT}/${TOTAL_ISSUES} complete)
291
+
292
+ Dependencies (all done):
293
+ ✓ #${dep1_number} — ${dep1_title}
294
+ ✓ #${dep2_number} — ${dep2_title}
295
+ (or: None — this is an independent issue)
296
+
297
+ Unblocks:
298
+ ○ #${next1_number} — ${next1_title}
299
+ ○ #${next2_number} — ${next2_title}
300
+ (or: None — no downstream dependencies)
301
+
302
+ ───────────────────────────────────────────────────────────────
303
+ ```
304
+
305
+ If other unblocked alternatives exist:
306
+ ```bash
307
+ ALT_COUNT=$(echo "$DEPENDENCY_RESULT" | python3 -c "
308
+ import json,sys
309
+ r = json.load(sys.stdin)
310
+ print(len(r['unblocked']) - 1)
311
+ ")
312
+ ```
313
+
314
+ If ALT_COUNT > 0:
315
+ ```
316
+ Also unblocked:
317
+ #${alt1_number} — ${alt1_title} (${alt1_gsd_route})
318
+ #${alt2_number} — ${alt2_title} (${alt2_gsd_route})
319
+ ```
320
+ </step>
321
+
322
+ <step name="offer_run">
323
+ **Offer to run the recommended issue:**
324
+
325
+ ```
326
+ AskUserQuestion(
327
+ header: "Ready to Start",
328
+ question: "Run /mgw:run #${REC_NUMBER} now?",
329
+ options: [
330
+ { label: "Yes", description: "Start the pipeline for this issue" },
331
+ { label: "No", description: "Just viewing — I'll run it later" },
332
+ { label: "Pick different", description: "Choose a different unblocked issue" }
333
+ ]
334
+ )
335
+ ```
336
+
337
+ **If "Yes":** Display the command to run:
338
+ ```
339
+ Start the pipeline:
340
+
341
+ /mgw:run ${REC_NUMBER}
342
+
343
+ <sub>/clear first → fresh context window</sub>
344
+ ```
345
+
346
+ Note: /mgw:next is read-only (allowed-tools don't include Task). It cannot invoke
347
+ /mgw:run directly. It displays the command for the user to run.
348
+
349
+ **If "Pick different":** Display the alternatives list and ask user to pick:
350
+ ```
351
+ AskUserQuestion(
352
+ header: "Select Issue",
353
+ question: "Which issue do you want to work on?",
354
+ options: [alt_issues_as_options]
355
+ )
356
+ ```
357
+
358
+ Then re-display the brief for the selected issue.
359
+
360
+ **If "No":** Exit cleanly.
361
+ </step>
362
+
363
+ </process>
364
+
365
+ <success_criteria>
366
+ - [ ] project.json loaded and current milestone identified (MLST-02)
367
+ - [ ] Dependency graph computed from depends_on_slugs
368
+ - [ ] Single recommended issue surfaced (dependency order, then phase order)
369
+ - [ ] Full brief displayed: number, title, GSD route, labels, dependencies, what it unblocks, milestone context
370
+ - [ ] Alternatives listed when multiple issues are unblocked
371
+ - [ ] Blocking chain shown when nothing is unblocked
372
+ - [ ] Live GitHub verification attempted for recommended issue
373
+ - [ ] Offer to run /mgw:run displayed
374
+ - [ ] Read-only: no state modifications, no pipeline execution
375
+ </success_criteria>
package/commands/pr.md ADDED
@@ -0,0 +1,277 @@
1
+ ---
2
+ name: mgw:pr
3
+ description: Create a pull request from GSD artifacts and linked issue context
4
+ argument-hint: "[issue-number] [--base <branch>]"
5
+ allowed-tools:
6
+ - Bash
7
+ - Read
8
+ - Write
9
+ - Edit
10
+ - Glob
11
+ - Grep
12
+ - Task
13
+ ---
14
+
15
+ <objective>
16
+ Create a PR with context pulled from GSD artifacts (SUMMARY.md, VERIFICATION.md)
17
+ and the linked GitHub issue. Builds a structured PR description with summary,
18
+ testing procedures, and cross-references.
19
+
20
+ Works in two modes:
21
+ 1. **Linked mode:** Issue number provided or found in .mgw/active/ — pulls issue
22
+ context, GSD artifacts, and cross-refs into the PR.
23
+ 2. **Standalone mode:** No issue — builds PR from GSD artifacts in .planning/ or
24
+ from the branch diff.
25
+ </objective>
26
+
27
+ <execution_context>
28
+ @~/.claude/commands/mgw/workflows/state.md
29
+ @~/.claude/commands/mgw/workflows/github.md
30
+ @~/.claude/commands/mgw/workflows/gsd.md
31
+ @~/.claude/commands/mgw/workflows/validation.md
32
+ </execution_context>
33
+
34
+ <context>
35
+ $ARGUMENTS — optional: [issue-number] [--base <branch>]
36
+ </context>
37
+
38
+ <process>
39
+
40
+ <step name="detect_mode">
41
+ **Determine linked vs standalone mode:**
42
+
43
+ If issue number in $ARGUMENTS → linked mode.
44
+ Else if exactly one file in .mgw/active/ → linked mode with that issue.
45
+ Else → standalone mode.
46
+
47
+ Parse `--base <branch>` if provided, default to repo default branch:
48
+ ```bash
49
+ DEFAULT_BRANCH=$(gh repo view --json defaultBranchRef -q .defaultBranchRef.name)
50
+ ```
51
+ </step>
52
+
53
+ <step name="gather_gsd_artifacts">
54
+ **Gather GSD artifacts:**
55
+
56
+ **Linked mode:** Read state file for gsd_artifacts.path, then:
57
+ ```bash
58
+ # Structured summary data via gsd-tools (returns JSON with one_liner, key_files, tech_added, patterns, decisions)
59
+ SUMMARY_DATA=$(node ~/.claude/get-shit-done/bin/gsd-tools.cjs summary-extract "${gsd_artifacts_path}/*SUMMARY*" 2>/dev/null || echo '{}')
60
+ # Also read raw artifacts for full context
61
+ SUMMARY_RAW=$(cat ${gsd_artifacts_path}/*SUMMARY* 2>/dev/null)
62
+ VERIFICATION=$(cat ${gsd_artifacts_path}/*VERIFICATION* 2>/dev/null)
63
+ # Progress table for details section
64
+ PROGRESS_TABLE=$(node ~/.claude/get-shit-done/bin/gsd-tools.cjs progress table --raw 2>/dev/null || echo "")
65
+
66
+ # Milestone/phase context for PR body
67
+ MILESTONE_TITLE=""
68
+ PHASE_INFO=""
69
+ DEPENDENCY_CHAIN=""
70
+ if [ -f "${REPO_ROOT}/.mgw/project.json" ]; then
71
+ MILESTONE_TITLE=$(python3 -c "
72
+ import json
73
+ p = json.load(open('${REPO_ROOT}/.mgw/project.json'))
74
+ for m in p['milestones']:
75
+ for i in m.get('issues', []):
76
+ if i.get('github_number') == ${ISSUE_NUMBER}:
77
+ print(m['name'])
78
+ break
79
+ " 2>/dev/null || echo "")
80
+
81
+ PHASE_INFO=$(python3 -c "
82
+ import json
83
+ p = json.load(open('${REPO_ROOT}/.mgw/project.json'))
84
+ for m in p['milestones']:
85
+ for i in m.get('issues', []):
86
+ if i.get('github_number') == ${ISSUE_NUMBER}:
87
+ total = len(m.get('issues', []))
88
+ idx = [x['github_number'] for x in m['issues']].index(${ISSUE_NUMBER}) + 1
89
+ print(f\"Phase {i['phase_number']}: {i['phase_name']} (issue {idx}/{total} in milestone)\")
90
+ break
91
+ " 2>/dev/null || echo "")
92
+
93
+ DEPENDENCY_CHAIN=$(python3 -c "
94
+ import json
95
+ p = json.load(open('${REPO_ROOT}/.mgw/project.json'))
96
+ refs = json.load(open('${REPO_ROOT}/.mgw/cross-refs.json'))
97
+ blockers = [l['b'].split(':')[1] for l in refs.get('links', [])
98
+ if l.get('type') == 'blocked-by' and l['a'] == 'issue:${ISSUE_NUMBER}']
99
+ blocks = [l['a'].split(':')[1] for l in refs.get('links', [])
100
+ if l.get('type') == 'blocked-by' and l['b'] == 'issue:${ISSUE_NUMBER}']
101
+ parts = []
102
+ if blockers: parts.append('Blocked by: ' + ', '.join(f'#{b}' for b in blockers))
103
+ if blocks: parts.append('Unblocks: ' + ', '.join(f'#{b}' for b in blocks))
104
+ print(' | '.join(parts) if parts else '')
105
+ " 2>/dev/null || echo "")
106
+ fi
107
+ ```
108
+
109
+ **Standalone mode:** Search .planning/ for recent artifacts:
110
+ ```bash
111
+ # Check quick tasks first, then phases
112
+ SUMMARY_PATH=$(ls -t .planning/quick/*/SUMMARY.md .planning/phases/*/SUMMARY.md 2>/dev/null | head -1)
113
+ if [ -n "$SUMMARY_PATH" ]; then
114
+ SUMMARY_DATA=$(node ~/.claude/get-shit-done/bin/gsd-tools.cjs summary-extract "$SUMMARY_PATH" 2>/dev/null || echo '{}')
115
+ SUMMARY_RAW=$(cat "$SUMMARY_PATH" 2>/dev/null)
116
+ fi
117
+ ```
118
+
119
+ If no GSD artifacts found → fall back to `git log ${base}..HEAD --oneline` for PR content.
120
+ </step>
121
+
122
+ <step name="build_pr_body">
123
+ **Build PR description:**
124
+
125
+ Spawn task agent to compose the PR body:
126
+
127
+ ```
128
+ Task(
129
+ prompt="
130
+ <files_to_read>
131
+ - ./CLAUDE.md (Project instructions — if exists, follow all guidelines)
132
+ - .agents/skills/ (Project skills — if dir exists, list skills, read SKILL.md for each, follow relevant rules)
133
+ </files_to_read>
134
+
135
+ Build a GitHub PR description from these inputs.
136
+
137
+ <issue_context>
138
+ ${issue_title_and_body_if_linked}
139
+ Issue: #${ISSUE_NUMBER} (or 'none — standalone')
140
+ </issue_context>
141
+
142
+ <gsd_summary_structured>
143
+ ${SUMMARY_DATA}
144
+ </gsd_summary_structured>
145
+
146
+ <gsd_summary_raw>
147
+ ${SUMMARY_RAW}
148
+ </gsd_summary_raw>
149
+
150
+ <gsd_verification>
151
+ ${VERIFICATION}
152
+ </gsd_verification>
153
+
154
+ <cross_refs>
155
+ ${CROSS_REFS_FOR_THIS_ISSUE}
156
+ </cross_refs>
157
+
158
+ <commits>
159
+ ${GIT_LOG_BASE_TO_HEAD}
160
+ </commits>
161
+
162
+ <milestone_context>
163
+ Milestone: ${MILESTONE_TITLE}
164
+ Phase: ${PHASE_INFO}
165
+ Dependencies: ${DEPENDENCY_CHAIN}
166
+ </milestone_context>
167
+
168
+ <output_format>
169
+ Return EXACTLY two sections separated by ===TESTING===:
170
+
171
+ SECTION 1 — PR body:
172
+ ## Summary
173
+ - [2-4 bullet points of what changed and why]
174
+ - [Use one_liner from gsd_summary_structured if available]
175
+
176
+ ${if_linked: 'Closes #${ISSUE_NUMBER}'}
177
+
178
+ ${if MILESTONE_TITLE non-empty:
179
+ ## Milestone Context
180
+ - **Milestone:** ${MILESTONE_TITLE}
181
+ - **Phase:** ${PHASE_INFO}
182
+ - **Dependencies:** ${DEPENDENCY_CHAIN}
183
+ }
184
+
185
+ ## Changes
186
+ - [File-level changes grouped by system/module]
187
+ - [Use key_files from gsd_summary_structured if available]
188
+
189
+ ## Cross-References
190
+ ${cross_ref_list_or_omit_if_none}
191
+
192
+ ${if PROGRESS_TABLE non-empty:
193
+ <details>
194
+ <summary>GSD Progress</summary>
195
+
196
+ ${PROGRESS_TABLE}
197
+
198
+ </details>
199
+ }
200
+
201
+ ===TESTING===
202
+
203
+ SECTION 2 — Testing procedures comment:
204
+ ## Testing Procedures
205
+ - [ ] [Step-by-step verification checklist]
206
+ - [ ] [Derived from VERIFICATION.md if available]
207
+ - [ ] [Or from the changes themselves]
208
+ </output_format>
209
+ ",
210
+ subagent_type="general-purpose",
211
+ model="sonnet",
212
+ description="Build PR description for #${ISSUE_NUMBER}"
213
+ )
214
+ ```
215
+
216
+ Split agent output on `===TESTING===` into $PR_BODY and $TESTING_COMMENT.
217
+ </step>
218
+
219
+ <step name="create_pr">
220
+ **Create the PR:**
221
+
222
+ Determine PR title:
223
+ - Linked: from issue title, prefixed with type (fix:, feat:, etc.)
224
+ - Standalone: from first commit message or branch name
225
+
226
+ ```bash
227
+ CURRENT_BRANCH=$(git branch --show-current)
228
+ gh pr create --title "${PR_TITLE}" --base "${BASE_BRANCH}" --body "${PR_BODY}"
229
+ ```
230
+
231
+ Capture PR number and URL from output.
232
+ </step>
233
+
234
+ <step name="post_testing">
235
+ **Post testing procedures as PR comment:**
236
+
237
+ ```bash
238
+ gh pr comment ${PR_NUMBER} --body "${TESTING_COMMENT}"
239
+ ```
240
+ </step>
241
+
242
+ <step name="update_state">
243
+ **Update .mgw/ state (linked mode only):**
244
+
245
+ Update state file:
246
+ - Set linked_pr to PR number
247
+ - Set pipeline_stage to "pr-created"
248
+
249
+ Add cross-ref: issue → PR link in cross-refs.json.
250
+ </step>
251
+
252
+ <step name="report">
253
+ **Report:**
254
+
255
+ ```
256
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
257
+ MGW ► PR CREATED
258
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
259
+
260
+ PR #${PR_NUMBER}: ${PR_TITLE}
261
+ URL: ${PR_URL}
262
+ ${if_linked: "Closes: #${ISSUE_NUMBER}"}
263
+ Testing procedures posted as PR comment.
264
+ ```
265
+ </step>
266
+
267
+ </process>
268
+
269
+ <success_criteria>
270
+ - [ ] Mode detected correctly (linked vs standalone)
271
+ - [ ] GSD artifacts found and read (or fallback to git log)
272
+ - [ ] PR body includes summary, changes, cross-refs, and Closes #N
273
+ - [ ] PR created via gh pr create
274
+ - [ ] Testing procedures posted as separate PR comment
275
+ - [ ] State file updated with PR number (linked mode)
276
+ - [ ] Cross-ref added (linked mode)
277
+ </success_criteria>