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.
- package/bin/runcastle.js +4605 -2723
- package/drizzle/0037_amusing_steve_rogers.sql +7 -0
- package/drizzle/0038_project_docs_commit_prefix.sql +1 -0
- package/drizzle/0039_chat_session_kind.sql +3 -0
- package/drizzle/0039_review_verdict.sql +5 -0
- package/drizzle/0040_new_maestro.sql +10 -0
- package/drizzle/0041_round_harpoon.sql +2 -0
- package/drizzle/meta/0037_snapshot.json +1118 -0
- package/drizzle/meta/0040_snapshot.json +1212 -0
- package/drizzle/meta/0041_snapshot.json +1226 -0
- package/drizzle/meta/_journal.json +35 -0
- package/index.js +4599 -2718
- package/package.json +2 -1
- package/sandcastle-template/Containerfile +12 -3
- package/sandcastle-template/Dockerfile +12 -3
- package/skills/burner/implement-ticket.md +4 -1
- package/skills/burner/review-ticket.md +21 -3
- package/skills/burner/verify-fixes.md +11 -3
- package/skills/packs/README.md +42 -28
- package/skills/packs/runcastle/skills/code-review/SKILL.md +10 -0
- package/skills/packs/runcastle/skills/converge/SKILL.md +8 -6
- package/skills/packs/runcastle/skills/ideate/SKILL.md +5 -5
- package/skills/packs/runcastle/skills/prepare/SKILL.md +1 -1
- package/skills/packs/runcastle/skills/project/SKILL.md +26 -11
- package/skills/packs/runcastle/skills/project/references/health-sweeps.md +7 -4
- package/skills/packs/runcastle/skills/project/references/triage.md +183 -0
- package/skills/packs/runcastle/skills/qa/SKILL.md +4 -3
- package/skills/packs/runcastle/skills/revisit/SKILL.md +28 -15
- package/skills/packs/runcastle/skills/spec/SKILL.md +2 -2
- package/skills/packs/runcastle/skills/tickets/SKILL.md +22 -6
- package/skills/packs/runcastle/skills/waypoint/SKILL.md +2 -2
- package/web/assets/index-L11zqTP_.css +1 -0
- package/web/assets/index-x1_wkP9-.js +187 -0
- package/web/index.html +2 -2
- package/web/assets/index-D697YlOb.css +0 -1
- 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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
+
```
|
package/skills/packs/README.md
CHANGED
|
@@ -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
|
|
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
|
-
###
|
|
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
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
`
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
`
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
-
|
|
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.
|
|
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:
|
|
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
|
|
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 }`.
|
|
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.
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
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),
|
|
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
|
-
- `
|
|
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 **
|
|
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
|
-
**
|
|
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. **
|
|
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,
|
|
29
|
-
|
|
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
|