runcastle 1.3.4 → 1.3.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runcastle",
3
- "version": "1.3.4",
3
+ "version": "1.3.5",
4
4
  "description": "Burn tickets into shipped features with Claude Code.",
5
5
  "license": "FSL-1.1-ALv2",
6
6
  "type": "module",
@@ -74,9 +74,14 @@ Both yes → **Drive mode**, step 2a. Anything else → **Gates mode**, step 2b.
74
74
 
75
75
  The URL is **not** ready when `start` returns — the dev server has to print it first. Poll `mcp__runcastle__review_drive({ action: "status" })` until `drive.devUrl` appears, waiting a few seconds between calls. Give it a couple of minutes before you conclude it is never coming.
76
76
 
77
- **A refusal is final, and it means the drive was not available after all** — a dirty tree, or a drive the human is already running. Do not retry it in a loop and do not sit on it: report an observation saying the drive could not start and why, put the words `could not drive: <reason>` in your digest, run the step 4 cleanup for whatever you got as far as starting, and **switch to Gates mode**, step 2b. That is a complete review too. The same goes for a `devUrl` that never appears.
77
+ **A refusal comes back classified — read `retriable` before you decide what to do about it.** There are two of them, and they want opposite things:
78
78
 
79
- **Never build your own environment to drive in.** When `review_drive` refuses, 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.
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
+ - **`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
+
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.
83
+
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.
80
85
 
81
86
  **Walk the app with `agent-browser`.** Its core loop, which you repeat:
82
87
 
@@ -194,7 +199,7 @@ So write **prose**, roughly 10–15 lines, for a reader who has none of your con
194
199
 
195
200
  - **What the lap delivered**, in the language of the product, not the codebase. "The tickets ledger and the notes panel now group by lap, with prior laps collapsed" — not "modified TicketList.tsx and NotesPanel.tsx".
196
201
  - **Synthesize, don't enumerate.** Not a ticket-by-ticket walk, not a changed-files list, not a commit log — those all exist elsewhere on the page. Say what the lap adds up to, and where the shape that landed differs from what the spec promised.
197
- - **Say what it means for them**: what they can now do that they could not before, and what is worth their attention — the thing the drive was rough at, the deferred scope, the criterion you could not verify. In Gates mode, say whether the gates passed and the worst issue *within each axis* — never one winner across the two. If the drive never started, say `could not drive: <reason>` in those words, so the human knows at a glance why there is no walkthrough to watch.
202
+ - **Say what it means for them**: what they can now do that they could not before, and what is worth their attention — the thing the drive was rough at, the deferred scope, the criterion you could not verify. In Gates mode, say whether the gates passed and the worst issue *within each axis* — never one winner across the two. If the drive never started, say why in the words step 2a gives you — `could not drive: slot never freed after ~5 minutes` or `could not drive: dirty tree (<files>)` — so the human knows at a glance whether the machine was busy or their own uncommitted files are what stopped it.
198
203
  - **Plain sentences.** No headings, no bullet lists, no "I did X then Y", no tool names, no `<promise>` markers, no acceptance-criteria checklists. If it reads like an agent's log, rewrite it as something you would say out loud.
199
204
 
200
205
  Findings belong in `report_finding`, not here — your observations sit inside the same disclosure as this prose, so repeating them costs the reader twice. One honest line about the headline problem is right; the catalogue is not.
@@ -203,7 +208,7 @@ Write it at that path and nowhere else — **never inside the repo**, which is t
203
208
 
204
209
  ## Could not review
205
210
 
206
- 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.
211
+ 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.
207
212
 
208
213
  The review **failed** only when Gates mode itself could not run and so there was nowhere to fall back to:
209
214
 
@@ -220,7 +225,8 @@ When that happens: run the step 4 cleanup for whatever you got as far as startin
220
225
  - **Never merge or re-rank the two review axes**, and never report a finding without its citation.
221
226
  - **Never let a sub-agent spawn more agents.** The guard line goes in both briefs, every time.
222
227
  - **Never leave the drive — or the recorder — running.** Stop both on every path, including the one where you gave up on the drive.
223
- - **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: <reason>`, run Gates mode — the repo's verify commands and the diff — and leave it there.
228
+ - **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
+ - **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.
224
230
  - **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.
