@snipcodeit/mgw 0.2.2 → 0.4.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,510 @@
1
+ ---
2
+ name: mgw:run/triage
3
+ description: Validate input, load state, preflight comment check, and post triage update
4
+ ---
5
+
6
+ <step name="validate_and_load">
7
+ **Validate input and load state:**
8
+
9
+ Store repo root and default branch (used throughout):
10
+ ```bash
11
+ REPO_ROOT=$(git rev-parse --show-toplevel)
12
+ DEFAULT=$(gh repo view --json defaultBranchRef -q .defaultBranchRef.name)
13
+ ```
14
+
15
+ Parse $ARGUMENTS for issue number and flags. If issue number missing:
16
+ ```
17
+ AskUserQuestion(
18
+ header: "Issue Number Required",
19
+ question: "Which issue number do you want to run the pipeline for?",
20
+ followUp: null
21
+ )
22
+ ```
23
+
24
+ Extract flags from $ARGUMENTS:
25
+ ```bash
26
+ RETRY_FLAG=false
27
+ for ARG in $ARGUMENTS; do
28
+ case "$ARG" in
29
+ --retry) RETRY_FLAG=true ;;
30
+ esac
31
+ done
32
+ ```
33
+
34
+ Check for existing state: `${REPO_ROOT}/.mgw/active/${ISSUE_NUMBER}-*.json`
35
+
36
+ If no state file exists → issue not triaged yet. Run triage inline:
37
+ - Inform user: "Issue #${ISSUE_NUMBER} hasn't been triaged. Running triage first."
38
+ - Execute the mgw:issue triage flow (steps from issue.md) inline.
39
+ - After triage, reload state file.
40
+
41
+ If state file exists → load it. **Run migrateProjectState() to ensure retry fields exist:**
42
+ ```bash
43
+ node -e "
44
+ const { migrateProjectState } = require('./lib/state.cjs');
45
+ migrateProjectState();
46
+ " 2>/dev/null || true
47
+ ```
48
+
49
+ Check pipeline_stage:
50
+ - "triaged" → proceed to GSD execution
51
+ - "planning" / "executing" → resume from where we left off
52
+ - "blocked" → "Pipeline for #${ISSUE_NUMBER} is blocked by a stakeholder comment. Review the issue comments, resolve the blocker, then re-run."
53
+ - "pr-created" / "done" → "Pipeline already completed for #${ISSUE_NUMBER}. Run /mgw:sync to reconcile."
54
+ - "failed" → Check for --retry flag:
55
+ - If --retry NOT present:
56
+ ```
57
+ Pipeline for #${ISSUE_NUMBER} has failed (failure class: ${last_failure_class || "unknown"}).
58
+ dead_letter: ${dead_letter}
59
+
60
+ To retry: /mgw:run ${ISSUE_NUMBER} --retry
61
+ To inspect: /mgw:issue ${ISSUE_NUMBER}
62
+ ```
63
+ STOP.
64
+ - If --retry present and dead_letter === true:
65
+ ```bash
66
+ # Clear dead_letter and reset retry state via resetRetryState()
67
+ node -e "
68
+ const { loadActiveIssue } = require('./lib/state.cjs');
69
+ const { resetRetryState } = require('./lib/retry.cjs');
70
+ const fs = require('fs'), path = require('path');
71
+ const activeDir = path.join(process.cwd(), '.mgw', 'active');
72
+ const files = fs.readdirSync(activeDir);
73
+ const file = files.find(f => f.startsWith('${ISSUE_NUMBER}-') && f.endsWith('.json'));
74
+ if (!file) { console.error('No state file for #${ISSUE_NUMBER}'); process.exit(1); }
75
+ const filePath = path.join(activeDir, file);
76
+ const state = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
77
+ const reset = resetRetryState(state);
78
+ reset.pipeline_stage = 'triaged';
79
+ fs.writeFileSync(filePath, JSON.stringify(reset, null, 2));
80
+ console.log('Retry state cleared for #${ISSUE_NUMBER}');
81
+ "
82
+ # Remove pipeline-failed label
83
+ gh issue edit ${ISSUE_NUMBER} --remove-label "pipeline-failed" 2>/dev/null || true
84
+ ```
85
+ Log: "MGW: dead_letter cleared for #${ISSUE_NUMBER} via --retry flag. Re-queuing."
86
+ Continue pipeline (treat as triaged).
87
+ - If --retry present and dead_letter !== true (manual retry of non-dead-lettered failure):
88
+ ```bash
89
+ node -e "
90
+ const { resetRetryState } = require('./lib/retry.cjs');
91
+ const fs = require('fs'), path = require('path');
92
+ const activeDir = path.join(process.cwd(), '.mgw', 'active');
93
+ const files = fs.readdirSync(activeDir);
94
+ const file = files.find(f => f.startsWith('${ISSUE_NUMBER}-') && f.endsWith('.json'));
95
+ if (!file) { console.error('No state file'); process.exit(1); }
96
+ const filePath = path.join(activeDir, file);
97
+ const state = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
98
+ const reset = resetRetryState(state);
99
+ reset.pipeline_stage = 'triaged';
100
+ fs.writeFileSync(filePath, JSON.stringify(reset, null, 2));
101
+ console.log('Retry state reset for #${ISSUE_NUMBER}');
102
+ "
103
+ gh issue edit ${ISSUE_NUMBER} --remove-label "pipeline-failed" 2>/dev/null || true
104
+ ```
105
+ Continue pipeline.
106
+ - "needs-info" → Check for --force flag in $ARGUMENTS:
107
+ If --force NOT present:
108
+ ```
109
+ Pipeline for #${ISSUE_NUMBER} is blocked by triage gate (needs-info).
110
+ The issue requires more detail before execution can begin.
111
+
112
+ To override: /mgw:run ${ISSUE_NUMBER} --force
113
+ To review: /mgw:issue ${ISSUE_NUMBER} (re-triage after updating the issue)
114
+ ```
115
+ STOP.
116
+ If --force present:
117
+ Log warning: "MGW: WARNING — Overriding needs-info gate for #${ISSUE_NUMBER}. Proceeding with --force."
118
+ Update state: pipeline_stage = "triaged", add override_log entry.
119
+ Continue pipeline.
120
+ - "needs-security-review" → Check for --security-ack flag in $ARGUMENTS:
121
+ If --security-ack NOT present:
122
+ ```
123
+ Pipeline for #${ISSUE_NUMBER} requires security review.
124
+ This issue was flagged as high security risk during triage.
125
+
126
+ To acknowledge and proceed: /mgw:run ${ISSUE_NUMBER} --security-ack
127
+ To review: /mgw:issue ${ISSUE_NUMBER} (re-triage)
128
+ ```
129
+ STOP.
130
+ If --security-ack present:
131
+ Log warning: "MGW: WARNING — Acknowledging security risk for #${ISSUE_NUMBER}. Proceeding with --security-ack."
132
+ Update state: pipeline_stage = "triaged", add override_log entry.
133
+ Continue pipeline.
134
+
135
+ **Route selection via gsd-adapter (runs after loading issue state):**
136
+
137
+ Use `selectGsdRoute()` from `lib/gsd-adapter.cjs` to determine the GSD execution
138
+ path. This centralizes the routing decision so it is auditable and consistent
139
+ across all pipeline commands:
140
+
141
+ ```bash
142
+ GSD_ROUTE=$(node -e "
143
+ const { selectGsdRoute } = require('./lib/gsd-adapter.cjs');
144
+ const issue = $(cat ${REPO_ROOT}/.mgw/active/${STATE_FILE});
145
+ const { loadProjectState } = require('./lib/state.cjs');
146
+ const projectState = loadProjectState() || {};
147
+ const route = selectGsdRoute(issue, projectState);
148
+ console.log(route);
149
+ ")
150
+ # GSD_ROUTE is one of: quick | plan-phase | diagnose | execute-only | verify-only
151
+ ```
152
+
153
+ **Cross-milestone detection (runs after loading issue state):**
154
+
155
+ Check if this issue belongs to a non-active GSD milestone:
156
+
157
+ ```bash
158
+ CROSS_MILESTONE_WARN=$(node -e "
159
+ const { loadProjectState, resolveActiveMilestoneIndex } = require('./lib/state.cjs');
160
+ const state = loadProjectState();
161
+ if (!state) { console.log('none'); process.exit(0); }
162
+
163
+ const activeGsdId = state.active_gsd_milestone;
164
+
165
+ // Find this issue's milestone in project.json
166
+ const issueNum = ${ISSUE_NUMBER};
167
+ let issueMilestone = null;
168
+ for (const m of (state.milestones || [])) {
169
+ if ((m.issues || []).some(i => i.github_number === issueNum)) {
170
+ issueMilestone = m;
171
+ break;
172
+ }
173
+ }
174
+
175
+ if (!issueMilestone) { console.log('none'); process.exit(0); }
176
+
177
+ const issueGsdId = issueMilestone.gsd_milestone_id;
178
+
179
+ // No active_gsd_milestone set (legacy schema): no warning
180
+ if (!activeGsdId) { console.log('none'); process.exit(0); }
181
+
182
+ // Issue is in the active milestone: no warning
183
+ if (issueGsdId === activeGsdId) { console.log('none'); process.exit(0); }
184
+
185
+ // Issue is in a different milestone
186
+ const gsdRoute = '${GSD_ROUTE}';
187
+ if (gsdRoute === 'quick' || gsdRoute === 'gsd:quick') {
188
+ console.log('isolation:' + issueMilestone.name + ':' + (issueGsdId || 'unlinked'));
189
+ } else {
190
+ console.log('warn:' + issueMilestone.name + ':' + (issueGsdId || 'unlinked') + ':' + activeGsdId);
191
+ }
192
+ ")
193
+
194
+ case "$CROSS_MILESTONE_WARN" in
195
+ none)
196
+ # No cross-milestone issue — proceed normally
197
+ ;;
198
+ isolation:*)
199
+ MILESTONE_NAME=$(echo "$CROSS_MILESTONE_WARN" | cut -d':' -f2)
200
+ GSD_ID=$(echo "$CROSS_MILESTONE_WARN" | cut -d':' -f3)
201
+
202
+ # Re-validate route against live GitHub labels (project.json may be stale from triage time)
203
+ LIVE_LABELS=$(gh issue view ${ISSUE_NUMBER} --json labels --jq '[.labels[].name] | join(",")' 2>/dev/null || echo "")
204
+ QUICK_CONFIRMED=false
205
+ if echo "$LIVE_LABELS" | grep -qiE "gsd-route:quick|gsd:quick|quick"; then
206
+ QUICK_CONFIRMED=true
207
+ fi
208
+
209
+ if [ "$QUICK_CONFIRMED" = "true" ]; then
210
+ echo ""
211
+ echo "NOTE: Issue #${ISSUE_NUMBER} belongs to milestone '${MILESTONE_NAME}' (GSD: ${GSD_ID})"
212
+ echo " Confirmed gsd:quick via live labels — running in isolation."
213
+ echo ""
214
+ else
215
+ # Route mismatch: project.json says quick but labels don't confirm it
216
+ echo ""
217
+ echo "⚠️ Route mismatch for cross-milestone issue #${ISSUE_NUMBER}:"
218
+ echo " project.json route: quick (set at triage time)"
219
+ echo " Live GitHub labels: ${LIVE_LABELS:-none}"
220
+ echo " Labels do not confirm gsd:quick — treating as plan-phase (requires milestone context)."
221
+ echo ""
222
+ echo "Options:"
223
+ echo " 1) Switch active milestone to '${GSD_ID}' and continue"
224
+ echo " 2) Re-triage this issue (/mgw:issue ${ISSUE_NUMBER}) to update its route"
225
+ echo " 3) Abort"
226
+ echo ""
227
+ read -p "Choice [1/2/3]: " ROUTE_MISMATCH_CHOICE
228
+ case "$ROUTE_MISMATCH_CHOICE" in
229
+ 1)
230
+ node -e "
231
+ const { loadProjectState, writeProjectState } = require('./lib/state.cjs');
232
+ const state = loadProjectState();
233
+ state.active_gsd_milestone = '${GSD_ID}';
234
+ writeProjectState(state);
235
+ console.log('Switched active_gsd_milestone to: ${GSD_ID}');
236
+ "
237
+ # Validate ROADMAP.md matches (same check as option 1 in warn case)
238
+ ROADMAP_VALID=$(python3 -c "
239
+ import os
240
+ if not os.path.exists('.planning/ROADMAP.md'):
241
+ print('missing')
242
+ else:
243
+ with open('.planning/ROADMAP.md') as f:
244
+ content = f.read()
245
+ print('match' if '${GSD_ID}' in content else 'mismatch')
246
+ " 2>/dev/null || echo "missing")
247
+ if [ "$ROADMAP_VALID" != "match" ]; then
248
+ node -e "
249
+ const { loadProjectState, writeProjectState } = require('./lib/state.cjs');
250
+ const state = loadProjectState();
251
+ state.active_gsd_milestone = '$(echo "$CROSS_MILESTONE_WARN" | cut -d':' -f4)';
252
+ writeProjectState(state);
253
+ " 2>/dev/null || true
254
+ echo "Switch rolled back — ROADMAP.md does not match '${GSD_ID}'."
255
+ echo "Run /gsd:new-milestone to update ROADMAP.md first."
256
+ exit 0
257
+ fi
258
+ ;;
259
+ 2)
260
+ echo "Re-triage with: /mgw:issue ${ISSUE_NUMBER}"
261
+ exit 0
262
+ ;;
263
+ *)
264
+ echo "Aborted."
265
+ exit 0
266
+ ;;
267
+ esac
268
+ fi
269
+ ;;
270
+ warn:*)
271
+ ISSUE_MILESTONE=$(echo "$CROSS_MILESTONE_WARN" | cut -d':' -f2)
272
+ ISSUE_GSD=$(echo "$CROSS_MILESTONE_WARN" | cut -d':' -f3)
273
+ ACTIVE_GSD=$(echo "$CROSS_MILESTONE_WARN" | cut -d':' -f4)
274
+ echo ""
275
+ echo "⚠️ Cross-milestone issue detected:"
276
+ echo " Issue #${ISSUE_NUMBER} belongs to: '${ISSUE_MILESTONE}' (GSD: ${ISSUE_GSD})"
277
+ echo " Active GSD milestone: ${ACTIVE_GSD}"
278
+ echo ""
279
+ echo "This issue requires plan-phase work that depends on ROADMAP.md context."
280
+ echo "Running it against the wrong active milestone may produce incorrect plans."
281
+ echo ""
282
+ echo "Options:"
283
+ echo " 1) Switch active milestone to '${ISSUE_GSD}' and continue"
284
+ echo " 2) Continue anyway (not recommended)"
285
+ echo " 3) Abort — run /gsd:new-milestone to set up the correct milestone first"
286
+ echo ""
287
+ read -p "Choice [1/2/3]: " MILESTONE_CHOICE
288
+ case "$MILESTONE_CHOICE" in
289
+ 1)
290
+ node -e "
291
+ const { loadProjectState, writeProjectState } = require('./lib/state.cjs');
292
+ const state = loadProjectState();
293
+ state.active_gsd_milestone = '${ISSUE_GSD}';
294
+ writeProjectState(state);
295
+ console.log('Switched active_gsd_milestone to: ${ISSUE_GSD}');
296
+ "
297
+ # Validate ROADMAP.md matches the new active milestone
298
+ ROADMAP_VALID=$(python3 -c "
299
+ import os
300
+ if not os.path.exists('.planning/ROADMAP.md'):
301
+ print('missing')
302
+ else:
303
+ with open('.planning/ROADMAP.md') as f:
304
+ content = f.read()
305
+ print('match' if '${ISSUE_GSD}' in content else 'mismatch')
306
+ " 2>/dev/null || echo "missing")
307
+ if [ "$ROADMAP_VALID" = "match" ]; then
308
+ echo "Active milestone updated. ROADMAP.md confirmed for '${ISSUE_GSD}'."
309
+ else
310
+ # Roll back — ROADMAP.md doesn't match
311
+ node -e "
312
+ const { loadProjectState, writeProjectState } = require('./lib/state.cjs');
313
+ const state = loadProjectState();
314
+ state.active_gsd_milestone = '${ACTIVE_GSD}';
315
+ writeProjectState(state);
316
+ " 2>/dev/null || true
317
+ echo "Switch rolled back — ROADMAP.md does not match '${ISSUE_GSD}'."
318
+ echo "Run /gsd:new-milestone to update ROADMAP.md first."
319
+ exit 0
320
+ fi
321
+ ;;
322
+ 2)
323
+ echo "Proceeding with cross-milestone issue (may affect plan quality)."
324
+ ;;
325
+ *)
326
+ echo "Aborted. Run /gsd:new-milestone then /mgw:project to align milestones."
327
+ exit 0
328
+ ;;
329
+ esac
330
+ ;;
331
+ esac
332
+ ```
333
+ </step>
334
+
335
+ <step name="preflight_comment_check">
336
+ **Pre-flight comment check — detect new comments since triage:**
337
+
338
+ Before GSD execution begins, check if new comments have been posted on the issue
339
+ since triage. This prevents executing against a stale plan when stakeholders have
340
+ posted material changes, blockers, or scope updates.
341
+
342
+ ```bash
343
+ # Fetch current comment count from GitHub
344
+ CURRENT_COMMENTS=$(gh issue view $ISSUE_NUMBER --json comments --jq '.comments | length' 2>/dev/null || echo "0")
345
+ STORED_COMMENTS="${triage.last_comment_count}" # From state file
346
+
347
+ # If stored count is missing (pre-comment-tracking state), skip check
348
+ if [ -z "$STORED_COMMENTS" ] || [ "$STORED_COMMENTS" = "null" ] || [ "$STORED_COMMENTS" = "0" ]; then
349
+ STORED_COMMENTS=0
350
+ fi
351
+ ```
352
+
353
+ If new comments detected (`CURRENT_COMMENTS > STORED_COMMENTS`):
354
+
355
+ 1. **Fetch new comment bodies:**
356
+ ```bash
357
+ NEW_COUNT=$(($CURRENT_COMMENTS - $STORED_COMMENTS))
358
+ NEW_COMMENTS=$(gh issue view $ISSUE_NUMBER --json comments \
359
+ --jq "[.comments[-${NEW_COUNT}:]] | .[] | {author: .author.login, body: .body, createdAt: .createdAt}" 2>/dev/null)
360
+ ```
361
+
362
+ 2. **Spawn classification agent:**
363
+ ```
364
+ Task(
365
+ prompt="
366
+ <files_to_read>
367
+ - ./CLAUDE.md (Project instructions — if exists, follow all guidelines)
368
+ </files_to_read>
369
+
370
+ Classify new comments on GitHub issue #${ISSUE_NUMBER}.
371
+
372
+ <issue_context>
373
+ Title: ${issue_title}
374
+ Current pipeline stage: ${pipeline_stage}
375
+ GSD Route: ${gsd_route}
376
+ Triage scope: ${triage.scope}
377
+ </issue_context>
378
+
379
+ <new_comments>
380
+ ${NEW_COMMENTS}
381
+ </new_comments>
382
+
383
+ <classification_rules>
384
+ Classify each comment (and the overall batch) into ONE of:
385
+
386
+ - **material** — Comment changes scope, requirements, acceptance criteria, or design.
387
+ Examples: 'Actually we also need to handle X', 'Changed the requirement to Y',
388
+ 'Don't forget about edge case Z'.
389
+
390
+ - **informational** — Status update, acknowledgment, question that doesn't change scope, +1.
391
+ Examples: 'Looks good', 'Thanks for picking this up', 'What's the ETA?', '+1'.
392
+
393
+ - **blocking** — Explicit instruction to stop or wait. Must contain clear hold language.
394
+ Examples: 'Don't work on this yet', 'Hold off', 'Blocked by external dependency',
395
+ 'Wait for design review'.
396
+
397
+ If ANY comment in the batch is blocking, overall classification is blocking.
398
+ If ANY comment is material (and none blocking), overall classification is material.
399
+ Otherwise, informational.
400
+ </classification_rules>
401
+
402
+ <output_format>
403
+ Return ONLY valid JSON:
404
+ {
405
+ \"classification\": \"material|informational|blocking\",
406
+ \"reasoning\": \"Brief explanation of why this classification was chosen\",
407
+ \"new_requirements\": [\"list of new requirements if material, empty array otherwise\"],
408
+ \"blocking_reason\": \"reason if blocking, empty string otherwise\"
409
+ }
410
+ </output_format>
411
+ ",
412
+ subagent_type="general-purpose",
413
+ description="Classify comments on #${ISSUE_NUMBER}"
414
+ )
415
+ ```
416
+
417
+ 3. **React based on classification:**
418
+
419
+ | Classification | Action |
420
+ |---------------|--------|
421
+ | **informational** | Log: "MGW: ${NEW_COUNT} new comment(s) reviewed — informational, continuing." Update `triage.last_comment_count` in state file. Continue pipeline. |
422
+ | **material** | Log: "MGW: Material comment(s) detected — scope may have changed." Update state: add new_requirements to triage context. Update `triage.last_comment_count`. Re-read issue body for updated requirements. Continue with enriched context (pass new_requirements to planner). Check for security keywords in material comments (see below). |
423
+ | **blocking** | Log: "MGW: Blocking comment detected — pipeline paused." Update state: `pipeline_stage = "blocked"`. Apply mgw:blocked label. Post comment on issue: `> **MGW** . \`pipeline-blocked\` . Blocked by stakeholder comment. Reason: ${blocking_reason}`. Stop pipeline execution. |
424
+
425
+ **Security keyword check for material comments:**
426
+ ```bash
427
+ SECURITY_KEYWORDS="security|vulnerability|CVE|exploit|injection|XSS|CSRF|auth bypass"
428
+ if echo "$NEW_COMMENTS" | grep -qiE "$SECURITY_KEYWORDS"; then
429
+ # Add warning to gate_result and prompt user
430
+ echo "MGW: Security-related comment detected. Re-triage recommended."
431
+ # Prompt: "Security-related comment detected. Re-triage recommended. Continue or re-triage?"
432
+ fi
433
+ ```
434
+
435
+ **When blocking comment detected — apply label:**
436
+ ```bash
437
+ remove_mgw_labels_and_apply ${ISSUE_NUMBER} "mgw:blocked"
438
+ ```
439
+
440
+ If no new comments detected, continue normally.
441
+ </step>
442
+
443
+ <step name="post_triage_update">
444
+ **Post work-starting comment on issue:**
445
+
446
+ Note: The triage gate evaluation and triage-complete/triage-blocked comment are now
447
+ posted IMMEDIATELY during /mgw:issue. This step posts a separate work-starting
448
+ notification when pipeline execution actually begins in run.md.
449
+
450
+ Gather enrichment data from triage state:
451
+ ```bash
452
+ SCOPE_SIZE="${triage.scope.size}" # small|medium|large
453
+ FILE_COUNT="${triage.scope.file_count}"
454
+ SYSTEM_LIST="${triage.scope.systems}"
455
+ FILE_LIST="${triage.scope.files}"
456
+ CONFLICTS="${triage.conflicts}"
457
+ ROUTE_REASONING="${triage.route_reasoning}"
458
+ 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'))}")
459
+
460
+ # Load milestone/phase context from project.json if available
461
+ MILESTONE_CONTEXT=""
462
+ if [ -f "${REPO_ROOT}/.mgw/project.json" ]; then
463
+ MILESTONE_CONTEXT=$(node -e "
464
+ const { loadProjectState, resolveActiveMilestoneIndex } = require('./lib/state.cjs');
465
+ const state = loadProjectState();
466
+ if (!state) process.exit(0);
467
+ // Search all milestones for the issue (not just active) to handle cross-milestone lookups
468
+ for (const m of (state.milestones || [])) {
469
+ for (const i of (m.issues || [])) {
470
+ if (i.github_number === ${ISSUE_NUMBER}) {
471
+ console.log('Milestone: ' + m.name + ' | Phase ' + i.phase_number + ': ' + i.phase_name);
472
+ process.exit(0);
473
+ }
474
+ }
475
+ }
476
+ " 2>/dev/null || echo "")
477
+ fi
478
+ ```
479
+
480
+ Post the work-starting comment directly (no sub-agent — guarantees it happens):
481
+
482
+ ```bash
483
+ WORK_STARTING_BODY=$(cat <<COMMENTEOF
484
+ > **MGW** · \`work-starting\` · ${TIMESTAMP}
485
+ > ${MILESTONE_CONTEXT}
486
+
487
+ ### Work Starting
488
+
489
+ | | |
490
+ |---|---|
491
+ | **Route** | \`${gsd_route}\` — ${ROUTE_REASONING} |
492
+ | **Scope** | ${SCOPE_SIZE} — ${FILE_COUNT} files across ${SYSTEM_LIST} |
493
+ | **Conflicts** | ${CONFLICTS} |
494
+
495
+ Work begins on branch \`${BRANCH_NAME}\`.
496
+
497
+ <details>
498
+ <summary>Affected Files</summary>
499
+
500
+ ${FILE_LIST as bullet points}
501
+
502
+ </details>
503
+ COMMENTEOF
504
+ )
505
+
506
+ gh issue comment ${ISSUE_NUMBER} --body "$WORK_STARTING_BODY" 2>/dev/null || true
507
+ ```
508
+
509
+ Log comment in state file (at `${REPO_ROOT}/.mgw/active/`).
510
+ </step>
@@ -0,0 +1,95 @@
1
+ ---
2
+ name: mgw:run/worktree
3
+ description: Create isolated worktree for issue work
4
+ ---
5
+
6
+ <step name="create_worktree">
7
+ **Create isolated worktree for issue work:**
8
+
9
+ Derive branch and worktree path:
10
+ ```bash
11
+ BRANCH_NAME="issue/${ISSUE_NUMBER}-${slug}"
12
+ WORKTREE_DIR="${REPO_ROOT}/.worktrees/${BRANCH_NAME}"
13
+ ```
14
+
15
+ **Check for active work on this issue by another developer:**
16
+ ```bash
17
+ # Check 1: Remote branch already exists (another machine pushed work)
18
+ REMOTE_BRANCHES=$(git ls-remote --heads origin "issue/${ISSUE_NUMBER}-*" 2>/dev/null | awk '{print $2}' | sed 's|refs/heads/||')
19
+ if [ -n "$REMOTE_BRANCHES" ]; then
20
+ echo "WARNING: Remote branch(es) already exist for issue #${ISSUE_NUMBER}:"
21
+ echo "$REMOTE_BRANCHES" | sed 's/^/ /'
22
+ echo ""
23
+ AskUserQuestion(
24
+ header: "Active Work Detected",
25
+ question: "Remote branch exists for #${ISSUE_NUMBER}. Another developer may be working on this. Proceed anyway?",
26
+ options: [
27
+ { label: "Proceed", description: "Create a new worktree anyway (may cause conflicts)" },
28
+ { label: "Abort", description: "Stop pipeline — coordinate with the other developer first" }
29
+ ]
30
+ )
31
+ if [ "$USER_CHOICE" = "Abort" ]; then
32
+ echo "Pipeline aborted — coordinate with the developer who owns the existing branch."
33
+ exit 1
34
+ fi
35
+ fi
36
+
37
+ # Check 2: Open PR already exists for this issue
38
+ EXISTING_PR=$(gh pr list --search "issue/${ISSUE_NUMBER}-" --state open --json number,headRefName --jq '.[0].number' 2>/dev/null || echo "")
39
+ if [ -n "$EXISTING_PR" ]; then
40
+ echo "WARNING: Open PR #${EXISTING_PR} already exists for issue #${ISSUE_NUMBER}."
41
+ echo "Creating a new worktree will produce a conflicting PR."
42
+ AskUserQuestion(
43
+ header: "Open PR Exists",
44
+ question: "PR #${EXISTING_PR} is already open for this issue. Proceed with a new worktree?",
45
+ options: [
46
+ { label: "Proceed", description: "Create new worktree anyway" },
47
+ { label: "Abort", description: "Stop — review the existing PR first" }
48
+ ]
49
+ )
50
+ if [ "$USER_CHOICE" = "Abort" ]; then
51
+ exit 1
52
+ fi
53
+ fi
54
+ ```
55
+
56
+ Ensure .worktrees/ is gitignored:
57
+ ```bash
58
+ mkdir -p "$(dirname "${WORKTREE_DIR}")"
59
+ if ! git check-ignore -q .worktrees 2>/dev/null; then
60
+ echo ".worktrees/" >> "${REPO_ROOT}/.gitignore"
61
+ fi
62
+ ```
63
+
64
+ Create worktree with feature branch:
65
+ ```bash
66
+ # If worktree already exists (resume in same session), skip creation
67
+ if [ -d "${WORKTREE_DIR}" ]; then
68
+ echo "Worktree exists, reusing"
69
+ # If branch already exists (resume from prior session)
70
+ elif git show-ref --verify --quiet "refs/heads/${BRANCH_NAME}"; then
71
+ git worktree add "${WORKTREE_DIR}" "${BRANCH_NAME}"
72
+ # New branch (first run)
73
+ else
74
+ git worktree add "${WORKTREE_DIR}" -b "${BRANCH_NAME}"
75
+ fi
76
+ ```
77
+
78
+ **Switch working directory to worktree:**
79
+ ```bash
80
+ cd "${WORKTREE_DIR}"
81
+ ```
82
+
83
+ Update state (at `${REPO_ROOT}/.mgw/active/`): add branch to linked_branches.
84
+ Add cross-ref (at `${REPO_ROOT}/.mgw/cross-refs.json`): issue → branch.
85
+
86
+ **Apply in-progress label:**
87
+ ```bash
88
+ remove_mgw_labels_and_apply ${ISSUE_NUMBER} "mgw:in-progress"
89
+ ```
90
+
91
+ **PATH CONVENTION for remaining steps:**
92
+ - File operations, git commands, and agent work use **relative paths** (CWD = worktree)
93
+ - `.mgw/` state operations use **absolute paths**: `${REPO_ROOT}/.mgw/`
94
+ (`.mgw/` is gitignored — it only exists in the main repo, not the worktree)
95
+ </step>