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

@@ -82,3 +82,9 @@ CMS_URL=http://localhost:3001
# SANDCASTLE_DECOMPOSE_ITERATIONS=10 # decompose: read PRD, write epic + stories, commit
# SANDCASTLE_IMPLEMENTER_ITERATIONS=30 # implementer: full TDD slice (red test → green impl → gates → commit)
# SANDCASTLE_REVIEWER_ITERATIONS=10 # reviewer: read diff + task, return decision
# Reject-cycle cap. After this many reviewer rejects on the same slice, the
# dispatch loop gives up on that slice and exits 1 with the last rejection
# notes printed. Bump for tricky slices; lower for fast-feedback iteration.
#
# SANDCASTLE_MAX_ATTEMPTS=3