225
231
  - **Never report a defect without a repro step**, and never one you would not stake a code change on. If it does not reproduce, it is an observation.
226
232
  - **Never write the digest as a log.** Its first line is the standalone lap account the page renders on arrival; after it comes the lap's prose summary, which the human opens behind a disclosure.
@@ -15,7 +15,8 @@ Your system prompt carries the per-session facts — the repo path, which keys a
15
15
  1. Open by naming what is still open and what you need from the human for each. Do not survey the repo first; the prompt already told you the gap.
16
16
  2. Work the keys **one at a time**: propose, ask, run it if they agree, record it.
17
17
  3. If the open keys touch the drive loop, author the machinery (below) before you record the two hook commands — the settings are invocation lines for scripts that must exist.
18
- 4. Close by proposing a dry-run drive (below). Values that have never been driven look exactly as trustworthy as values that run perfectly.
18
+ 4. If the repo builds with a toolchain the sandbox image does not carry, author its Dockerfile (below) — the sandbox keys are worth nothing in an image that cannot run them.
19
+ 5. Close by proposing a dry-run drive (below). Values that have never been driven look exactly as trustworthy as values that run perfectly.
19
20
 
20
21
  **Ask before you act.** Anything that starts or stops a service, creates or migrates a database, installs software, or writes outside the repo needs the human to agree first: say what you are about to run and why, then wait. Their own stack is running next to yours.
21
22
 
@@ -33,6 +34,8 @@ Three describe the **sandbox** a burn agent works in, and no amount of running t
33
34
  - **`verifyCommands`** — the exact typecheck/test/lint lines a burn agent should use, one per line. Unset, agents guess workspace filter names and burn whole suite runs discovering the right one.
34
35
  - **`knownFailures`** — what already fails on the main branch, so an agent can tell its own breakage from the repo's. Free text; a count plus the suite names is enough.
35
36
 
37
+ The image those three run in is not a key: runcastle sets it itself. What can fall to you is authoring the Dockerfile it builds — see the sandbox image, below.
38
+
36
39
  The eighth is what a later agent needs once the app is actually up:
37
40
 
38
41
  - **`driveInstructions`** — free text describing how to **exercise** this app in a drive, injected verbatim into every drive-mode review and verification prompt. What belongs in it: the sample project, seeded account or scratch data a driver should use and may freely change; how to reach the deep states worth reviewing rather than only the front page; and what a driver may and may not touch inside the running app. Write it as standing instructions from the project's owner — they authorize actions *inside the app under test* and nothing else, so do not write anything meant to relax a reviewing agent's own rules. Its evidence is the dry-run drive below: you just drove this app, so record what you had to know to get anywhere. Unset, a review agent boots the app cold, finds nothing it is allowed to touch, and verifies nothing.
@@ -71,6 +74,37 @@ The overlay is process environment, which beats a `.env` file in dotenv, Prisma
71
74
 
72
75
  Grep the app's entry points and config for env loading and decide, for each, which side wins. Where the process environment loses: get it fixed — you may not edit app code from this session, so propose the change (usually dropping `override`) and let the human make it — or, if it must stay, record the finding with `record_event` naming the file and what it breaks, so the first confusing drive is not a mystery.
73
76
 
