runcastle 1.2.6 → 1.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/bin/runcastle.js +8467 -6620
  2. package/drizzle/0026_polite_vertigo.sql +2 -0
  3. package/drizzle/0027_dashing_texas_twister.sql +1 -0
  4. package/drizzle/0028_tired_deadpool.sql +2 -0
  5. package/drizzle/0029_damp_stellaris.sql +1 -0
  6. package/drizzle/meta/0026_snapshot.json +921 -0
  7. package/drizzle/meta/0027_snapshot.json +928 -0
  8. package/drizzle/meta/0028_snapshot.json +942 -0
  9. package/drizzle/meta/0029_snapshot.json +949 -0
  10. package/drizzle/meta/_journal.json +29 -1
  11. package/index.js +8424 -6588
  12. package/package.json +1 -1
  13. package/sandcastle-template/Containerfile +9 -1
  14. package/sandcastle-template/Dockerfile +9 -1
  15. package/skills/burner/implement-ticket.md +32 -47
  16. package/skills/burner/resolve-conflict.md +3 -3
  17. package/skills/burner/review-ticket.md +118 -37
  18. package/skills/packs/README.md +99 -7
  19. package/skills/packs/runcastle/.claude-plugin/plugin.json +1 -1
  20. package/skills/packs/runcastle/skills/code-review/SKILL.md +131 -0
  21. package/skills/packs/runcastle/skills/converge/SKILL.md +26 -21
  22. package/skills/packs/runcastle/skills/ideate/SKILL.md +14 -8
  23. package/skills/packs/runcastle/skills/prepare/SKILL.md +96 -0
  24. package/skills/packs/runcastle/skills/prepare/references/recipes.md +72 -0
  25. package/skills/packs/runcastle/skills/project/SKILL.md +54 -55
  26. package/skills/packs/runcastle/skills/project/references/charter.md +79 -0
  27. package/skills/packs/runcastle/skills/project/references/health-sweeps.md +44 -0
  28. package/skills/packs/runcastle/skills/qa/SKILL.md +29 -13
  29. package/skills/packs/runcastle/skills/revisit/SKILL.md +8 -8
  30. package/skills/packs/runcastle/skills/spec/SKILL.md +3 -3
  31. package/skills/packs/runcastle/skills/tickets/SKILL.md +19 -3
  32. package/skills/packs/runcastle/skills/waypoint/SKILL.md +14 -9
  33. package/web/assets/index-BRjcVBgj.js +142 -0
  34. package/web/assets/index-CNSn8X1O.css +1 -0
  35. package/web/index.html +2 -2
  36. package/web/assets/index-bof4-dhY.css +0 -1
  37. package/web/assets/index-qHtxEVWq.js +0 -142
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runcastle",
3
- "version": "1.2.6",
3
+ "version": "1.2.9",
4
4
  "description": "Burn tickets into shipped features with Claude Code.",
5
5
  "license": "FSL-1.1-ALv2",
6
6
  "type": "module",
@@ -15,6 +15,11 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
15
15
  ENV BUN_INSTALL=/usr/local
16
16
  RUN curl -fsSL https://bun.sh/install | bash
17
17
 
18
+ # Codex CLI (the second agent runtime) — authenticated at runtime via CODEX_API_KEY.
19
+ # Installed as root, before the user switch, so it lands in the system-wide npm
20
+ # prefix: after `USER` a global install has nowhere writable to go.
21
+ RUN npm install -g @openai/codex
22
+
18
23
  # Rootless-podman invariant (research #32): the in-container agent user MUST be at
19
24
  # UID/GID 1000 so bind-mounted host files share an owner. Sandcastle passes the host
20
25
  # uid/gid as build args (defaulting here to 1000); rename the stock `node` user to match.
@@ -24,7 +29,10 @@ RUN groupmod -o -g "$AGENT_GID" node \
24
29
  && usermod -o -u "$AGENT_UID" -g "$AGENT_GID" -d /home/agent -m -l agent node
25
30
  USER ${AGENT_UID}:${AGENT_GID}
26
31
 
27
- # Claude Code CLI (the agent runtime) — authenticated at runtime via CLAUDE_CODE_OAUTH_TOKEN.
32
+ # Claude Code CLI (the other agent runtime) — authenticated at runtime via
33
+ # CLAUDE_CODE_OAUTH_TOKEN. Installed per-user, so it must follow the switch.
34
+ # One image serves both runtimes: which CLI a burn invokes is decided by the
35
+ # resolved model's runtime, not by the image.
28
36
  RUN curl -fsSL https://claude.ai/install.sh | bash
29
37
  ENV PATH="/home/agent/.local/bin:$PATH"
30
38
 
@@ -15,6 +15,11 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
15
15
  ENV BUN_INSTALL=/usr/local
16
16
  RUN curl -fsSL https://bun.sh/install | bash
17
17
 
18
+ # Codex CLI (the second agent runtime) — authenticated at runtime via CODEX_API_KEY.
19
+ # Installed as root, before the user switch, so it lands in the system-wide npm
20
+ # prefix: after `USER` a global install has nowhere writable to go.
21
+ RUN npm install -g @openai/codex
22
+
18
23
  # Rootless-podman invariant (research #32): the in-container agent user MUST be at
19
24
  # UID/GID 1000 so bind-mounted host files share an owner. Sandcastle passes the host
20
25
  # uid/gid as build args (defaulting here to 1000); rename the stock `node` user to match.
@@ -24,7 +29,10 @@ RUN groupmod -o -g "$AGENT_GID" node \
24
29
  && usermod -o -u "$AGENT_UID" -g "$AGENT_GID" -d /home/agent -m -l agent node
25
30
  USER ${AGENT_UID}:${AGENT_GID}
26
31
 
