diff --git a/commands/issue-code-generation.md b/commands/issue-code-generation.md index 61a63bb..e922f04 100644 --- a/commands/issue-code-generation.md +++ b/commands/issue-code-generation.md @@ -8,22 +8,27 @@ Full issue → code → AC validation pipeline. 2. Pass the full issue JSON to the `issue-router` agent - If status is `NEEDS_REVIEW`, stop and surface the classification uncertainty for human decision - The router returns a **type** (`bug`, `feature`, `refactor`, `security`) used in step 4 + - Write checkpoint: pipe agent output to `node scripts/checkpoint.mjs write --repo 01-router` 3. Pass the full issue JSON to the `ticket-analyst` agent - If status is `BLOCKED` or `NEEDS_REVIEW`, stop and surface the output for human decision + - Write checkpoint: pipe agent output to `node scripts/checkpoint.mjs write --repo 02-brief` 4. Pass the `### Brief` block from ticket-analyst to the code-builder variant selected by the router: - `bug` → `code-builder-bug` - `feature` → `code-builder-feature` - `refactor` → `code-builder-refactor` - `security` or unclassified → `code-builder` (generic fallback) - If status is `BLOCKED` or `NEEDS_REVIEW`, stop and surface the output + - Write checkpoint: pipe agent output to `node scripts/checkpoint.mjs write --repo 03-patch` 5. **If `--strict` flag is present**, pass to the `code-challenger` agent: the `### Patch` block, the original acceptance criteria, and the issue type - If status is `BLOCKED` or `NEEDS_REVIEW`, stop and surface the output for human decision before continuing - On `DONE`, carry the `### Handoff` block (critical points list) forward to step 6 - Skip this step entirely if `--strict` was not passed + - Write checkpoint: pipe agent output to `node scripts/checkpoint.mjs write --repo 04-challenges` 6. Pass to the `code-reviewer` agent: the `### Patch` block, the original acceptance criteria, and any type-specific evidence blocks produced by the builder: - `bug` → include `### Reproduction` - `refactor` → include `### Non-regression evidence` - `feature` / `security` / fallback → `### Patch` only - If `--strict` was used, also include the `### Handoff` block from code-challenger so the reviewer is aware of pre-identified risks + - Write checkpoint: pipe agent output to `node scripts/checkpoint.mjs write --repo 05-review` 7. If code-reviewer status is `NEEDS_REVIEW`, write the review report and stop — present blocking issues to the user 8. If code-reviewer status is `DONE`, present the full patch to the user for review and merge approval diff --git a/commands/resume.md b/commands/resume.md new file mode 100644 index 0000000..67ebcba --- /dev/null +++ b/commands/resume.md @@ -0,0 +1,45 @@ +# /resume + +Resume an interrupted `/issue-code-generation` pipeline from the first missing checkpoint. + +## Invocation + +``` +/resume --repo [--strict] +``` + +`--repo` is required and must match the repository used in the original `/issue-code-generation` run — checkpoints are namespaced by repo so runs against different repositories with the same issue number do not collide. + +Pass `--strict` if the original run used `--strict`. This flag is **required** to ensure `code-challenger` is not skipped when resuming a strict run interrupted before `04-challenges` was written. + +## Steps + +1. Run `node scripts/checkpoint.mjs list --repo ` to get the list of already-completed steps +2. Display the completed steps to the user so they can confirm the resume point +3. Determine the full expected step sequence based on flags: + - Without `--strict`: `01-router`, `02-brief`, `03-patch`, `05-review` + - With `--strict`: `01-router`, `02-brief`, `03-patch`, `04-challenges`, `05-review` + - The first step absent from the checkpoint list is the resume point +4. For each step **before** the resume point, load the saved output via `node scripts/checkpoint.mjs read --repo ` and treat it as the agent's output — **do not re-run the agent** +5. The issue JSON is never checkpointed. Whenever the resume point is `02-brief` or later, re-fetch it with `node scripts/gh-get-issue.mjs --repo ` before running any agent that needs it +6. Resume the pipeline from the first missing step, following the same logic as `/issue-code-generation`: + + **`01-router` missing** — fetch the issue JSON with `node scripts/gh-get-issue.mjs --repo `, run `issue-router`, pipe output to `node scripts/checkpoint.mjs write --repo 01-router`, then continue from `02-brief` + + **`02-brief` missing** — re-fetch the issue JSON with `node scripts/gh-get-issue.mjs --repo `, load `01-router` checkpoint, run `ticket-analyst` with the fresh issue JSON, pipe output to `node scripts/checkpoint.mjs write --repo 02-brief`, then continue from `03-patch` + + **`03-patch` missing** — re-fetch the issue JSON with `node scripts/gh-get-issue.mjs --repo `, load `01-router` and `02-brief` checkpoints, run the code-builder variant indicated by the router type, pipe output to `node scripts/checkpoint.mjs write --repo 03-patch`; if `--strict` was passed continue to `04-challenges`, otherwise continue to `05-review` + + **`04-challenges` missing** *(only reached when `--strict` was passed)* — load `03-patch` checkpoint, run `code-challenger` with the patch and original acceptance criteria, pipe output to `node scripts/checkpoint.mjs write --repo 04-challenges`, then continue to `05-review` + + **`05-review` missing** — load `03-patch` checkpoint (and `04-challenges` if `--strict` was passed), run `code-reviewer`, pipe output to `node scripts/checkpoint.mjs write --repo 05-review` + +7. Apply the same stop conditions as `/issue-code-generation`: halt on `NEEDS_REVIEW` or `BLOCKED` and surface the output for human decision +8. On completion, present the final patch to the user for review and merge approval + +## Notes + +- `listCheckpoints` returns steps sorted lexicographically — the `01-` … `05-` numeric prefixes ensure correct order +- `node scripts/checkpoint.mjs write` reads content from stdin to preserve multiline Markdown structure +- If all expected steps are already checkpointed, inform the user that the pipeline is complete and show the `05-review` checkpoint content +- If no checkpoints exist for ``, inform the user and suggest running `/issue-code-generation ` instead diff --git a/scripts/checkpoint.mjs b/scripts/checkpoint.mjs new file mode 100644 index 0000000..41ccdf2 --- /dev/null +++ b/scripts/checkpoint.mjs @@ -0,0 +1,119 @@ +#!/usr/bin/env node +// Checkpoint read/write for the checkpoint-resume pattern. +// Files land in ~/dev/checkpoints////.md +// Usage: node scripts/checkpoint.mjs write --repo (content from stdin) +// node scripts/checkpoint.mjs read --repo +// node scripts/checkpoint.mjs list --repo + +import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'; +import { homedir } from 'node:os'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const CHECKPOINTS_ROOT = join(homedir(), 'dev', 'checkpoints'); + +// Allowlist patterns — reject anything containing path separators or traversal sequences. +const REPO_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*\/[a-zA-Z0-9][a-zA-Z0-9._-]*$/; +const ISSUE_ID_RE = /^[1-9]\d*$/; +const STEP_RE = /^\d{2}-[a-z][a-z0-9-]*$/; + +function assertRepo(repo) { + if (!REPO_RE.test(String(repo))) { + throw new Error(`Invalid repo "${repo}" — must be owner/repo (e.g. "acme/my-app")`); + } +} + +function assertIssueId(issueId) { + if (!ISSUE_ID_RE.test(String(issueId))) { + throw new Error(`Invalid issueId "${issueId}" — must be a positive integer`); + } +} + +function assertStep(step) { + if (!STEP_RE.test(step)) { + throw new Error(`Invalid step "${step}" — must match NN-slug (e.g. "01-router")`); + } +} + +function checkpointDir(repo, issueId) { + const [owner, repoName] = repo.split('/'); + return join(CHECKPOINTS_ROOT, owner, repoName, String(issueId)); +} + +export function writeCheckpoint(repo, issueId, step, content) { + assertRepo(repo); + assertIssueId(issueId); + assertStep(step); + const dir = checkpointDir(repo, issueId); + mkdirSync(dir, { recursive: true, mode: 0o700 }); + const filePath = join(dir, `${step}.md`); + writeFileSync(filePath, content, { encoding: 'utf8', mode: 0o600 }); + return filePath; +} + +export function readCheckpoint(repo, issueId, step) { + assertRepo(repo); + assertIssueId(issueId); + assertStep(step); + const filePath = join(checkpointDir(repo, issueId), `${step}.md`); + if (!existsSync(filePath)) return null; + return readFileSync(filePath, 'utf8'); +} + +export function listCheckpoints(repo, issueId) { + assertRepo(repo); + assertIssueId(issueId); + const dir = checkpointDir(repo, issueId); + if (!existsSync(dir)) return []; + return readdirSync(dir) + .filter(f => f.endsWith('.md') && STEP_RE.test(f.slice(0, -3))) + .map(f => f.slice(0, -3)) + .sort(); +} + +async function readStdin() { + const chunks = []; + for await (const chunk of process.stdin) chunks.push(chunk); + return Buffer.concat(chunks).toString('utf8'); +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + const args = process.argv.slice(2); + const cmd = args[0]; + const repoIdx = args.indexOf('--repo'); + const repo = repoIdx !== -1 ? args[repoIdx + 1] : undefined; + const positional = args.filter((_, i) => i !== 0 && i !== repoIdx && i !== repoIdx + 1); + const [issueId, step] = positional; + + function requireArgs(...names) { + const vals = { cmd, repo, issueId, step }; + const missing = names.filter(n => !vals[n]); + if (missing.length) { + console.error(`Missing required argument(s): ${missing.join(', ')}`); + console.error('Usage: node scripts/checkpoint.mjs write|read|list --repo [step]'); + process.exit(1); + } + } + + if (cmd === 'write') { + requireArgs('repo', 'issueId', 'step'); + const content = await readStdin(); + const path = writeCheckpoint(repo, issueId, step, content); + console.log(`Checkpoint written: ${path}`); + } else if (cmd === 'read') { + requireArgs('repo', 'issueId', 'step'); + const content = readCheckpoint(repo, issueId, step); + if (content === null) { + console.error(`Checkpoint not found: ${repo}/${issueId}/${step}`); + process.exit(1); + } + process.stdout.write(content); + } else if (cmd === 'list') { + requireArgs('repo', 'issueId'); + const steps = listCheckpoints(repo, issueId); + console.log(steps.length ? steps.join('\n') : '(none)'); + } else { + console.error('Usage: node scripts/checkpoint.mjs write|read|list --repo [step]'); + process.exit(1); + } +}