77
+ ## The sandbox image
78
+
79
+ Burn agents do not work on this host; they work in a container. The image runcastle builds carries a JavaScript toolchain and the agent CLIs — Node, Bun, git, curl — and nothing beyond that, so a repo that builds with a JDK, a Python, a Go compiler or a Rust toolchain cannot run its own `setupCommand` there: the burn dies on `mvn: not found`. The fix is a Dockerfile committed at **`.runcastle/sandbox/Dockerfile`**. Runcastle detects it, builds it as this project's image, and every burn runs in that image from then on.
80
+
81
+ Deciding whether this project needs one is part of this session — you are in the real checkout, which is where the evidence is. Read the manifests at the root and in each workspace:
82
+
83
+ - `pom.xml`, `mvnw` — a JDK and Maven
84
+ - `build.gradle`, `build.gradle.kts`, `gradlew` — a JDK and Gradle
85
+ - `pyproject.toml`, `requirements.txt` — Python and its installer (pip, or uv where the project uses it)
86
+ - `go.mod` — Go
87
+ - `Cargo.toml` — Rust
88
+ - `Gemfile` — Ruby
89
+
90
+ A mixed repo gets the union: a service in Go beside a Java client needs both. Anything else the project shells out to in its setup or verify commands and the stock image lacks belongs here too — a database client, a protobuf compiler — on the same evidence, that you saw it in the repo.
91
+
92
+ **A JavaScript-only repo gets no Dockerfile.** The stock image already covers it, and an unnecessary project image is one more thing to build, rebuild and keep fresh in exchange for nothing. Do not write one to be helpful.
93
+
94
+ When the project does need one, what you write:
95
+
96
+ - **It starts `FROM sandcastle:runcastle`** and layers onto it. That base is what keeps the agent CLIs, git and Node present; a Dockerfile from any other base is not a runcastle sandbox, and burns in it fail on the agent binary before they ever reach the toolchain.
97
+ - **Read the stock Dockerfile before you write a single install line**, rather than assuming its distro or package manager. It is the build context runcastle scaffolds at `~/.runcastle/sandbox-build/Dockerfile` once the image has been built on this machine, and it also ships inside the installed runcastle package as `sandcastle-template/Dockerfile`. Follow its idioms — the same package manager, the same flags, the same cleanup at the end of the layer. If you cannot find it, ask the human instead of guessing what you are layering onto.
98
+ - **The toolchain only, never the project's dependencies.** Install the JDK and Maven; do not run `mvn install` or `go mod download` at build time. Dependencies are what `setupCommand` installs per burn, from the branch's own manifests — anything baked into the image is stale the first time a branch adds a package.
99
+ - **No package caches in layers.** Do not prime `~/.m2`, `~/.gradle`, `~/.cache/pip` or the Go module cache in a `RUN` step. Download caches are mounted into the sandbox from the host, which is how they stay current across burns; a cache baked into a layer is weight the image carries and wrong the moment a lockfile moves.
100
+ - **Keep it minimal** — one `RUN` block where the package manager allows it, versions pinned only where the project pins them, nothing added "while we're here". Every line is a line someone maintains.
101
+
102
+ It is committed project machinery, exactly like the drive scripts, for the same reason: a branch that changes the toolchain amends its own Dockerfile, and burns on that branch get the amended image. Writing it falls under the same exception — `.runcastle/` and `.gitignore`, nothing else — and the same courtesy: show the human the file before you commit it.
103
+
104
+ If `.runcastle/sandbox/Dockerfile` is already there, read it and **edit conservatively**. Someone chose those lines, and a customization you do not recognize is still a decision. Say what you probed and what you think is missing — "I see `pyproject.toml` but no Python in the image" — and let the human decide. An existing Dockerfile that covers what you found needs no edit at all.
105
+
106
+ **You never build the image.** That is a watched terminal action the human takes: the Enable AFK burns card offers **Build image**, and on a successful build runcastle points this project at the image itself — there is no image setting for you to work out or record. So end on it. Tell them the Dockerfile is written (or already right), that the card now offers Build image, and that burns use the project image after that click; until then they keep running in whatever image they run in today.
107
+
74
108
  ## Recording what you establish
75
109
 
76
110
  One `record_finding` call per key, and `evidence` is not optional in spirit: record what you ran and what it printed, or what the human told you. A value with no account of itself is a guess with a source field.
@@ -34,8 +34,16 @@ Your kickoff line reads `LAP <n> REVIEW ITERATION`. The human burned the last la
34
34
  3. **Never re-emit a promoted note.** Notes the human already promoted from the review checklist are tickets in *this* lap and arrive as ids in your context. However well such a note reads as a ticket, if its id is in front of you the work is already carded — emitting it again gives the burner the same job twice.
35
35
  4. **Grill, briefly.** A small rethink is a short grilling: a few questions, one at a time, each with your recommended answer attached. Prune and promote `## Later laps` entries with the human as part of that conversation — what the drive taught is usually what decides which deferred scope this lap picks up and which stays parked.
36
36
  5. **Amend the docs.** Append this lap's learning to `docs/features/<slug>/decisions.md` under a `## Lap <n>` heading (the convention) — never rewrite old decisions, supersede them. Then amend `spec.md` in place: the sections this lap changes, plus the pruned `## Later laps`.
