Skip to content

Commit 57f13af

Browse files
authored
Merge pull request #246 from snipcodeit/issue/237-add-resume-detection-and-auto-resume-to
feat(run): add resume detection and auto-resume to mgw:run startup
2 parents 6af638f + cb4f32e commit 57f13af

3 files changed

Lines changed: 362 additions & 10 deletions

File tree

commands/run/triage.md

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,140 @@ migrateProjectState();
4646
" 2>/dev/null || true
4747
```
4848

49+
**Checkpoint detection — check for resumable progress before stage routing:**
50+
51+
After loading state and running migration, detect whether a prior pipeline run left
52+
a checkpoint with meaningful progress (beyond triage). If found, present the user
53+
with Resume/Fresh/Skip options before proceeding.
54+
55+
```bash
56+
# Detect checkpoint with progress beyond triage
57+
CHECKPOINT_DATA=$(node -e "
58+
const { detectCheckpoint, resumeFromCheckpoint } = require('./lib/state.cjs');
59+
const cp = detectCheckpoint(${ISSUE_NUMBER});
60+
if (!cp) {
61+
console.log('none');
62+
} else {
63+
const resume = resumeFromCheckpoint(${ISSUE_NUMBER});
64+
console.log(JSON.stringify(resume));
65+
}
66+
" 2>/dev/null || echo "none")
67+
```
68+
69+
If checkpoint is found (`CHECKPOINT_DATA !== "none"`):
70+
71+
Parse the checkpoint data and display to the user:
72+
```bash
73+
CHECKPOINT_STEP=$(echo "$CHECKPOINT_DATA" | node -e "
74+
const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf-8'));
75+
console.log(d.checkpoint.pipeline_step);
76+
")
77+
RESUME_ACTION=$(echo "$CHECKPOINT_DATA" | node -e "
78+
const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf-8'));
79+
console.log(d.resumeAction);
80+
")
81+
RESUME_STAGE=$(echo "$CHECKPOINT_DATA" | node -e "
82+
const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf-8'));
83+
console.log(d.resumeStage);
84+
")
85+
COMPLETED_STEPS=$(echo "$CHECKPOINT_DATA" | node -e "
86+
const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf-8'));
87+
console.log(d.completedSteps.join(', '));
88+
")
89+
ARTIFACTS_COUNT=$(echo "$CHECKPOINT_DATA" | node -e "
90+
const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf-8'));
91+
console.log(d.checkpoint.artifacts.length);
92+
")
93+
STARTED_AT=$(echo "$CHECKPOINT_DATA" | node -e "
94+
const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf-8'));
95+
console.log(d.checkpoint.started_at || 'unknown');
96+
")
97+
UPDATED_AT=$(echo "$CHECKPOINT_DATA" | node -e "
98+
const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf-8'));
99+
console.log(d.checkpoint.updated_at || 'unknown');
100+
")
101+
```
102+
103+
Display checkpoint state and prompt user:
104+
```
105+
AskUserQuestion(
106+
header: "Checkpoint Detected for #${ISSUE_NUMBER}",
107+
question: "A prior pipeline run left progress at step '${CHECKPOINT_STEP}'.
108+
109+
| | |
110+
|---|---|
111+
| **Last step** | ${CHECKPOINT_STEP} |
112+
| **Completed steps** | ${COMPLETED_STEPS} |
113+
| **Artifacts** | ${ARTIFACTS_COUNT} file(s) |
114+
| **Resume action** | ${RESUME_ACTION} → stage: ${RESUME_STAGE} |
115+
| **Started** | ${STARTED_AT} |
116+
| **Last updated** | ${UPDATED_AT} |
117+
118+
How would you like to proceed?",
119+
options: [
120+
{ label: "Resume", description: "Resume from checkpoint — skip completed steps (${COMPLETED_STEPS}), jump to ${RESUME_STAGE}" },
121+
{ label: "Fresh", description: "Discard checkpoint and re-run pipeline from scratch" },
122+
{ label: "Skip", description: "Skip this issue entirely" }
123+
]
124+
)
125+
```
126+
127+
Handle user choice:
128+
129+
| Choice | Action |
130+
|--------|--------|
131+
| **Resume** | Load checkpoint context. Set `pipeline_stage` in state to `${RESUME_STAGE}`. Log: "MGW: Resuming #${ISSUE_NUMBER} from checkpoint (step: ${CHECKPOINT_STEP}, action: ${RESUME_ACTION})." Skip triage/worktree stages that already completed and jump directly to the resume stage in the pipeline. The `resume.context` object carries step-specific data (e.g., `quick_dir`, `plan_num`, `phase_number`) needed by the target stage. |
132+
| **Fresh** | Clear checkpoint via `clearCheckpoint()`. Reset `pipeline_stage` to `"triaged"`. Log: "MGW: Checkpoint cleared for #${ISSUE_NUMBER}. Starting fresh." Continue with normal pipeline flow. |
133+
| **Skip** | Log: "MGW: Skipping #${ISSUE_NUMBER} per user request." STOP pipeline. |
134+
135+
```bash
136+
case "$USER_CHOICE" in
137+
Resume)
138+
# Load resume context and jump to the appropriate stage
139+
node -e "
140+
const fs = require('fs'), path = require('path');
141+
const activeDir = path.join(process.cwd(), '.mgw', 'active');
142+
const files = fs.readdirSync(activeDir);
143+
const file = files.find(f => f.startsWith('${ISSUE_NUMBER}-') && f.endsWith('.json'));
144+
const filePath = path.join(activeDir, file);
145+
const state = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
146+
// The pipeline_stage already reflects prior progress — do not overwrite
147+
// unless the resume target is more advanced than current stage
148+
console.log('Resuming from checkpoint: ' + JSON.stringify(state.checkpoint.resume));
149+
" 2>/dev/null || true
150+
# Set RESUME_MODE=true — downstream stages check this flag to skip completed work
151+
RESUME_MODE=true
152+
RESUME_CONTEXT="${CHECKPOINT_DATA}"
153+
;;
154+
Fresh)
155+
node -e "
156+
const { clearCheckpoint } = require('./lib/state.cjs');
157+
clearCheckpoint(${ISSUE_NUMBER});
158+
console.log('Checkpoint cleared for #${ISSUE_NUMBER}');
159+
" 2>/dev/null || true
160+
# Reset pipeline_stage to triaged for fresh start
161+
node -e "
162+
const fs = require('fs'), path = require('path');
163+
const activeDir = path.join(process.cwd(), '.mgw', 'active');
164+
const files = fs.readdirSync(activeDir);
165+
const file = files.find(f => f.startsWith('${ISSUE_NUMBER}-') && f.endsWith('.json'));
166+
const filePath = path.join(activeDir, file);
167+
const state = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
168+
state.pipeline_stage = 'triaged';
169+
fs.writeFileSync(filePath, JSON.stringify(state, null, 2));
170+
" 2>/dev/null || true
171+
RESUME_MODE=false
172+
;;
173+
Skip)
174+
echo "MGW: Skipping #${ISSUE_NUMBER} per user request."
175+
exit 0
176+
;;
177+
esac
178+
```
179+
180+
If no checkpoint found (or checkpoint is at triage step only), continue with
181+
normal pipeline stage routing below.
182+
49183
**Initialize checkpoint** when pipeline first transitions past triage:
50184
```bash
51185
# Checkpoint initialization — called once when pipeline execution begins.
@@ -67,7 +201,6 @@ updateCheckpoint(${ISSUE_NUMBER}, {
67201
});
68202
" 2>/dev/null || true
69203
```
70-
71204
Check pipeline_stage:
72205
- "triaged" → proceed to GSD execution
73206
- "planning" / "executing" → resume from where we left off

commands/workflows/state.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,91 @@ GSD phase directory (`.planning/phases/{NN}-{slug}/`) to operate in.
647647
Issues created outside of `/mgw:project` (e.g., manually filed bugs) will not have
648648
a `phase_number`. In this case, `/mgw:run` falls back to the quick pipeline.
649649

650+
## Checkpoint Resume Detection
651+
652+
When `mgw:run` starts for an issue, the validate_and_load step checks whether a prior
653+
pipeline run left a checkpoint with progress beyond the initial triage step. This enables
654+
resuming interrupted sessions without re-doing completed work.
655+
656+
### Resume Detection Functions (lib/state.cjs)
657+
658+
| Function | Signature | Returns | Description |
659+
|----------|-----------|---------|-------------|
660+
| `detectCheckpoint` | `(issueNumber)` | `object\|null` | Checks if active state file has a non-null checkpoint with `pipeline_step` beyond `"triage"`. Returns the checkpoint data if resumable, `null` otherwise. |
661+
| `resumeFromCheckpoint` | `(issueNumber)` | `object\|null` | Returns checkpoint data plus computed `resumeStage`, `resumeAction`, and `completedSteps`. Maps `resume.action` to the pipeline stage to jump to. |
662+
| `clearCheckpoint` | `(issueNumber)` | `{ cleared: boolean }` | Resets the checkpoint field to `null` in the active state file. Used for "Fresh start" option. |
663+
664+
### Resume Action to Stage Mapping
665+
666+
The `resume.action` field in the checkpoint tells `resumeFromCheckpoint()` which pipeline
667+
stage to jump to:
668+
669+
| resume.action | resumeStage | Meaning |
670+
|---------------|-------------|---------|
671+
| `run-plan-checker` | `planning` | Plan exists, needs quality check |
672+
| `spawn-executor` | `executing` | Plan complete, execute next |
673+
| `continue-execution` | `executing` | Mid-execution resume |
674+
| `spawn-verifier` | `verifying` | Execution done, verify next |
675+
| `create-pr` | `pr-pending` | Verification done, create PR |
676+
| `begin-execution` | `planning` | Triage done, begin planning |
677+
| `null` / unknown | `planning` | Safe default |
678+
679+
### Resume Detection Flow
680+
681+
```
682+
mgw:run #N starts
683+
|
684+
v
685+
Load state file → migrateProjectState()
686+
|
687+
v
688+
detectCheckpoint(N)
689+
|
690+
+---> null (no checkpoint or triage-only) → proceed with normal stage routing
691+
|
692+
+---> checkpoint found → display state to user
693+
|
694+
v
695+
AskUserQuestion: Resume / Fresh / Skip
696+
|
697+
+---> Resume: load checkpoint context, set RESUME_MODE=true,
698+
| jump to resume.action stage (skip completed steps)
699+
|
700+
+---> Fresh: clearCheckpoint(N), reset pipeline_stage to "triaged",
701+
| continue normal pipeline
702+
|
703+
+---> Skip: exit pipeline for this issue
704+
```
705+
706+
### Pipeline Step Order
707+
708+
The `CHECKPOINT_STEP_ORDER` constant defines the ordered progression of checkpoint steps:
709+
710+
```
711+
triage → plan → execute → verify → pr
712+
```
713+
714+
Only checkpoints with `pipeline_step` at index > 0 (beyond `"triage"`) are considered
715+
resumable. A checkpoint at `"triage"` means nothing meaningful has been completed yet.
716+
717+
### Resume Context
718+
719+
When resuming, the `resume.context` object carries step-specific data needed by the
720+
target stage. The context shape varies by `resume.action`:
721+
722+
| resume.action | Context fields |
723+
|---------------|----------------|
724+
| `spawn-executor` | `{ quick_dir, plan_num }` |
725+
| `run-plan-checker` | `{ quick_dir, plan_num }` |
726+
| `spawn-verifier` | `{ quick_dir, plan_num }` |
727+
| `create-pr` | `{ quick_dir, plan_num }` |
728+
| `continue-execution` | `{ phase_number }` |
729+
| `begin-execution` | `{ gsd_route, branch }` |
730+
731+
Downstream pipeline stages read `resume.context` to pick up where the prior run left
732+
off. For example, the executor stage uses `quick_dir` and `plan_num` to locate the
733+
existing plan files rather than re-creating them.
734+
650735
## Consumers
651736

652737
| Pattern | Referenced By |
@@ -661,5 +746,6 @@ a `phase_number`. In this case, `/mgw:run` falls back to the quick pipeline.
661746
| Project state | milestone.md, next.md, ask.md |
662747
| Gate result schema | issue.md (populate), run.md (validate) |
663748
| Board status sync | board-sync.md (utility), issue.md (triage transitions), run.md (pipeline transitions) |
749+
| Checkpoint resume | run.md (detect + prompt), milestone.md (detect resume point for failed issues) |
664750
| Checkpoint writes | triage.md (init), execute.md (plan/execute/verify), pr-create.md (pr) |
665751
| Atomic writes | lib/state.cjs (`atomicWriteJson`, `updateCheckpoint`) |

0 commit comments

Comments
 (0)