@snipcodeit/mgw 0.1.1 → 0.1.3

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.
package/README.md CHANGED
@@ -1,8 +1,9 @@
1
1
  # MGW — My GSD Workflow
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/mgw)](https://www.npmjs.com/package/mgw)
3
+ [![npm version](https://img.shields.io/npm/v/@snipcodeit/mgw)](https://www.npmjs.com/package/@snipcodeit/mgw)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@snipcodeit/mgw)](https://www.npmjs.com/package/@snipcodeit/mgw)
4
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
- [![node](https://img.shields.io/node/v/mgw)](https://nodejs.org)
6
+ [![node](https://img.shields.io/node/v/@snipcodeit/mgw)](https://nodejs.org)
6
7
  [![GitHub stars](https://img.shields.io/github/stars/snipcodeit/mgw)](https://github.com/snipcodeit/mgw)
7
8
 
8
9
  > Issue in. PR out. No excuses.
@@ -95,6 +96,9 @@ If you're already using Claude Code and GSD for development, MGW is the missing
95
96
  | `/mgw:review <n>` | Review and classify new comments on an issue since last triage |
96
97
  | `/mgw:link <ref> <ref>` | Cross-reference issues, PRs, and branches (including milestone ↔ GSD milestone maps-to links) |
97
98
  | `/mgw:status [n]` | Project dashboard — milestone progress, issue stages, open PRs |
99
+ | `/mgw:roadmap [--set-dates] [--post-discussion]` | Render project milestones as a roadmap table; optionally set GitHub due dates or post as a Discussion |
100
+ | `/mgw:assign <n> [user]` | Claim or reassign an issue; resolves GitHub noreply co-author tag |
101
+ | `/mgw:board [--sync]` | Create, configure, and sync a GitHub Projects v2 board |
98
102
  | `/mgw:sync` | Reconcile local state with GitHub; verifies GSD milestone consistency |
99
103
  | `/mgw:help` | Command reference |
100
104
 
@@ -184,9 +188,12 @@ MGW tracks pipeline state in a local `.mgw/` directory (gitignored, per-develope
184
188
  42-fix-auth.json Issue state: triage results, pipeline stage, artifacts
185
189
  completed/ Archived after PR merge
186
190
  cross-refs.json Bidirectional issue/PR/branch/milestone links
187
- vision-draft.md (Fresh projects) Rolling decisions from questioning loop
188
- vision-brief.json (Fresh projects) Structured Vision Brief (MoSCoW, personas, scope)
191
+ vision-research.json (Fresh projects) Domain research from vision-researcher agent
192
+ vision-draft.md (Fresh projects) Rolling decisions from questioning loop
193
+ vision-brief.json (Fresh projects) Structured Vision Brief (MoSCoW, personas, scope)
194
+ vision-handoff.md (Fresh projects) Condensed brief handed off to gsd:new-project
189
195
  alignment-report.json (GSD-Only projects) GSD state mapped for milestone backfill
196
+ drift-report.json (Diverged projects) Reconciliation table from drift-analyzer agent
190
197
  ```
191
198
 
192
199
  Pipeline stages flow: `new` → `triaged` → `planning` → `executing` → `verifying` → `pr-created` → `done` (or `failed`/`blocked`). Bugs routed to `gsd:diagnose-issues` pass through `diagnosing` before `planning`.
@@ -248,9 +255,9 @@ mgw --version
248
255
 
249
256
  # Slash commands (installed automatically by postinstall)
250
257
  ls ~/.claude/commands/mgw/
251
- # ask.md board.md help.md init.md issue.md issues.md link.md milestone.md
252
- # next.md pr.md project.md review.md run.md status.md sync.md
253
- # update.md workflows/
258
+ # ask.md assign.md board.md help.md init.md issue.md issues.md link.md
259
+ # milestone.md next.md pr.md project.md review.md roadmap.md run.md
260
+ # status.md sync.md update.md workflows/
254
261
  ```
255
262
 
256
263
  Then in Claude Code:
@@ -266,7 +273,7 @@ Not all commands work via `npx`. The CLI has two tiers:
266
273
  | Tier | Commands | Requirements |
267
274
  |------|----------|--------------|
268
275
  | **CLI-only** (works with npx) | `issues`, `sync`, `link`, `help`, `--help`, `--version` | Node.js >= 18, `gh` CLI |
269
- | **AI-powered** (requires full install) | `run`, `init`, `project`, `milestone`, `next`, `issue`, `update`, `pr`, `ask`, `review` | Node.js >= 18, `gh` CLI, Claude Code CLI, GSD |
276
+ | **AI-powered** (requires full install) | `run`, `init`, `project`, `milestone`, `next`, `issue`, `update`, `pr`, `ask`, `review`, `assign`, `board`, `roadmap`, `status` | Node.js >= 18, `gh` CLI, Claude Code CLI, GSD |
270
277
 
271
278
  AI-powered commands call `claude -p` under the hood and require the [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code/overview) to be installed and authenticated. The slash command `.md` files must also be deployed to `~/.claude/commands/mgw/` for the full pipeline to work — this happens automatically via `postinstall`. Use `npx @snipcodeit/mgw` to explore the CLI and verify your GitHub setup before committing to a full install.
272
279
 
@@ -336,12 +343,15 @@ lib/
336
343
  claude.cjs Claude Code invocation helpers
337
344
  github.cjs GitHub CLI wrappers (issues, PRs, milestones, Projects v2)
338
345
  gsd.cjs GSD integration
346
+ gsd-adapter.cjs GSD route adapter (maps triage results to GSD spawn args)
339
347
  state.cjs .mgw/ state management (migrateProjectState, resolveActiveMilestoneIndex)
340
348
  output.cjs Logging and formatting
349
+ retry.cjs Retry logic for GitHub API calls
341
350
  templates.cjs Template system
342
351
  template-loader.cjs Output validation (JSON Schema) + parseRoadmap()
343
352
  commands/ Slash command source files (deployed to ~/.claude/commands/mgw/ at install time)
344
353
  ask.md Contextual question routing during milestone execution
354
+ assign.md Claim/reassign issues; resolves GitHub noreply co-author tag
345
355
  board.md GitHub Projects v2 board management
346
356
  help.md Command reference display
347
357
  init.md One-time repo bootstrap (state, templates, labels)
@@ -350,6 +360,7 @@ commands/ Slash command source files (deployed to ~/.claude/com
350
360
  issue.md Deep triage with agent analysis
351
361
  next.md Next unblocked issue picker (surfaces failed issues as advisory)
352
362
  review.md Comment review and classification since last triage
363
+ roadmap.md Milestone roadmap table; optional GitHub due-date setter and Discussion post
353
364
  run.md Autonomous pipeline orchestrator (cross-milestone enforcement)
354
365
  milestone.md Milestone execution with dependency ordering and failed-issue recovery
355
366
  update.md Structured GitHub comment templates
@@ -491,7 +502,7 @@ If rate-limited, wait for the reset window (usually under an hour) or reduce the
491
502
 
492
503
  ```bash
493
504
  # Remove CLI
494
- npm unlink mgw
505
+ npm uninstall -g @snipcodeit/mgw
495
506
 
496
507
  # Remove slash commands
497
508
  rm -rf ~/.claude/commands/mgw/
package/commands/board.md CHANGED
@@ -176,6 +176,81 @@ for name, data in fields.items():
176
176
  fi
177
177
  ```
178
178
 
179
+ **Board discovery: check GitHub for an existing board before creating a new one:**
180
+
181
+ One lightweight GraphQL list call. Searches the first 20 user/org projects for a title
182
+ containing the project name. If found, registers it in project.json and exits — no fields
183
+ created, no board duplicated. Only runs when `BOARD_CONFIGURED = false`.
184
+
185
+ ```bash
186
+ echo "Checking GitHub for existing boards..."
187
+ DISCOVERED=$(node -e "
188
+ const { findExistingBoard, getProjectFields } = require('./lib/github.cjs');
189
+ const board = findExistingBoard('${OWNER}', '${PROJECT_NAME}');
190
+ if (!board) { process.stdout.write(''); process.exit(0); }
191
+ const fields = getProjectFields('${OWNER}', board.number) || {};
192
+ console.log(JSON.stringify({ ...board, fields }));
193
+ " 2>/dev/null || echo "")
194
+
195
+ if [ -n "$DISCOVERED" ]; then
196
+ DISC_NUMBER=$(echo "$DISCOVERED" | python3 -c "import json,sys; print(json.load(sys.stdin)['number'])")
197
+ DISC_URL=$(echo "$DISCOVERED" | python3 -c "import json,sys; print(json.load(sys.stdin)['url'])")
198
+ DISC_NODE_ID=$(echo "$DISCOVERED" | python3 -c "import json,sys; print(json.load(sys.stdin)['nodeId'])")
199
+ DISC_TITLE=$(echo "$DISCOVERED" | python3 -c "import json,sys; print(json.load(sys.stdin)['title'])")
200
+ DISC_FIELDS=$(echo "$DISCOVERED" | python3 -c "import json,sys; print(json.dumps(json.load(sys.stdin).get('fields', {})))")
201
+
202
+ echo " Found existing board: #${DISC_NUMBER} \"${DISC_TITLE}\" — ${DISC_URL}"
203
+
204
+ python3 << PYEOF
205
+ import json
206
+
207
+ with open('${MGW_DIR}/project.json') as f:
208
+ project = json.load(f)
209
+
210
+ fields = json.loads('''${DISC_FIELDS}''') if '${DISC_FIELDS}' not in ('', '{}') else {}
211
+
212
+ project['project']['project_board'] = {
213
+ 'number': int('${DISC_NUMBER}'),
214
+ 'url': '${DISC_URL}',
215
+ 'node_id': '${DISC_NODE_ID}',
216
+ 'fields': fields
217
+ }
218
+
219
+ with open('${MGW_DIR}/project.json', 'w') as f:
220
+ json.dump(project, f, indent=2)
221
+
222
+ print('project.json updated')
223
+ PYEOF
224
+
225
+ echo ""
226
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
227
+ echo " MGW ► EXISTING BOARD REGISTERED"
228
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
229
+ echo ""
230
+ echo "Board: #${DISC_NUMBER} — ${DISC_URL}"
231
+ echo "Node ID: ${DISC_NODE_ID}"
232
+ echo ""
233
+ if [ "$DISC_FIELDS" != "{}" ] && [ -n "$DISC_FIELDS" ]; then
234
+ echo "Fields registered:"
235
+ echo "$DISC_FIELDS" | python3 -c "
236
+ import json,sys
237
+ fields = json.load(sys.stdin)
238
+ for name, data in fields.items():
239
+ ftype = data.get('type', '?')
240
+ print(f' {name}: {data.get(\"field_id\",\"?\")} ({ftype})')
241
+ " 2>/dev/null
242
+ else
243
+ echo " (no custom fields found — run /mgw:board configure to add them)"
244
+ fi
245
+ echo ""
246
+ echo "To see board items: /mgw:board show"
247
+ exit 0
248
+ fi
249
+
250
+ echo " No existing board found — creating new board..."
251
+ echo ""
252
+ ```
253
+
179
254
  **Get owner and repo node IDs (required for GraphQL mutations):**
180
255
 
181
256
  ```bash
package/commands/issue.md CHANGED
@@ -287,16 +287,15 @@ MISSING_FIELDS_LIST = gate_result.missing_fields formatted as "- ${field}" list
287
287
  ```
288
288
 
289
289
  Use the "Gate Blocked Comment" template from @~/.claude/commands/mgw/workflows/github.md.
290
- Post comment and apply label:
290
+ Post comment and apply label using the highest-severity blocker (security > detail > validity):
291
291
  ```bash
292
- # Use remove_mgw_labels_and_apply pattern from github.md
293
- # For validity failures:
294
- # pipeline_stage = "needs-info", label = "mgw:needs-info"
295
- # For security failures:
296
- # pipeline_stage = "needs-security-review", label = "mgw:needs-security-review"
297
- # For detail failures:
298
- # pipeline_stage = "needs-info", label = "mgw:needs-info"
299
- # If multiple blockers, use the highest-severity label (security > detail > validity)
292
+ # For validity or detail failures:
293
+ remove_mgw_labels_and_apply ${ISSUE_NUMBER} "mgw:needs-info"
294
+
295
+ # For security failures (highest severity — takes precedence over needs-info):
296
+ remove_mgw_labels_and_apply ${ISSUE_NUMBER} "mgw:needs-security-review"
297
+
298
+ # If multiple blockers, apply security label if security gate failed; otherwise needs-info.
300
299
  ```
301
300
 
302
301
  **If gates passed (gate_result.status == "passed"):**
@@ -316,7 +315,7 @@ ROUTE_REASONING = triage reasoning
316
315
 
317
316
  Post comment and apply label:
318
317
  ```bash
319
- gh issue edit ${ISSUE_NUMBER} --add-label "mgw:triaged" 2>/dev/null
318
+ remove_mgw_labels_and_apply ${ISSUE_NUMBER} "mgw:triaged"
320
319
  ```
321
320
  </step>
322
321
 
@@ -414,8 +414,10 @@ Track state for progress table:
414
414
  ```bash
415
415
  COMPLETED_ISSUES=()
416
416
  FAILED_ISSUES=()
417
+ FAILED_ISSUES_WITH_CLASS=() # Entries: "issue_number:failure_class" for results display
417
418
  BLOCKED_ISSUES=()
418
419
  SKIPPED_ISSUES=()
420
+ LABEL_DRIFT_ISSUES=() # Issues where label reconciliation detected drift
419
421
  ISSUES_RUN=0
420
422
  ```
421
423
 
@@ -561,6 +563,43 @@ COMMENTEOF
561
563
  description="Run pipeline for #${ISSUE_NUMBER}"
562
564
  )
563
565
 
566
+ # ── POST-WORK: Post-subagent label verification ──
567
+ # Read the pipeline_stage from the issue's active state file after Task() returns
568
+ ISSUE_STAGE=$(node -e "
569
+ const { loadActiveIssue } = require('./lib/state.cjs');
570
+ const state = loadActiveIssue(${ISSUE_NUMBER});
571
+ console.log((state && state.pipeline_stage) ? state.pipeline_stage : 'unknown');
572
+ " 2>/dev/null || echo "unknown")
573
+
574
+ # Determine the expected MGW label for this pipeline stage
575
+ EXPECTED_LABEL=$(python3 -c "
576
+ stage_to_label = {
577
+ 'done': '',
578
+ 'pr-created': '',
579
+ 'verifying': 'mgw:in-progress',
580
+ 'executing': 'mgw:in-progress',
581
+ 'planning': 'mgw:in-progress',
582
+ 'blocked': 'mgw:blocked',
583
+ 'failed': '',
584
+ }
585
+ print(stage_to_label.get('${ISSUE_STAGE}', ''))
586
+ ")
587
+
588
+ # Compare expected label against live GitHub labels
589
+ LIVE_LABELS=$(gh issue view ${ISSUE_NUMBER} --json labels --jq '[.labels[].name] | join(",")' 2>/dev/null || echo "")
590
+
591
+ if [ -n "$EXPECTED_LABEL" ] && ! echo "$LIVE_LABELS" | grep -q "$EXPECTED_LABEL"; then
592
+ echo "MGW WARNING: label drift on #${ISSUE_NUMBER} — expected $EXPECTED_LABEL, live: $LIVE_LABELS" >&2
593
+ LABEL_DRIFT="drift"
594
+ else
595
+ LABEL_DRIFT="ok"
596
+ fi
597
+
598
+ # Track drifted issues for milestone summary
599
+ if [ "$LABEL_DRIFT" = "drift" ]; then
600
+ LABEL_DRIFT_ISSUES+=("$ISSUE_NUMBER")
601
+ fi
602
+
564
603
  # ── POST-WORK: Detect result and post completion comment ──
565
604
  # Check if PR was created by looking for state file or PR
566
605
  PR_NUMBER=$(gh pr list --head "issue/${ISSUE_NUMBER}-*" --json number -q '.[0].number' 2>/dev/null || echo "")
@@ -620,9 +659,30 @@ COMMENTEOF
620
659
  echo " ✓ #${ISSUE_NUMBER} — PR #${PR_NUMBER} created"
621
660
 
622
661
  else
623
- # Failure — post failure comment
662
+ # Failure — read failure_class from active issue state, then post failure comment
624
663
  FAILED_ISSUES+=("$ISSUE_NUMBER")
625
664
 
665
+ # Read failure_class and dead_letter from the active issue state file
666
+ ISSUE_FAILURE_CLASS=$(node -e "
667
+ const { loadActiveIssue } = require('./lib/state.cjs');
668
+ const state = loadActiveIssue(${ISSUE_NUMBER});
669
+ console.log((state && state.last_failure_class) ? state.last_failure_class : 'unknown');
670
+ " 2>/dev/null || echo "unknown")
671
+
672
+ ISSUE_DEAD_LETTER=$(node -e "
673
+ const { loadActiveIssue } = require('./lib/state.cjs');
674
+ const state = loadActiveIssue(${ISSUE_NUMBER});
675
+ console.log(state && state.dead_letter === true ? 'true' : 'false');
676
+ " 2>/dev/null || echo "false")
677
+
678
+ ISSUE_RETRY_COUNT=$(node -e "
679
+ const { loadActiveIssue } = require('./lib/state.cjs');
680
+ const state = loadActiveIssue(${ISSUE_NUMBER});
681
+ console.log((state && typeof state.retry_count === 'number') ? state.retry_count : 0);
682
+ " 2>/dev/null || echo "0")
683
+
684
+ FAILED_ISSUES_WITH_CLASS+=("${ISSUE_NUMBER}:${ISSUE_FAILURE_CLASS}")
685
+
626
686
  FAIL_BODY=$(cat <<COMMENTEOF
627
687
  > **MGW** · \`pipeline-failed\` · ${DONE_TIMESTAMP}
628
688
  > Milestone: ${MILESTONE_NAME} | Phase ${PHASE_NUM}: ${PHASE_NAME}
@@ -630,16 +690,22 @@ COMMENTEOF
630
690
  ### Pipeline Failed
631
691
 
632
692
  Issue #${ISSUE_NUMBER} did not produce a PR.
633
- Check the execution log for details.
693
+
694
+ | | |
695
+ |---|---|
696
+ | **Failure class** | \`${ISSUE_FAILURE_CLASS}\` |
697
+ | **Retries attempted** | ${ISSUE_RETRY_COUNT} of 3 |
698
+ | **Dead-lettered** | ${ISSUE_DEAD_LETTER} |
634
699
 
635
700
  Dependents of this issue will be skipped.
701
+ To retry after resolving root cause: \`/mgw:run ${ISSUE_NUMBER} --retry\`
636
702
  COMMENTEOF
637
703
  )
638
704
 
639
705
  gh issue comment ${ISSUE_NUMBER} --body "$FAIL_BODY" 2>/dev/null || true
640
706
  gh issue edit ${ISSUE_NUMBER} --add-label "pipeline-failed" 2>/dev/null || true
641
707
  gh label create "pipeline-failed" --description "Pipeline execution failed" --color "d73a4a" --force 2>/dev/null || true
642
- echo " ✗ #${ISSUE_NUMBER} — Failed (no PR created)"
708
+ echo " ✗ #${ISSUE_NUMBER} — Failed (class: ${ISSUE_FAILURE_CLASS}, no PR created)"
643
709
  fi
644
710
 
645
711
  # Update project.json checkpoint (MLST-05)
@@ -684,16 +750,20 @@ Every comment posted during milestone orchestration includes:
684
750
  <details>
685
751
  <summary>Milestone Progress ({done}/{total} complete)</summary>
686
752
 
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 |
753
+ | # | Issue | Status | PR | Failure Class | Label Drift |
754
+ |---|-------|--------|----|---------------|-------------|
755
+ | N | title | ✓ Done | #PR | | ok |
756
+ | M | title | ✗ Failed | — | `permanent` | — |
757
+ | K | title | ○ Pending | — | | — |
758
+ | J | title | ◆ Running | — | | ok |
759
+ | L | title | ⊘ Blocked | — | | — |
694
760
 
695
761
  </details>
696
762
  ```
763
+
764
+ The **Failure Class** column surfaces `last_failure_class` from the active issue state file.
765
+ The **Label Drift** column shows the result of post-subagent label reconciliation: `ok` (labels matched expected), `drift` (label mismatch detected — MGW WARNING logged), or `—` (not checked / issue not run).
766
+ Values: `transient` (retried and exhausted), `permanent` (unrecoverable), `needs-info` (ambiguous issue), `unknown` (no state file or pre-retry issue), `—` (not failed).
697
767
  </step>
698
768
 
699
769
  <step name="post_loop">
@@ -793,9 +863,18 @@ writeProjectState(state);
793
863
 
794
864
  5. Milestone mapping verification:
795
865
 
796
- After advancing to the next milestone, check its GSD linkage:
866
+ After advancing to the next milestone, check its GSD linkage using `getGsdState()`
867
+ from `lib/gsd-adapter.cjs` to read current GSD execution state (.planning/STATE.md
868
+ and ROADMAP.md) alongside the project.json milestone map:
797
869
 
798
870
  ```bash
871
+ # Read current GSD state from .planning/ via the adapter
872
+ GSD_STATE=$(node -e "
873
+ const { getGsdState } = require('./lib/gsd-adapter.cjs');
874
+ const state = getGsdState();
875
+ console.log(JSON.stringify(state));
876
+ " 2>/dev/null || echo "null")
877
+
799
878
  NEXT_MILESTONE_CHECK=$(node -e "
800
879
  const { loadProjectState, resolveActiveMilestoneIndex } = require('./lib/state.cjs');
801
880
  const state = loadProjectState();
@@ -906,6 +985,23 @@ Draft release created: ${RELEASE_TAG}
906
985
 
907
986
  **If some issues failed:**
908
987
 
988
+ Build failure class lookup from `FAILED_ISSUES_WITH_CLASS` array for display:
989
+ ```bash
990
+ # Build failure class map: { issue_number → failure_class }
991
+ FAILURE_CLASS_MAP=$(python3 -c "
992
+ import json, sys
993
+
994
+ entries = '${FAILED_ISSUES_WITH_CLASS[@]}'.split()
995
+ result = {}
996
+ for entry in entries:
997
+ if ':' in entry:
998
+ num, cls = entry.split(':', 1)
999
+ result[num] = cls
1000
+ print(json.dumps(result))
1001
+ " 2>/dev/null || echo "{}")
1002
+ ```
1003
+
1004
+ Display results table including failure_class for each failed issue:
909
1005
  ```
910
1006
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
911
1007
  MGW ► MILESTONE ${MILESTONE_NUM} INCOMPLETE
@@ -913,15 +1009,78 @@ Draft release created: ${RELEASE_TAG}
913
1009
 
914
1010
  ${MILESTONE_NAME}
915
1011
 
916
- | # | Issue | Status | PR |
917
- |---|-------|--------|----|
918
- ${results_table}
1012
+ | # | Issue | Status | PR | Failure Class |
1013
+ |---|-------|--------|----|---------------|
1014
+ ${results_table_with_failure_class}
919
1015
 
920
1016
  Completed: ${TOTAL_DONE}/${TOTAL_ISSUES}
921
1017
  Failed: ${TOTAL_FAILED}
922
1018
  Blocked: ${TOTAL_BLOCKED}
1019
+ ```
923
1020
 
924
- Milestone NOT closed. Resolve failures and re-run:
1021
+ For each failed issue, present recovery options:
1022
+ ```bash
1023
+ for ENTRY in "${FAILED_ISSUES_WITH_CLASS[@]}"; do
1024
+ FAIL_NUM=$(echo "$ENTRY" | cut -d':' -f1)
1025
+ FAIL_CLASS=$(echo "$ENTRY" | cut -d':' -f2)
1026
+
1027
+ echo ""
1028
+ echo " Failed: #${FAIL_NUM} (class: ${FAIL_CLASS})"
1029
+ AskUserQuestion(
1030
+ header: "Recovery — Issue #${FAIL_NUM}",
1031
+ question: "Issue #${FAIL_NUM} failed (failure class: ${FAIL_CLASS}). What would you like to do?",
1032
+ options: [
1033
+ {
1034
+ label: "Retry",
1035
+ description: "Reset retry state via resetRetryState() and re-run /mgw:run #${FAIL_NUM} --retry"
1036
+ },
1037
+ {
1038
+ label: "Skip",
1039
+ description: "Mark as skipped and continue to next issue (dependents will remain blocked)"
1040
+ },
1041
+ {
1042
+ label: "Abort",
1043
+ description: "Stop milestone recovery here"
1044
+ }
1045
+ ]
1046
+ )
1047
+
1048
+ case "$RECOVERY_CHOICE" in
1049
+ Retry)
1050
+ # Call resetRetryState() to clear retry_count, last_failure_class, dead_letter
1051
+ node -e "
1052
+ const { resetRetryState } = require('./lib/retry.cjs');
1053
+ const fs = require('fs'), path = require('path');
1054
+ const activeDir = path.join(process.cwd(), '.mgw', 'active');
1055
+ const files = fs.readdirSync(activeDir);
1056
+ const file = files.find(f => f.startsWith('${FAIL_NUM}-') && f.endsWith('.json'));
1057
+ if (!file) { console.error('No state file for #${FAIL_NUM}'); process.exit(1); }
1058
+ const filePath = path.join(activeDir, file);
1059
+ const state = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
1060
+ const reset = resetRetryState(state);
1061
+ reset.pipeline_stage = 'triaged';
1062
+ fs.writeFileSync(filePath, JSON.stringify(reset, null, 2));
1063
+ console.log('Retry state cleared for #${FAIL_NUM}');
1064
+ "
1065
+ # Remove pipeline-failed label before re-run
1066
+ gh issue edit ${FAIL_NUM} --remove-label "pipeline-failed" 2>/dev/null || true
1067
+ # Re-run the pipeline for this issue
1068
+ /mgw:run ${FAIL_NUM} --retry
1069
+ ;;
1070
+ Skip)
1071
+ echo " ⊘ #${FAIL_NUM} — Skipped (will not retry)"
1072
+ ;;
1073
+ Abort)
1074
+ echo "Milestone recovery aborted at #${FAIL_NUM}."
1075
+ break
1076
+ ;;
1077
+ esac
1078
+ done
1079
+ ```
1080
+
1081
+ After recovery loop:
1082
+ ```
1083
+ Milestone NOT closed. Re-run after resolving remaining failures:
925
1084
  /mgw:milestone ${MILESTONE_NUM}
926
1085
  ```
927
1086
 
@@ -943,7 +1102,13 @@ gh issue comment ${FIRST_ISSUE_NUMBER} --body "$FINAL_RESULTS_COMMENT"
943
1102
  - [ ] Sequential execution via /mgw:run Task() delegation (MLST-01)
944
1103
  - [ ] Per-issue checkpoint to project.json after completion (MLST-05)
945
1104
  - [ ] Failure handling: skip failed, label, comment, block dependents
1105
+ - [ ] failure_class surfaced in results table and failure comment for each failed issue
1106
+ - [ ] Retry option calls resetRetryState() then re-invokes /mgw:run --retry for failed issues
1107
+ - [ ] FAILED_ISSUES_WITH_CLASS tracks "number:class" for display in results table
946
1108
  - [ ] Progress table in every GitHub comment
1109
+ - [ ] Post-subagent label reconciliation run per issue after Task() returns
1110
+ - [ ] LABEL_DRIFT tracked per issue (ok/drift) and shown in progress table Label Drift column
1111
+ - [ ] Label drift issues logged as MGW WARNING to stderr
947
1112
  - [ ] Milestone close + draft release on full completion
948
1113
  - [ ] current_milestone pointer advanced on completion
949
1114
  - [ ] --interactive flag pauses between issues