@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,952 @@
1
+ ---
2
+ name: mgw:milestone
3
+ description: Execute a milestone's issues in dependency order — auto-sync, rate-limit guard, per-issue checkpoint
4
+ argument-hint: "[milestone-number] [--interactive] [--dry-run]"
5
+ allowed-tools:
6
+ - Bash
7
+ - Read
8
+ - Write
9
+ - Edit
10
+ - Glob
11
+ - Grep
12
+ - Task
13
+ - AskUserQuestion
14
+ ---
15
+
16
+ <objective>
17
+ Orchestrate execution of all issues in a milestone by delegating each to `/mgw:run`
18
+ in dependency order. Sequential execution (one issue at a time), autonomous by default.
19
+
20
+ Handles: dependency resolution (topological sort), pre-sync against GitHub, rate limit
21
+ guard, per-issue checkpointing to project.json, failure cascading (skip failed, block
22
+ dependents, continue unblocked), resume detection, milestone close + draft release on
23
+ completion, and auto-advance to next milestone.
24
+
25
+ The `--interactive` flag pauses between issues for user confirmation.
26
+ The `--dry-run` flag shows the execution plan without running anything.
27
+ </objective>
28
+
29
+ <execution_context>
30
+ @~/.claude/commands/mgw/workflows/state.md
31
+ @~/.claude/commands/mgw/workflows/github.md
32
+ @~/.claude/commands/mgw/workflows/gsd.md
33
+ @~/.claude/commands/mgw/workflows/validation.md
34
+ </execution_context>
35
+
36
+ <context>
37
+ Milestone number: $ARGUMENTS (optional — defaults to current_milestone from project.json)
38
+ Flags: --interactive, --dry-run
39
+ </context>
40
+
41
+ <process>
42
+
43
+ <step name="parse_arguments">
44
+ **Parse $ARGUMENTS for milestone number and flags:**
45
+
46
+ ```bash
47
+ MILESTONE_NUM=""
48
+ INTERACTIVE=false
49
+ DRY_RUN=false
50
+
51
+ for ARG in $ARGUMENTS; do
52
+ case "$ARG" in
53
+ --interactive) INTERACTIVE=true ;;
54
+ --dry-run) DRY_RUN=true ;;
55
+ [0-9]*) MILESTONE_NUM="$ARG" ;;
56
+ esac
57
+ done
58
+ ```
59
+
60
+ If no milestone number provided, read from project.json:
61
+ ```bash
62
+ REPO_ROOT=$(git rev-parse --show-toplevel)
63
+ MGW_DIR="${REPO_ROOT}/.mgw"
64
+
65
+ if [ -z "$MILESTONE_NUM" ]; then
66
+ if [ ! -f "${MGW_DIR}/project.json" ]; then
67
+ echo "No project initialized. Run /mgw:project first."
68
+ exit 1
69
+ fi
70
+ # Resolve active milestone index (0-based) and convert to 1-indexed milestone number
71
+ ACTIVE_IDX=$(node -e "
72
+ const { loadProjectState, resolveActiveMilestoneIndex } = require('./lib/state.cjs');
73
+ const state = loadProjectState();
74
+ console.log(resolveActiveMilestoneIndex(state));
75
+ ")
76
+ if [ "$ACTIVE_IDX" -lt 0 ]; then
77
+ echo "No active milestone set. Run /mgw:project to initialize or set active_gsd_milestone."
78
+ exit 1
79
+ fi
80
+ MILESTONE_NUM=$((ACTIVE_IDX + 1))
81
+ fi
82
+ ```
83
+ </step>
84
+
85
+ <step name="validate_and_sync">
86
+ **Run validate_and_load then batch staleness check (MLST-03):**
87
+
88
+ Follow initialization procedure from @~/.claude/commands/mgw/workflows/state.md:
89
+ - Ensure .mgw/, active/, completed/ exist
90
+ - Ensure .gitignore entries
91
+ - Initialize cross-refs.json if missing
92
+
93
+ Run batch staleness check (non-blocking):
94
+ ```bash
95
+ # Batch staleness check from state.md
96
+ # If check fails (network error, API limit), log warning and continue
97
+ check_batch_staleness "${MGW_DIR}" 2>/dev/null || echo "MGW: Staleness check skipped (network unavailable)"
98
+ ```
99
+
100
+ This satisfies MLST-03: pre-sync before starting.
101
+ </step>
102
+
103
+ <step name="load_milestone">
104
+ **Load project.json and extract milestone data:**
105
+
106
+ ```bash
107
+ PROJECT_JSON=$(cat "${MGW_DIR}/project.json")
108
+ MILESTONE_NAME=$(echo "$PROJECT_JSON" | python3 -c "
109
+ import json,sys
110
+ p = json.load(sys.stdin)
111
+ idx = ${MILESTONE_NUM} - 1
112
+ if idx < 0 or idx >= len(p['milestones']):
113
+ print('ERROR: Milestone ${MILESTONE_NUM} not found')
114
+ sys.exit(1)
115
+ m = p['milestones'][idx]
116
+ print(m['name'])
117
+ ")
118
+
119
+ MILESTONE_GH_NUMBER=$(echo "$PROJECT_JSON" | python3 -c "
120
+ import json,sys
121
+ p = json.load(sys.stdin)
122
+ print(p['milestones'][${MILESTONE_NUM} - 1].get('github_number', ''))
123
+ ")
124
+
125
+ ISSUES_JSON=$(echo "$PROJECT_JSON" | python3 -c "
126
+ import json,sys
127
+ p = json.load(sys.stdin)
128
+ m = p['milestones'][${MILESTONE_NUM} - 1]
129
+ print(json.dumps(m['issues']))
130
+ ")
131
+
132
+ TOTAL_ISSUES=$(echo "$ISSUES_JSON" | python3 -c "import json,sys; print(len(json.load(sys.stdin)))")
133
+ ```
134
+
135
+ Display:
136
+ ```
137
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
138
+ MGW ► MILESTONE ${MILESTONE_NUM}: ${MILESTONE_NAME}
139
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
140
+
141
+ Issues: ${TOTAL_ISSUES}
142
+ Mode: ${INTERACTIVE ? "Interactive" : "Autonomous"}
143
+ ```
144
+ </step>
145
+
146
+ <step name="resolve_execution_order">
147
+ **Topological sort of issues by dependency (Kahn's algorithm):**
148
+
149
+ ```bash
150
+ SORTED_ISSUES=$(echo "$ISSUES_JSON" | python3 -c "
151
+ import json, sys
152
+ from collections import defaultdict, deque
153
+
154
+ issues = json.load(sys.stdin)
155
+
156
+ # Build slug-to-issue mapping
157
+ slug_to_issue = {}
158
+ num_to_issue = {}
159
+ for issue in issues:
160
+ title = issue.get('title', '')
161
+ slug = title.lower().replace(' ', '-')[:40]
162
+ slug_to_issue[slug] = issue
163
+ num_to_issue[issue['github_number']] = issue
164
+
165
+ # Build adjacency list and in-degree map
166
+ in_degree = defaultdict(int)
167
+ graph = defaultdict(list)
168
+ all_slugs = set()
169
+
170
+ for issue in issues:
171
+ title = issue.get('title', '')
172
+ slug = title.lower().replace(' ', '-')[:40]
173
+ all_slugs.add(slug)
174
+ for dep_slug in issue.get('depends_on_slugs', []):
175
+ if dep_slug in slug_to_issue:
176
+ graph[dep_slug].append(slug)
177
+ in_degree[slug] += 1
178
+
179
+ # Kahn's algorithm with phase_number tiebreak
180
+ queue = [s for s in all_slugs if in_degree[s] == 0]
181
+ order = []
182
+ while queue:
183
+ # Stable sort: prefer lower phase_number
184
+ current = min(queue, key=lambda s: slug_to_issue[s].get('phase_number', 999))
185
+ queue.remove(current)
186
+ order.append(current)
187
+ for neighbor in graph[current]:
188
+ in_degree[neighbor] -= 1
189
+ if in_degree[neighbor] == 0:
190
+ queue.append(neighbor)
191
+
192
+ # Cycle detection
193
+ if len(order) < len(all_slugs):
194
+ cycled = [s for s in all_slugs if s not in order]
195
+ print(json.dumps({'error': 'cycle', 'involved': cycled}))
196
+ sys.exit(1)
197
+
198
+ # Output ordered issues
199
+ result = [slug_to_issue[s] for s in order]
200
+ print(json.dumps(result))
201
+ ")
202
+ ```
203
+
204
+ **If cycle detected:** Error with cycle details and refuse to proceed:
205
+ ```
206
+ Circular dependency detected in milestone issues.
207
+ Involved: ${cycled_slugs}
208
+ Resolve the circular dependency in project.json or GitHub labels before running.
209
+ ```
210
+
211
+ **Filter out completed issues:**
212
+ ```bash
213
+ UNFINISHED=$(echo "$SORTED_ISSUES" | python3 -c "
214
+ import json,sys
215
+ issues = json.load(sys.stdin)
216
+ unfinished = [i for i in issues if i.get('pipeline_stage') not in ('done',)]
217
+ print(json.dumps(unfinished))
218
+ ")
219
+
220
+ UNFINISHED_COUNT=$(echo "$UNFINISHED" | python3 -c "import json,sys; print(len(json.load(sys.stdin)))")
221
+ DONE_COUNT=$((TOTAL_ISSUES - UNFINISHED_COUNT))
222
+ ```
223
+
224
+ If all done:
225
+ ```
226
+ All ${TOTAL_ISSUES} issues complete. Milestone already finished.
227
+ Run /mgw:sync to finalize.
228
+ ```
229
+ </step>
230
+
231
+ <step name="rate_limit_guard">
232
+ **Check API rate limit before starting loop (MLST-04):**
233
+
234
+ ```bash
235
+ # From github.md Rate Limit pattern
236
+ RATE_JSON=$(gh api rate_limit --jq '.resources.core' 2>/dev/null)
237
+
238
+ if [ -n "$RATE_JSON" ]; then
239
+ REMAINING=$(echo "$RATE_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin)['remaining'])")
240
+ RESET_EPOCH=$(echo "$RATE_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin)['reset'])")
241
+ RESET_TIME=$(date -d "@${RESET_EPOCH}" '+%H:%M:%S' 2>/dev/null || echo "unknown")
242
+
243
+ # Conservative: 25 calls per issue
244
+ ESTIMATED_CALLS=$((UNFINISHED_COUNT * 25))
245
+ SAFE_ISSUES=$((REMAINING / 25))
246
+
247
+ if [ "$REMAINING" -lt "$ESTIMATED_CALLS" ]; then
248
+ echo "Rate limit: ${REMAINING} calls remaining, need ~${ESTIMATED_CALLS} for ${UNFINISHED_COUNT} issues."
249
+ echo "Can safely run ${SAFE_ISSUES} of ${UNFINISHED_COUNT} issues."
250
+ echo "Limit resets at ${RESET_TIME}."
251
+ # Cap loop at safe count
252
+ MAX_ISSUES=$SAFE_ISSUES
253
+ else
254
+ MAX_ISSUES=$UNFINISHED_COUNT
255
+ fi
256
+ else
257
+ echo "MGW: Rate limit check unavailable — proceeding without cap"
258
+ MAX_ISSUES=$UNFINISHED_COUNT
259
+ fi
260
+ ```
261
+ </step>
262
+
263
+ <step name="post_start_hook">
264
+ **Post milestone-start announcement to GitHub Discussions (or first-issue comment fallback):**
265
+
266
+ Runs once before the execute loop. Skipped if --dry-run is set. Failure is non-blocking — a warning is logged and execution continues.
267
+
268
+ ```bash
269
+ if [ "$DRY_RUN" = true ]; then
270
+ echo "MGW: Skipping milestone-start announcement (dry-run mode)"
271
+ else
272
+ # Gather board URL from project.json if present (non-blocking)
273
+ BOARD_URL=$(echo "$PROJECT_JSON" | python3 -c "
274
+ import json,sys
275
+ p = json.load(sys.stdin)
276
+ m = p['milestones'][${MILESTONE_NUM} - 1]
277
+ print(m.get('board_url', ''))
278
+ " 2>/dev/null || echo "")
279
+
280
+ # Build issues JSON array with assignee and gsd_route per issue
281
+ ISSUES_PAYLOAD=$(echo "$ISSUES_JSON" | python3 -c "
282
+ import json,sys
283
+ issues = json.load(sys.stdin)
284
+ result = []
285
+ for i in issues:
286
+ result.append({
287
+ 'number': i.get('github_number', 0),
288
+ 'title': i.get('title', '')[:60],
289
+ 'assignee': i.get('assignee') or None,
290
+ 'gsdRoute': i.get('gsd_route', 'plan-phase')
291
+ })
292
+ print(json.dumps(result))
293
+ " 2>/dev/null || echo "[]")
294
+
295
+ # Get first issue number for fallback comment (non-blocking)
296
+ FIRST_ISSUE_NUM=$(echo "$ISSUES_JSON" | python3 -c "
297
+ import json,sys
298
+ issues = json.load(sys.stdin)
299
+ print(issues[0]['github_number'] if issues else '')
300
+ " 2>/dev/null || echo "")
301
+
302
+ REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null || echo "")
303
+
304
+ REPO="$REPO" \
305
+ MILESTONE_NAME="$MILESTONE_NAME" \
306
+ BOARD_URL="$BOARD_URL" \
307
+ ISSUES_PAYLOAD="$ISSUES_PAYLOAD" \
308
+ FIRST_ISSUE_NUM="$FIRST_ISSUE_NUM" \
309
+ node -e "
310
+ const { postMilestoneStartAnnouncement } = require('./lib/index.cjs');
311
+ const result = postMilestoneStartAnnouncement({
312
+ repo: process.env.REPO,
313
+ milestoneName: process.env.MILESTONE_NAME,
314
+ boardUrl: process.env.BOARD_URL || undefined,
315
+ issues: JSON.parse(process.env.ISSUES_PAYLOAD || '[]'),
316
+ firstIssueNumber: process.env.FIRST_ISSUE_NUM ? parseInt(process.env.FIRST_ISSUE_NUM) : undefined
317
+ });
318
+ if (result.posted) {
319
+ const detail = result.url ? ': ' + result.url : '';
320
+ console.log('MGW: Milestone-start announcement posted via ' + result.method + detail);
321
+ } else {
322
+ console.log('MGW: Milestone-start announcement skipped (Discussions unavailable, no fallback)');
323
+ }
324
+ " 2>/dev/null || echo "MGW: Announcement step failed (non-blocking) — continuing"
325
+ fi
326
+ ```
327
+ </step>
328
+
329
+ <step name="dry_run">
330
+ **If --dry-run flag: display execution plan and exit:**
331
+
332
+ ```bash
333
+ if [ "$DRY_RUN" = true ]; then
334
+ # Build execution plan table
335
+ # Show: order, issue number, title, status, depends on, blocks
336
+ fi
337
+ ```
338
+
339
+ Display:
340
+ ```
341
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
342
+ MGW ► DRY RUN — Milestone ${MILESTONE_NUM}
343
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
344
+
345
+ | Order | Issue | Title | Status | Depends On | Blocks |
346
+ |-------|-------|-------|--------|------------|--------|
347
+ | 1 | #N | title | ○ Pending | — | #M, #K |
348
+ | 2 | #M | title | ○ Pending | #N | #K |
349
+ | 3 | #K | title | ○ Pending | #N, #M | — |
350
+
351
+ Issues: ${TOTAL_ISSUES} total, ${DONE_COUNT} done, ${UNFINISHED_COUNT} remaining
352
+ Rate limit: ${REMAINING} calls available (~${SAFE_ISSUES} issues safe)
353
+ Estimated API calls: ~${ESTIMATED_CALLS}
354
+
355
+ No issues executed. Remove --dry-run to start.
356
+ ```
357
+
358
+ Exit after display.
359
+ </step>
360
+
361
+ <step name="resume_detection">
362
+ **Check for in-progress issues and clean up partial state:**
363
+
364
+ ```bash
365
+ IN_PROGRESS=$(echo "$UNFINISHED" | python3 -c "
366
+ import json,sys
367
+ issues = json.load(sys.stdin)
368
+ in_prog = [i for i in issues if i.get('pipeline_stage') not in ('new', 'done', 'failed')]
369
+ print(json.dumps(in_prog))
370
+ ")
371
+
372
+ IN_PROGRESS_COUNT=$(echo "$IN_PROGRESS" | python3 -c "import json,sys; print(len(json.load(sys.stdin)))")
373
+ ```
374
+
375
+ If in-progress issues exist:
376
+ ```bash
377
+ if [ "$IN_PROGRESS_COUNT" -gt 0 ]; then
378
+ echo "Resuming milestone — ${IN_PROGRESS_COUNT} in-progress issue(s) detected"
379
+
380
+ # For each in-progress issue:
381
+ # 1. Check if worktree exists
382
+ for ISSUE_NUM in $(echo "$IN_PROGRESS" | python3 -c "
383
+ import json,sys
384
+ for i in json.load(sys.stdin):
385
+ print(i['github_number'])
386
+ "); do
387
+ # Clean up lingering worktree (restart from scratch per design decision)
388
+ WORKTREE_PATH=$(git worktree list --porcelain 2>/dev/null | grep -B1 "issue/${ISSUE_NUM}" | head -1 | sed 's/worktree //')
389
+ if [ -n "$WORKTREE_PATH" ] && [ -d "$WORKTREE_PATH" ]; then
390
+ git worktree remove "$WORKTREE_PATH" --force 2>/dev/null
391
+ echo " Cleaned up partial worktree for #${ISSUE_NUM}"
392
+ fi
393
+
394
+ # Reset pipeline_stage to 'new' (will be re-run from scratch)
395
+ node -e "
396
+ const { loadProjectState, resolveActiveMilestoneIndex, writeProjectState } = require('./lib/state.cjs');
397
+ const state = loadProjectState();
398
+ const idx = resolveActiveMilestoneIndex(state);
399
+ if (idx < 0) { console.error('No active milestone'); process.exit(1); }
400
+ const milestone = state.milestones[idx];
401
+ const issue = (milestone.issues || []).find(i => i.github_number === ${ISSUE_NUM});
402
+ if (issue) { issue.pipeline_stage = 'new'; }
403
+ writeProjectState(state);
404
+ "
405
+ done
406
+ fi
407
+ ```
408
+ </step>
409
+
410
+ <step name="execute_loop">
411
+ **Sequential loop over sorted issues (MLST-01, MLST-05):**
412
+
413
+ Track state for progress table:
414
+ ```bash
415
+ COMPLETED_ISSUES=()
416
+ FAILED_ISSUES=()
417
+ BLOCKED_ISSUES=()
418
+ SKIPPED_ISSUES=()
419
+ ISSUES_RUN=0
420
+ ```
421
+
422
+ For each issue in sorted order:
423
+ ```bash
424
+ for ISSUE_DATA in $(echo "$UNFINISHED" | python3 -c "
425
+ import json,sys
426
+ for i in json.load(sys.stdin):
427
+ # Output as compact JSON per line
428
+ print(json.dumps(i))
429
+ "); do
430
+ ISSUE_NUMBER=$(echo "$ISSUE_DATA" | python3 -c "import json,sys; print(json.load(sys.stdin)['github_number'])")
431
+ ISSUE_TITLE=$(echo "$ISSUE_DATA" | python3 -c "import json,sys; print(json.load(sys.stdin)['title'])")
432
+ GSD_ROUTE=$(echo "$ISSUE_DATA" | python3 -c "import json,sys; print(json.load(sys.stdin).get('gsd_route','plan-phase'))")
433
+
434
+ # 1. Check if blocked by a failed issue
435
+ IS_BLOCKED=false
436
+ for FAILED_NUM in "${FAILED_ISSUES[@]}"; do
437
+ DEPS=$(echo "$ISSUE_DATA" | python3 -c "import json,sys; print(','.join(json.load(sys.stdin).get('depends_on_slugs',[])))")
438
+ # Check if any dependency maps to a failed issue
439
+ # If blocked: IS_BLOCKED=true
440
+ done
441
+
442
+ if [ "$IS_BLOCKED" = true ]; then
443
+ BLOCKED_ISSUES+=("$ISSUE_NUMBER")
444
+ echo " ⊘ #${ISSUE_NUMBER} — Blocked (dependency failed)"
445
+ # Update project.json: pipeline_stage = "blocked" (treat as skipped)
446
+ continue
447
+ fi
448
+
449
+ # 2. Check rate limit still OK
450
+ if [ "$ISSUES_RUN" -ge "$MAX_ISSUES" ]; then
451
+ echo "Rate limit cap reached. Stopping after ${ISSUES_RUN} issues."
452
+ echo "${REMAINING} API calls remaining. Limit resets at ${RESET_TIME}."
453
+ break
454
+ fi
455
+
456
+ # 3. Quick GitHub check — is issue still open?
457
+ ISSUE_STATE=$(gh issue view ${ISSUE_NUMBER} --json state -q .state 2>/dev/null || echo "OPEN")
458
+ if [ "$ISSUE_STATE" != "OPEN" ]; then
459
+ echo " ⊘ #${ISSUE_NUMBER} — Skipped (issue ${ISSUE_STATE})"
460
+ SKIPPED_ISSUES+=("$ISSUE_NUMBER")
461
+ continue
462
+ fi
463
+
464
+ # 4. Display terminal output
465
+ echo "Running issue #${ISSUE_NUMBER}..."
466
+
467
+ # ── PRE-WORK: Post triage/work-started comment on issue ──
468
+ # The ORCHESTRATOR posts this, not the inner agent. This guarantees it happens.
469
+ PHASE_NUM=$(echo "$ISSUE_DATA" | python3 -c "import json,sys; print(json.load(sys.stdin).get('phase_number','?'))")
470
+ PHASE_NAME=$(echo "$ISSUE_DATA" | python3 -c "import json,sys; print(json.load(sys.stdin).get('phase_name',''))")
471
+ TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
472
+
473
+ # Build milestone progress table for this comment
474
+ PROGRESS_TABLE=$(echo "$SORTED_ISSUES" | python3 -c "
475
+ import json, sys
476
+ issues = json.load(sys.stdin)
477
+ completed = set(${COMPLETED_ISSUES_JSON:-'[]'})
478
+ current = ${ISSUE_NUMBER}
479
+ lines = ['| # | Issue | Status | PR |', '|---|-------|--------|----|']
480
+ for i in issues:
481
+ num = i['github_number']
482
+ title = i['title'][:45]
483
+ if num in completed:
484
+ lines.append(f'| {num} | {title} | ✓ Done | — |')
485
+ elif num == current:
486
+ lines.append(f'| **{num}** | **{title}** | ◆ In Progress | — |')
487
+ else:
488
+ lines.append(f'| {num} | {title} | ○ Pending | — |')
489
+ print('\n'.join(lines))
490
+ ")
491
+
492
+ WORK_STARTED_BODY=$(cat <<COMMENTEOF
493
+ > **MGW** · \`work-started\` · ${TIMESTAMP}
494
+ > Milestone: ${MILESTONE_NAME} | Phase ${PHASE_NUM}: ${PHASE_NAME}
495
+
496
+ ### Work Started
497
+
498
+ | | |
499
+ |---|---|
500
+ | **Issue** | #${ISSUE_NUMBER} — ${ISSUE_TITLE} |
501
+ | **Route** | \`${GSD_ROUTE}\` |
502
+ | **Phase** | ${PHASE_NUM} of ${TOTAL_PHASES} — ${PHASE_NAME} |
503
+ | **Milestone** | ${MILESTONE_NAME} |
504
+
505
+ <details>
506
+ <summary>Milestone Progress (${#COMPLETED_ISSUES[@]}/${TOTAL_ISSUES} complete)</summary>
507
+
508
+ ${PROGRESS_TABLE}
509
+
510
+ </details>
511
+ COMMENTEOF
512
+ )
513
+
514
+ gh issue comment ${ISSUE_NUMBER} --body "$WORK_STARTED_BODY" 2>/dev/null || true
515
+ gh issue edit ${ISSUE_NUMBER} --add-assignee @me 2>/dev/null || true
516
+
517
+ # ── MAIN WORK: Spawn /mgw:run via Task() ──
518
+ # The agent focuses on: worktree → GSD execution → PR creation
519
+ # Comment posting is handled by THIS orchestrator, not the agent.
520
+ Task(
521
+ prompt="
522
+ <files_to_read>
523
+ - ./CLAUDE.md (Project instructions — if exists, follow all guidelines)
524
+ - .agents/skills/ (Project skills — if dir exists, list skills, read SKILL.md for each, follow relevant rules)
525
+ </files_to_read>
526
+
527
+ Run the MGW pipeline for issue #${ISSUE_NUMBER}.
528
+ Read ~/.claude/commands/mgw/run.md for the workflow steps.
529
+
530
+ **Your responsibilities (the orchestrator handles status comments):**
531
+ 1. validate_and_load — load issue state from .mgw/active/
532
+ 2. create_worktree — create isolated git worktree for issue branch
533
+ 3. execute_gsd_quick or execute_gsd_milestone (route: ${GSD_ROUTE})
534
+ 4. create_pr — push branch and create PR with this EXACT body structure:
535
+
536
+ PR BODY MUST include these sections IN ORDER:
537
+ ## Summary
538
+ - 2-4 bullets of what was built and why
539
+
540
+ Closes #${ISSUE_NUMBER}
541
+
542
+ ## Milestone Context
543
+ - **Milestone:** ${MILESTONE_NAME}
544
+ - **Phase:** ${PHASE_NUM} — ${PHASE_NAME}
545
+ - **Issue:** ${ISSUES_RUN + 1} of ${TOTAL_ISSUES} in milestone
546
+
547
+ ## Changes
548
+ - File-level changes grouped by module
549
+
550
+ ## Test Plan
551
+ - Verification checklist
552
+
553
+ 5. cleanup_and_complete — clean up worktree, update .mgw/ state
554
+
555
+ **Do NOT post issue comments** — the orchestrator handles all GitHub comments.
556
+
557
+ Issue title: ${ISSUE_TITLE}
558
+ GSD route: ${GSD_ROUTE}
559
+ ",
560
+ subagent_type="general-purpose",
561
+ description="Run pipeline for #${ISSUE_NUMBER}"
562
+ )
563
+
564
+ # ── POST-WORK: Detect result and post completion comment ──
565
+ # Check if PR was created by looking for state file or PR
566
+ PR_NUMBER=$(gh pr list --head "issue/${ISSUE_NUMBER}-*" --json number -q '.[0].number' 2>/dev/null || echo "")
567
+ PR_URL=""
568
+ if [ -z "$PR_NUMBER" ]; then
569
+ # Try broader search
570
+ PR_NUMBER=$(gh pr list --state all --search "Closes #${ISSUE_NUMBER}" --json number -q '.[0].number' 2>/dev/null || echo "")
571
+ fi
572
+ if [ -n "$PR_NUMBER" ]; then
573
+ PR_URL=$(gh pr view "$PR_NUMBER" --json url -q .url 2>/dev/null || echo "")
574
+ fi
575
+
576
+ DONE_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
577
+
578
+ if [ -n "$PR_NUMBER" ]; then
579
+ # Success — post PR-ready comment
580
+ COMPLETED_ISSUES+=("$ISSUE_NUMBER")
581
+ COMPLETED_ISSUES_JSON=$(printf '%s\n' "${COMPLETED_ISSUES[@]}" | python3 -c "import json,sys; print(json.dumps([int(x.strip()) for x in sys.stdin if x.strip()]))")
582
+
583
+ # Rebuild progress table with updated state
584
+ DONE_PROGRESS=$(echo "$SORTED_ISSUES" | python3 -c "
585
+ import json, sys
586
+ issues = json.load(sys.stdin)
587
+ completed = set(${COMPLETED_ISSUES_JSON})
588
+ lines = ['| # | Issue | Status | PR |', '|---|-------|--------|----|']
589
+ for i in issues:
590
+ num = i['github_number']
591
+ title = i['title'][:45]
592
+ if num in completed:
593
+ lines.append(f'| {num} | {title} | ✓ Done | — |')
594
+ else:
595
+ lines.append(f'| {num} | {title} | ○ Pending | — |')
596
+ print('\n'.join(lines))
597
+ ")
598
+
599
+ PR_READY_BODY=$(cat <<COMMENTEOF
600
+ > **MGW** · \`pr-ready\` · ${DONE_TIMESTAMP}
601
+ > Milestone: ${MILESTONE_NAME} | Phase ${PHASE_NUM}: ${PHASE_NAME}
602
+
603
+ ### PR Ready
604
+
605
+ **PR #${PR_NUMBER}** — ${PR_URL}
606
+
607
+ Testing procedures posted on the PR.
608
+ This issue will auto-close when the PR is merged.
609
+
610
+ <details>
611
+ <summary>Milestone Progress (${#COMPLETED_ISSUES[@]}/${TOTAL_ISSUES} complete)</summary>
612
+
613
+ ${DONE_PROGRESS}
614
+
615
+ </details>
616
+ COMMENTEOF
617
+ )
618
+
619
+ gh issue comment ${ISSUE_NUMBER} --body "$PR_READY_BODY" 2>/dev/null || true
620
+ echo " ✓ #${ISSUE_NUMBER} — PR #${PR_NUMBER} created"
621
+
622
+ else
623
+ # Failure — post failure comment
624
+ FAILED_ISSUES+=("$ISSUE_NUMBER")
625
+
626
+ FAIL_BODY=$(cat <<COMMENTEOF
627
+ > **MGW** · \`pipeline-failed\` · ${DONE_TIMESTAMP}
628
+ > Milestone: ${MILESTONE_NAME} | Phase ${PHASE_NUM}: ${PHASE_NAME}
629
+
630
+ ### Pipeline Failed
631
+
632
+ Issue #${ISSUE_NUMBER} did not produce a PR.
633
+ Check the execution log for details.
634
+
635
+ Dependents of this issue will be skipped.
636
+ COMMENTEOF
637
+ )
638
+
639
+ gh issue comment ${ISSUE_NUMBER} --body "$FAIL_BODY" 2>/dev/null || true
640
+ gh issue edit ${ISSUE_NUMBER} --add-label "pipeline-failed" 2>/dev/null || true
641
+ gh label create "pipeline-failed" --description "Pipeline execution failed" --color "d73a4a" --force 2>/dev/null || true
642
+ echo " ✗ #${ISSUE_NUMBER} — Failed (no PR created)"
643
+ fi
644
+
645
+ # Update project.json checkpoint (MLST-05)
646
+ STAGE=$([ -n "$PR_NUMBER" ] && echo "done" || echo "failed")
647
+ node -e "
648
+ const { loadProjectState, resolveActiveMilestoneIndex, writeProjectState } = require('./lib/state.cjs');
649
+ const state = loadProjectState();
650
+ const idx = resolveActiveMilestoneIndex(state);
651
+ if (idx < 0) { console.error('No active milestone'); process.exit(1); }
652
+ const milestone = state.milestones[idx];
653
+ const issue = (milestone.issues || []).find(i => i.github_number === ${ISSUE_NUMBER});
654
+ if (issue) { issue.pipeline_stage = '${STAGE}'; }
655
+ writeProjectState(state);
656
+ "
657
+
658
+ ISSUES_RUN=$((ISSUES_RUN + 1))
659
+
660
+ # If --interactive: pause between issues
661
+ if [ "$INTERACTIVE" = true ]; then
662
+ AskUserQuestion(
663
+ header: "Issue Complete",
664
+ question: "#${ISSUE_NUMBER} done. Continue to next issue?",
665
+ options: [
666
+ { label: "Continue", description: "Proceed to next unblocked issue" },
667
+ { label: "Skip next", description: "Skip next issue and continue" },
668
+ { label: "Abort", description: "Stop milestone execution here" }
669
+ ]
670
+ )
671
+ # Handle response: Continue → proceed, Skip → skip next, Abort → break
672
+ fi
673
+ done
674
+ ```
675
+
676
+ **Progress table format for GitHub comments:**
677
+
678
+ Every comment posted during milestone orchestration includes:
679
+ ```markdown
680
+ **Issue #N — {Status}** {symbol}
681
+
682
+ {Status-specific detail (PR link, failure reason, etc.)}
683
+
684
+ <details>
685
+ <summary>Milestone Progress ({done}/{total} complete)</summary>
686
+
687
+ | # | Issue | Status | PR | Stage |
688
+ |---|-------|--------|----|-------|
689
+ | N | title | ✓ Done | #PR | done |
690
+ | M | title | ✗ Failed | — | failed |
691
+ | K | title | ○ Pending | — | new |
692
+ | J | title | ◆ Running | — | executing |
693
+ | L | title | ⊘ Blocked | — | blocked-by:#N |
694
+
695
+ </details>
696
+ ```
697
+ </step>
698
+
699
+ <step name="post_loop">
700
+ **After loop completes — finalize milestone:**
701
+
702
+ Build final results table:
703
+ ```bash
704
+ TOTAL_DONE=$((DONE_COUNT + ${#COMPLETED_ISSUES[@]}))
705
+ TOTAL_FAILED=${#FAILED_ISSUES[@]}
706
+ TOTAL_BLOCKED=${#BLOCKED_ISSUES[@]}
707
+ TOTAL_SKIPPED=${#SKIPPED_ISSUES[@]}
708
+ ```
709
+
710
+ **If ALL issues completed (pipeline_stage == 'done' for all):**
711
+
712
+ 1. Close GitHub milestone:
713
+ ```bash
714
+ REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner)
715
+ gh api "repos/${REPO}/milestones/${MILESTONE_GH_NUMBER}" --method PATCH \
716
+ -f state="closed" 2>/dev/null
717
+ ```
718
+
719
+ 2. Create draft release:
720
+ ```bash
721
+ RELEASE_TAG="milestone-${MILESTONE_NUM}-complete"
722
+ # Build release body with: milestone name, issues completed, PR links, stats
723
+ RELEASE_BODY="## Milestone ${MILESTONE_NUM}: ${MILESTONE_NAME}
724
+
725
+ ### Issues Completed
726
+ ${issues_completed_list}
727
+
728
+ ### Pull Requests
729
+ ${pr_links_list}
730
+
731
+ ### Stats
732
+ - Issues: ${TOTAL_ISSUES}
733
+ - PRs created: ${pr_count}
734
+
735
+ ---
736
+ *Auto-generated by MGW milestone orchestration*"
737
+
738
+ gh release create "$RELEASE_TAG" --draft \
739
+ --title "Milestone ${MILESTONE_NUM}: ${MILESTONE_NAME}" \
740
+ --notes "$RELEASE_BODY" 2>/dev/null
741
+ ```
742
+
743
+ 3. Finalize GSD milestone state (archive phases, clean up):
744
+ ```bash
745
+ # Only run if .planning/phases exists (GSD was used for this milestone)
746
+ if [ -d ".planning/phases" ]; then
747
+ EXECUTOR_MODEL=$(node ~/.claude/get-shit-done/bin/gsd-tools.cjs resolve-model gsd-executor --raw)
748
+ Task(
749
+ prompt="
750
+ <files_to_read>
751
+ - ./CLAUDE.md (Project instructions -- if exists, follow all guidelines)
752
+ - .planning/ROADMAP.md (Current roadmap to archive)
753
+ - .planning/REQUIREMENTS.md (Requirements to archive)
754
+ </files_to_read>
755
+
756
+ Complete the GSD milestone. Follow the complete-milestone workflow:
757
+ @~/.claude/get-shit-done/workflows/complete-milestone.md
758
+
759
+ This archives the milestone's ROADMAP and REQUIREMENTS to .planning/milestones/,
760
+ cleans up ROADMAP.md for the next milestone, and tags the release in git.
761
+
762
+ Milestone: ${MILESTONE_NAME}
763
+ ",
764
+ subagent_type="gsd-executor",
765
+ model="${EXECUTOR_MODEL}",
766
+ description="Complete GSD milestone: ${MILESTONE_NAME}"
767
+ )
768
+ fi
769
+ ```
770
+
771
+ 4. Advance active milestone pointer in project.json:
772
+ ```bash
773
+ node -e "
774
+ const { loadProjectState, resolveActiveMilestoneIndex, writeProjectState } = require('./lib/state.cjs');
775
+ const state = loadProjectState();
776
+ const currentIdx = resolveActiveMilestoneIndex(state);
777
+ const nextMilestone = (state.milestones || [])[currentIdx + 1];
778
+ if (nextMilestone) {
779
+ // New schema: point active_gsd_milestone at the next milestone's gsd_milestone_id
780
+ state.active_gsd_milestone = nextMilestone.gsd_milestone_id || null;
781
+ // Backward compat: if next milestone has no gsd_milestone_id, fall back to legacy integer
782
+ if (!state.active_gsd_milestone) {
783
+ state.current_milestone = currentIdx + 2; // next 1-indexed
784
+ }
785
+ } else {
786
+ // All milestones complete — clear the active pointer
787
+ state.active_gsd_milestone = null;
788
+ state.current_milestone = currentIdx + 2; // past end, signals completion
789
+ }
790
+ writeProjectState(state);
791
+ "
792
+ ```
793
+
794
+ 5. Milestone mapping verification:
795
+
796
+ After advancing to the next milestone, check its GSD linkage:
797
+
798
+ ```bash
799
+ NEXT_MILESTONE_CHECK=$(node -e "
800
+ const { loadProjectState, resolveActiveMilestoneIndex } = require('./lib/state.cjs');
801
+ const state = loadProjectState();
802
+ const activeIdx = resolveActiveMilestoneIndex(state);
803
+
804
+ if (activeIdx < 0 || activeIdx >= state.milestones.length) {
805
+ console.log('none');
806
+ process.exit(0);
807
+ }
808
+
809
+ const nextMilestone = state.milestones[activeIdx];
810
+ if (!nextMilestone) {
811
+ console.log('none');
812
+ process.exit(0);
813
+ }
814
+
815
+ const gsdId = nextMilestone.gsd_milestone_id;
816
+ const name = nextMilestone.name;
817
+
818
+ if (!gsdId) {
819
+ console.log('unlinked:' + name);
820
+ } else {
821
+ console.log('linked:' + name + ':' + gsdId);
822
+ }
823
+ ")
824
+
825
+ case "$NEXT_MILESTONE_CHECK" in
826
+ none)
827
+ echo "All milestones complete — project is done!"
828
+ ;;
829
+ unlinked:*)
830
+ NEXT_NAME=$(echo "$NEXT_MILESTONE_CHECK" | cut -d':' -f2-)
831
+ echo ""
832
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
833
+ echo " Next milestone '${NEXT_NAME}' has no GSD milestone linked."
834
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
835
+ echo ""
836
+ echo "Before running /mgw:milestone for the next milestone:"
837
+ echo " 1) Run /gsd:new-milestone to create GSD state for '${NEXT_NAME}'"
838
+ echo " 2) Run /mgw:project extend to link the new GSD milestone"
839
+ echo ""
840
+ ;;
841
+ linked:*)
842
+ NEXT_NAME=$(echo "$NEXT_MILESTONE_CHECK" | cut -d':' -f2)
843
+ GSD_ID=$(echo "$NEXT_MILESTONE_CHECK" | cut -d':' -f3)
844
+ # Verify ROADMAP.md matches expected GSD milestone
845
+ ROADMAP_CHECK=$(python3 -c "
846
+ import os, sys
847
+ if not os.path.exists('.planning/ROADMAP.md'):
848
+ print('no_roadmap')
849
+ sys.exit()
850
+ with open('.planning/ROADMAP.md') as f:
851
+ content = f.read()
852
+ if '${GSD_ID}' in content:
853
+ print('match')
854
+ else:
855
+ print('mismatch')
856
+ " 2>/dev/null || echo "no_roadmap")
857
+
858
+ case "$ROADMAP_CHECK" in
859
+ match)
860
+ echo "Next milestone '${NEXT_NAME}' (GSD: ${GSD_ID}) — ROADMAP.md is ready."
861
+ ;;
862
+ mismatch)
863
+ echo "Next milestone '${NEXT_NAME}' links to GSD milestone '${GSD_ID}'"
864
+ echo " but .planning/ROADMAP.md does not contain that milestone ID."
865
+ echo " Run /gsd:new-milestone to update ROADMAP.md before proceeding."
866
+ ;;
867
+ no_roadmap)
868
+ echo "NOTE: Next milestone '${NEXT_NAME}' (GSD: ${GSD_ID}) linked."
869
+ echo " No .planning/ROADMAP.md found — run /gsd:new-milestone when ready."
870
+ ;;
871
+ esac
872
+ ;;
873
+ esac
874
+ ```
875
+
876
+ 6. Display completion banner:
877
+ ```
878
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
879
+ MGW ► MILESTONE ${MILESTONE_NUM} COMPLETE ✓
880
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
881
+
882
+ ${MILESTONE_NAME}
883
+
884
+ | # | Issue | Status | PR |
885
+ |---|-------|--------|----|
886
+ ${results_table}
887
+
888
+ Issues: ${TOTAL_ISSUES} | PRs: ${pr_count}
889
+ Milestone closed on GitHub.
890
+ Draft release created: ${RELEASE_TAG}
891
+
892
+ ───────────────────────────────────────────────────────────────
893
+
894
+ ## ▶ Next Up
895
+
896
+ **Milestone ${NEXT_MILESTONE_NUM}** — next milestone
897
+
898
+ /mgw:milestone ${NEXT_MILESTONE_NUM}
899
+
900
+ <sub>/clear first → fresh context window</sub>
901
+
902
+ ───────────────────────────────────────────────────────────────
903
+ ```
904
+
905
+ 7. Check if next milestone exists and offer auto-advance (only if no failures in current).
906
+
907
+ **If some issues failed:**
908
+
909
+ ```
910
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
911
+ MGW ► MILESTONE ${MILESTONE_NUM} INCOMPLETE
912
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
913
+
914
+ ${MILESTONE_NAME}
915
+
916
+ | # | Issue | Status | PR |
917
+ |---|-------|--------|----|
918
+ ${results_table}
919
+
920
+ Completed: ${TOTAL_DONE}/${TOTAL_ISSUES}
921
+ Failed: ${TOTAL_FAILED}
922
+ Blocked: ${TOTAL_BLOCKED}
923
+
924
+ Milestone NOT closed. Resolve failures and re-run:
925
+ /mgw:milestone ${MILESTONE_NUM}
926
+ ```
927
+
928
+ 8. Post final results table as GitHub comment on the first issue in the milestone:
929
+ ```bash
930
+ gh issue comment ${FIRST_ISSUE_NUMBER} --body "$FINAL_RESULTS_COMMENT"
931
+ ```
932
+ </step>
933
+
934
+ </process>
935
+
936
+ <success_criteria>
937
+ - [ ] project.json loaded and milestone validated (MLST-01)
938
+ - [ ] Batch staleness check run before execution (MLST-03)
939
+ - [ ] Rate limit checked and execution capped if needed (MLST-04)
940
+ - [ ] Dependency resolution via topological sort (MLST-01)
941
+ - [ ] Cycle detection with clear error reporting
942
+ - [ ] Resume detection with partial state cleanup
943
+ - [ ] Sequential execution via /mgw:run Task() delegation (MLST-01)
944
+ - [ ] Per-issue checkpoint to project.json after completion (MLST-05)
945
+ - [ ] Failure handling: skip failed, label, comment, block dependents
946
+ - [ ] Progress table in every GitHub comment
947
+ - [ ] Milestone close + draft release on full completion
948
+ - [ ] current_milestone pointer advanced on completion
949
+ - [ ] --interactive flag pauses between issues
950
+ - [ ] --dry-run flag shows plan without executing
951
+ - [ ] Terminal output is minimal during run
952
+ </success_criteria>