runcastle 1.3.6 → 1.3.10

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 (36) hide show
  1. package/bin/runcastle.js +4605 -2723
  2. package/drizzle/0037_amusing_steve_rogers.sql +7 -0
  3. package/drizzle/0038_project_docs_commit_prefix.sql +1 -0
  4. package/drizzle/0039_chat_session_kind.sql +3 -0
  5. package/drizzle/0039_review_verdict.sql +5 -0
  6. package/drizzle/0040_new_maestro.sql +10 -0
  7. package/drizzle/0041_round_harpoon.sql +2 -0
  8. package/drizzle/meta/0037_snapshot.json +1118 -0
  9. package/drizzle/meta/0040_snapshot.json +1212 -0
  10. package/drizzle/meta/0041_snapshot.json +1226 -0
  11. package/drizzle/meta/_journal.json +35 -0
  12. package/index.js +4599 -2718
  13. package/package.json +2 -1
  14. package/sandcastle-template/Containerfile +12 -3
  15. package/sandcastle-template/Dockerfile +12 -3
  16. package/skills/burner/implement-ticket.md +4 -1
  17. package/skills/burner/review-ticket.md +21 -3
  18. package/skills/burner/verify-fixes.md +11 -3
  19. package/skills/packs/README.md +42 -28
  20. package/skills/packs/runcastle/skills/code-review/SKILL.md +10 -0
  21. package/skills/packs/runcastle/skills/converge/SKILL.md +8 -6
  22. package/skills/packs/runcastle/skills/ideate/SKILL.md +5 -5
  23. package/skills/packs/runcastle/skills/prepare/SKILL.md +1 -1
  24. package/skills/packs/runcastle/skills/project/SKILL.md +26 -11
  25. package/skills/packs/runcastle/skills/project/references/health-sweeps.md +7 -4
  26. package/skills/packs/runcastle/skills/project/references/triage.md +183 -0
  27. package/skills/packs/runcastle/skills/qa/SKILL.md +4 -3
  28. package/skills/packs/runcastle/skills/revisit/SKILL.md +28 -15
  29. package/skills/packs/runcastle/skills/spec/SKILL.md +2 -2
  30. package/skills/packs/runcastle/skills/tickets/SKILL.md +22 -6
  31. package/skills/packs/runcastle/skills/waypoint/SKILL.md +2 -2
  32. package/web/assets/index-L11zqTP_.css +1 -0
  33. package/web/assets/index-x1_wkP9-.js +187 -0
  34. package/web/index.html +2 -2
  35. package/web/assets/index-D697YlOb.css +0 -1
  36. package/web/assets/index-jbVdnCzO.js +0 -186
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runcastle",
3
- "version": "1.3.6",
3
+ "version": "1.3.10",
4
4
  "description": "Burn tickets into shipped features with Claude Code.",
5
5
  "license": "FSL-1.1-ALv2",
6
6
  "type": "module",
@@ -29,6 +29,7 @@
29
29
  "sandcastle-template"
30
30
  ],
