Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions commands/issue-code-generation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <owner/repo> <issueId> 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 <owner/repo> <issueId> 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 <owner/repo> <issueId> 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 <owner/repo> <issueId> 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 <owner/repo> <issueId> 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
45 changes: 45 additions & 0 deletions commands/resume.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# /resume

Resume an interrupted `/issue-code-generation` pipeline from the first missing checkpoint.

## Invocation

```
/resume <issueId> --repo <owner/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 <owner/repo> <issueId>` 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 <owner/repo> <issueId> <step>` 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 <issueId> --repo <owner/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 <issueId> --repo <owner/repo>`, run `issue-router`, pipe output to `node scripts/checkpoint.mjs write --repo <owner/repo> <issueId> 01-router`, then continue from `02-brief`

**`02-brief` missing** — re-fetch the issue JSON with `node scripts/gh-get-issue.mjs <issueId> --repo <owner/repo>`, load `01-router` checkpoint, run `ticket-analyst` with the fresh issue JSON, pipe output to `node scripts/checkpoint.mjs write --repo <owner/repo> <issueId> 02-brief`, then continue from `03-patch`

**`03-patch` missing** — re-fetch the issue JSON with `node scripts/gh-get-issue.mjs <issueId> --repo <owner/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 <owner/repo> <issueId> 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 <owner/repo> <issueId> 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 <owner/repo> <issueId> 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 `<issueId>`, inform the user and suggest running `/issue-code-generation <issueId>` instead
119 changes: 119 additions & 0 deletions scripts/checkpoint.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#!/usr/bin/env node
// Checkpoint read/write for the checkpoint-resume pattern.
// Files land in ~/dev/checkpoints/<owner>/<repo>/<issueId>/<step>.md
// Usage: node scripts/checkpoint.mjs write --repo <owner/repo> <issueId> <step> (content from stdin)
// node scripts/checkpoint.mjs read --repo <owner/repo> <issueId> <step>
// node scripts/checkpoint.mjs list --repo <owner/repo> <issueId>

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 <owner/repo> <issueId> [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 <owner/repo> <issueId> [step]');
process.exit(1);
}
}
Loading