feat(work): dispatch loops + auto-ticks state on approve

Previously the orchestrator ran exactly one implementer + reviewer pair,
printed "(Automatic state mutation by the orchestrator is v2.)", and
exited — the human had to tick the bullet, flip story status, rebuild
state, and re-invoke for every slice. V2 closes the loop:

- Parses the JSON the implementer + reviewer prompts ask the agents to
  emit (`parseAgentJson` — tolerates both ```json fenced and bare
  trailing { ... } shapes). The reviewer's `decision` and the
  implementer's `status` are the orchestrator's discriminators.
- On approve: ticks the bullet in `_story.md` and writes it back. If
  the story now has zero unchecked bullets, flips its frontmatter
  `status: in-progress → done`; if all sibling stories are also done,
  flips the epic's frontmatter the same way. Commits the mutation on
  the host as a separate `chore(work): tick/finish ...` commit so the
  implementer's slice commit stays clean. `_state.json` regenerates
  via the existing pre-commit `rebuild-state` hook.
- On reject: re-dispatches the implementer with the reviewer's notes
  appended to TASK_FILE_CONTENT, bounded by SANDCASTLE_MAX_ATTEMPTS
  (default 3). On the (max+1)th reject the loop exits 1 with the last
  notes printed.
- After every approved slice, calls findNextTask again and dispatches
  the next ready bullet — including across story boundaries (the
  state-builder treats any non-done story with satisfied deps as
  ready, so flipping story 01 to done unblocks story 02 automatically).
- Flags: `--once` (legacy single-slice behavior) and `--max-tasks N`
  bound the loop. Default is unlimited — matches the
  continuous-execution preference.

Auth/sandbox setup is now pulled out of the per-iteration path so the
loop reuses one sandbox across slices.
This commit is contained in:
2026-05-13 19:43:11 +02:00
parent 1bbe866a5c
commit edbc6a8fad
3 changed files with 398 additions and 106 deletions

View File

@@ -13,7 +13,13 @@
* ready Prints every ready story
* blocked Prints every blocked story + what each is waiting on
* dispatch Print the next dispatch plan; with --execute invokes
* sandcastle to run the implementer + reviewer pair
* sandcastle to run the implementer + reviewer pair.
* --execute LOOPS through every ready task by default;
* bound with --once or --max-tasks N. After each
* approved slice the orchestrator ticks the bullet,
* flips story/epic status if complete, and commits
* the state mutation as `chore(work): ...` on top of
* the implementer's slice commit.
* decompose <id> Validate an approved PRD + print the decompose plan;
* with --execute invokes sandcastle's decomposer agent
* to write the epic folder + per-story files
@@ -135,7 +141,7 @@ function usage() {
"Usage: pnpm work <rebuild-state|status|next|ready|blocked|dispatch|decompose|prd-ship>",
);
console.log(
" dispatch Print the next dispatch plan (use --execute to invoke sandcastle)",
" dispatch Print the next dispatch plan (use --execute to invoke sandcastle; loops by default — bound with --once / --max-tasks N)",
);
console.log(
" decompose <prd-id> Decompose an approved PRD into epic + stories (use --execute to invoke sandcastle)",