@snipcodeit/mgw 0.1.1 → 0.1.2

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/commands/run.md CHANGED
@@ -57,7 +57,7 @@ REPO_ROOT=$(git rev-parse --show-toplevel)
57
57
  DEFAULT=$(gh repo view --json defaultBranchRef -q .defaultBranchRef.name)
58
58
  ```
59
59
 
60
- Parse $ARGUMENTS for issue number. If missing:
60
+ Parse $ARGUMENTS for issue number and flags. If issue number missing:
61
61
  ```
62
62
  AskUserQuestion(
63
63
  header: "Issue Number Required",
@@ -66,6 +66,16 @@ AskUserQuestion(
66
66
  )
67
67
  ```
68
68
 
69
+ Extract flags from $ARGUMENTS:
70
+ ```bash
71
+ RETRY_FLAG=false
72
+ for ARG in $ARGUMENTS; do
73
+ case "$ARG" in
74
+ --retry) RETRY_FLAG=true ;;
75
+ esac
76
+ done
77
+ ```
78
+
69
79
  Check for existing state: `${REPO_ROOT}/.mgw/active/${ISSUE_NUMBER}-*.json`
70
80
 
71
81
  If no state file exists → issue not triaged yet. Run triage inline:
@@ -73,11 +83,71 @@ If no state file exists → issue not triaged yet. Run triage inline:
73
83
  - Execute the mgw:issue triage flow (steps from issue.md) inline.
74
84
  - After triage, reload state file.
75
85
 
76
- If state file exists → load it. Check pipeline_stage:
86
+ If state file exists → load it. **Run migrateProjectState() to ensure retry fields exist:**
87
+ ```bash
88
+ node -e "
89
+ const { migrateProjectState } = require('./lib/state.cjs');
90
+ migrateProjectState();
91
+ " 2>/dev/null || true
92
+ ```
93
+
94
+ Check pipeline_stage:
77
95
  - "triaged" → proceed to GSD execution
78
96
  - "planning" / "executing" → resume from where we left off
79
97
  - "blocked" → "Pipeline for #${ISSUE_NUMBER} is blocked by a stakeholder comment. Review the issue comments, resolve the blocker, then re-run."
80
98
  - "pr-created" / "done" → "Pipeline already completed for #${ISSUE_NUMBER}. Run /mgw:sync to reconcile."
99
+ - "failed" → Check for --retry flag:
100
+ - If --retry NOT present:
101
+ ```
102
+ Pipeline for #${ISSUE_NUMBER} has failed (failure class: ${last_failure_class || "unknown"}).
103
+ dead_letter: ${dead_letter}
104
+
105
+ To retry: /mgw:run ${ISSUE_NUMBER} --retry
106
+ To inspect: /mgw:issue ${ISSUE_NUMBER}
107
+ ```
108
+ STOP.
109
+ - If --retry present and dead_letter === true:
110
+ ```bash
111
+ # Clear dead_letter and reset retry state via resetRetryState()
112
+ node -e "
113
+ const { loadActiveIssue } = require('./lib/state.cjs');
114
+ const { resetRetryState } = require('./lib/retry.cjs');
115
+ const fs = require('fs'), path = require('path');
116
+ const activeDir = path.join(process.cwd(), '.mgw', 'active');
117
+ const files = fs.readdirSync(activeDir);
118
+ const file = files.find(f => f.startsWith('${ISSUE_NUMBER}-') && f.endsWith('.json'));
119
+ if (!file) { console.error('No state file for #${ISSUE_NUMBER}'); process.exit(1); }
120
+ const filePath = path.join(activeDir, file);
121
+ const state = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
122
+ const reset = resetRetryState(state);
123
+ reset.pipeline_stage = 'triaged';
124
+ fs.writeFileSync(filePath, JSON.stringify(reset, null, 2));
125
+ console.log('Retry state cleared for #${ISSUE_NUMBER}');
126
+ "
127
+ # Remove pipeline-failed label
128
+ gh issue edit ${ISSUE_NUMBER} --remove-label "pipeline-failed" 2>/dev/null || true
129
+ ```
130
+ Log: "MGW: dead_letter cleared for #${ISSUE_NUMBER} via --retry flag. Re-queuing."
131
+ Continue pipeline (treat as triaged).
132
+ - If --retry present and dead_letter !== true (manual retry of non-dead-lettered failure):
133
+ ```bash
134
+ node -e "
135
+ const { resetRetryState } = require('./lib/retry.cjs');
136
+ const fs = require('fs'), path = require('path');
137
+ const activeDir = path.join(process.cwd(), '.mgw', 'active');
138
+ const files = fs.readdirSync(activeDir);
139
+ const file = files.find(f => f.startsWith('${ISSUE_NUMBER}-') && f.endsWith('.json'));
140
+ if (!file) { console.error('No state file'); process.exit(1); }
141
+ const filePath = path.join(activeDir, file);
142
+ const state = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
143
+ const reset = resetRetryState(state);
144
+ reset.pipeline_stage = 'triaged';
145
+ fs.writeFileSync(filePath, JSON.stringify(reset, null, 2));
146
+ console.log('Retry state reset for #${ISSUE_NUMBER}');
147
+ "
148
+ gh issue edit ${ISSUE_NUMBER} --remove-label "pipeline-failed" 2>/dev/null || true
149
+ ```
150
+ Continue pipeline.
81
151
  - "needs-info" → Check for --force flag in $ARGUMENTS:
82
152
  If --force NOT present:
83
153
  ```