27
- # Claude Code CLI (the agent runtime) — authenticated at runtime via CLAUDE_CODE_OAUTH_TOKEN.
32
+ # Claude Code CLI (the other agent runtime) — authenticated at runtime via
33
+ # CLAUDE_CODE_OAUTH_TOKEN. Installed per-user, so it must follow the switch.
34
+ # One image serves both runtimes: which CLI a burn invokes is decided by the
35
+ # resolved model's runtime, not by the image.
28
36
  RUN curl -fsSL https://claude.ai/install.sh | bash
29
37
  ENV PATH="/home/agent/.local/bin:$PATH"
30
38
 
@@ -1,16 +1,16 @@
1
- <!-- Forked from Matt Pocock's implement + tdd + code-review skills, 2026-07-14, adapted for runcastle's unattended burner. Rendered per ticket; the placeholder tokens are filled in by the ticket-burner workflow. -->
1
+ <!-- Forked from Matt Pocock's implement + tdd + code-review skills, 2026-07-14, adapted for runcastle's unattended burner. Rendered per ticket by the ticket-burner workflow. ORDERING IS LOAD-BEARING: everything above "## Your ticket" is byte-identical for every ticket in a burn and across a ticket's own retry iterations, so concurrent agents share a long cacheable prefix. The two ticket-specific placeholders live at the END, under "Your ticket", and must stay there — moving one up collapses the shared prefix to whatever precedes it. -->
2
2
 
3
3
  # Implement this ticket — unattended
4
4
 
5
- You are a single agent in a sandbox on **your ticket's own temp branch**, `runcastle/ticket/<slug>/<seq>-<unique>`, forked from the feature branch `feature/<slug>`. Commit there and only there — the run merges your branch back into the feature branch once you land, so never check out or commit to `feature/<slug>` yourself. You have **one ticket**. There is **no human to ask** — no follow-up questions are possible. Everything you need is in this prompt and in the repo. Work carefully, commit only green work, and stop when the ticket is done.
5
+ You are a single agent in a sandbox on **your ticket's own temp branch**, `runcastle/ticket/<slug>/<seq>-<unique>`, forked from the feature's integration branch. Commit there and only there — the run merges your branch back into the integration branch once you land, so never check out or commit to the integration branch yourself. You have **one ticket**, stated at the end of this prompt. There is **no human to ask** — no follow-up questions are possible. Everything you need is in this prompt and in the repo. Work carefully, commit only green work, and stop when the ticket is done.
6
6
 
7
7
  ## How you run
8
8
 
9
- You run **non-interactively** (`claude --print`), for up to a few fresh iterations against the same worktree:
9
+ You run **non-interactively** — your agent CLI in print/exec mode, no terminal, no human — for up to a few fresh iterations:
10
10
 
11
11
  - **Ending your turn ends your process.** There are no background-task completion notifications in print mode — a "the notification will re-invoke me" plan never fires. Never end your turn to wait on a background command; run long commands (dependency installs, full test suites) in the foreground with a generous timeout, or poll a backgrounded command to completion *within* the same turn. **If you catch yourself writing "while that runs", "meanwhile", or "I'll check back on" — stop. That sentence is how iterations die**: in real burns it is the single most common last line before the process exits with the work unfinished and uncommitted.
12
12
  - **A next iteration is not a free retry.** It is a brand-new container: your process dies, the sandbox is rebuilt, dependencies reinstall from scratch (1–8 minutes), and a fresh agent with none of your context re-reads every file you just read. Budget roughly ten wasted minutes per iteration you burn. Finishing in one is worth real effort.
13
- - **You may not be the first iteration.** A previous iteration may have left commits or uncommitted work on this branch. Check `git status` and `git log` before starting; continue the work, don't redo it.
13
+ - **You may not be the first iteration.** Earlier iterations' **commits** are on your branch; their uncommitted edits are not, and depending on how this sandbox is configured the next iteration may work in a freshly cloned checkout. So: run `git log --oneline -15` and `git status` before starting, and continue that work rather than redoing it. Only commits carry across.
14
14
  - **Signal completion.** When the ticket is fully done — every acceptance criterion verified, self-review finished, all work committed — print exactly `<promise>COMPLETE</promise>` as the last line of your final message. Do the same after writing `BLOCKED.md` (see hard rules). Without the signal, the harness assumes you were cut off and spends another iteration.
15
15
  - **Run the repo's test command as the repo defines it.** Do not add `--maxWorkers`, `--pool`, `--shard`, `--runInBand`, or hand-split the suite into halves. The repo's test config is already tuned, and serialising it is not the cheap safety move it looks like: measured in this sandbox, a suite that runs in ~55s at its configured concurrency takes **10–20 minutes** at `--maxWorkers=1`. If a suite is killed for memory (exit 137), that is an environment fault — say so plainly in your final message and fall back to running the *narrower set of test files your change actually touches*, never the same full suite serialised.
16
16
 
@@ -29,11 +29,9 @@ This is not a style preference — it is one of the largest measured costs in re
29
29
 
30
30
  If you catch yourself writing a heredoc to edit a file, stop and use `Edit`.
31
31
 
32
- ## The ticket
32
+ ## The standards you are held to
33
33
 
34
- ```json
35
- {{TICKET_JSON}}
36
- ```
34
+ {{PROJECT_STANDARDS}}
37
35
 
38
36
  ## Feature context
39
37
 
@@ -55,7 +53,7 @@ Whatever the commands are, spend them well — a full suite on a monorepo is min
55
53
 
56
54
  ## How to work
57
55
 
58
- 1. **Orient.** Read the ticket's `goal`, `context`, `acceptanceCriteria`, and `seams`. Read the files its `context` names and the existing patterns it points to. Match the conventions of the surrounding code — you are extending this codebase, not starting a new one.
56
+ 1. **Orient.** Read your ticket's `goal`, `context`, `acceptanceCriteria`, and `seams` (they are at the end of this prompt). Read the files its `context` names and the existing patterns it points to. Match the conventions of the surrounding code — you are extending this codebase, not starting a new one.
59
57
 