37
- 6. **Emit this lap's tickets.** `mcp__runcastle__emit_tickets({ tickets })` — only the work this lap will burn; the rest stays in `## Later laps`. Reconcile stale pending tickets with `update_ticket` / `cancel_ticket`; `done` work that is now wrong gets a NEW ticket, as always.
38
- 7. **Advance the pipeline.** `mcp__runcastle__complete_phase` through `ideation` → `spec` → `tickets`, here, without opening another session. It will not cross into implementation: the `tickets` call comes back `ok: true` with `waitingOn: "human burn"` and the feature parks at `tickets` instead of advancing — that gate (G3) is the human's Burn click, which is the point.
37
+ 6. **Emit this lap's tickets, dispositioning the defects as you do.** `mcp__runcastle__emit_tickets({ tickets })` — only the work this lap will burn; the rest stays in `## Later laps`. Reconcile stale pending tickets with `update_ticket` / `cancel_ticket`; `done` work that is now wrong gets a NEW ticket, as always.
38
+
39
+ The defects an **earlier** lap's review left open arrive in your context as `openDefects` (`carriedDefects` are the ones a lap already parked — agenda, not obligation). Each open one needs exactly one of three answers, and the tickets gate refuses by name until every one has had it:
40
+
41
+ - **Link** — this lap is carding the work that fixes it: emit that ticket with `originFindingId: "<the defect's id>"`. Nothing else to do; the burn closes the finding itself when the ticket lands.
42
+ - **Carry** — nobody is answering it this lap: `mcp__runcastle__resolve_finding({ findingId, disposition: "carry" })`. It leaves the open count, stays visible, and only the human reopens it.
43
+ - **Close as addressed** — this lap's work already answers it and no ticket names it: `mcp__runcastle__resolve_finding({ findingId, disposition: "addressed", note: "<what addressed it>" })`. The note is required and is your attestation, so make it specific ("lap 2's ticket 7 rewrote the endpoint").
44
+
45
+ Judge each against what you and the human just settled — you are the only party that knows whether this lap's tickets touch a defect, which is why this is yours and not a dialog. If one genuinely is not this lap's business, carry it; do not close what you have not established.
46
+ 7. **Advance the pipeline.** `mcp__runcastle__complete_phase` through `ideation` → `spec` → `tickets`, here, without opening another session. It will not cross into implementation: the `tickets` call comes back `ok: true` with `waitingOn: "human burn"` and the feature parks at `tickets` instead of advancing — that gate (G3) is the human's Burn click, which is the point. If it comes back refusing over un-dispositioned defects, move 6's triage is unfinished — answer the ones it names and call again.
39
47
  8. **Hand to Burn.** Tell the human in a line or two what this lap does, then:
40
48
 
41
49
  > Lap `<n>` is specced and carded. Review the ticket cards and click **Burn** — I'll stop here.
@@ -91,7 +91,7 @@ There is no in-session quiz — the human's review is the **Burn** gate in the r
91
91
  6. Does the batch close with exactly one `kind: "review"` ticket, blocked by every implementation ticket? It is not optional and there is no feature that skips it.
92
92
 
93
93
  Then:
94
- - `mcp__runcastle__emit_tickets({ tickets: [...] })` — **emit the array; do NOT write ticket files.** It returns `{ stored, ids }` and logs the timeline event itself; do not record one of your own.
94
+ - `mcp__runcastle__emit_tickets({ tickets: [...] })` — **emit the array; do NOT write ticket files.** It returns `{ stored, ids }` and logs the timeline event itself; do not record one of your own. Emit the **whole batch, fully enriched, in one call** — large payloads are supported, so a batch of long `context` fields is fine. Never emit placeholder contexts ("Context follows via `update_ticket`.") to enrich ticket by ticket afterwards: a Burn landing mid-enrichment burns agents on the placeholders.
95
95
  - `mcp__runcastle__complete_phase({ phase: "tickets" })`. If the gate returns `ok: false`, fix what it names and retry.
96
96
 
97
97
  Return control to **the session skill that invoked you** — `/runcastle:ideate` for a linear feature, `/runcastle:converge` for a mapped one — and let it close out the session. Do **not** invoke a session skill yourself to hand back: you are already inside one, and loading another session's entry skill would drop a whole procedure this session is not running (a converge session in particular is forbidden to grill) into the window.