@@ -107,6 +177,24 @@ If state file exists → load it. Check pipeline_stage:
107
177
  Update state: pipeline_stage = "triaged", add override_log entry.
108
178
  Continue pipeline.
109
179
 
180
+ **Route selection via gsd-adapter (runs after loading issue state):**
181
+
182
+ Use `selectGsdRoute()` from `lib/gsd-adapter.cjs` to determine the GSD execution
183
+ path. This centralizes the routing decision so it is auditable and consistent
184
+ across all pipeline commands:
185
+
186
+ ```bash
187
+ GSD_ROUTE=$(node -e "
188
+ const { selectGsdRoute } = require('./lib/gsd-adapter.cjs');
189
+ const issue = $(cat ${REPO_ROOT}/.mgw/active/${STATE_FILE});
190
+ const { loadProjectState } = require('./lib/state.cjs');
191
+ const projectState = loadProjectState() || {};
192
+ const route = selectGsdRoute(issue, projectState);
193
+ console.log(route);
194
+ ")
195
+ # GSD_ROUTE is one of: quick | plan-phase | diagnose | execute-only | verify-only
196
+ ```
197
+
110
198
  **Cross-milestone detection (runs after loading issue state):**
111
199
 
112
200
  Check if this issue belongs to a non-active GSD milestone:
@@ -465,7 +553,7 @@ SYSTEM_LIST="${triage.scope.systems}"
465
553
  FILE_LIST="${triage.scope.files}"
466
554
  CONFLICTS="${triage.conflicts}"
467
555
  ROUTE_REASONING="${triage.route_reasoning}"
468
- TIMESTAMP=$(node ~/.claude/get-shit-done/bin/gsd-tools.cjs current-timestamp --raw 2>/dev/null || date -u +"%Y-%m-%dT%H:%M:%SZ")
556
+ TIMESTAMP=$(node -e "try{process.stdout.write(require('./lib/gsd-adapter.cjs').getTimestamp())}catch(e){process.stdout.write(new Date().toISOString().replace(/\\.\\d{3}Z$/,'Z'))}")
469
557
 
470
558
  # Load milestone/phase context from project.json if available
471
559
  MILESTONE_CONTEXT=""
@@ -524,6 +612,24 @@ Log comment in state file (at `${REPO_ROOT}/.mgw/active/`).
524
612
 
525
613
  Only run this step if gsd_route is "gsd:quick" or "gsd:quick --full".
526
614
 
615
+ **Retry loop initialization:**
616
+ ```bash
617
+ # Load retry state from .mgw/active/ state file
618
+ RETRY_COUNT=$(node -e "
619
+ const { loadActiveIssue } = require('./lib/state.cjs');
620
+ const state = loadActiveIssue(${ISSUE_NUMBER});
621
+ console.log((state && typeof state.retry_count === 'number') ? state.retry_count : 0);
622
+ " 2>/dev/null || echo "0")
623
+ EXECUTION_SUCCEEDED=false
624
+ ```
625
+
626
+ **Begin retry loop** — wraps the GSD quick execution (steps 1–11 below) with transient-failure retry:
627
+
628
+ ```
629
+ RETRY_LOOP:
630
+ while canRetry(issue_state) AND NOT EXECUTION_SUCCEEDED:
631
+ ```
632
+
527
633
  Update pipeline_stage to "executing" in state file (at `${REPO_ROOT}/.mgw/active/`).