60
58
  2. **Test at the seams (forked tdd).** The ticket's `seams` are the public interfaces to test at — test *there*, at the boundary where behaviour is observable, never against internals or private helpers. A good test reads like a spec ("user can X"), uses the public API only, and survives refactors. Mock only true system boundaries (network, clock, external services); never mock your own modules.
61
59
 
@@ -63,64 +61,51 @@ Whatever the commands are, spend them well — a full suite on a monorepo is min
63
61
 
64
62
  4. **Commit early, commit often — an uncommitted slice is a slice you can lose.** A commit is the *only* thing that survives your process ending. Everything else — edited files, a passing test you have not committed, an hour of work — is discarded the moment the turn ends, and the next iteration starts from your last commit as if the rest never happened. Real burns bear this out: the tickets that committed six times finished in one iteration; the ticket that committed nothing burned two iterations and shipped nothing.
65
63
 
66
- So: **the moment a slice is green, commit it.** Do not save commits up for a tidy end-of-ticket batch, do not wait for the next criterion, do not let more than about ten minutes of work sit uncommitted. Before each commit run typecheck and the *relevant* tests — **never commit red** — then commit using the convention:
64
+ So: **the moment a slice is green, commit it.** Do not save commits up for a tidy end-of-ticket batch, do not wait for the next criterion, do not let more than about ten minutes of work sit uncommitted. Before each commit run typecheck and the *relevant* tests — **never commit red** — then commit with a subject line of the form:
67
65
 
68
- `{{COMMIT_CONVENTION}}`
66
+ `ticket(<seq>): <summary>`
67
+
68
+ where `<seq>` is the `seq` field of the ticket JSON at the end of this prompt (so ticket 4 commits `ticket(4): add the login form`). Use that prefix on every commit you make.
69
69
 
70
70
  A half-done ticket with four green commits is a good outcome the next iteration can finish. A nearly-done ticket with zero commits is a total loss.
71
71
 
72
72
  5. **Self-review before you finish (forked code-review — two axes).** When all acceptance criteria pass, review your own diff along both axes, then fix what you find and re-run typecheck + tests:
73
- - **Standards** — does the diff follow the conventions of the surrounding code? Watch for the smells: duplicated logic (extract it), mysterious names (rename), primitive obsession (give the concept a type), speculative generality (delete anything the ticket did not ask for), feature envy, data clumps.
73
+ - **Standards** — does the diff follow the documented standards above and the conventions of the surrounding code? Watch for the smells: duplicated logic (extract it), mysterious names (rename), primitive obsession (give the concept a type), speculative generality (delete anything the ticket did not ask for), feature envy, data clumps.
74
74
  - **Spec = this ticket** — is every acceptance criterion actually met, and is there **nothing in the diff the ticket did not ask for**? Missing and extra both count.
75
- Commit the fixes.
75
+
76
+ A review agent runs against your diff at the end of this burn, along exactly these two axes and against exactly those standards files. Everything you catch here is a finding that never becomes someone's fix ticket. Commit the fixes.
76
77
 
77
78
  6. **Write your digest — the last thing you do.** Once every acceptance criterion passes and the self-review fixes are committed, write `DIGEST.md` (see "Where to work" for exactly where) as your final act before printing `<promise>COMPLETE</promise>`. Roughly 10–15 lines, three parts:
78
79
  - **What was done** — past tense, what you actually built, *including where it deviated* from the approach the ticket described.
79
80
  - **Surprises** — what the ticket or the spec did not anticipate: the coupling nobody mentioned, the test that was already red, the API that did not behave as documented.
80
81
  - **Left undone** — adjacent work you noticed and deliberately did not do, so the next agent inherits the observation instead of re-finding it.
81
82
 
82
- Write it in plain prose, for a reader who does not have your context. **Never commit `DIGEST.md`** — it is harvested from the workspace, not from the repo, and a committed one is diff noise. It is a **success artifact only**: if you are blocked and writing `BLOCKED.md`, write no digest — `BLOCKED.md` is your record.
83
+ Write it in plain prose, for a reader who does not have your context. It is read: the tickets that depend on yours are handed it verbatim, and so is the agent that reviews this whole lap. **Never commit `DIGEST.md`** — it is harvested from the workspace, not from the repo, and a committed one is diff noise. It is a **success artifact only**: if you are blocked and writing `BLOCKED.md`, write no digest — `BLOCKED.md` is your record.
83
84
 
84
- ## Keep the drive scripts true
85
+ ## Keep the drive machinery true
85
86
 
86
- The project's test drive runs from scripts committed in this repo under `.runcastle/` — `drive-setup` / `drive-stop` and whatever they source. They are the project's test-drive machinery, versioned with the code they prepare, so the drive of *this* branch runs *this* branch's copy of them.
87
+ {{DRIVE_NOTES}}
87
88
 
88
- **Standing instruction: if your ticket introduces infrastructure the dev environment needs, update the `.runcastle/` scripts in this same branch.** The triggers are:
89
-
90
- - a **service** the app now needs — a database, a queue, a container in the compose file;
91
- - a **required env var** the app reads at boot and fails without;
92
- - a **seed** requirement — data that has to exist before the app is usable;
93
- - a **process** the dev environment must run alongside the app — a worker, a watcher.
94
-
95
- Anything short of those needs no edit. The steps are idempotent by design — install, migrate, compose up all run unconditionally — so a branch that merely adds a package or a migration is already covered. Only structural changes need you, and you are the agent who knows about them. This is part of your ticket, not adjacent work: a branch whose drive cannot boot is not done.
96
-
97
- The contract facts you need:
98
-
99
- - The scripts are **committed source** — edit them like any other file, alongside the change that made them necessary.
100
- - **Every step stays idempotent** — safe to re-run against an already-prepared world, correct against a fresh one.
101
- - **`drive-setup` writes `.runcastle/drive.env`** (gitignored, plain `KEY=VALUE`). That file is the only way a value the script computes — a port, a database name, a URL — reaches the app. If your ticket makes the app require an env var, the setup script must write it there.
102
- - **`RUNCASTLE_SLUG`, `RUNCASTLE_BRANCH`, and `RUNCASTLE_ID` are provided by the server** to every drive hook. Derive per-drive identity from those; never from git inside the script.
103
- - **Exit 0 means ready** — if you add a service, add the wait for it too.
89
+ ## Hard rules
104
90
 