31
31
  "dependencies": {
32
+ "@anthropic-ai/claude-agent-sdk": "^0.3.281",
32
33
  "@clack/prompts": "^1.1.0",
33
34
  "@hono/mcp": "0.3.1",
34
35
  "@hono/trpc-server": "0.4.2",
@@ -18,7 +18,12 @@ RUN curl -fsSL https://bun.sh/install | bash
18
18
  # Codex CLI (the second agent runtime) — authenticated at runtime via CODEX_API_KEY.
19
19
  # Installed as root, before the user switch, so it lands in the system-wide npm
20
20
  # prefix: after `USER` a global install has nowhere writable to go.
21
- RUN npm install -g @openai/codex
21
+ # Pinned to the host's `codex --version`, passed in by Rebuild. The ARG sits
22
+ # directly above its RUN on purpose: a changed version busts the cache for this
23
+ # layer and the ones after it, and nothing before it. Empty installs latest.
24
+ ARG CODEX_VERSION
25
+ RUN npm install -g @openai/codex@${CODEX_VERSION:-latest} \
26
+ || { echo "runcastle: Codex CLI ${CODEX_VERSION:-latest} install failed — is that version published?" >&2; exit 1; }
22
27
 
23
28
  # Rootless-podman invariant (research #32): the in-container agent user MUST be at
24
29
  # UID/GID 1000 so bind-mounted host files share an owner. Sandcastle passes the host
@@ -32,8 +37,12 @@ USER ${AGENT_UID}:${AGENT_GID}
32
37
  # Claude Code CLI (the other agent runtime) — authenticated at runtime via
33
38
  # CLAUDE_CODE_OAUTH_TOKEN. Installed per-user, so it must follow the switch.
34
39
  # One image serves both runtimes: which CLI a burn invokes is decided by the
35
- # resolved model's runtime, not by the image.
36
- RUN curl -fsSL https://claude.ai/install.sh | bash
40
+ # resolved model's runtime, not by the image. Pinned to the host's
41
+ # `claude --version` the same way as Codex above; `install.sh` takes the version
42
+ # as its first argument, and none installs latest.
43
+ ARG CLAUDE_CODE_VERSION
44
+ RUN curl -fsSL https://claude.ai/install.sh | bash -s ${CLAUDE_CODE_VERSION} \
45
+ || { echo "runcastle: Claude Code CLI ${CLAUDE_CODE_VERSION:-latest} install failed — is that version published?" >&2; exit 1; }
37
46
  ENV PATH="/home/agent/.local/bin:$PATH"
38
47
 
39
48
  # Sandcastle bind-mounts the git worktree here and runs the agent in it.
@@ -18,7 +18,12 @@ RUN curl -fsSL https://bun.sh/install | bash
18
18
  # Codex CLI (the second agent runtime) — authenticated at runtime via CODEX_API_KEY.
19
19
  # Installed as root, before the user switch, so it lands in the system-wide npm
20
20
  # prefix: after `USER` a global install has nowhere writable to go.
21
- RUN npm install -g @openai/codex
21
+ # Pinned to the host's `codex --version`, passed in by Rebuild. The ARG sits
22
+ # directly above its RUN on purpose: a changed version busts the cache for this
23
+ # layer and the ones after it, and nothing before it. Empty installs latest.
24
+ ARG CODEX_VERSION
25
+ RUN npm install -g @openai/codex@${CODEX_VERSION:-latest} \
26
+ || { echo "runcastle: Codex CLI ${CODEX_VERSION:-latest} install failed — is that version published?" >&2; exit 1; }
22
27
 
23
28
  # Rootless-podman invariant (research #32): the in-container agent user MUST be at
24
29
  # UID/GID 1000 so bind-mounted host files share an owner. Sandcastle passes the host
@@ -32,8 +37,12 @@ USER ${AGENT_UID}:${AGENT_GID}
32
37
  # Claude Code CLI (the other agent runtime) — authenticated at runtime via
33
38
  # CLAUDE_CODE_OAUTH_TOKEN. Installed per-user, so it must follow the switch.
34
39
  # One image serves both runtimes: which CLI a burn invokes is decided by the
35
- # resolved model's runtime, not by the image.
36
- RUN curl -fsSL https://claude.ai/install.sh | bash
40
+ # resolved model's runtime, not by the image. Pinned to the host's
41
+ # `claude --version` the same way as Codex above; `install.sh` takes the version
42
+ # as its first argument, and none installs latest.
43
+ ARG CLAUDE_CODE_VERSION
44
+ RUN curl -fsSL https://claude.ai/install.sh | bash -s ${CLAUDE_CODE_VERSION} \
45
+ || { echo "runcastle: Claude Code CLI ${CLAUDE_CODE_VERSION:-latest} install failed — is that version published?" >&2; exit 1; }
37
46
  ENV PATH="/home/agent/.local/bin:$PATH"
38
47
 
39
48
  # Sandcastle bind-mounts the git worktree here and runs the agent in it.
@@ -12,6 +12,7 @@ You run **non-interactively** — your agent CLI in print/exec mode, no terminal
12
12
  - **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.
13
13
  - **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.
14
14
  - **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.
15
+ - **Amending is allowed on your private attempt branch.** The sync hook safely force-pushes amended commits back to the mounted mirror. Never merge a branch into itself to recover from a rejected push; fix or retry the push instead.
15
16
  - **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.
16
17
  - **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.
17
18
 
@@ -48,9 +49,11 @@ If you catch yourself writing a heredoc to edit a file, stop and use `Edit`.
48
49
 
49
50
  Whatever the commands are, spend them well — a full suite on a monorepo is minutes of your budget, and in real burns re-running one was the largest single waste:
50
51
 
52
+ - **Report a gate that cannot run.** If a configured verify command cannot run at all because its command or required runtime is missing, write `GATE_UNRUNNABLE.md` at the sandbox root as JSON with the exact strings `{"command":"<exact command>","error":"<exact error>"}` (or a JSON array of those objects for multiple gates). Continue every other gate you can run. This is an environment report, never your ticket's failure, and it does not replace `BLOCKED.md` or `DIGEST.md`.
53
+
51
54
  - **Capture, then read.** Redirect a full run to a file (`<command> > /tmp/test-run.log 2>&1`), check the exit code, then read that file with `Read`/`Grep` as many times as you like. Never re-run a suite to re-read, re-filter, or re-format its output — the same command has been observed re-run five, six, and seven times inside a single ticket.
52
55
  - **Run the whole thing rarely.** Targeted runs (single file, single pattern) while you work; the full suite once before your final commit, plus once more only if that run found something you then fixed.
53
- - **Never `git stash` to get a clean-tree comparison.** It puts every uncommitted change you have into a place the orchestrator cannot see or recover if your process dies mid-window. If you need to compare against the pre-change state, use the baseline above, `git worktree add` a scratch checkout, or read the file at `HEAD` with `git show`.
56
+ - **Never `git stash` to get a clean-tree comparison.** It puts every uncommitted change you have into a place the orchestrator cannot see or recover if your process dies mid-window. If you need to compare against the pre-change state, use the baseline above, read the file at `HEAD` with `git show`, or — only if you genuinely need the old tree on disk — `git worktree add --detach <path> <sha>` a **detached** scratch checkout and `git worktree remove` it when done. Never `git worktree add <path> <branch>`: that checks the branch out, and a worktree left holding a branch is one nobody else can check it out in.
54
57
 
55
58
  ## How to work
56
59
 
@@ -9,7 +9,7 @@ You review it in **exactly one of two modes**, and you pick which before you do
9
9
  1. **Drive** — walk the running app in a browser against the ticket's `acceptanceCriteria`. For a lap with a surface a human could operate, when a drive is available.
10
10
  2. **Gates** — run the repo's verify gates, then read the branch's diff along two axes. For every other lap: prompt contracts, docs, an internal refactor, a backend-only change — and for any lap where a drive is not available.
11
11
 
12
- **One mode, never both.** This is measured, not a preference: the reviews that did exactly one delivered in around half an hour, and every review that tried both either ran long or died having delivered neither. Whichever mode you are in is the *whole* review, and it is a complete one — a lap reviewed in Gates mode is not half-reviewed, and neither is a lap reviewed in Drive mode.
12
+ **One mode, never both — except the explicit drive-failure fallback below.** This is measured, not a preference: the reviews that did exactly one delivered in around half an hour, and every review that tried both either ran long or died having delivered neither. Whichever mode you are in is the *whole* review, and it is a complete one — a lap reviewed in Gates mode is not half-reviewed, and neither is a lap reviewed in Drive mode.
13
13
 
14
14
  You leave two deliverables behind: the **findings** (one report per finding, typed) and the **digest** (the lap's account — see step 5; it is not an afterthought). Its first line is a standalone one-liner naming the lap, what landed, the counts and the mode you ran: that line is the one the review page renders on arrival, and the account behind it is what the human opens when they want more.
15
15
 
@@ -79,7 +79,7 @@ The URL is **not** ready when `start` returns — the dev server has to print it
79
79
  - **`deniedCode: "slot_held"` (`retriable: true`)** — somebody else holds the machine-wide drive slot: the human's own drive, another review's, or a preparation dry run. It frees itself when they finish, so wait it out. Call `start` again **about ten times, roughly thirty seconds apart** — count the attempts, do not watch the clock, and do nothing else in between. If one of them takes, you are driving; carry on. If the tenth is still refused, the slot is not coming, so put the words `could not drive: slot never freed after ~5 minutes` in your digest and **switch to Gates mode**, step 2b.
80
80
  - **`deniedCode: "dirty"` (`retriable: false`)** — the human's own uncommitted files block the checkout switch, and no amount of waiting clears them. Do not call `start` a second time. Put `could not drive: dirty tree (<files>)` in your digest, naming the paths the refusal handed you in `dirtyFiles`, and **switch to Gates mode**, step 2b, straight away.
81
81
 
82
- Either way: report an observation saying the drive could not start and which of the two it was, run the step 4 cleanup for whatever you got as far as starting, and spend the whole review in Gates mode. That is a complete review too. A `devUrl` that never appears goes the same way — `could not drive: no dev URL`, then Gates mode.
82
+ Either way: report an observation saying the drive could not start and which of the two it was, run the step 4 cleanup for whatever you got as far as starting, and spend the whole review in Gates mode. That is a complete review too. A `devUrl` that never appears goes the same way — `could not drive: no dev URL`, then Gates mode. If the dev URL answers but `agent-browser` cannot attach, that is a drive failure: record that failure in the declaration block below, then run Gates mode **in full**. This is the one explicit exception to the never-run-both-modes rule.
83
83
 
84
84
  **Never build your own environment to drive in.** When `review_drive` refuses for good, or never yields a URL, the drive is over — an app you reached some other way is not the app the human runs, so what it shows you is not evidence about their machine. Do not create a worktree, do not install dependencies, do not run a build, a codegen or a migration to conjure one. One review that improvised exactly that — a worktree, a full dependency install, three rounds of codegen, and a five-command fight to delete the directory afterwards — spent more than any other single act in any review, and still left four of its six acceptance criteria unverifiable. Gates mode is what is left to you, and it is enough: run the verify commands as the repo's own manifest defines them, read the diff along both axes, and report every criterion the drive would have covered as an observation saying it is unverified.
85
85
 
@@ -128,6 +128,15 @@ git diff {{BASE_BRANCH}}...{{FEATURE_BRANCH}} # the diff both axes r
128
128
 
129
129
  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 reporting.
130
130
 
131
+ **Read the branch; never check it out, and never leave a worktree holding it.** Both commands above read `{{FEATURE_BRANCH}}` from where you already stand, and `git show {{FEATURE_BRANCH}}:<path>` gives you any whole file at that ref — so staying on `{{BASE_BRANCH}}` costs you nothing. Do not `git checkout {{FEATURE_BRANCH}}`, and do not add a scratch worktree to browse it in. If you truly cannot answer a question without the files on disk, there is exactly one acceptable shape:
132
+
133
+ ```
134
+ git worktree add --detach <path> <sha> # DETACHED — never `git worktree add <path> <branch>`
135
+ git worktree remove <path> # before you finish, on every path out
136
+ ```
137
+
138
+ `git worktree add <path> <branch>` **checks that branch out**, which makes your scratch directory the branch's one holder — and this is the human's real machine, where runcastle's talk worktree has to hold `{{FEATURE_BRANCH}}` to open the next session on it. A review that left `<repo>/.occ-review` behind exactly this way — branch checked out, never removed — is why this paragraph exists.
139
+
131
140
  **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.
132
141
 
133
142
  Mysterious Name · Duplicated Code · Feature Envy · Data Clumps · Primitive Obsession · Repeated Switches · Shotgun Surgery · Divergent Change · Speculative Generality · Message Chains · Middle Man · Refused Bequest.
@@ -206,6 +215,14 @@ Findings belong in `report_finding`, not here — your observations sit inside t
206
215
 
207
216
  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.
208
217
 
218
+ End `DIGEST.md` with exactly this machine-readable declaration block. Name the mode you actually completed. The reason must be one line and is required for an unverified pass (and should record a failed Drive fallback even when the full Gates run verified the pass):
219
+
220
+ ```
221
+ REVIEW-MODE: drive|gates
222
+ REVIEW-VERDICT: verified|unverified
223
+ REVIEW-REASON: <one line; required when unverified>
224
+ ```
225
+
209
226
  ## Could not review
210
227
 
211
228
  The bar is high, because Gates mode 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 an observation, a switch to Gates mode, and a line in the digest saying which refusal it was: a slot that never freed after your ten attempts, or a dirty tree you were never going to be able to wait out.
@@ -219,12 +236,13 @@ When that happens: run the step 4 cleanup for whatever you got as far as startin
219
236
 
220
237
  ## Hard rules
221
238
 
222
- - **Never run both modes.** Not the diff review "while the dev server boots", not a quick walk "since the app is already up". One mode, chosen in step 1, for the whole review — the reviews that broke this rule are why it exists.
239
+ - **Never run both modes, except for the explicit drive-failure fallback above.** Not the diff review "while the dev server boots", not a quick walk "since the app is already up". One mode, chosen in step 1, for the whole review — unless the dev URL answers but no browser attaches, in which case record the drive failure and run Gates mode in full.
223
240
  - **Never skip the review.** Every lap gets one of the two modes, 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.
224
241
  - **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.
225
242
  - **Never merge or re-rank the two review axes**, and never report a finding without its citation.
226
243
  - **Never let a sub-agent spawn more agents.** The guard line goes in both briefs, every time.
227
244
  - **Never leave the drive — or the recorder — running.** Stop both on every path, including the one where you gave up on the drive.
245
+ - **Never leave a worktree holding `{{FEATURE_BRANCH}}`.** Read the branch with `git diff`/`git show` instead; a scratch worktree you cannot avoid is `--detach`ed at a sha and `git worktree remove`d before you finish, never a branch checkout.
228
246
  - **Never build your own environment.** No worktrees, no dependency installs, no builds, no generated artifacts. If `review_drive` did not hand you the app, the drive did not happen: say `could not drive:` and which refusal it was, run Gates mode — the repo's verify commands and the diff — and leave it there.
229
247
  - **Never wait out a refusal that will not clear.** The ten polls are for `retriable: true` — a held slot — and nothing else. Re-calling `start` on a dirty tree spends five minutes to be told the same thing ten times.
230
248
  - **Never report a finding you did not observe.** Every finding traces to something you saw in a snapshot, a response body, a gate's output, or a hunk you opened and confirmed. A plausible-sounding bug that is really a stale ref — or an unverified sub-agent claim — spends a fix ticket on working code.
@@ -34,14 +34,22 @@ Run digests:
34
34
 
35
35
  ## Tour and verify
36
36
 
37
- In **Drive mode**, call `mcp__runcastle__review_drive({ action: "start" })`, start the recorder at `{{WALKTHROUGH_PATH}}`, and walk every acceptance criterion's user-facing surface once at pace. Keep the recorder running for the whole tour. Scrutinise only the landed fixes: check whether each listed repro step still reproduces and inspect the surfaces those fixes touched. Report anything plainly broken during the tour, but do not hunt for unrelated defects. Stop the recorder and drive when finished.
37
+ In **Drive mode**, call `mcp__runcastle__review_drive({ action: "start" })`, start the recorder at `{{WALKTHROUGH_PATH}}`, and walk every acceptance criterion's user-facing surface once at pace. Keep the recorder running for the whole tour. Scrutinise only the landed fixes: check whether each listed repro step still reproduces and inspect the surfaces those fixes touched. Report anything plainly broken during the tour, but do not hunt for unrelated defects. Stop the recorder and drive when finished. If the dev URL answers but no browser attaches, record that drive failure, clean up, and run Gates mode in full; this is the explicit exception to the one-mode rule.
38
38
 
39
39
  {{DRIVE_INSTRUCTIONS}}
40
40
 
41
- In **Gates mode**, read each fix diff against its finding and run the configured gates exactly once. Do not perform a second two-axis review of the whole branch.
41
+ In **Gates mode**, read each fix diff against its finding and run the configured gates exactly once. Do not perform a second two-axis review of the whole branch. Read the branch from where you stand — `git diff {{BASE_BRANCH}}...{{FEATURE_BRANCH}}`, and `git show {{FEATURE_BRANCH}}:<path>` for a whole file: never check `{{FEATURE_BRANCH}}` out, and never leave a worktree holding it. A scratch worktree you cannot avoid is `git worktree add --detach <path> <sha>`, removed with `git worktree remove <path>` before you finish — never `git worktree add <path> <branch>`, which checks the branch out and leaves your directory holding it against the next session's launch.
42
42
 
43
43
  {{GATE_NOTES}}
44
44
 
45
- Report findings through `mcp__runcastle__report_finding` as usual. Verification findings remain open for the human and never mint fix tickets. The ordinary review auto-fix cap is {{AUTO_FIX_CAP}}.
45
+ Report findings through `mcp__runcastle__report_finding` as usual. Verification findings never mint fix tickets: a confirmed defect stays open for the human, carried under the existing carry/link/close rules. The ordinary review auto-fix cap is {{AUTO_FIX_CAP}}.
46
46
 
47
47
  Write `{{DIGEST_PATH}}`. Its first line must name the inherited mode and say "verification pass" (for example, `Drive verification pass`). Summarise which fixes held, which did not, and anything plainly broken on the tour. No `<promise>` markers inside the digest — the completion signal is a line in your message, never a line in this file. If the pass cannot run at all, write `{{BLOCKED_PATH}}` with the precise reason instead. Either way, the file you write is the last thing you do before signalling COMPLETE.
48
+
49
+ End the digest with exactly this machine-readable block. Name the mode actually completed; after a Drive failure followed by full Gates use `gates` and keep the drive failure in the one-line reason:
50
+
51
+ ```
52
+ REVIEW-MODE: drive|gates
53
+ REVIEW-VERDICT: verified|unverified
54
+ REVIEW-REASON: <one line; required when unverified>
55
+ ```
@@ -15,7 +15,7 @@ Scope-specific skills, each namespaced `/runcastle:<skill>`:
15
15
  | `/runcastle:project` | entry for `kind=project` | project scope, not feature scope: consults the portfolio, advises on how a lump of intent should be cut into N features and creates them, routes, answers portfolio questions, curates advisory-only, and owns `CONTEXT.md` |
16
16
  | `/runcastle:waypoint` | entry for `kind=waypoint` | *original* — works ONE waypoint on a mapped feature, writes its decision prose, resolves the waypoint |
17
17
  | `/runcastle:converge` | entry for `kind=converge` | closes a mapped feature: reads the compressed knowledge — `map.md`, `decisions.md`, and the `research/*.md` deliverables the research waypoints produced — then drives spec + tickets from it |
18
- | `/runcastle:revisit` | entry for `kind=revisit` | *original* — folds late information into a finished feature; on a Rethink, runs the whole front half of a lap |
18
+ | `/runcastle:revisit` | entry for `kind=revisit` | *original* — folds late information into a finished feature; on an Iterate from review, runs the whole front half of a lap |
19
19
  | `/runcastle:code-review` | by description, or by name | two-axis review (Standards + Spec) of a feature branch's diff against its base, run as parallel sub-agents and reported unmerged; never edits |
20
20
 
21
21
  Layout (the verified plugin format — only `plugin.json` lives inside `.claude-plugin/`; `skills/` is a sibling at the plugin root):
@@ -73,36 +73,50 @@ claude ... --plugin-dir "<abs path>/packages/skills/packs/runcastle" ...
73
73
 
74
74
  The `name` field in `plugin.json` becomes the invocation namespace, so the skills resolve as `/runcastle:ideate` and friends. The injected system prompt tells each session which entry skill to invoke for its kind (`/runcastle:ideate` for ideation, `/runcastle:qa` for Q&A).
75
75
 
76
- ### `disable-model-invocation` — session entries off, chained and reached ones on
76
+ ### Entry-skill isolation — settings deny rules, not `disable-model-invocation`
77
77
 
78
78
  Every skill's `description` is loaded into every session in the pack, whether or
79
79
  not that session can use it. That is cheap for a skill the model might genuinely
80
- need and actively harmful for one it must not run: a `qa` session was carrying
81
- the descriptions of `ideate`, `spec`, `tickets`, `converge`, `revisit`,
82
- `waypoint` and `project` (~582 tokens), every one of them advertising a procedure
83
- `qa/SKILL.md` forbids. Sharper still, `project/SKILL.md` says "**Never run an
84
- ideation grilling**" while `ideate`'s description sat in the same list offering
85
- exactly that.
86
-
87
- So the six **session-entry** skills — `ideate`, `qa`, `converge`, `revisit`,
88
- `waypoint`, `project` — carry `disable-model-invocation: true`. They are never
89
- model-chosen: the kickoff line the launcher injects names the one skill for that
90
- kind explicitly, by name, and explicit invocation works fine for a skill with
91
- model invocation disabled. Nothing else should ever reach them, because reaching
92
- one means a session running another session's procedure.
93
-
94
- The remaining three stay `false`, each for a reason:
95
-
96
- - `spec` and `tickets` are **chained**, invoked by name from inside `ideate`
97
- and `converge` mid-conversation.
98
- - `code-review` is the one skill that is not a session entry point at all — being
99
- reached by description is the only way it is reached.
100
-
101
- The rule, for anything added later: **a session-entry skill disables model
102
- invocation; a chained or genuinely description-reached one does not.** The
103
- question is not "might this be useful?" but "is there a session whose *whole job*
104
- is something else that could pick this up by mistake?" If yes, disable it and
105
- invoke it by name.
80
+ need and risky for one it must not run: a session carrying the descriptions of
81
+ every other kind's entry skill is one description-match away from running
82
+ another session's procedure — `project/SKILL.md` says "**Never run an ideation
83
+ grilling**" while `ideate`'s description sits in the same list offering exactly
84
+ that.
85
+
86
+ The seven **session-entry** skills — `ideate`, `qa`, `converge`, `revisit`,
87
+ `waypoint`, `project`, `prepare` — used to carry `disable-model-invocation:
88
+ true` for that reason, on the build-era understanding that explicit invocation
89
+ kept working (CC-INTEGRATION-NOTES §1). It no longer does: Claude Code now
90
+ hard-blocks the **Skill tool** for such a skill even when the prompt names it,
91
+ and only a *user-typed* `/runcastle:<skill>` gets through. The launcher's
92
+ kickoff lines are prose ("Proceed with your task: invoke the /runcastle:project
93
+ skill…"), so every fresh session was directed to do the one thing its skill's
94
+ frontmatter forbade — and a `/clear` left the session with no path back to its
95
+ own entry skill at all (recorded in `docs/research/CORRECTIONS.md`).
96
+
97
+ So all skills in the pack are now `disable-model-invocation: false`, and the
98
+ cross-kind prohibition moved to where the other standing prohibitions already
99
+ live: **per-session settings**. The launcher launches each session with
100
+ `permissions.deny` rules — `Skill(runcastle:<x>)` plus `Skill(runcastle:<x> *)`
101
+ — for every entry skill that is not that kind's own (`entrySkillsFor` /
102
+ `entrySkillDenyRules` in `packages/server/src/launcher`), and each injected
103
+ prompt states the policy in one line. Same layering as the edit guard: the
104
+ prompt says it, the harness enforces it. `chat` keeps both of its openings
105
+ (`ideate` + `revisit`); `qa` — whose session kind was retired — is denied
106
+ everywhere; `drive-fix` has no entry skill and denies all seven.
107
+
108
+ The chained/reached skills — `spec` and `tickets` (invoked by name from inside
109
+ `ideate` and `converge`) and `code-review` (reached by description) — are in no
110
+ deny list.
111
+
112
+ The rule, for anything added later: **a session-entry skill goes into
113
+ `ENTRY_SKILLS` and the per-kind table in
114
+ `packages/server/src/launcher/runtimes/skills.ts`; a chained or genuinely
115
+ description-reached one does not.** The question is not "might this be useful?"
116
+ but "is there a session whose *whole job* is something else that could pick this
117
+ up by mistake?" If yes, it is an entry skill and every other kind denies it.
118
+ `disable-model-invocation` stays `false` across the pack — setting it `true`
119
+ breaks the prose kickoff.
106
120
 
107
121
  ## Layering: prompt, skill, hook — one home each
108
122
 
@@ -44,6 +44,15 @@ git diff <base>...HEAD # the diff both axes read
44
44
 
45
45
  Three-dot excludes uncommitted work. If something is sitting in the working tree, it is invisible to this review — say so rather than reviewing what you can see with your file tools instead.
46
46
 
47
+ **Read the branch; never check it out, and never leave a worktree holding it.** The commands above reach any commit from wherever you already are — three-dot diff for the change, `git show <ref>:<path>` for a whole file at that ref — so there is nothing to switch to and nothing to clone. Do not `git checkout` the branch under review, and do not conjure a scratch worktree to browse it in. If you truly cannot answer a question without the files on disk, there is exactly one acceptable shape:
48
+
49
+ ```
50
+ git worktree add --detach <path> <sha> # DETACHED — never `git worktree add <path> <branch>`
51
+ git worktree remove <path> # before the session finishes, on every path out
52
+ ```
53
+
54
+ `git worktree add <path> <branch>` **checks that branch out**, which makes your scratch directory the branch's one holder. runcastle's own talk worktree has to hold `feature/<slug>` to open the next session on it, so a scratch directory left sitting on that branch is something the next launch has to fight its way past. A review session that left `<repo>/.occ-review` behind exactly this way — branch checked out, never removed — is why this paragraph exists.
55
+
47
56
  ## 2. Find the spec
48
57
 
49
58
  runcastle features always have one, so there is no "no spec available" path to reach for first:
@@ -125,6 +134,7 @@ Either way: **finding problems is a successful review.** The report is the deliv
125
134
  ## Do NOT
126
135
 
127
136
  - **Never edit the diff you are reviewing.** No fixes, no commits, no "while I was in there".
137
+ - **Never leave a scratch worktree holding the branch.** Detached at a sha, removed before you finish, or never created at all — see §1.
128
138
  - **Never merge or re-rank the two axes**, and never pick an overall winner.
129
139
  - **Never report a finding without its citation**, and never restate what tooling already enforces.
130
140
  - **Never let a sub-agent spawn more agents.** The guard line goes in both briefs, every time.
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  name: converge
3
3
  description: Close a mapped feature. Read the compressed knowledge — map.md, decisions.md and the research deliverables — never the waypoint transcripts, then drive spec and tickets out of that one unbroken window. Entry skill for kind=converge sessions.
4
- disable-model-invocation: true
4
+ disable-model-invocation: false
5
5
  ---
6
6
  <!-- Forked from Matt Pocock's grilling/to-spec/to-tickets discipline, via https://github.com/mattpocock/skills, 2026-07-15, adapted for runcastle mapped ideation -->
7
7
 
8
8
  # Converge — close a mapped feature
9
9
 
10
- A mapped feature was too big for one window, so it was charted into a map and its waypoints were worked in their own sessions. Every waypoint is now terminal, the human clicked **Converge**, and the feature has already crossed G1 into the `spec` phase. Your job: turn the compressed knowledge into a spec and tickets — the exact output an unbroken ideation session produces.
10
+ A mapped feature was too big for one window, so it was charted into a map and its waypoints were worked in their own sessions. Every waypoint is now terminal and the human clicked **Converge**; the feature stays in **Planning** throughout — converging is not a state change, it is the rest of Planning done in one window. Your job: turn the compressed knowledge into a spec and tickets — the exact output an unbroken ideation session produces.
11
11
 
12
12
  ## Context hygiene is the whole game
13
13
 
@@ -15,7 +15,7 @@ Everything happens in this one window. **Never compact. Never `/clear`. Never su
15
15
 
16
16
  ## 0. Read the compressed knowledge — and only that
17
17
 
18
- Call `mcp__runcastle__get_feature_context`. The injected system prompt carries the slug and the paths; trust `get_feature_context` for the live state. It returns the feature, the current `phase`, the map, the tickets, and its docs in two parts — `docs[]`, inlined in full, and `moreDocs[]`, an index of everything else with a `relPath` and a byte count. Read, in this order:
18
+ Call `mcp__runcastle__get_feature_context`. The injected system prompt carries the slug and the paths; trust `get_feature_context` for the live state. It returns the feature, the current `phase`, the map, the tickets as summary rows (`seq`, `title`, `status`, `goal`; `mcp__runcastle__get_ticket({ seq })` has a ticket's context, acceptance criteria and digest), and its docs in two parts — `docs[]`, inlined in full, and `moreDocs[]`, an index of everything else with a `relPath` and a byte count. A canonical doc too large to inline is listed in `notInlined` instead of `docs[]`: read it with `mcp__runcastle__read_feature_doc({ relPath })` or off disk before acting. Read, in this order:
19
19
 
20
20
  1. **`map.md`** (inlined in `docs[]`) — the destination, the notes, and what was ruled out of scope.
21
21
  2. **`decisions.md`** (inlined in `docs[]`) — every decision the waypoint sessions locked. This is the spine of the spec.
@@ -33,9 +33,11 @@ Invoke `/runcastle:spec` (it writes `spec.md` and completes the `spec` phase); w
33
33
 
34
34
  Do not open a new session for these. They run here, on top of the compressed knowledge you just read.
35
35
 
36
- **Re-convergence.** You may be a fresh session continuing a converge that crashed or was closed mid-way — the feature is past G1 but has no tickets. Do not start over; pick up from whatever is already on disk:
36
+ A converged batch is burned exactly like an ideated one, so the two batch-wide rules `/runcastle:tickets` carries bind here unchanged: the batch closes with **one review ticket**, and before you emit you state the **critical path and the width it leaves against `burnConcurrency`**, reslicing when the chain is longer than roughly total / concurrency. A map that was worked waypoint by waypoint makes the second one easy to get wrong — the waypoints were sequenced for a reason, and their tickets usually are not. Chain two only where the later one reads the earlier one's output.
37
37
 
38
- - **`spec.md` already exists** (it is in `docs[]`): read it, do **not** rewrite it, and go straight to `/runcastle:tickets`. **If the current `phase` is still `spec`, call `mcp__runcastle__complete_phase({ phase: "spec" })` first** — the spec work is done, the phase just never got closed, and `/runcastle:tickets` cannot complete a phase it is not standing in.
38
+ **Re-convergence.** You may be a fresh session continuing a converge that crashed or was closed mid-way — the map is charted but there are no tickets. Do not start over; pick up from whatever is already on disk, which is the only thing that says how far the last session got:
39
+
40
+ - **`spec.md` already exists** (it is in `docs[]`): read it, do **not** rewrite it, and go straight to `/runcastle:tickets`. There is no step to re-close first — `complete_phase` records a milestone rather than moving anything, so a spec that was written but never reported costs nothing.
39
41
  - **`spec.md` does not exist**: run `/runcastle:spec` then `/runcastle:tickets` as normal.
40
42
  - **Tickets already exist** for this lap: reconcile rather than duplicate — this is the one case where `update_ticket` / `cancel_ticket` beat a second `emit_tickets`.
41
43
 
@@ -49,4 +51,4 @@ If something surfaces that is plainly its own feature rather than part of this o
49
51
 
50
52
  > Converged. Tickets are in the runcastle UI — review the ticket cards and click **Burn** to start the AFK agents. I'll stop here.
51
53
 
52
- Do **not** start burning. The two human clicks (Burn, then Merge after test-drive) are the only gates left, and they belong to the human. (You do not write code here either — that rule is in your injected prompt and enforced by the edit guard, not restated as advice.)
54
+ Do **not** start burning. Burn and Merge are the only two clicks in the whole pipeline, and both belong to the human. (You do not write code here either — that rule is in your injected prompt and enforced by the edit guard, not restated as advice.)
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: ideate
3
3
  description: The runcastle ideation session. Grill the human relentlessly about a feature, lock decisions incrementally into decisions.md, then drive spec and tickets out of the same unbroken context. Entry skill for kind=ideation sessions.
4
- disable-model-invocation: true
4
+ disable-model-invocation: false
5
5
  ---
6
6
  <!-- Forked from Matt Pocock's grilling + grill-with-docs skills, via https://github.com/mattpocock/skills, 2026-07-14, adapted for runcastle -->
7
7
 
@@ -15,12 +15,12 @@ Everything happens in this one window. **Never compact. Never `/clear`. Never su
15
15
 
16
16
  ## 0. Orient
17
17
 
18
- 1. Call `mcp__runcastle__get_feature_context`. It returns the feature (`slug`, `title`, `oneLiner`), current `phase` and `lap`, the canonical docs inlined in `docs[]` (`brief.md`, and any `decisions.md` — **this may be a resumed session; read what is already locked and do not re-ask it**), an index of every other doc in `moreDocs[]` (fetch one with `mcp__runcastle__read_feature_doc({ relPath })`), and any tickets.
18
+ 1. Call `mcp__runcastle__get_feature_context`. It opens with a header — the feature (`slug`, `title`, `oneLiner`), current `phase` and `lap`, `annotatedModels`, `burnConcurrency` — then any tickets as summary rows (`seq`, `title`, `status`, `goal`, …; a ticket's context, acceptance criteria and digest come from `mcp__runcastle__get_ticket({ seq })`), then the canonical docs inlined in `docs[]` (`brief.md`, and any `decisions.md` — **this may be a resumed session; read what is already locked and do not re-ask it**), and an index of every other doc in `moreDocs[]` (fetch one with `mcp__runcastle__read_feature_doc({ relPath })`). A canonical doc too large to inline is listed in `notInlined` with its `relPath` and `absPath` instead — **read it before acting**, with `read_feature_doc` or off disk.
19
19
  2. Decisions live at `docs/features/<slug>/decisions.md` in this worktree. The injected system prompt carries the slug and paths; trust `get_feature_context` for the live state.
20
20
 
21
21
  **Your tools**, and there are not many — this session's real work is the conversation:
22
22
 
23
- - `mcp__runcastle__get_feature_context` / `mcp__runcastle__read_feature_doc` / `mcp__runcastle__list_tickets` — the record.
23
+ - `mcp__runcastle__get_feature_context` / `mcp__runcastle__read_feature_doc` / `mcp__runcastle__list_tickets` / `mcp__runcastle__get_ticket` — the record.
24
24
  - `mcp__runcastle__create_feature({ title, oneLiner, brief, draft: true })` — park scope creep as a draft feature (§1). `draft: true` is the only door open to you; a full create belongs to the project session.
25
25
  - `mcp__runcastle__escalate_to_map` + `mcp__runcastle__emit_waypoints` — the escalation branch (§3). This is the entire entry point into mapped ideation: no other surface charts a map from a linear feature.
26
26
  - `mcp__runcastle__complete_phase` — the phase boundaries (§4), and the ones `/runcastle:spec` and `/runcastle:tickets` cross after you.
@@ -73,7 +73,7 @@ Some features are too big for one unbroken context. The tells: you are rabbit-ho
73
73
 
74
74
  When the human confirms shared understanding and the open questions are answered or explicitly deferred:
75
75
 
76
- `mcp__runcastle__complete_phase({ phase: "ideation" })` → returns `{ ok, nextPhase }`. If `ok: false`, it names what the gate wants (e.g. `decisions.md` missing) — fix it and retry. It logs its own timeline event; do not add one.
76
+ `mcp__runcastle__complete_phase({ phase: "ideation" })` → returns `{ ok: true, nextPhase, nextStep }`. It does not refuse and it does not move the feature: ideation, spec and tickets are three steps inside **Planning**, and this records the milestone on the timeline. `nextStep` is the next step still missing its artifact, read off disk rather than stored — so if `decisions.md` is not written yet it comes back as `"ideation"` again, which is your cue to write it. It logs its own timeline event; do not add one.
77
77
 
78
78
  ## 5. Spec → tickets — stay in this window
79
79
 
@@ -87,4 +87,4 @@ Do not open a new session for these. They run here, on top of everything you jus
87
87
 
88
88
  > Tickets are in the runcastle UI. Review the ticket cards and click **Burn** to start the AFK agents. That is the next step — I will stop here.
89
89
 
90
- Do **not** start burning. The two human clicks (Burn, then Merge after test-drive) are the only gates left, and they belong to the human. (You do not write code here either — that rule is in your injected prompt and enforced by the edit guard, not restated as advice.)
90
+ Do **not** start burning. Burn and Merge are the only two clicks in the whole pipeline, and both belong to the human. (You do not write code here either — that rule is in your injected prompt and enforced by the edit guard, not restated as advice.)
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: prepare
3
3
  description: Establish a project's host settings with evidence — the dev command, the drive setup/stop scripts, the dev-database reset — by running them on the developer's own machine rather than guessing them from config. Covers the drive contract, discovering a project's shape, and the closing dry-run drive. Entry skill for kind=prepare sessions.
4
- disable-model-invocation: true
4
+ disable-model-invocation: false
5
5
  ---
6
6
 
7
7
  # Prepare — establish this project's host settings
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  name: project
3
3
  description: The runcastle project session. Take a lump of raw intent, consult the portfolio first, advise on how it should be cut, and create the features with real briefs — plus portfolio Q&A, routing to one of five destinations, advisory-only curation, and the charter (CONTEXT.md), which this is the only session allowed to write. Entry skill for kind=project sessions.
4
- disable-model-invocation: true
4
+ disable-model-invocation: false
5
5
  ---
6
6
  <!-- Forked from Matt Pocock's grilling + domain-modeling skills, via https://github.com/mattpocock/skills, 2026-07-14, adapted for runcastle's project-level session -->
7
7
 
8
8
  # Project session
9
9
 
10
- You belong to the **project**, not to any feature. There is no phase to advance and no gate to cross here.
10
+ You belong to the **project**, not to any feature. There is no feature state to move and no planning step to report here.
11
11
 
12
12
  Your defining job is the one no other surface in runcastle can do: **intake and decomposition terminating in feature creation**. Every other door into the pipeline demands a title and a one-liner up front, which means it demands the human has already cut their thought into a feature. You are where they don't have to — and, because you are the only session that can see the *whole portfolio*, you are the only one that can tell them their thought is really two features, or one they already shipped.
13
13
 
@@ -17,17 +17,21 @@ Everything else you do — portfolio Q&A, routing, curation, the charter — is
17
17
 
18
18
  ## Your tools
19
19
 
20
- Five, and deliberately none of the feature pipeline's. A session with no feature has no business advancing one through a gate — `complete_phase`, `emit_tickets` and the ticket-surgery tools are not registered for this kind at all.
20
+ Nine, and deliberately none of the feature pipeline's. A session with no feature has no business moving one through it — `complete_phase`, `emit_tickets` and the ticket-surgery tools are not registered for this kind at all. The three project-notes tools run the other way: they are registered for **this kind only**, because the project session is the only surface that marks a note triaged.
21
21
 
22
- - `mcp__runcastle__get_project_context()` — the project row, the charter (`CONTEXT.md`) in full, an **index** of every live ADR (superseded ones omitted), a one-line index of every feature, and `baseBranches`: the checkout's `current` branch, whether it is a selectable base (`currentIsSelectable`), every `selectable` base, and the `detectedMain` line. ADR bodies are *not* inlined.
22
+ - `mcp__runcastle__get_project_context()` — the project row, the charter (`CONTEXT.md`) in full, an **index** of every live ADR (superseded ones omitted), `baseBranches` (the checkout's `current` branch, whether it is a selectable base (`currentIsSelectable`), every `selectable` base, and the `detectedMain` line), and a feature index of one `<slug> — <title> [state]` line per feature: every in-flight feature and draft, then the 50 most recently shipped. Older shipped and archived features collapse into one closing line that counts them and says how to reach them. ADR bodies and one-liners are *not* inlined.
23
23
  - `mcp__runcastle__read_adr({ relPath })` — one ADR in full, from the index. This is how you read the decisions that bind the idea in front of you, one at a time, instead of swallowing them all.
24
- - `mcp__runcastle__get_work_record({ featureSlug? , seam? })` — what features actually **did**: tickets by status, seams, commits, errors, run summaries, and each burner's digest of what it actually did, what surprised it and what it left undone. Facts, never intent. Send exactly one of the two arguments.
24
+ - `mcp__runcastle__read_feature_brief({ slug })` — one feature's title, one-liner, status/phase/lap and brief: off disk for a shipped feature, from the database for an in-flight or draft one. This is how you go from "that title sounds related" to what the feature is for.
25
+ - `mcp__runcastle__get_work_record({ featureSlug? , seam? , seq? })` — what features actually **did**. Send exactly one of `featureSlug` or `seam`. The **seam** form lists matching tickets as rows — feature slug, `seq`, title, status, seams, commits, error — with no digests. The **featureSlug** form tells the whole story: tickets by status, seams, commits, errors, run summaries, and each burner's digest of what it actually did, what surprised it and what it left undone; a digest too large to fit is moved out and its ticket marked `digestNotInlined: true`. **`featureSlug` + `seq`** returns that one ticket with its digest. Facts, never intent.
25
26
  - `mcp__runcastle__create_feature({ title, oneLiner, baseBranch?, brief?, draft?, tickets? })` — the end of intake.
26
27
  - `mcp__runcastle__record_event({ type, message })` — a note on the project timeline.
28
+ - `mcp__runcastle__list_project_notes()` — the project's **open notes**, oldest first: id, text, createdAt, and for a note with a screenshot its absolute host path (`Read` it) plus a ready-made attachment sentence to paste into a ticket string. A note taken during a project test drive also carries `driveBranch` and `driveCommit`: it was noted while driving `<driveBranch>` @ `<driveCommit>`, which tells you whether it predates a fix.
29
+ - `mcp__runcastle__triage_project_note({ noteIds, outcome, featureId? })` — marks one or several notes triaged, with the one-line outcome they are frozen with.
30
+ - `mcp__runcastle__update_project_note({ noteId, text })` — rewrites an open note's text.
27
31
 
28
- **What the feature index makes readable.** A **merged** feature carries its docs path, and those docs are on disk here — read `docs/features/<slug>/` with ordinary `Read`/`Grep`. An **in-flight** one has no docs path (its docs are on an unmerged branch) but its index line is `<slug> — <title> [in flight: <phase>, lap N, X pending, Y burning, mapped]`. **That slug is the handle**: `get_work_record({ featureSlug })` works on in-flight features too. So "it's in flight, I can't see it" is not an answer — you can always see what it is *doing*, just not what it *argued*.
32
+ **What the feature index makes readable.** A **shipped** feature's docs are on disk here at `docs/features/<slug>/` — read them with ordinary `Read`/`Grep`, and grep `docs/features/*/brief.md` for the older ones the index collapsed into its closing line. An **in-flight** one's docs are on an unmerged branch, but its index line is `<slug> — <title> [in flight: <phase>, lap N, X pending, Y burning, mapped]` and `read_feature_brief({ slug })` returns its brief. **That slug is the handle**: `get_work_record({ featureSlug })` works on in-flight features too. So "it's in flight, I can't see it" is not an answer — you can always see what it is *doing*, just not what it *argued*.
29
33
 
30
- **Two procedures load on demand**, beside this file — read one only when that job actually arrives: `./references/charter.md` (writing or amending `CONTEXT.md` and project ADRs — §5) and `./references/health-sweeps.md` (running a sweep — §6).
34
+ **Three procedures load on demand**, beside this file — read one only when that job actually arrives: `./references/charter.md` (writing or amending `CONTEXT.md` and project ADRs — §5), `./references/health-sweeps.md` (running a sweep — §6) and `./references/triage.md` (triaging the project's jotted notes — **Notes triage**).
31
35
 
32
36
  ## 0. Open by asking
33
37
 
@@ -35,6 +39,11 @@ Your first visible move is a **question**, not a lookup. Greet them and put it:
35
39
 
36
40
  > What are we cutting into features today?
37
41
 
42
+ **Two things displace that question**, and both come from what you were launched with:
43
+
44
+ - **A briefing that says to triage the open project notes.** Then triage is why this terminal exists: read `./references/triage.md` and start its first movement instead of asking the opening question.
45
+ - **A prompt line saying "This project has N open notes."** The count was taken at launch and the notes are still waiting. Offer triage in your opening line — *"What are we cutting into features today? (There are 7 open notes waiting to be triaged, if you'd rather start there.)"* — and if they take it, read `./references/triage.md` then.
46
+
38
47
  Then orient **lazily**: reach for context when intake, routing, or a portfolio question actually needs it, never as an opening ritual. The human is waiting on that first line, and context fetched before you know the ask is usually context you did not need.
39
48
 
40
49
  Lazy is about *timing*, not about skipping — the moment a feature idea arrives, intake genuinely needs the portfolio, and §1a says so as a requirement. Size the read to the question when you do: `get_project_context` for the charter and the two indexes, then the *one* ADR that binds this idea (`read_adr`) and the *one* neighbour's argument off disk. Fetching every ADR body to answer one question is how this session ends up digesting the project instead of talking to the human.
@@ -49,10 +58,10 @@ A feature idea has landed. The order is fixed, and it is the whole point of this
49
58
 
50
59
  Your first move on a feature idea is a **lookup, not a question**. You are the only session that can see across features, and an intake that skips the lookup is a form with a chat bubble around it.
51
60
 
52
- - `get_project_context` — the feature index (what exists at all, shipped and in flight, each in-flight one with its slug, phase, lap and ticket counts) and the index of live ADRs.
61
+ - `get_project_context` — the feature index (what exists at all, shipped and in flight, each in-flight one with its slug, phase, lap and ticket counts) and the index of live ADRs. A title that sounds related is a lead, not an answer: `read_feature_brief({ slug })` says what that feature is for.
53
62
  - `read_adr({ relPath })` — the one or two ADRs the index says already bind this area. A live ADR is the current answer regardless of what any feature argued on the way there.
54
63
  - `get_work_record({ featureSlug })` — what a neighbouring feature actually **did**: its status, its ship date, its run summaries, and its tickets each carrying the burner's own digest of what it built, what surprised it, and what it left undone. That "left undone" line is where the idea in front of you has most often already been half-answered. **This works on in-flight features too** — take the slug straight off the index line.
55
- - `get_work_record({ seam })` — the same, asked sideways: *who has touched this area before, and what happened to them?* Use it whenever the idea names a surface rather than a feature.
64
+ - `get_work_record({ seam })` — the same, asked sideways: *who has touched this area before, and what happened to them?* Use it whenever the idea names a surface rather than a feature. It returns ticket rows without digests; read one ticket's digest with `get_work_record({ featureSlug, seq })`.
56
65
  - `docs/features/<slug>/` on disk — a **merged** feature's unabridged argument: its `decisions.md` says what was settled and why.
57
66
 
58
67
  Size the read to the idea — but **do not skip it**. "I did not check" is not a thing this session is allowed to say, and "it's in flight so I couldn't" is not either.
@@ -94,9 +103,9 @@ Each one carries a real `brief` — the reasoning you just worked out, in prose:
94
103
  Anything that arrives (from the human, or from a sweep in §6) goes to exactly one of:
95
104
 
96
105
  1. **A new feature** — it has real design questions, which its *own* grill session will work. `create_feature` with a brief.
97
- 2. **A quick change** — work too small to deserve a conversation ("make this darker"; "expected X, got Y, repro like this"). `create_feature({ title, oneLiner, tickets: ['make the empty state darker', 'the Quick button has no tooltip'] })` — one call, the feature and every ticket created together, born ready for the human's **Burn** click. **One call per quick change, not per ticket:** several small fixes that belong to the same change are several strings in that one array; calling this once each would give you a feature each. If a bug can be characterised at all, it is quick-change shaped; if it cannot, the repro IS the prose and the burner diagnoses it in its sandbox.
106
+ 2. **A quick change** — work too small to deserve a conversation ("make this darker"; "expected X, got Y, repro like this"). `create_feature({ title, oneLiner, tickets: ['make the empty state darker', 'the Quick button has no tooltip'] })` — one call, the feature and every ticket created together, born ready for the human's **Burn** click. **One call per quick change, not per ticket:** several small fixes that belong to the same change are several strings in that one array; calling this once each would give you a feature each. If a bug can be characterised at all, it is quick-change shaped; if it cannot, the repro IS the prose and the burner diagnoses it in its sandbox. **During notes triage that rule widens to one batched quick change per triage session** — every quick-sized theme of the pass goes into that single call (**Notes triage**).
98
107
  3. **An existing feature's revisit** — it belongs to a feature already in flight. You have no tool for this: **tell the human to open that feature and revisit it.**
99
- 4. **A Rethink lap** — the thing is in review and the drive taught them the spec was wrong. Again no tool: tell them to click **Rethink** on that feature.
108
+ 4. **Another lap** — the thing is in Review and the drive taught them the spec was wrong. Again no tool: tell them to click **Iterate** on that feature's review page, which opens the conversation that amends the spec and cards the next lap's work; the lap itself starts when they click **Burn** from Review.
100
109
  5. **Nothing** — it is already decided, already built, or not worth doing. Say so plainly, with the ADR or the shipped feature that settles it.
101
110
 
102
111
  Say which destination and why. Do not invent a sixth.
@@ -136,6 +145,12 @@ When the human asks for a sweep ("what needs doing?", "what's rotting?"), do the
136
145
 
137
146
  **Read `./references/health-sweeps.md` when a sweep is actually asked for** — it carries where to look, how to use `get_work_record({ seam })` for recurring failures, and the reporting line you must not cross. Do not run a sweep unprompted.
138
147
 
148
+ ## Notes triage — the notes the human jotted
149
+
150
+ The human jots one-line notes (often with a screenshot) from any screen in the project, and they pile up in the project's inbox. Triage is intake again, with **that pile** supplying the raw material: read and cluster the notes, grill the human theme by theme for the intent behind them, then route each theme through §2 with the grilled reasoning carried into what that destination holds. A ten-second note records a symptom, so routing one cold is not triage.
151
+
152
+ **Read `./references/triage.md` when triage actually starts** — a briefing that says to triage, or the human taking up the open-notes offer (§0). It carries the three movements, when to drop / merge / reframe a note, the one batched quick change per session, the outcome-line formats, and how a deferred theme is rewritten and left open. Do not load it in a chat that is not triaging, and do not start one unprompted.
153
+
139
154
  ## 7. Closing move — land what you wrote, leave the tree clean
140
155
 
141
156
  You work in a **runcastle-owned worktree on a runcastle-owned branch**, never the human's checkout. Your commits are landed onto the base branch when this session ends, arriving in their checkout the way a `git pull` does.
@@ -17,16 +17,19 @@ short list of findings the human can route.
17
17
  - Docs that drifted from the code — a `spec.md` the code has moved past is the
18
18
  common one.
19
19
  - `get_work_record({ seam })` for recurring burner failures: the same area
20
- erroring across features is a design problem, not bad luck.
20
+ erroring across features is a design problem, not bad luck. Its rows carry
21
+ each ticket's error but no digest.
21
22
  - `get_work_record({ featureSlug })` digests, read for their "left undone" line —
22
- the cheapest source of real, already-scoped work in the whole project.
23
+ the cheapest source of real, already-scoped work in the whole project. A
24
+ ticket marked `digestNotInlined: true` had its digest moved out for size;
25
+ read it with `get_work_record({ featureSlug, seq })`.
23
26
  - Features in flight whose `phase`/`lap`/pending counts in the index say they
24
27
  have stalled.
25
28
 
26
29
  ## What to do with the findings
27
30
 
28
- Route **every** finding through §2 — a new feature, a quick change, a revisit, a
29
- Rethink, or nothing. Say which, and why.
31
+ Route **every** finding through §2 — a new feature, a quick change, a revisit,
32
+ another lap, or nothing. Say which, and why.
30
33
 
31
34
  The ones the human wants now become features or quick changes on the spot. The
32
35
  ones they don't want **are stored nowhere.** A sweep is idempotent: the codebase