528
634
 
529
635
  Determine flags:
@@ -725,6 +831,77 @@ node ~/.claude/get-shit-done/bin/gsd-tools.cjs commit "docs(quick-${next_num}):
725
831
  ```
726
832
 
727
833
  Update state (at `${REPO_ROOT}/.mgw/active/`): gsd_artifacts.path = $QUICK_DIR, pipeline_stage = "verifying".
834
+
835
+ **Retry loop — on execution failure:**
836
+
837
+ If any step above fails (executor or verifier agent returns error, summary missing, etc.), capture the error and apply retry logic:
838
+
839
+ ```bash
840
+ # On failure — classify and decide whether to retry
841
+ FAILURE_CLASS=$(node -e "
842
+ const { classifyFailure, canRetry, incrementRetry, getBackoffMs } = require('./lib/retry.cjs');
843
+ const { loadActiveIssue } = require('./lib/state.cjs');
844
+ const fs = require('fs'), path = require('path');
845
+
846
+ const activeDir = path.join(process.cwd(), '.mgw', 'active');
847
+ const files = fs.readdirSync(activeDir);
848
+ const file = files.find(f => f.startsWith('${ISSUE_NUMBER}-') && f.endsWith('.json'));
849
+ const filePath = path.join(activeDir, file);
850
+ let issueState = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
851
+
852
+ // Classify the failure from the error context
853
+ const error = { message: '${EXECUTION_ERROR_MESSAGE}' };
854
+ const result = classifyFailure(error);
855
+ console.error('Failure classified as: ' + result.class + ' — ' + result.reason);
856
+
857
+ // Persist failure class to state
858
+ issueState.last_failure_class = result.class;
859
+
860
+ if (result.class === 'transient' && canRetry(issueState)) {
861
+ const backoff = getBackoffMs(issueState.retry_count || 0);
862
+ issueState = incrementRetry(issueState);
863
+ fs.writeFileSync(filePath, JSON.stringify(issueState, null, 2));
864
+ // Output: backoff ms so shell can sleep
865
+ console.log('retry:' + backoff + ':' + result.class);
866
+ } else {
867
+ // Permanent failure or retries exhausted — dead-letter
868
+ issueState.dead_letter = true;
869
+ fs.writeFileSync(filePath, JSON.stringify(issueState, null, 2));
870
+ console.log('dead_letter:' + result.class);
871
+ }
872
+ ")
873
+
874
+ case "$FAILURE_CLASS" in
875
+ retry:*)
876
+ BACKOFF_MS=$(echo "$FAILURE_CLASS" | cut -d':' -f2)
877
+ BACKOFF_SEC=$(( (BACKOFF_MS + 999) / 1000 ))
878
+ echo "MGW: Transient failure detected — retrying in ${BACKOFF_SEC}s (retry ${RETRY_COUNT})..."
879
+ sleep "$BACKOFF_SEC"
880
+ RETRY_COUNT=$((RETRY_COUNT + 1))
881
+ # Loop back to retry
882
+ ;;
883
+ dead_letter:*)
884
+ FAILURE_CLASS_NAME=$(echo "$FAILURE_CLASS" | cut -d':' -f2)
885
+ EXECUTION_SUCCEEDED=false
886
+ # Break out of retry loop — handled in post_execution_update
887
+ break
888
+ ;;
889
+ esac
890
+ ```
891
+
892
+ On successful execution (EXECUTION_SUCCEEDED=true): break out of retry loop, clear last_failure_class:
893
+ ```bash
894
+ node -e "
895
+ const fs = require('fs'), path = require('path');
896
+ const activeDir = path.join(process.cwd(), '.mgw', 'active');
897
+ const files = fs.readdirSync(activeDir);
898
+ const file = files.find(f => f.startsWith('${ISSUE_NUMBER}-') && f.endsWith('.json'));
899
+ const filePath = path.join(activeDir, file);
900
+ const state = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
901
+ state.last_failure_class = null;
902
+ fs.writeFileSync(filePath, JSON.stringify(state, null, 2));
903
+ " 2>/dev/null || true
904
+ ```
728
905
  </step>
729
906
 
730
907
  <step name="execute_gsd_milestone">
@@ -732,13 +909,25 @@ Update state (at `${REPO_ROOT}/.mgw/active/`): gsd_artifacts.path = $QUICK_DIR,
732
909
 
733
910
  Only run this step if gsd_route is "gsd:new-milestone".
734
911
 
912
+ **Retry loop initialization** (same pattern as execute_gsd_quick):
913
+ ```bash
914
+ RETRY_COUNT=$(node -e "
915
+ const { loadActiveIssue } = require('./lib/state.cjs');
916
+ const state = loadActiveIssue(${ISSUE_NUMBER});
917
+ console.log((state && typeof state.retry_count === 'number') ? state.retry_count : 0);
918
+ " 2>/dev/null || echo "0")
919
+ EXECUTION_SUCCEEDED=false
920
+ ```
921
+
922
+ **Begin retry loop** — wraps the phase-execution loop (steps 2b–2e below) with transient-failure retry. Step 2 (milestone roadmap creation) is NOT wrapped in the retry loop — roadmap creation failures are always treated as permanent (require human intervention).
923
+
735
924
  This is the most complex path. The orchestrator needs to:
736
925
 
737
926
  **Resolve models for milestone agents:**
738
927
  ```bash
739
- PLANNER_MODEL=$(node ~/.claude/get-shit-done/bin/gsd-tools.cjs resolve-model gsd-planner --raw)
740
- EXECUTOR_MODEL=$(node ~/.claude/get-shit-done/bin/gsd-tools.cjs resolve-model gsd-executor --raw)
741
- VERIFIER_MODEL=$(node ~/.claude/get-shit-done/bin/gsd-tools.cjs resolve-model gsd-verifier --raw)
928
+ PLANNER_MODEL=$(node -e "process.stdout.write(require('./lib/gsd-adapter.cjs').resolveModel('gsd-planner'))")
929
+ EXECUTOR_MODEL=$(node -e "process.stdout.write(require('./lib/gsd-adapter.cjs').resolveModel('gsd-executor'))")
930
+ VERIFIER_MODEL=$(node -e "process.stdout.write(require('./lib/gsd-adapter.cjs').resolveModel('gsd-verifier'))")
742
931
  ```
743
932
 
744
933
  1. **Discussion phase trigger for large-scope issues:**
@@ -747,7 +936,7 @@ If the issue was triaged with large scope and `gsd_route == "gsd:new-milestone"`
747
936
  a scope proposal comment and set the discussing stage before proceeding to phase execution:
748
937
 
749
938
  ```bash
750
- DISCUSS_TIMESTAMP=$(node ~/.claude/get-shit-done/bin/gsd-tools.cjs current-timestamp --raw 2>/dev/null || date -u +"%Y-%m-%dT%H:%M:%SZ")
939
+ DISCUSS_TIMESTAMP=$(node -e "try{process.stdout.write(require('./lib/gsd-adapter.cjs').getTimestamp())}catch(e){process.stdout.write(new Date().toISOString().replace(/\\.\\d{3}Z$/,'Z'))}")
751
940
 
752
941
  # Build scope breakdown from triage data
753
942
  SCOPE_SIZE="${triage.scope.size}"
@@ -970,19 +1159,99 @@ COMMENTEOF
970
1159
  gh issue comment ${ISSUE_NUMBER} --body "$PHASE_BODY" 2>/dev/null || true
971
1160
  ```
972
1161
 
1162
+ **Retry loop — on phase execution failure** (apply same pattern as execute_gsd_quick):
1163
+
1164
+ If a phase's executor or verifier fails, capture the error and apply retry logic via `classifyFailure()`, `canRetry()`, `incrementRetry()`, and `getBackoffMs()` from `lib/retry.cjs`. Only the failing phase is retried (restart from step 2b for that phase). If the failure is transient and `canRetry()` is true: sleep backoff, call `incrementRetry()`, loop. If permanent or retries exhausted: set `dead_letter = true`, set `last_failure_class`, break the retry loop.
1165
+
1166
+ On successful completion of all phases: clear `last_failure_class`, set `EXECUTION_SUCCEEDED=true`.
1167
+
973
1168
  After ALL phases complete → update pipeline_stage to "verifying" (at `${REPO_ROOT}/.mgw/active/`).
974
1169
  </step>
975
1170
 
976
1171
  <step name="post_execution_update">
977
- **Post execution-complete comment on issue:**
1172
+ **Post execution-complete comment on issue (or failure comment if dead_letter):**
1173
+
1174
+ Read `dead_letter` and `last_failure_class` from current issue state:
1175
+ ```bash
1176
+ DEAD_LETTER=$(node -e "
1177
+ const { loadActiveIssue } = require('./lib/state.cjs');
1178
+ const state = loadActiveIssue(${ISSUE_NUMBER});
1179
+ console.log(state && state.dead_letter === true ? 'true' : 'false');
1180
+ " 2>/dev/null || echo "false")
1181
+
1182
+ LAST_FAILURE_CLASS=$(node -e "
1183
+ const { loadActiveIssue } = require('./lib/state.cjs');
1184
+ const state = loadActiveIssue(${ISSUE_NUMBER});
1185
+ console.log((state && state.last_failure_class) ? state.last_failure_class : 'unknown');
1186
+ " 2>/dev/null || echo "unknown")
1187
+ ```
1188
+
1189
+ **If dead_letter === true — post failure comment and halt:**
1190
+ ```bash
1191
+ if [ "$DEAD_LETTER" = "true" ]; then
1192
+ FAIL_TIMESTAMP=$(node ~/.claude/get-shit-done/bin/gsd-tools.cjs current-timestamp --raw 2>/dev/null || date -u +"%Y-%m-%dT%H:%M:%SZ")
1193
+ RETRY_COUNT_CURRENT=$(node -e "
1194
+ const { loadActiveIssue } = require('./lib/state.cjs');
1195
+ const state = loadActiveIssue(${ISSUE_NUMBER});
1196
+ console.log((state && typeof state.retry_count === 'number') ? state.retry_count : 0);
1197
+ " 2>/dev/null || echo "0")
1198
+
1199
+ FAIL_BODY=$(cat <<COMMENTEOF
1200
+ > **MGW** · \`pipeline-failed\` · ${FAIL_TIMESTAMP}
1201
+ > ${MILESTONE_CONTEXT}
1202
+
1203
+ ### Pipeline Failed
1204
+
1205
+ Issue #${ISSUE_NUMBER} — ${issue_title}
1206
+
1207
+ | | |
1208
+ |---|---|
1209
+ | **Failure class** | \`${LAST_FAILURE_CLASS}\` |
1210
+ | **Retries attempted** | ${RETRY_COUNT_CURRENT} of 3 |
1211
+ | **Status** | Dead-lettered — requires human intervention |
1212
+
1213
+ **Failure class meaning:**
1214
+ - \`transient\` — retry exhausted (rate limit, network, or overload)
1215
+ - \`permanent\` — unrecoverable (auth, missing deps, bad config)
1216
+ - \`needs-info\` — issue is ambiguous or incomplete
1217
+
1218
+ **To retry after resolving root cause:**
1219
+ \`\`\`
1220
+ /mgw:run ${ISSUE_NUMBER} --retry
1221
+ \`\`\`
1222
+ COMMENTEOF
1223
+ )
1224
+
1225
+ gh issue comment ${ISSUE_NUMBER} --body "$FAIL_BODY" 2>/dev/null || true
1226
+ gh issue edit ${ISSUE_NUMBER} --add-label "pipeline-failed" 2>/dev/null || true
1227
+ gh label create "pipeline-failed" --description "Pipeline execution failed" --color "d73a4a" --force 2>/dev/null || true
1228
+
1229
+ # Update pipeline_stage to failed
1230
+ node -e "
1231
+ const fs = require('fs'), path = require('path');
1232
+ const activeDir = path.join(process.cwd(), '.mgw', 'active');
1233
+ const files = fs.readdirSync(activeDir);
1234
+ const file = files.find(f => f.startsWith('${ISSUE_NUMBER}-') && f.endsWith('.json'));
1235
+ const filePath = path.join(activeDir, file);
1236
+ const state = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
1237
+ state.pipeline_stage = 'failed';
1238
+ fs.writeFileSync(filePath, JSON.stringify(state, null, 2));
1239
+ " 2>/dev/null || true
1240
+
1241
+ echo "MGW: Pipeline dead-lettered for #${ISSUE_NUMBER} (class: ${LAST_FAILURE_CLASS}). Use --retry after fixing root cause."
1242
+ exit 1
1243
+ fi
1244
+ ```
1245
+
1246
+ **Otherwise — post execution-complete comment:**
978
1247
 
979
- After GSD execution completes, post a structured update before creating the PR:
1248
+ After GSD execution completes successfully, post a structured update before creating the PR:
980
1249
 
981
1250
  ```bash
982
1251
  COMMIT_COUNT=$(git rev-list ${DEFAULT_BRANCH}..HEAD --count 2>/dev/null || echo "0")
983
1252
  TEST_STATUS=$(npm test 2>&1 >/dev/null && echo "passing" || echo "failing")
984
1253
  FILE_CHANGES=$(git diff --stat ${DEFAULT_BRANCH}..HEAD 2>/dev/null | tail -1)
985
- EXEC_TIMESTAMP=$(node ~/.claude/get-shit-done/bin/gsd-tools.cjs current-timestamp --raw 2>/dev/null || date -u +"%Y-%m-%dT%H:%M:%SZ")
1254
+ EXEC_TIMESTAMP=$(node -e "try{process.stdout.write(require('./lib/gsd-adapter.cjs').getTimestamp())}catch(e){process.stdout.write(new Date().toISOString().replace(/\\.\\d{3}Z$/,'Z'))}")
986
1255
  ```
987
1256
 
988
1257
  Post the execution-complete comment directly (no sub-agent — guarantees it happens):
@@ -1205,7 +1474,7 @@ ONE_LINER=$(node ~/.claude/get-shit-done/bin/gsd-tools.cjs summary-extract "${gs
1205
1474
  Post structured PR-ready comment directly (no sub-agent — guarantees it happens):
1206
1475
 
1207
1476
  ```bash
1208
- DONE_TIMESTAMP=$(node ~/.claude/get-shit-done/bin/gsd-tools.cjs current-timestamp --raw 2>/dev/null || date -u +"%Y-%m-%dT%H:%M:%SZ")
1477
+ DONE_TIMESTAMP=$(node -e "try{process.stdout.write(require('./lib/gsd-adapter.cjs').getTimestamp())}catch(e){process.stdout.write(new Date().toISOString().replace(/\\.\\d{3}Z$/,'Z'))}")
1209
1478
 
1210
1479
  PR_READY_BODY=$(cat <<COMMENTEOF
1211
1480
  > **MGW** · \`pr-ready\` · ${DONE_TIMESTAMP}
@@ -1265,12 +1534,17 @@ Next:
1265
1534
  - [ ] Issue number validated and state loaded (or triage run first)
1266
1535
  - [ ] Pipeline refuses needs-info without --force
1267
1536
  - [ ] Pipeline refuses needs-security-review without --security-ack
1537
+ - [ ] --retry flag clears dead_letter state, removes pipeline-failed label, and re-queues issue
1538
+ - [ ] migrateProjectState() called at load time to ensure retry fields exist on active issue files
1268
1539
  - [ ] Isolated worktree created (.worktrees/ gitignored)
1269
1540
  - [ ] mgw:in-progress label applied during execution
1270
1541
  - [ ] Pre-flight comment check performed (new comments classified before execution)
1271
1542
  - [ ] mgw:blocked label applied when blocking comments detected
1272
1543
  - [ ] Work-starting comment posted on issue (route, scope, branch)
1273
1544
  - [ ] GSD pipeline executed in worktree (quick or milestone route)
1545
+ - [ ] Transient execution failures retried up to 3 times with exponential backoff
1546
+ - [ ] Failure comment includes failure_class from classifyFailure()
1547
+ - [ ] dead_letter=true set when retries exhausted or failure is permanent
1274
1548
  - [ ] New-milestone route triggers discussion phase with mgw:discussing label
1275
1549
  - [ ] Execution-complete comment posted on issue (commits, changes, test status)
1276
1550
  - [ ] PR created with summary, milestone context, testing procedures, cross-refs