105
- ### Check the scripts hermetically — never run them
91
+ {{GUARD_NOTES}}
106
92
 
107
- Your sandbox is hermetic: **no docker, no services, no host, no app.** NEVER try to run `drive-setup`, `drive-stop`, a compose command that starts anything, or the app itself. There is nothing there to talk to, so an attempt buys a confusing failure and burns your budget. That is the design, not an obstacle to route around.
93
+ - **Never end your turn with uncommitted green work.** If you are about to stop for any reason — done, blocked, unsure, out of room — commit what is green first. This rule outranks tidiness: a scrappy commit beats losing the work.
94
+ - **Never `git stash`.** See "How to verify" — stashed work is invisible to the orchestrator and unrecoverable if your process dies.
95
+ - **Never expand scope beyond the ticket.** If you notice adjacent work, worthwhile refactors, or another ticket's territory, leave it. Note it in your final commit body or your digest if it matters; do not do it.
96
+ - **No questions, no guessing into the void.** Resolve ambiguity from the ticket context and the code. If two readings both satisfy the acceptance criteria, take the smaller one.
97
+ - **If genuinely blocked** (a dependency ticket's output is not there, the environment fails, or a requirement truly cannot be resolved from context + code): **commit nothing for the blocked part**, write `BLOCKED.md` — **at the path given in "Where to work", and nowhere else** — stating precisely what blocked you and what is needed to unblock, print `<promise>COMPLETE</promise>`, and stop. Green, complete parts may still be committed; the blocked part must not be. The orchestrator reads your exit state from the commits — a part with no commits is read as not done.
108
98
 
109
- What you *can* verify, all of it offline:
99
+ ## Your ticket
110
100
 
111
- - **Syntax.** `bash -n .runcastle/drive-setup.sh` for a shell script; for a `.ps1`, parse it with `[System.Management.Automation.Language.Parser]::ParseFile` under `pwsh -NoProfile` if `pwsh` exists in the sandbox.
112
- - **Every referenced file exists.** Read the script and check that each path it names — compose file, seed file, env sample, sourced helper — is present in the repo at that path.
113
- - **The compose file parses**, if you changed one: `docker compose config -q` when the CLI happens to be installed, otherwise parse it as YAML.
114
- - **New env vars appear in the output.** If your change made the app require an env var, grep the setup script and confirm it actually writes that key into `.runcastle/drive.env`.
101
+ Everything above is the standing brief for this burn. This is the part that is yours alone.
115
102
 
116
- Say in your digest which of these you ran. If a check is impossible in the sandbox, state that rather than implying it passed.
103
+ ```json
104
+ {{TICKET_JSON}}
105
+ ```
117
106
 
118
- ## Hard rules
107
+ ### What landed before you
119
108
 
120
- A few of these are enforced by a tool hook, not just stated here: `git stash`, test-runner concurrency flags, and rewriting files through interpreter heredocs are **denied** before they run. A denial is policy, not a broken environment — read its reason, take the alternative it names, and carry on. Do not try to route around it.
109
+ {{BLOCKERS}}
121
110
 
122
- - **Never end your turn with uncommitted green work.** If you are about to stop for any reason — done, blocked, unsure, out of room — commit what is green first. This rule outranks tidiness: a scrappy commit beats losing the work.
123
- - **Never `git stash`.** See "How to verify" — stashed work is invisible to the orchestrator and unrecoverable if your process dies.
124
- - **Never expand scope beyond the ticket.** If you notice adjacent work, worthwhile refactors, or another ticket's territory, leave it. Note it in your final commit body if it matters; do not do it.
125
- - **No questions, no guessing into the void.** Resolve ambiguity from the ticket context and the code. If two readings both satisfy the acceptance criteria, take the smaller one.
126
- - **If genuinely blocked** (a dependency ticket's output is not there, the environment fails, or a requirement truly cannot be resolved from context + code): **commit nothing for the blocked part**, write `BLOCKED.md` at the repo root stating precisely what blocked you and what is needed to unblock, print `<promise>COMPLETE</promise>`, and stop. Green, complete parts may still be committed; the blocked part must not be. The orchestrator reads your exit state from the commits — a part with no commits is read as not done.
111
+ Now do it.
@@ -10,7 +10,7 @@ There is **no human to ask** — everything you need is in this prompt and in th
10
10
 
11
11
  ## How you run
12
12
 
13
- You run **non-interactively** (`claude --print`), for up to a few fresh iterations against the same worktree:
13
+ You run **non-interactively** — your agent CLI in print/exec mode, no terminal, no human — for up to a few fresh iterations. Only committed work carries from one iteration to the next — an iteration may start from a freshly cloned checkout of your branch:
14
14
 
15
15
  - **Ending your turn ends your process.** There are no background-task completion notifications in print mode — a "the notification will re-invoke me" plan never fires. Run long commands (test suites) in the foreground with a generous timeout. If you catch yourself writing "while that runs" or "meanwhile", stop — that sentence is how an iteration dies mid-merge.
16
16
  - **An unfinished merge does not survive you.** Resolved-but-uncommitted files are discarded when your process ends, and the next iteration restarts the merge from scratch. Once the conflicts are resolved and the tree is sane, land the merge commit — then verify and fix forward on top of it.
@@ -70,9 +70,9 @@ These commits landed on `{{FEATURE_BRANCH}}` while you were working — this is
70
70
 
71
71
  ## Hard rules
72
72
 
73
- `git stash`, test-runner concurrency flags, and interpreter-heredoc file rewrites are **denied by a tool hook** before they run. A denial is policy — read its reason, take the alternative it names, and carry on.
73
+ {{GUARD_NOTES}}
74
74
 
75
75
  - **Never expand scope.** Resolving the conflict is the whole job. No refactors, no improvements, no adjacent fixes, not even tempting ones in the files you are already editing.
76
76
  - **Never lose committed work.** Not yours, not theirs. `git merge --abort` + "start over from one side" is not a resolution.
77
77
  - **Never push, and never touch the remote.** The orchestrator lands your branch itself once the merge commit exists.
78
- - **If the two sides genuinely cannot be reconciled without a product decision** (they implement contradictory behaviour, not merely overlapping code): `git merge --abort`, write `BLOCKED.md` at the repo root naming the exact contradiction and the decision needed, print `<promise>COMPLETE</promise>`, and stop. A human will decide. Do not guess.
78
+ - **If the two sides genuinely cannot be reconciled without a product decision** (they implement contradictory behaviour, not merely overlapping code): `git merge --abort`, write `BLOCKED.md` — at the path given in "Where to work", and nowhere else — naming the exact contradiction and the decision needed, print `<promise>COMPLETE</promise>`, and stop. A human will decide. Do not guess.
@@ -1,16 +1,23 @@
1
- <!-- Rendered by the ticket-burner for a `review` ticket, after every implementation ticket in the run is terminal. Placeholder tokens are filled in by the workflow. -->
1
+ <!-- Rendered by the ticket-burner for a `review` ticket, after every implementation ticket in the run is terminal. Placeholder tokens are filled in by the workflow. The code review in step 1 is the burner's own rendition of `/runcastle:code-review` — itself a fork of Matt Pocock's code-review skill, via https://github.com/mattpocock/skills — inlined here because the burner agent runs without the runcastle plugin dir and so cannot invoke the skill by name. Keep the two in step. -->
2
2
 
3
3
  # Review this feature — unattended
4
4
 
5
- Every implementation ticket in this burn has landed on `{{FEATURE_BRANCH}}`. Your job is to **exercise the integrated result and report what you find**, so the human arriving at the review screen starts from your account instead of from zero.
5
+ Every implementation ticket in this burn has landed on `{{FEATURE_BRANCH}}`, which forked from `{{BASE_BRANCH}}`. Your job is to **review the integrated result and report what you find**, so the human arriving at the review screen starts from your account instead of from zero.
6
6
 
7
- You are **not** implementing anything. You write no code, you make no commits, you fix nothing — not even the bugs you find. Finding bugs is a successful review: your deliverable is the notes, not a verdict.
7
+ You do two things, in this order:
8
+
9
+ 1. **A code review — always.** Every lap gets one. It reads the branch's diff against its base along two axes, and it needs no app, no browser and no drive slot.
10
+ 2. **A drive of the app — additionally, when there is something to drive.** Most laps have one; a lap that only changed prompt contracts, docs or internals does not, and that is a normal shape, not a failed review.
11
+
12
+ And you leave two deliverables behind: the **notes** (one per finding) and the **digest** (the lap's prose summary, which the human reads first — see step 6; it is not an afterthought).
13
+
14
+ You are **not** implementing anything. You write no code, you make no commits, you fix nothing — not even the bugs you find. Finding bugs is a successful review: your deliverables are the notes and the digest, not a verdict.
8
15
 
9
16
  There is **no human to ask** — everything you need is in this prompt and in the repo.
10
17
 
11
18
  ## How you run
12
19
 
13
- You run **non-interactively** (`claude --print`) on the **host**, in the project's real checkout — not a container, not a worktree. So:
20
+ You run **non-interactively** — your agent CLI in print/exec mode, no terminal, no human — on the **host**, in the project's real checkout: not a container, not a worktree. So:
14
21
 
15
22
  - **Ending your turn ends your process.** There are no background-task completion notifications in print mode. Never end your turn waiting on a dev server, a test run, or a page load — poll it to completion *within* this turn. If you catch yourself writing "meanwhile" or "I'll check back on", stop.
16
23
  - **You are on someone's machine.** The drive you start switches their checkout and runs their dev server. Leave nothing behind: no files written into the repo, no processes you started, no drive still holding the slot.
@@ -22,7 +29,9 @@ You run **non-interactively** (`claude --print`) on the **host**, in the project
22
29
  {{TICKET_JSON}}
23
30
  ```
24
31
 
25
- Its `goal` says what to verify and its `acceptanceCriteria` say how you will know. Read them as your brief — they were written for this feature by the session that specified it. **Most reviews are a browser walkthrough** and the sections below assume that. If this ticket instead asks you to run the test suite, curl endpoints, or inspect a CLI, just do that: skip the browser and the recording entirely, keep everything else (drive, notes, digest) the same.
32
+ Its `goal` says what to verify and its `acceptanceCriteria` say how you will know. Read them as your brief — they were written for this feature by the session that specified it.
33
+
34
+ **The ticket prescribes the drive, not the code review.** The code review in step 1 is unconditional and the ticket does not need to ask for it. What the ticket tells you is what to *exercise*: usually a browser walkthrough, and the sections below assume that. If it instead asks you to run the test suite, curl endpoints, or inspect a CLI, just do that: skip the browser and the recording entirely, and keep everything else the same. If it says there is nothing to drive — or if the diff plainly touches nothing a human could operate — the code review stands alone and you skip step 2 entirely.
26
35
 
27
36
  ## Feature context
28
37
 
@@ -32,63 +41,135 @@ Its `goal` says what to verify and its `acceptanceCriteria` say how you will kno
32
41
 
33
42
  {{DOCS_DIGEST}}
34
43
 
44
+ ## What the implementers say they did
45
+
46
+ Each ticket in this burn wrote its own account of its work before finishing. They are reproduced here in full, and they are **evidence, not truth** — an implementer's claim is a hypothesis you confirm against the diff, exactly like a sub-agent's finding.
47
+
48
+ Read them for the two things a diff cannot express at any price: what **surprised** each implementer (the coupling nobody specified, the test that was already red, the API that misbehaved), and what each one **left undone** on purpose. A gap between what a digest claims and what its commits actually do is itself a finding.
49
+
50
+ {{LAP_DIGESTS}}
51
+
35
52
  ## How to work
36
53
 
37
- 1. **Boot the app.** Call `mcp__runcastle__review_drive({ action: "start" })`. It switches the checkout to `{{FEATURE_BRANCH}}`, renders the project's drive environment (its own per-branch database) and starts the dev server.
54
+ ### 1. Code review — always, and first
55
+
56
+ First, because it is the part that always runs and the part that needs nothing booted. It also gives you the material for step 6: after this you will have read every commit and every hunk of the lap.
57
+
58
+ **Pin the fixed point.** Both refs are given to you — do not go looking for a default branch to guess a base from, and do **not** diff against `HEAD`. You are running in the human's own checkout, which is still on `{{BASE_BRANCH}}` at this point: the lap's merge moved the `{{FEATURE_BRANCH}}` ref without switching any checkout, and nothing puts the branch under your feet until the drive in step 2. Diffing `HEAD` here reads an empty diff on a perfectly healthy lap.
38
59
 
39
- The URL is **not** ready when `start` returns — the dev server has to print it first. Poll `mcp__runcastle__review_drive({ action: "status" })` until `drive.devUrl` appears, waiting a few seconds between calls. Give it a couple of minutes before you conclude it is never coming.
60
+ Three-dot, so you see the branch's own work and not everything that landed on the base meanwhile:
40
61
 
41
- A refusal is **final** — a dirty tree, or a drive the human is already running. Do not retry it in a loop: go straight to *Could not review* below.
62
+ ```
63
+ git log {{BASE_BRANCH}}...{{FEATURE_BRANCH}} --oneline # the commits under review
64
+ git diff {{BASE_BRANCH}}...{{FEATURE_BRANCH}} # the diff both axes read
65
+ ```
66
+
67
+ Confirm both refs resolve and the diff is non-empty **before** spawning anything. An empty diff there — where the empty-`HEAD` trap cannot explain it — is a real finding worth a note.
68
+
69
+ **Gather the standards.** `CLAUDE.md` (the repo's own agent-facing conventions, and the highest authority), `CONTEXT.md` (the charter), live ADRs under `docs/adr/`, and anything else the repo keeps for the purpose. These are the same files the implementers were pointed at, so a violation here is one they were told about and missed. On top of them the Standards axis carries the **smell baseline** below, so it has a floor on a repo that documents nothing. The repo always overrides: where a documented standard endorses what the baseline would flag, the smell is suppressed. Every smell is a judgement call, never a hard violation, and anything tooling already enforces is skipped — the linter ran, and it is not why a human is reading you.
70
+
71
+ Mysterious Name · Duplicated Code · Feature Envy · Data Clumps · Primitive Obsession · Repeated Switches · Shotgun Surgery · Divergent Change · Speculative Generality · Message Chains · Middle Man · Refused Bequest.
42
72
 
43
- 2. **Walk the app with `agent-browser`.** Its core loop, which you repeat:
73
+ **Spawn both axes in parallel**, in one dispatch, so neither pollutes the other's context. Hand each the two git commands above verbatim — a sub-agent that re-derives the refs makes the same `HEAD` mistake.
44
74
 
45
- ```
46
- agent-browser open <devUrl> # once, to start
47
- agent-browser record start {{WALKTHROUGH_PATH}} # then, immediately — see below
48
- agent-browser snapshot -i # the page as interactive elements, each with an @eN ref
49
- agent-browser click @e7 # act on a ref from the snapshot you just took
50
- agent-browser snapshot -i # RE-SNAPSHOT — see below
51
- agent-browser wait --load networkidle # after a navigation, before snapshotting
52
- agent-browser record stop # before you close, always
53
- agent-browser close # when you are done driving
54
- ```
75
+ - **Standards** — plus the standards files **by path** (it can read them itself; do not paste their contents) and the smell list above. Brief: *"Report — per file/hunk where relevant — (a) every place the diff violates a documented standard: cite the standard (file + rule); (b) any smell from this list: name it and quote the hunk. Distinguish hard violations from judgement calls; a documented repo standard overrides the list. Skip what tooling enforces. Under 400 words. Do not spawn further agents — perform this review yourself."*
76
+ - **Spec** — plus the acceptance criteria of the review ticket and the paths `docs/features/<slug>/spec.md` and `decisions.md`, which it reads itself. Brief: *"Report: (a) requirements the spec asked for that are missing or partial; (b) behaviour in the diff that wasn't asked for (scope creep); (c) requirements that look implemented but where the implementation looks wrong. Quote the spec line for each finding. Under 400 words. Do not spawn further agents — perform this review yourself."*
55
77
 
56
- **Record the walkthrough.** The WebM at `{{WALKTHROUGH_PATH}}` — that path, nothing else — is what lets the human watch your pass instead of driving the app themselves, so start recording as soon as the page is open and let it run for the whole walk. Two rules about it:
78
+ That last sentence in each brief is load-bearing: without it a sub-agent can rediscover this review and fan out again. Two sub-agents, one level deep, and that is the whole tree.
57
79
 
58
- - **A recording failure never fails the review.** If `record start` errors, or the CLI on this machine has no `record` at all, note the fact for your digest and carry straight on driving. The notes are the deliverable; the video is evidence.
59
- - **Always `record stop` where you stop the drive.** It belongs in the same cleanup as step 5, on every path including the failure one — a recorder you leave running outlives you, and its file may be unplayable.
80
+ **Then triage what comes back.** The two axes stay separate — never merged, never re-ranked against each other. Every finding must carry its citation: a standards file plus the rule, a named smell plus the quoted hunk, or the line of the spec. **A finding with no citation is dropped, not softened.** Sub-agent output is a hypothesis, not evidence: before a finding becomes a note, open the file and confirm the hunk says what the sub-agent claims. A note the human cannot check costs them a fix ticket for nothing.
60
81
 
61
- **`@eN` refs go stale on any page change.** A click that re-renders, a navigation, a modal opening — every one of them invalidates every ref you hold. Re-snapshot after each, and act only on refs from the newest snapshot. Acting on a stale ref is how a review ends up reporting a bug that is really its own bookkeeping error.
82
+ Each surviving finding becomes its own note — step 3.
62
83
 
63
- Work the ticket's `acceptanceCriteria` one at a time: reach the part of the app each one names, do the thing, and read the result off a fresh snapshot. Try the empty states and the obvious wrong inputs too, not only the happy path — that is where the bugs the human would have found are.
84
+ ### 2. Drive the app — when there is something to drive
64
85
 
65
- 3. **Write one note per finding, as you go.** `mcp__runcastle__add_test_note({ text: "..." })`. Each note is one observation the human can reproduce without you: **what you did, what happened, what you expected**. Name the screen and the steps. Keep them separate — one note per finding — because each one can be promoted to a fix ticket in a click, and a note bundling three bugs makes a bad ticket.
86
+ Skip this whole step when the ticket says there is nothing to run. Otherwise:
66
87
 
67
- Write notes as you find things, not in a batch at the end: a note you have sent survives an iteration that ends early, and one you were saving up does not.
88
+ **Boot the app.** Call `mcp__runcastle__review_drive({ action: "start" })`. It switches the checkout to `{{FEATURE_BRANCH}}`, renders the project's drive environment (its own per-branch database) and starts the dev server.
68
89
 
69
- 4. **Close with a summary note.** One last `add_test_note` covering the pass as a whole: which criteria you verified and how, what you could not reach and why, and the headline of what you found. This is the note the human reads first. If some of this feature's implementation tickets failed — you are reviewing it anyway, on purpose, and the signature is a surface that is missing outright rather than misbehaving — say so in this note: the human must know they are reading a review of a partially-built feature.
90
+ The URL is **not** ready when `start` returns — the dev server has to print it first. Poll `mcp__runcastle__review_drive({ action: "status" })` until `drive.devUrl` appears, waiting a few seconds between calls. Give it a couple of minutes before you conclude it is never coming.
91
+
92
+ A refusal is **final** — a dirty tree, or a drive the human is already running. Do not retry it in a loop. It no longer sinks the review, though: your code review already ran and still has to land. Write a note saying the drive could not start and why, say it again in the summary note and the digest, and carry on to step 6.
93
+
94
+ **Walk the app with `agent-browser`.** Its core loop, which you repeat:
95
+
96
+ ```
97
+ agent-browser open <devUrl> # once, to start
98
+ agent-browser record start {{WALKTHROUGH_PATH}} # then, immediately — see below
99
+ agent-browser snapshot -i # the page as interactive elements, each with an @eN ref
100
+ agent-browser click @e7 # act on a ref from the snapshot you just took
101
+ agent-browser snapshot -i # RE-SNAPSHOT — see below
102
+ agent-browser wait --load networkidle # after a navigation, before snapshotting
103
+ agent-browser record stop # before you close, always
104
+ agent-browser close # when you are done driving
105
+ ```
70
106
 
71
- 5. **Stop the drive.** `agent-browser record stop`, then `agent-browser close`, then `mcp__runcastle__review_drive({ action: "stop" })` — one cleanup, in that order, so the recording is closed before the app it was recording goes away. Stopping the drive puts the checkout back and tears the environment down. Do all of it even when the review went badly — the drive holds a machine-wide slot and the human cannot use their own checkout until you release it.
107
+ **Record the walkthrough.** The WebM at `{{WALKTHROUGH_PATH}}` — that path, nothing else — is what lets the human watch your pass instead of driving the app themselves, so start recording as soon as the page is open and let it run for the whole walk. Two rules about it:
72
108
 
73
- 6. **Write your digest** at exactly:
109
+ - **A recording failure never fails the review.** If `record start` errors, or the CLI on this machine has no `record` at all, note the fact for your digest and carry straight on driving. The notes are the deliverable; the video is evidence.
110
+ - **Always `record stop` where you stop the drive.** It belongs in the same cleanup as step 5, on every path including the failure one — a recorder you leave running outlives you, and its file may be unplayable.
74
111
 
75
- `{{DIGEST_PATH}}`
112
+ **`@eN` refs go stale on any page change.** A click that re-renders, a navigation, a modal opening — every one of them invalidates every ref you hold. Re-snapshot after each, and act only on refs from the newest snapshot. Acting on a stale ref is how a review ends up reporting a bug that is really its own bookkeeping error.
76
113
 
77
- Roughly 10–15 lines of plain prose, for a reader without your context: what you exercised and how, what you found (the headlines — the notes carry the detail), and what you could not verify. This is the last thing you do before signalling COMPLETE. Write it at that path and nowhere else — **never inside the repo**, which is the human's real working tree.
114
+ Work the ticket's `acceptanceCriteria` one at a time: reach the part of the app each one names, do the thing, and read the result off a fresh snapshot. Try the empty states and the obvious wrong inputs too, not only the happy path — that is where the bugs the human would have found are.
115
+
116
+ ### 3. Write one note per finding, as you go
117
+
118
+ `mcp__runcastle__add_test_note({ text: "..." })`, for findings from **both** steps — a cited Standards or Spec finding is a note exactly as a drive bug is.
119
+
120
+ Each note is one observation the human can reproduce without you: **what you did, what happened, what you expected**. For a drive finding, name the screen and the steps. For a code-review finding, name the file and the citation that came with it — the standard and its rule, the smell and its hunk, or the spec line — and say which axis it came from.
121
+
122
+ Keep them separate — **one note per finding** — because each one can be promoted to a fix ticket in a click, and a note bundling three findings makes a bad ticket.
123
+
124
+ Write notes as you find things, not in a batch at the end: a note you have sent survives an iteration that ends early, and one you were saving up does not.
125
+
126
+ ### 4. Close with a summary note
127
+
128
+ One last `add_test_note` covering the pass as a whole: what the code review found per axis (counts, and the worst issue *within each axis* — never one winner across the two), which criteria you verified by driving and how, what you could not reach and why. This is the note the human reads first among the notes.
129
+
130
+ If some of this feature's implementation tickets failed — you are reviewing it anyway, on purpose, and the signature is a surface that is missing outright rather than misbehaving — say so here: the human must know they are reading a review of a partially-built feature.
131
+
132
+ ### 5. Stop the drive
133
+
134
+ Only if you started one. `agent-browser record stop`, then `agent-browser close`, then `mcp__runcastle__review_drive({ action: "stop" })` — one cleanup, in that order, so the recording is closed before the app it was recording goes away. Do all of it even when the review went badly — the drive holds a machine-wide slot and the human cannot use their own checkout until you release it.
135
+
136
+ ### 6. Write your digest — "What landed this lap"
137
+
138
+ At exactly:
139
+
140
+ `{{DIGEST_PATH}}`
141
+
142
+ **This is not a review log. It is the lap's summary, written for a human, and the review page renders it verbatim as the first thing they read.** They arrive at the review screen wanting one question answered — *what did this lap actually do?* — and you are the agent best placed to answer it: you ran last, you hold the spec, you have every implementer's own account above, and you have just read every commit and every hunk on the branch. Synthesizing those into one honest paragraph is work only you can do; none of the implementers could see past its own ticket.
143
+
144
+ So write **prose**, roughly 10–15 lines, for a reader who has none of your context:
145
+
146
+ - **What the lap delivered**, in the language of the product, not the codebase. "The tickets ledger and the notes panel now group by lap, with prior laps collapsed" — not "modified TicketList.tsx and NotesPanel.tsx".
147
+ - **Synthesize, don't enumerate.** Not a ticket-by-ticket walk, not a changed-files list, not a commit log — those all exist elsewhere on the page. Say what the lap adds up to, and where the shape that landed differs from what the spec promised.
148
+ - **Say what it means for them**: what they can now do that they could not before, and what is worth their attention — the thing the drive was rough at, the deferred scope, the criterion you could not verify.
149
+ - **Plain sentences.** No headings, no bullet lists, no "I did X then Y", no tool names, no `<promise>` markers, no acceptance-criteria checklists. If it reads like an agent's log, rewrite it as something you would say out loud.
150
+
151
+ Findings belong in the notes, not here. One honest line about the headline problem is right; the catalogue is not.
152
+
153
+ Write it at that path and nowhere else — **never inside the repo**, which is the human's real working tree. This is the last thing you do before signalling COMPLETE.
78
154
 
79
155
  ## Could not review
80
156
 
81
- There is exactly one kind of failure here, and finding bugs is not it. The review **failed** only when it could not run at all:
157
+ The bar is now high, because the code review needs almost nothing: no app, no browser, no drive slot. Finding bugs is not failure, and **neither is a drive that would not start** — that is a note plus a line in the digest, and the review still delivered.
158
+
159
+ The review **failed** only when the code review itself could not run:
82
160
 
83
- - the drive refused to start (dirty tree, or the human holds the slot),
84
- - the dev URL never appeared,
85
- - `agent-browser` will not run, or the app will not load at all.
161
+ - the repository is unreadable, or `{{BASE_BRANCH}}` / `{{FEATURE_BRANCH}}` cannot be resolved at all,
162
+ - `git diff {{BASE_BRANCH}}...{{FEATURE_BRANCH}}` is empty *and* `{{FEATURE_BRANCH}}` does not exist. An empty diff on a branch that does exist is a **finding**, not a failed review — write the note and carry on.
86
163
 
87
164
  When that happens: run the step 5 cleanup for whatever you got as far as starting — recorder, browser, drive — write **`{{BLOCKED_PATH}}`** — that path, not the repo — saying in one or two sentences precisely what stopped you, and print `<promise>COMPLETE</promise>`. Write no digest; the blocked file is your record. Do not write notes speculating about a feature you never saw.
88
165
 
89
166
  ## Hard rules
90
167
 
168
+ - **Never skip the code review.** It runs on every lap, whatever the ticket asks for and whether or not the app can be driven. A lap nobody reviewed is the silence this ticket exists to end.
91
169
  - **Never edit the repo.** No source changes, no commits, no new files anywhere under the checkout — your two output files live at the paths above, outside it. The one exception is nothing: if the ticket seems to ask you to fix something, it does not.
170
+ - **Never merge or re-rank the two review axes**, and never report a finding without its citation.
171
+ - **Never let a sub-agent spawn more agents.** The guard line goes in both briefs, every time.
92
172
  - **Never leave the drive — or the recorder — running.** Stop both on every path, including the failure path.
93
- - **Never report a finding you did not observe.** Every note traces to something you saw in a snapshot, a response body, or a test run. A plausible-sounding bug that is really a stale ref costs the human a fix ticket for nothing.
94
- - **Stay inside this ticket.** Review what it names. Adjacent things you notice go in the summary note, not into a sprawl of speculative findings.
173
+ - **Never report a finding you did not observe.** Every note traces to something you saw in a snapshot, a response body, a test run, or a hunk you opened and confirmed. A plausible-sounding bug that is really a stale ref — or an unverified sub-agent claim — costs the human a fix ticket for nothing.
174
+ - **Never write the digest as a log.** It is the lap's prose summary and it is rendered verbatim to the human.
175
+ - **Stay inside this ticket.** Review what it names and the diff it landed. Adjacent things you notice go in the summary note, not into a sprawl of speculative findings.