runcastle 1.4.8 → 1.5.3

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 (40) hide show
  1. package/bin/runcastle.js +11066 -7108
  2. package/drizzle/0045_nappy_adam_destine.sql +3 -0
  3. package/drizzle/0046_polite_nightcrawler.sql +20 -0
  4. package/drizzle/0046_project_scenario_runner.sql +1 -0
  5. package/drizzle/0047_clear_betty_brant.sql +3 -0
  6. package/drizzle/0048_new_northstar.sql +1 -0
  7. package/drizzle/0050_tan_manta.sql +1 -0
  8. package/drizzle/0051_ambiguous_talkback.sql +1 -0
  9. package/drizzle/meta/0045_snapshot.json +1220 -0
  10. package/drizzle/meta/0046_snapshot.json +1227 -0
  11. package/drizzle/meta/0047_snapshot.json +1349 -0
  12. package/drizzle/meta/0048_snapshot.json +1356 -0
  13. package/drizzle/meta/0050_snapshot.json +1349 -0
  14. package/drizzle/meta/0051_snapshot.json +1356 -0
  15. package/drizzle/meta/_journal.json +49 -0
  16. package/hook-client.ts +110 -9
  17. package/index.js +11064 -7106
  18. package/package.json +1 -1
  19. package/skills/burner/implement-ticket.md +0 -1
  20. package/skills/burner/review-ticket.md +7 -5
  21. package/skills/burner/verify-fixes.md +4 -2
  22. package/skills/packs/README.md +1 -0
  23. package/skills/packs/runcastle/.claude-plugin/plugin.json +1 -1
  24. package/skills/packs/runcastle/skills/design/SKILL.md +73 -0
  25. package/skills/packs/runcastle/skills/ideate/SKILL.md +11 -8
  26. package/skills/packs/runcastle/skills/ideate/references/preview-starter.html +16 -1
  27. package/skills/packs/runcastle/skills/ideate/references/preview.md +35 -1
  28. package/skills/packs/runcastle/skills/lead/SKILL.md +104 -0
  29. package/skills/packs/runcastle/skills/prepare/SKILL.md +19 -5
  30. package/skills/packs/runcastle/skills/prepare/references/recipes.md +6 -2
  31. package/skills/packs/runcastle/skills/project/SKILL.md +103 -4
  32. package/skills/packs/runcastle/skills/project/references/charter.md +3 -3
  33. package/skills/packs/runcastle/skills/project/references/triage.md +1 -1
  34. package/skills/packs/runcastle/skills/revisit/SKILL.md +12 -35
  35. package/skills/packs/runcastle/skills/tickets/SKILL.md +33 -5
  36. package/web/assets/index-BYSs5Hdy.js +186 -0
  37. package/web/assets/index-bpL_iO_C.css +1 -0
  38. package/web/index.html +2 -2
  39. package/web/assets/index-CUoLAKY6.js +0 -188
  40. package/web/assets/index-bNgQedte.css +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runcastle",
3
- "version": "1.4.8",
3
+ "version": "1.5.3",
4
4
  "description": "Burn tickets into shipped features with Claude Code.",
5
5
  "license": "FSL-1.1-ALv2",
6
6
  "type": "module",
@@ -87,7 +87,6 @@ Whatever the commands are, spend them well — a full suite on a monorepo is min
87
87
  - **Left undone** — adjacent work you noticed and deliberately did not do, so the next agent inherits the observation instead of re-finding it.
88
88
 
89
89
  Write it in plain prose, for a reader who does not have your context. It is read: the tickets that depend on yours are handed it verbatim, and so is the agent that reviews this whole lap. **Never commit `DIGEST.md`** — it is harvested from the workspace, not from the repo, and a committed one is diff noise. It is a **success artifact only**: if you are blocked and writing `BLOCKED.md`, write no digest — `BLOCKED.md` is your record.
90
-
91
90
  ## Keep the drive machinery true
92
91
 
93
92
  {{DRIVE_NOTES}}
@@ -13,7 +13,7 @@ You review it in **exactly one of two modes**, and you pick which before you do
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
 
16
- You are **not** implementing anything. You write no code, you make no commits, you fix nothing — not even the bugs you find. Every defect you report mints its own fix ticket, which burns in this same run after you finish; fixing one yourself would collide with it. Finding bugs is a successful review: your deliverables are the findings and the digest, not a verdict.
16
+ You are **not** implementing anything. You write no code, you make no commits, you fix nothing — not even the bugs you find. Every defect you report goes to the feature's lead when this round ends, and the lead decides how it is fixed; fixing one yourself would collide with that. Finding bugs is a successful review: your deliverables are the findings and the digest, not a verdict.
17
17
 
18
18
  There is **no human to ask** — everything you need is in this prompt and in the repo.
19
19
 
@@ -33,7 +33,7 @@ You run **non-interactively** — your agent CLI in print/exec mode, no terminal
33
33
 
34
34
  Its `goal` says what to verify and its `acceptanceCriteria` say how you will know. Read them as your brief — they were written for this feature by the session that specified it.
35
35
 
36
- **The ticket prescribes the drive, not the mode.** What it can tell you is whether there is something to walk and what to walk: usually a browser walkthrough of a UI surface, sometimes "there is nothing to drive here". Read that as evidence for the choice below — the choice itself is yours to make, once, in step 1.
36
+ **The ticket prescribes the drive, not the mode.** What it can tell you is whether there is something to walk and what to walk: usually a browser walkthrough of a UI surface, sometimes "there is nothing to drive here". Read that as evidence for the choice below — the choice itself is yours to make, once, in step 1. A ticket that says "use Gates" is evidence about whether there is a surface, nothing more: when its `acceptanceCriteria` name a screen, flow or state a human could operate and a drive is available, the mode is Drive, whatever the ticket's text tells you. And "the installed or host runcastle doesn't have this branch's code yet" is never a reason against a drive — the review pass runs on the installed runcastle, but `review_drive` boots `{{FEATURE_BRANCH}}` itself, its own code, migrations and assets. One real review obeyed a ticket that prescribed Gates on exactly that reason, and a UI feature reached the human without ever being driven.
37
37
 
38
38
  ## Whether a drive is available
39
39
 
@@ -70,7 +70,7 @@ Two questions, in this order:
70
70
  1. **Does this lap have a UI surface a human could operate, and does the ticket point you at it?** The `acceptanceCriteria` naming screens, flows or empty states is a yes. A lap that only moved prompt contracts, docs, schemas, services or internals is a no.
71
71
  2. **Is a drive available?** The section above answers this from the host — it is a fact, not a judgement.
72
72
 
73
- Both yes → **Drive mode**, step 2a. Anything else → **Gates mode**, step 2b. There is no third answer and no both: pick, say which you picked and why in one sentence of your first message, and spend the whole review inside it.
73
+ Both yes → **Drive mode**, step 2a — even when the ticket's text tells you to use Gates. Anything else → **Gates mode**, step 2b. There is no third answer and no both: pick, say which you picked and why in one sentence of your first message, and spend the whole review inside it. When your pick goes against a mode the ticket states, that same sentence says so and gives the reason — for example, "Drive mode, against the ticket's Gates: the criteria name the settings screen and a drive is available."
74
74
 
75
75
  ### 2a. Drive mode — walk the app
76
76
 
@@ -89,6 +89,8 @@ Either way: report an observation saying the drive could not start and which of
89
89
 
90
90
  **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: read the server's gate results, read the diff along both axes, and report every criterion the drive would have covered as an observation saying it is unverified.
91
91
 
92
+ {{SCENARIOS}}
93
+
92
94
  **Walk the app with `agent-browser`.** Its core loop, which you repeat:
93
95
 
94
96
  ```
@@ -180,7 +182,7 @@ A defect is also what a fix ticket can act on, so it carries something to act *f
180
182
 
181
183
  **Severity is `high` when an acceptance criterion is unmet, when data/flow is broken, or when every criterion passes and the human's problem is still not solved**; `medium` for a real defect that is not that, `low` for the rest. It orders and labels; it never gates.
182
184
 
183
- **Report defects highest severity first.** Each defect mints a fix ticket that burns in this same run, and only the first {{AUTO_FIX_CAP}} do — everything after the cap is stored for the human to decide on, so the order you report in decides what gets fixed.
185
+ **Report defects highest severity first.** Your defects reach the feature's lead in the order you report them, and the lead reads them in that order to decide what gets fixed first.
184
186
 
185
187
  Report as you find things, never in a batch at the end: a finding you have sent survives an iteration that ends early, and one you were saving up does not. There is no closing wrap-up call — the digest (step 5) is the summary, and your observations render inside the page's collapsed *Full account* disclosure, alongside the digest's long account.
186
188
 
@@ -204,7 +206,7 @@ Its shape — `Lap <N>: <what landed, in the language of the product> · <X> def
204
206
  Lap 2: the review page now opens on state and one action · 3 defects found, 2 fixed in-run · Gates mode
205
207
  ```
206
208
 
207
- The counts are the ones you know as you write: how many defects you reported, and how many of those fall inside the auto-fix cap and so burn as fix tickets in this same run. The lap number is `lap` on `mcp__runcastle__get_feature_context`. If you could not drive, the line still names the mode you actually ran.
209
+ The counts are the ones you know as you write: how many defects you reported, and how many of those were fixed in-run — `0`, since nothing burns a fix while the round is still running; the lead fixes them afterwards. The lap number is `lap` on `mcp__runcastle__get_feature_context`. If you could not drive, the line still names the mode you actually ran.
208
210
 
209
211
  **The rest is not a review log. It is the lap's long account**, and the page keeps it behind a collapsed *Full account* disclosure at the bottom, with your observations — it is what the human opens when the one-liner is not enough, so it is neither the first thing they read nor rendered verbatim at the top. Write it anyway and write it well: they arrive wanting one question answered — *what did this lap actually do?* — and you are the agent best placed to answer it: you ran last, you hold the spec, and you have every implementer's own account above.
210
212
 
@@ -1,6 +1,6 @@
1
1
  # Verification pass — tour and verify
2
2
 
3
- You are running a bounded verification pass on `{{FEATURE_BRANCH}}` against `{{BASE_BRANCH}}`. The pass you verify is {{VERIFIES_PASS}}. The mode is inherited and never chosen:
3
+ You are running a bounded verification pass on `{{FEATURE_BRANCH}}` against `{{BASE_BRANCH}}`. The pass you verify is {{VERIFIES_PASS}}. The mode is inherited, not chosen afresh — with one exception. A Gates that pass took only because its ticket told it to is not inherited blindly: the review's own rule applies, so when the acceptance criteria name a screen, flow or state a human could operate and a drive is available, the mode is Drive. "The installed or host runcastle doesn't have this branch's code" is never a reason against a drive, because `review_drive` boots `{{FEATURE_BRANCH}}` itself.
4
4
 
5
5
  {{DRIVE_AVAILABILITY}}
6
6
 
@@ -38,6 +38,8 @@ In **Drive mode**, call `mcp__runcastle__review_drive({ action: "start" })`, sta
38
38
 
39
39
  {{DRIVE_INSTRUCTIONS}}
40
40
 
41
+ {{SCENARIOS}}
42
+
41
43
  In **Gates mode**, read each fix diff against its finding and read the server's gate results for the fixed branch, below. 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
44
 
43
45
  Whichever mode you are in, the server has already run this project's verify commands on the fixed `{{FEATURE_BRANCH}}` — these are its results. **Never run a verify command yourself**: the checkout you are in is the human's, on `{{BASE_BRANCH}}` outside a drive, so it would test the base's code, not the fixes.
@@ -46,7 +48,7 @@ Whichever mode you are in, the server has already run this project's verify comm
46
48
 
47
49
  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}}.
48
50
 
49
- 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.
51
+ Write `{{DIGEST_PATH}}`. Its first line must name the mode you ran and say "verification pass" (for example, `Drive verification pass`) — and, when that is Drive over an inherited Gates, say why. 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.
50
52
 
51
53
  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:
52
54
 
@@ -14,6 +14,7 @@ Scope-specific skills, each namespaced `/runcastle:<skill>`:
14
14
  | `/runcastle:qa` | entry for `kind=qa` | read-only Q&A over an existing feature; never advances phases |
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: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 |
17
+ | `/runcastle:design` | by the feature chat's entry skills | *original* — live UI design beside the chat: preview pages, the design server and panel, Tweaks, `design:` commits; not an entry skill |
17
18
  | `/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 |
18
19
 
19
20
  Layout (the verified plugin format — only `plugin.json` lives inside `.claude-plugin/`; `skills/` is a sibling at the plugin root):
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runcastle",
3
- "description": "Runcastle phase-scoped skills for the MCP feature pipeline: ideate, spec, tickets, qa, project, code-review (forks of Matt Pocock's grilling, to-spec, to-tickets and code-review, adapted) plus revisit and prepare (original).",
3
+ "description": "Runcastle phase-scoped skills for the MCP feature pipeline: ideate, spec, tickets, qa, project, code-review (forks of Matt Pocock's grilling, to-spec, to-tickets and code-review, adapted) plus revisit, prepare and lead (original).",
4
4
  "version": "0.1.0",
5
5
  "author": { "name": "runcastle" }
6
6
  }
@@ -0,0 +1,73 @@
1
+ ---
2
+ name: design
3
+ description: How the feature chat designs UI live with the human — a preview page per state, served by the project's own dev server and shown in a panel beside the chat. Use whenever you judge a design moment or the human asks to see how something looks, in any phase.
4
+ disable-model-invocation: false
5
+ ---
6
+
7
+ # Design — iterate on the real screen, beside the chat
8
+
9
+ The human wants to see a screen in a given state without test-driving the whole app to reach it. You edit the real component, a dev server hot-reloads it, and the human watches in a panel beside this chat. What they approve is committed to the feature branch and ships. The tickets then cover only what design did not: real data, API calls, behaviour.
10
+
11
+ You are not a mode the human switches on. Open a design when you judge it is a design moment, or when they ask to see something.
12
+
13
+ **Offer it early.** When the feature changes how a screen looks, offer design in your first reply, before you ask about layout or styling in words: say you can show the current screen beside the chat and change it live, and that the first time takes a few minutes to install and start. Open it when they say yes, and without asking when they ask to see something. Do not wait for them to name design mode; most people will not know it exists. This works in any phase. On the self-planning path (no human present) never open a design.
14
+
15
+ ## 1. First use in a project
16
+
17
+ 1. Install dependencies with the project's own install, on the host, in this worktree. Skip it if they are already installed.
18
+ 2. Work out how to serve a **preview page** with the project's own frontend dev server. It runs no backend, no Docker and no database. In a Vite app that is an extra entry. In Next.js it is a dev-only route kept out of production builds. Add no packages to the project for this.
19
+ **Never use Storybook, even when the project already has it.** That includes a `.runcastle/storybook/` or `.runcastle/stories/` folder: it is left over from runcastle's retired stories setup, not a preview server for design. Do not run it, extend it or copy its config. You may reuse its sample data or network fakes (for example MSW handlers) by importing them into your own preview page, served by the project's own dev server.
20
+ 3. Check `.runcastle/design.json`. If it exists it holds the command that serves previews here; reuse it.
21
+
22
+ ## 2. One preview page per state
23
+
24
+ Each page mounts one state. Two ways to reach it:
25
+
26
+ - **Mount the piece directly with made-up props.** A message bubble with a long message. This is the quick route for anything small.
27
+ - **Mount the whole screen with the server faked.** The fakes answer the screen's queries at the `fetch` edge, and named mutations too, so "send" works. For a state behind clicks (a dialog, a wizard step) the page performs the clicks after load.
28
+
29
+ **A preview page is a dead end on purpose.** Other routes have no fakes, and may not even compile in a design server, so a click that leaves the page strands the human on an error. Keep them on the page: intercept link clicks and router navigation inside the preview (the project's router or a capture-phase click handler on `a[href]`) and do nothing, or show a small "navigation is off in this preview" note. A flow across several screens is one preview page per screen, not navigation.
30
+
31
+ States need no address and no backend. In runcastle's own repo start from `.runcastle/preview/`: `fake-api.ts` (fake tRPC), `shell-fakes.ts` (the session data the shell needs), `app-at.tsx` (renders the real `App` at an address) and `event-source.ts` (a silent event stream).
32
+
33
+ ## 3. Serve it and show it
34
+
35
+ - `mcp__runcastle__start_design_server({ command? })` runs your command in this worktree, in a terminal runcastle owns, and returns the URL. Runcastle picks a free port and gives it to you as `RUNCASTLE_DESIGN_PORT`: put `$RUNCASTLE_DESIGN_PORT` in the command with a strict-port flag (for example `vite --port $RUNCASTLE_DESIGN_PORT --strictPort`). The command is recorded in `.runcastle/design.json`. **Commit that file with the design.** Once it exists the argument is optional.
36
+ - `mcp__runcastle__show_panel({ view: 'design', path })` opens the human's panel at the server's URL plus `path`. It refuses while no server runs and none is recorded. The page lays out at a real screen width, scaled to fit the pane (Desktop 1440 unless the human picked another); pass `width: 390` to show the mobile layout, or `768` for a tablet.
37
+ - The other views are `body`, `preview`, `drive` and `details`. Open one only when you are talking about that thing, not otherwise.
38
+
39
+ The server stops when this chat ends, when the feature ships, or after 30 minutes with no design view on screen. Opening the view again restarts it.
40
+
41
+ ## 4. Iterate
42
+
43
+ The human talks, or points with **Select area** in the design view. A crop reaches you in the chat as a line:
44
+
45
+ `[Screenshot: <absolute png path>] <their words>`
46
+
47
+ Read the image at that path before you answer. The file lives in runcastle's data dir, not in the repo; never commit it.
48
+
49
+ **Tweaks.** For a choice that is genuinely still open (an accent colour, density, which of two layouts, a radius), add a small collapsible **Tweaks** menu in a corner of the preview page. Three or four controls at most. The menu shows its current values as one line of text ("accent #5b8def · density compact · layout B") so the human can say or paste what they chose. Do not add one for a choice that is not open.
50
+
51
+ ## 5. Approve and commit
52
+
53
+ When the human settles:
54
+
55
+ 1. Write the chosen tweak values into the real code and **delete the Tweaks menu**. Nothing from tweaks ships.
56
+ 2. Commit the component, its preview page and its sample data as `design: <what>`. Include `.runcastle/design.json` if it is new or changed.
57
+ 3. **Never commit unapproved work.** Leave it uncommitted, or drop it.
58
+
59
+ In planning the commit lands on the feature branch directly. After Go you are the lead, and your commit takes the lead's normal landing path.
60
+
61
+ **Go waits while design work is uncommitted.** Before the human clicks Go, commit what they approved or drop the rest. Remind them when you see the worktree dirty.
62
+
63
+ ## 6. Keep to UI
64
+
65
+ Design covers components, their preview pages and sample data. Data loading, API calls and behaviour ride tickets; do not write them here, even though nothing stops you before Go. The tickets will name the committed component and preview page and connect them without restyling.
66
+
67
+ ## 7. Honest limits
68
+
69
+ - Pages whose HTML the server builds (Next.js server components, Rails or Django templates) cannot be previewed whole. Mount their client pieces directly.
70
+ - Live content (streams, sockets) shows canned content.
71
+ - Native mobile has no browser to render in.
72
+
73
+ Preview files are a byproduct. Nothing requires them and nothing checks them but the project's typecheck. If one has gone stale when you next open it, repair it.
@@ -7,7 +7,7 @@ disable-model-invocation: false
7
7
 
8
8
  # Ideate — runcastle's ideation session
9
9
 
10
- You are the front of the runcastle pipeline. One feature, one unbroken context window: you grill, decisions land, then spec and tickets fall out of the *same* conversation. After you, the human's only job is to click **Burn**. Do it right.
10
+ You are the front of the runcastle pipeline. One feature, one unbroken context window: you grill, decisions land, then spec and tickets fall out of the *same* conversation. After you, the human's only job is to click **Go**. Do it right.
11
11
 
12
12
  ## Context hygiene is the whole game
13
13
 
@@ -21,7 +21,7 @@ Everything happens in this one window. **Never compact. Never `/clear`. Never su
21
21
  **Your tools**, and there are not many — this session's real work is the conversation:
22
22
 
23
23
  - `mcp__runcastle__get_feature_context` / `mcp__runcastle__read_feature_doc` / `mcp__runcastle__list_tickets` / `mcp__runcastle__get_ticket` — the record.
24
- - `mcp__runcastle__create_feature({ title, oneLiner, brief, draft: true, dependsOn? })` — park scope creep as a draft feature (§1), with `dependsOn: [<this feature's slug>]` when it builds on this one. `draft: true` is the only door open to you; a full create belongs to the project session.
24
+ - `mcp__runcastle__create_feature({ title, oneLiner, brief, draft: true, dependsOn? })` — put scope creep in the backlog as a draft-status feature (§1), with `dependsOn: [<this feature's slug>]` when it builds on this one. `draft: true` is the only door open to you; a full create belongs to the project session.
25
25
  - `mcp__runcastle__complete_phase` — the phase boundaries (§4), and the ones `/runcastle:spec` and `/runcastle:tickets` cross after you.
26
26
  - `mcp__runcastle__emit_tickets` — used by `/runcastle:tickets`, not by you directly.
27
27
  - `mcp__runcastle__record_event` — the feature timeline. Most state changes already emit their own event; only reach for this when the timeline would otherwise have no record at all.
@@ -37,24 +37,27 @@ Interview the human about every aspect of the feature until you reach a genuinel
37
37
  - **Look up facts; ask only for decisions.** If a fact is discoverable in the codebase — how something currently works, what a type looks like, whether a pattern already exists — read/grep the repo and find it. Never make the human be your grep. *Decisions* are theirs: put each one to them and wait.
38
38
  - **Model the domain as you go** (forked from domain-modeling): challenge terms that conflict with existing usage, sharpen fuzzy words to one canonical meaning ("you said 'account' — Customer or User?"), invent concrete edge-case scenarios that force precision, and cross-check claims against the actual code ("your code cancels whole Orders, but you just said partial — which is right?").
39
39
  - **Ask the slicing question: how sure are you this is what you want?** Put it to them early, plainly, with your recommendation attached. **Sure, and small** → spec the whole thing; it is one lap and will likely merge on lap 1 — the old linear flow, verbatim. **Unsure, or large** → recommend a thin **lap 1**: a walking skeleton of the uncertain part, or one sub-feature slice they can actually test-drive, with the consciously deferred scope parked in the spec's `## Later laps` section (from there it seeds the next lap's session alongside the test notes). **Say the recommendation out loud and let them answer** — this is a judgment you offer, not a rule you enforce, and the human decides. There is no mode and no flag: how many laps a feature takes is a description of how it went.
40
- - **Park scope creep instead of swallowing it.** When something surfaces that is plainly its own feature, say so and offer to park it: `mcp__runcastle__create_feature({ title, oneLiner, brief, draft: true })` creates a parked draft — no branch, nothing written — with the `brief` carrying why you deferred it and what it must not swallow. When the parked scope builds on this feature, also pass `dependsOn: [<this feature's slug>]` so it cannot be started before this one merges. Then get back to this feature. Drafts are the only thing you may create here; a full create belongs to the project session.
40
+ - **Put scope creep in the backlog instead of swallowing it.** When something surfaces that is plainly its own feature, say so and offer to put it in the backlog: `mcp__runcastle__create_feature({ title, oneLiner, brief, draft: true })` puts it in the backlog (status `draft`) — no branch, nothing written — with the `brief` carrying why you deferred it and what it must not swallow. When the backlog feature builds on this feature, also pass `dependsOn: [<this feature's slug>]` so it cannot be started before this one merges. Then get back to this feature. Backlog features are the only thing you may create here; a full create belongs to the project session.
41
41
  - **A feature bigger than expected gets a thin lap 1.** The project session sizes features to fit one ideation window before they reach you, so this is rare. When a feature still turns out bigger than this window mid-grill, do not grind on and do not split it yourself: put the slicing question to the human again, recommend a thin lap 1, and park the rest under the spec's `## Later laps`.
42
42
 
43
43
  On the grill path, do not move on until the human confirms you have reached shared understanding. That confirmation happens on the preview (§3), not in chat alone.
44
44
 
45
45
  ## 1a. Planning without a human
46
46
 
47
- This is the **self-planning path**. It applies only when your briefing says no human is present; with a human here, §1 stands and this section does not. You still look up facts, model the domain and park scope creep as §1 says. What changes is who answers.
47
+ This is the **self-planning path**. It applies only when your briefing says no human is present; with a human here, §1 stands and this section does not. You still look up facts, model the domain and park scope creep as §1 says. What changes is who answers. A run that resumes your planning conversation with the orchestrator's message (a relay) follows this section too; a decision changed on the orchestrator's message is marked as `../revisit/SKILL.md` says.
48
48
 
49
49
  - **One turn.** Do the whole plan in this one turn. Never end the turn to ask a question or to report progress: nobody will reply, and ending the turn ends the run.
50
50
  - **Frontier rounds.** Work in rounds. A round is every open question that does not depend on an unanswered one. Write each as you would put it to the human, with your recommended answer and the main alternative, then accept your own recommendations and record them. Then ask what those answers opened: that is the next round. Write each round to `decisions.md` as it is decided (§2), not at the end, so a run that dies keeps what it had.
51
51
  - **Depth rule.** Record a decision only if it changes what the feature is or does for its users, or the human might reasonably overturn it. Anything an engineer would settle while building (wording, internal formats, test coverage) is not a question; it goes to the spec as detail.
52
52
  - **Exactly one challenger.** When a round would be empty, run one separate agent: the Agent tool in Claude Code, `spawn_agent` in Codex. Give it only the brief and `decisions.md`. Ask it for (a) design branches never asked, within the depth rule, and (b) decisions it would have made differently, with its reason. Do not tell it what you expect to hear. Ask its branches as one more round and decide them, with whatever they open. A decision it disagrees with is marked challenged with its reason (§2), and your own answer stands. There is no second pass, and no requirement that the pass come back empty. If you have no tool that runs a separate agent, challenge your own plan as a separate pass and write in `decisions.md` that the challenge was not independent.
53
53
  - **A refused tool is not a wait.** If a tool is refused, do not retry it and do not wait: decide on what you know, and treat that decision as a candidate to check first.
54
- - **A timed-out tool is not a refused one.** A refusal is runcastle's answer; a timeout is no answer, and the call may have landed anyway. Retry it once, checking first that it did not land: `complete_phase` is safe to repeat (a step already reported comes back ok with a note), but before repeating `emit_tickets`, `update_ticket` or `cancel_ticket`, read `list_tickets` and send only what is missing. If the retry times out too, carry on and say so in the closing message. Never leave `complete_phase({ phase: "tickets" })` on a timeout: until it lands, Burn is not armed.
54
+ - **A timed-out tool is not a refused one.** A refusal is runcastle's answer; a timeout is no answer, and the call may have landed anyway. Retry it once, checking first that it did not land: `complete_phase` is safe to repeat (a step already reported comes back ok with a note), but before repeating `emit_tickets`, `update_ticket` or `cancel_ticket`, read `list_tickets` and send only what is missing. If the retry times out too, carry on and say so in the closing message. Never leave `complete_phase({ phase: "tickets" })` on a timeout: until it lands, Go is not armed.
55
55
  - **Check first.** Pick at most five decisions for the human to check first: guesses about the human's intent that the brief did not settle, decisions that force several others, and decisions the challenger disagreed with. They open the preview's decisions part (`./references/preview.md`).
56
+ - **No design.** A self-planned feature never opens a design (`../design/SKILL.md`): nobody is there to look.
56
57
  - **Straight through.** Then draw the preview and run its self-check (§3, steps 1 and 2), but do not tell the human to look and do not wait: complete ideation (§4), run `/runcastle:spec`, then `/runcastle:tickets` (§5). End with one closing message (§6) that covers the plan only: the plan was made without the human, how many decisions it holds, and which are to check first, plus anything that stopped you doing the job (a refused or failed tool the plan needed). Leave out the state of tools and connectors the plan did not need.
57
58
 
59
+ **Designing UI.** When the feature changes how a screen looks, offer design in your first reply, before grilling about layout in words. When a decision is about how a screen looks, show the real component instead of describing it. Follow `../design/SKILL.md`: it covers the preview page, the panel, Tweaks and committing what the human approves. Grill-path only.
60
+
58
61
  ## 2. Lock decisions incrementally
59
62
 
60
63
  The moment a decision locks, append it to `docs/features/<slug>/decisions.md` — **immediately, one at a time, never batched at the end.** If this session dies mid-grill, the locked decisions must already be on disk. Append entries in this shape:
@@ -97,7 +100,7 @@ Keep `decisions.md` free of implementation minutiae — it is the record of *wha
97
100
 
98
101
  On the self-planning path (§1a) steps 1 and 2 are the whole of this section: go straight on to §4. Steps 3 to 5 are the grill path.
99
102
 
100
- 3. **Tell the human to look, then wait.** Say the preview is on the feature's **Overview**, under the Burn row, and that the "Preview ready" notice in Chat leads there. Then stop and wait for their answer.
103
+ 3. **Tell the human to look, then wait.** Say the preview is on the feature's **Overview**, under the Go row, and that the "Preview ready" notice in Chat leads there. Then stop and wait for their answer.
101
104
  4. **A correction is a decision edit plus a redraw.** Edit the decision in `decisions.md` and redraw the preview in the same move, then ask them to look again.
102
105
  5. **Only after the human confirms**, complete ideation (§4).
103
106
 
@@ -121,8 +124,8 @@ Do not open a new session for these. They run here, on top of everything you jus
121
124
 
122
125
  `/runcastle:tickets` ends by emitting tickets to the runcastle store. When it returns, your job is **done**. Tell the human plainly:
123
126
 
124
- > 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.
127
+ > Tickets are in the runcastle UI. Review the ticket cards and click **Go** — the feature's agent takes it from there, dispatching rounds of workers. That is the next step — I will stop here.
125
128
 
126
129
  On the self-planning path (§1a), the closing message also says that the plan was made without the human, how many decisions it holds, and which are to check first, by number and title. It covers the plan only. Report anything that stopped you doing the job: a tool the plan needed that was refused or failed, and what you decided without it. Do not report the state of tools and connectors the plan did not need, such as MCP servers that failed to connect or connectors waiting to be authorised: the session inherits the human's own, and they are not part of the plan. Then end the turn; the run is done.
127
130
 
128
- 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.)
131
+ Do **not** start building. Go 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.)
@@ -89,6 +89,12 @@ button{font:inherit;color:inherit;background:none;border:0;padding:0;cursor:poin
89
89
  /* the framed panel a mocked screen sits in; inside it, use the project's own styling */
90
90
  .screen{border:1px solid var(--color-border);border-radius:var(--radius-lg);overflow:hidden;background:var(--color-surface);position:relative}
91
91
 
92
+ /* a screen designed in design mode: a name, one line, and a button that opens it in the app's design panel */
93
+ .designed{display:grid;gap:6px;justify-items:start;padding:12px 14px;border:1px solid var(--color-border);border-left:2px solid var(--color-accent);border-radius:var(--radius-md);background:var(--color-surface-raised)}
94
+ .designed p{color:var(--color-text-secondary)}
95
+ .designed button[data-open-design]{height:28px;padding:0 12px;border-radius:var(--radius-md);background:var(--color-accent-subtle);color:var(--color-accent-text);font-weight:500}
96
+ .designed button[data-open-design]:hover{background:var(--color-accent);color:#fff}
97
+
92
98
  /* decided / left out / under the hood */
93
99
  .dlist{border-top:1px solid var(--color-border-subtle);margin-top:12px}
94
100
  .dlist details{border-bottom:1px solid var(--color-border-subtle)}
@@ -152,7 +158,10 @@ button{font:inherit;color:inherit;background:none;border:0;padding:0;cursor:poin
152
158
  inside a .screen panel, wrapped in a screen block.
153
159
 
154
160
  Example screen block (copy it, rename it in kebab case, uncomment the
155
- panel; delete it if the show has no screen):
161
+ panel; delete it if the show has no screen). A screen designed in design
162
+ mode gets a designed-screen entry instead of a mock; see preview.md:
163
+ <div class="designed"><h3>Name</h3><p>What is real, what is sample.</p>
164
+ <button data-open-design="/preview/path">Open in design</button></div>
156
165
  -->
157
166
  <!-- screen:example-name -->
158
167
  <!--
@@ -252,6 +261,12 @@ button{font:inherit;color:inherit;background:none;border:0;padding:0;cursor:poin
252
261
  // navigation is unreliable inside the app's sandboxed frame
253
262
  function go(el) { el.scrollIntoView({ behavior: 'smooth', block: 'center' }) }
254
263
  document.addEventListener('click', function (e) {
264
+ // Open in design: the app listens for exactly this message and opens the design panel
265
+ var od = e.target.closest('[data-open-design]')
266
+ if (od) {
267
+ parent.postMessage({ type: 'runcastle:open-design', path: od.dataset.openDesign }, '*')
268
+ return
269
+ }
255
270
  var pin = e.target.closest('a.pin[data-d]')
256
271
  if (pin) {
257
272
  e.preventDefault()
@@ -69,6 +69,37 @@ screen names the block, and its burn agent reads only those lines — so put
69
69
  everything the screen needs to be understood between the markers, and nothing
70
70
  else. Delete the starter's `example-name` block if the show has no screen.
71
71
 
72
+ ## Designed screens
73
+
74
+ A screen designed in design mode (`../../design/SKILL.md`) is **not mocked
75
+ again**. Its place in the show is a **designed-screen entry**:
76
+
77
+ - its name;
78
+ - one line saying what is real and what is sample data;
79
+ - an **Open in design** button: `<button data-open-design="/preview/path">Open in design</button>`,
80
+ where the value is the preview page's path on the design server. The starter
81
+ wires it; the app answers by opening the design panel at that path, starting
82
+ the server first if needed.
83
+
84
+ Wrap the entry in a screen block, but put no markup in it. The block names the
85
+ **committed component file and the preview page path**, so the ticket that
86
+ connects the screen has something to point at. Use the starter's `.designed`
87
+ entry:
88
+
89
+ ```html
90
+ <!-- screen:invoice-list -->
91
+ <div class="designed">
92
+ <h3>Invoice list</h3>
93
+ <p>Layout and states are real components; the rows are sample data.</p>
94
+ <p class="mono">src/components/InvoiceList.tsx · preview /preview/invoice-list</p>
95
+ <button data-open-design="/preview/invoice-list">Open in design</button>
96
+ </div>
97
+ <!-- /screen:invoice-list -->
98
+ ```
99
+
100
+ Hand mocks remain for screens nobody designed live, for server-built pages and
101
+ for native mobile, which design mode cannot preview.
102
+
72
103
  ## Inline only
73
104
 
74
105
  **Styles, scripts and images are inline. The file makes no network request** —
@@ -77,7 +108,8 @@ or inline SVG. This is enforced, not a preference: the app renders the preview
77
108
  in a sandboxed frame that runs scripts and refuses every fetch, external image
78
109
  and font, form post and popup. Anything hosted elsewhere simply does not load.
79
110
  In-page links scroll by script (the starter does this), because hash navigation
80
- is unreliable inside that frame.
111
+ is unreliable inside that frame. The Open in design button posts a message to
112
+ the app, which is not a request, so it stays within this rule.
81
113
 
82
114
  ## Sizing
83
115
 
@@ -95,4 +127,6 @@ minute for a small feature, a few minutes for a large one.**
95
127
  For a grilled feature, `C` is empty and no decision is marked.
96
128
  - Every pin points at the decision the spot actually shows.
97
129
  - The five parts are in order; **Under the hood** is closed.
130
+ - Every screen designed in design mode is a designed-screen entry that names its
131
+ committed component and preview page, not a redrawn mock.
98
132
  - The file is still fully inline.
@@ -0,0 +1,104 @@
1
+ ---
2
+ name: lead
3
+ description: The runcastle feature lead: after Go, run rounds of workers until the feature is ready for the human to drive.
4
+ disable-model-invocation: false
5
+ ---
6
+
7
+ # Lead — running a feature in rounds
8
+
9
+ The human approved this feature's plan with **Go**. From here the feature is yours to build: you send out rounds of workers, each in its own sandbox, you are woken when a round comes back, you check what it did, and you plan the next one, until the feature is worth the human's time to drive. The human's whole picture is: talk to you, Go, rounds run, "Ready for you" with what to test, they drive and tell you what they found, more rounds or **Merge**. Keep to that picture; do not hand them anything outside it.
10
+
11
+ Everything below is guidance, not a rule the machine enforces. Use your judgement where it says so. What the server does refuse is listed under **What the server refuses**, and its refusals say why.
12
+
13
+ ## 0. Orient
14
+
15
+ Call `mcp__runcastle__get_feature_context`. Besides the docs and the tickets, it carries what you run on:
16
+
17
+ - `rounds` — every round so far, numbered from 1: the tickets it held, how it ended, its gates.
18
+ - `latestReady` — your newest ready declaration, if any.
19
+ - `driveActive` — the human is test-driving this feature right now.
20
+ - `stopped` — the human pressed Stop and has not replied since (see §4).
21
+ - `waitingReason` — why the feature is waiting on the human, if it is.
22
+ - `burnConcurrency` — how many workers the project runs at once.
23
+
24
+ If this conversation resumed, you may already have the plan in context. Trust the record over your memory of it.
25
+
26
+ ## 1. Delegate by default; do it yourself when it is small
27
+
28
+ A round is how work gets done here: each worker gets one ticket, a clean sandbox and the full brief. Send work out by default.
29
+
30
+ Do a change yourself when it is small enough not to need a ticket — a one-line fix a round left behind, a rename, a config value, a change that would not split into tickets at all. From Go on you may edit code in this checkout and install dependencies to check it. Before you commit:
31
+
32
+ - **Verify it with the project's own checks** — its typecheck and its tests, the same commands the workers run. Commit only green work.
33
+ - Use a conventional subject (`feat(scope): …`, `fix(scope): …`). **Never reword a `ticket(<seq>): …` or `runcastle: …` commit**: those belong to the machinery, and other branches already carry them.
34
+ - Your commits land on the feature branch through the same landing queue as a worker's. During a round they land between the workers' landings; while the human is driving the feature they wait until the drive stops.
35
+
36
+ If a "small" change keeps growing, stop and make it a ticket.
37
+
38
+ **Design.** When the human wants to see or change how a screen looks, show it live instead of sending a round: follow `../design/SKILL.md` (a preview page, the panel beside the chat, Tweaks). A design you commit takes the normal landing path above.
39
+
40
+ ## 2. Plan a round
41
+
42
+ 1. **Write the tickets** with `emit_tickets`, or reshape pending ones with `update_ticket` / `cancel_ticket`, following the tickets skill. Then **dispatch** with `dispatch_round({ seqs })`, naming exactly the pending tickets this round covers. Tickets you do not name wait for a later round. A ticket may stay blocked by tickets that landed or were cancelled in an earlier round: those count as satisfied, so keep those edges rather than re-emitting tickets. A blocker that is still pending or failed must be named in the same round.
43
+ 2. **Every worker task names its deliverable and how it is checked.** A worker with no stated check reports success on its own terms. Say what is built and what proves it: the test that goes green, the command that passes, the screen that shows it.
44
+ 3. **Title tickets `{group}: {task}`** — `Checkout: apply the coupon at payment`. The group is the area of the feature; the task is the one thing this worker does.
45
+ 4. **Keep the round to `burnConcurrency` workers or fewer.** That is a soft cap, not a limit: workers past it only queue. A wider round also has more to reconcile when it comes back.
46
+ 5. **Contract first when the work spans layers.** When workers on different layers must agree on a shape — types, an API, a schema — make one ticket land that contract in code (types, stubs, the route signature) and make the others `blockedBy` it in the same round. The engine runs the contract first and the rest in parallel against it. **Contracts live in code, never in a doc**: a document is a second thing to keep true, and workers trust the code.
47
+ 6. **UI work names the design references.** A ticket that touches UI tells its worker which design docs to follow — the project's own design system docs, and any design references in this feature's docs directory — and asks it to plan the screen, build it, then critique it against those docs with screenshots before it finishes.
48
+
49
+ Workers do not talk to each other or to you. They report through their digests, their ticket status and their commits. A worker that finds the contract wrong stops and says so in its digest; reconciling it is your job, between rounds.
50
+
51
+ ## 3. When a round comes back
52
+
53
+ You are woken with the round's outcome, each ticket's status, the verify-gate result, any review findings and where the digests are.
54
+
55
+ 1. **Trust the record over the prose.** Ticket status, the gates and the commits are what happened; a digest is a worker's account of it. Read each worker's digest with `get_ticket({ seq })`, and where the two disagree, believe the record.
56
+ 2. **Reconcile.** Did a worker report the contract wrong, or change something another worker relied on? Did a decision turn out wrong? Fix the contract in code, amend `decisions.md` and `spec.md` when what was decided changed, and update the pending tickets. A contract reported wrong is a reason to re-plan, not to push on.
57
+ 3. **Decide the next move**:
58
+ - small leftovers → fix them yourself (§1);
59
+ - more work, or a failed ticket to retry → plan the next round (§2);
60
+ - a question only the human can answer → ask it in chat and call `wait_for_you({ why })`;
61
+ - the feature does what the plan says → get it reviewed and declare ready (§6).
62
+
63
+ A failed round is just what came back. Re-plan, retry or fix it yourself.
64
+
65
+ ## 4. When to ask
66
+
67
+ There is no switch telling you how far to go on your own; you judge it.
68
+
69
+ - **Carry on through anything the plan already settled.** A fix round for what came back, a retry, a small fix you do yourself, the next round the plan already describes — do it, without asking.
70
+ - **Stop and ask before anything the plan did not settle**: a design decision, a scope change, a trade-off the human would have an opinion on. Say in chat what you need decided and what you would do, call `wait_for_you({ why })` with a few words ("which coupon rule wins?"), and end your turn. Their reply is the answer, and it may change the round.
71
+ - **After the human presses Stop, wait for their reply.** Dispatch nothing until they write to you; the server refuses a dispatch until then anyway.
72
+
73
+ ## 5. The runaway guard
74
+
75
+ When a round makes no progress on the same failure as the round before it, **stop**. Do not dispatch a third attempt at the same thing. Write the human a short status:
76
+
77
+ - **Stuck** — what is failing, in a sentence.
78
+ - **Tried** — what the last rounds did about it.
79
+ - **Needed** — what you need from them: a decision, access, a fact.
80
+
81
+ Then call `wait_for_you({ why: "stuck: <a few words>" })` and wait.
82
+
83
+ ## 6. Ready for the human
84
+
85
+ 1. **Review first.** Run a round that includes a review ticket (`kind: "review"`, as the tickets skill describes) after the last change to the work. Its findings come to you: fix them in a round, fix them yourself, or tell the human about them.
86
+ 2. **Declare ready** with `declare_ready({ scenario, whatToTest })`:
87
+ - `scenario` — a short title for the drive: "Checkout with a coupon".
88
+ - `whatToTest` — a short paragraph aimed at the drive: **what changed, where to look, and what would count as broken.** "Coupons now apply at payment. Open checkout with a cart over $20 and enter SAVE10: the total should drop by 10% before you pay. A coupon that applies twice, or a total that changes after payment, is broken."
89
+ 3. **Relay every warning it returns** — a last round with failures, work nothing has reviewed since it changed. It never refuses; the warnings are yours to pass on.
90
+
91
+ ## 7. While and after the human drives
92
+
93
+ Never dispatch while the feature is being driven (`driveActive`); the server refuses anyway. Your own commits wait until the drive stops.
94
+
95
+ The human's notes reach you as one message, with screenshots attached as files you can read, or as plain chat. Reply first with what you will do about each note, then do it: fix it yourself or run rounds. For a bug they report, reproduce it or trace it to a file and line before you card a fix; if you can do neither, make reproducing it the ticket's first acceptance criterion. "All good" is a valid message too. When you are satisfied again, declare ready with a new scenario.
96
+
97
+ ## What is not yours
98
+
99
+ - **Merge is the human's.** So is starting a backlog feature. Never merge, and never start one.
100
+ - **Workers are not yours to talk to.** Coordinate through tickets and rounds only.
101
+
102
+ ## What the server refuses
103
+
104
+ `dispatch_round` is refused, with the reason, while a round is already running, while the human is driving this feature, and after the human pressed Stop until they have replied. It is also refused when a ticket you name is blocked by a ticket outside the round that is still pending or failed (or does not exist): the refusal lists each such edge, and the fix is to name that blocker in the round too. `stop_round` stops the running round; work that already landed stays.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: prepare
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.
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, the scenario contract it feeds, discovering a project's shape, and the closing dry-run drive. Entry skill for kind=prepare sessions.
4
4
  disable-model-invocation: false
5
5
  ---
6
6
 
@@ -42,20 +42,32 @@ The eighth is what a later agent needs once the app is actually up:
42
42
 
43
43
  ## The drive contract
44
44
 
45
- A drive is a script **you** write, committed to the project, plus the two things only the server can do. Everything else — ports, database names, redis indexes, compose project names, URLs — the script computes for itself. Runcastle mandates these seven points and nothing else:
45
+ A drive is a script **you** write, committed to the project, plus the two things only the server can do. Everything else — ports, database names, redis indexes, compose project names, URLs — the script computes for itself. Runcastle mandates these eight points and nothing else:
46
46
 
47
47
  1. **The machinery lives in `.runcastle/`, committed to the repo.** Write the steps as real scripts and commit them. Versioning them with the code they prepare is the load-bearing part: a branch that adds a package, a service or a migration amends its own script, and the drive on that branch runs the amended version. Nothing inspects a diff, ever.
48
48
  2. **`driveSetupCommand` / `driveStopCommand` are one line each** — how to invoke those scripts. Logic in the setting instead of the script is logic no branch can amend.
49
- 3. **Identity comes in as `RUNCASTLE_*`.** Every drive hook and the dev pane are handed `RUNCASTLE_SLUG` (the feature slug), `RUNCASTLE_BRANCH` (the branch under the wheel) and `RUNCASTLE_ID` — that slug made identifier-safe (lowercase `[a-z0-9_]`, never leading with a digit, length-capped), so it is legal as a database, schema or container name. Derive every per-drive name from `RUNCASTLE_ID`. Never derive one from `git rev-parse`: the dry run drives under a synthetic identity on whatever branch is checked out.
49
+ 3. **Identity comes in as `RUNCASTLE_*`.** Every drive hook and the dev pane are handed `RUNCASTLE_SLUG` (the feature slug), `RUNCASTLE_BRANCH` (the branch under the wheel) and `RUNCASTLE_ID` — that slug made identifier-safe (lowercase `[a-z0-9_]`, never leading with a digit, length-capped), so it is legal as a database, schema or container name. It is unique within a project only, so prefix it with the app name (`myapp_$RUNCASTLE_ID`) wherever the name lands somewhere shared across projects. Derive every per-drive name from `RUNCASTLE_ID`. Never derive one from `git rev-parse`: the dry run drives under a synthetic identity on whatever branch is checked out.
50
50
  4. **Computed values go back out through `.runcastle/drive.env`.** Setup appends plain `KEY=VALUE` lines there; when it exits, the server parses that file and overlays it verbatim onto the dev pane and the stop hook, and shows the variable NAMES on the timeline. It is the only way a value your script computed reaches the dev server — a variable exported inside the script dies with the script. Truncate the file at the top of setup so a rerun does not accumulate stale lines.
51
51
  5. **`.runcastle/drive.env` MUST be gitignored.** Add the entry yourself. The server deletes the file when a drive ends, but a scratch file holding a connection string must never be one `git add -A` away from a commit.
52
52
  6. **Every step is unconditionally idempotent.** Install, migrate, seed, compose up — run them every time, never behind a "has anything changed?" check. A no-op on a clean tree is cheap; a skipped install on a branch that added a package is a dead drive. This is how the loop absorbs whatever a feature branch changed with no delta detection anywhere.
53
53
  7. **Exit 0 means the services are actually up.** The waits belong INSIDE the script — `docker compose up --wait`, a `pg_isready` loop, curl-until-healthy — because the dev pane starts the instant setup returns. The server waits for the app itself; it will not wait for your database.
54
+ 8. **Everything the app needs to reach its data goes out through `.runcastle/drive.env`** — above all the database URL, plus any cache or queue URL the app reads. Scenarios (below) and the dev server both get exactly that overlay and nothing else. A setup that hands the database to the dev server some other way — writing the app's own `.env.local`, say — leaves every scenario seeding the wrong database, or none, while the app looks fine.
54
55
 
55
56
  Stop undoes what setup made, **for this identity only**: drop the database it created, take its compose project down with its volumes, free its ports. The human's own stack is never yours.
56
57
 
57
58
  Writing those files is the one exception to a preparation session not editing the repo: you may write `.runcastle/` and `.gitignore` in this checkout and nothing else — a hook enforces it. Show the human the script before you commit it; it is their repo and their PR.
58
59
 
60
+ ## Scenarios
61
+
62
+ A drive can land the human in a seeded state instead of a blank app. That is the **scenario contract**, a peer of the drive contract above:
63
+
64
+ - A scenario is a file at `.runcastle/scenarios/<group>/<name>.<ext>`, committed. The folder is the group (`checkout/` → Checkout), the file name the display name (`expired-coupon.ts` → Expired coupon). Files and folders starting with `_` (e.g. `_fixtures/`) are shared code, never listed.
65
+ - The project's **scenario runner** is one command with a `{file}` placeholder (`bun {file}`, `bin/rails runner {file}`). Runcastle runs it in the drive's checkout with the `RUNCASTLE_*` identity, the `drive.env` overlay (point 8 is why that overlay must carry the database), and a fresh `RUNCASTLE_SCENARIO_RUN` per run that keeps entities unique.
66
+ - It prints `ENTRY_URL=<url>` (the last wins; relative resolves against the drive URL), and optionally `TOKEN=<value>`, a credential for API calls.
67
+ - A sibling `.http` file at the same path (`feed/blocked-only.ts` + `feed/blocked-only.http`) is that scenario's API call, sent from the drive's API view with **Try it**.
68
+
69
+ **You do not set scenarios up.** Record no runner, write no drive-only login route, add no fixtures. The first + New scenario chat in a drive does all of that, where it is proven by landing the human; a route written blind here is not. Your part is point 8: a `drive.env` that carries the data.
70
+
59
71
  ## Discover the shape before you author anything
60
72
 
61
73
  Projects differ in every dimension this touches, and a script fitted to a project you imagined is worse than no script at all. Your system prompt reports what the server could probe for itself — the platform, whether there is a compose file, whether `.runcastle/` already exists, which lockfile is present. Take those as read and find out the rest, by reading the repo and running things here:
@@ -121,14 +133,16 @@ Once the open drive keys are recorded, end the session by proposing one. Recorde
121
133
 
122
134
  On a yes, `dry_run_drive({ action })` runs it in two halves and you inspect between them:
123
135
 
124
- 1. **`start`** — the server runs `driveSetupCommand` with the identity variables, reads back the `.runcastle/drive.env` it wrote (the reply lists the variable NAMES it parsed) and spawns `devCommand` in a real drive pane under that overlay. Identity is the reserved slug `prep-dry-run`, so `RUNCASTLE_ID` is `prep_dry_run` and a script deriving from it makes e.g. `myapp_prep_dry_run`, on the current branch. Nothing is checked out.
125
- 2. **While it is up**, check what the server cannot: the variable names came back as you meant them, the temp database exists and is FRESH, the migrations applied, and the app actually RESPONDS at the sniffed URL — the server waits for it to answer before "Open app" goes live, but only you can say the page is the right one. `status` gives you the pane and the URL while you work.
136
+ 1. **`start`** — the server runs `driveSetupCommand` with the identity variables, reads back the `.runcastle/drive.env` it wrote (the reply lists the variable NAMES it parsed) and spawns `devCommand` in a real drive pane under that overlay. Identity is the reserved slug `prep-dry-run`, so `RUNCASTLE_ID` is `prep_dry_run` (in every project — the id is unique within a project only) and a script deriving from it makes e.g. `myapp_prep_dry_run`, on the current branch. Nothing is checked out.
137
+ 2. **While it is up**, check what the server cannot: the variable names came back as you meant them — and they include the database URL, or whatever the app reaches its data by (point 8). If they do not, the app is getting its data some other way that scenarios cannot see: tell the human, fix the setup script to write it to `drive.env`, and drive again. That holds for an existing setup you did not write, too. Then: the temp database exists and is FRESH, the migrations applied, and the app actually RESPONDS at the sniffed URL — the server waits for it to answer before "Open app" goes live, but only you can say the page is the right one. `status` gives you the pane and the URL while you work.
126
138
  3. **`stop`** — the server runs `driveStopCommand` under the same overlay. Then check the cleanup: temp database gone, no orphaned process, container or volume left behind.
127
139
 
128
140
  Anything off at any step, fix it — amend the script, or `record_finding` for a key — and run the WHOLE thing again until a pass is clean. Watch especially for a `myapp_prep_dry_run` left standing after the stop: an idempotent setup will happily reuse it on the next start, so a teardown that never worked reads exactly like a drive that did.
129
141
 
130
142
  The verified stamps are computed server-side from what the machinery observed, and only on a clean full pass. You cannot mark your own homework: your deeper checks decide whether to retry, never what gets stamped.
131
143
 
144
+ Close with one line telling the human that + New scenario in their next test drive sets up scenarios, so a drive can land them in the state they want to test.
145
+
132
146
  ## When nothing is open
133
147
 
134
148
  If your system prompt says every key is already set, this session is a **confirmation**, not a preparation. Say which values are recorded and how stale they are, ask the human whether they still hold, and stop. Do not re-derive a settled value, and do not propose a dry run unasked — replacing a measured value with a fresh guess makes preparation worse, not better.
@@ -13,6 +13,10 @@ Write the real thing in **whatever language this host actually runs**, which the
13
13
 
14
14
  The one thing that must NOT change when you translate: the contract (identity in via `RUNCASTLE_*`, computed values out via `.runcastle/drive.env`, unconditional idempotence, exit 0 means up).
15
15
 
16
+ Every recipe below writes the database URL (or its connection string) to `drive.env`. Keep that when you adapt one: scenarios reach the drive's data only through that overlay.
17
+
18
+ `RUNCASTLE_ID` is unique within a project only — every project's dry run is `prep_dry_run` and every project drive is `project_drive`, and two projects can drive at once — so anything shared across projects (a local Redis, a schema in a shared database) is named with the app prefix, `myapp_$RUNCASTLE_ID`, never the bare id.
19
+
16
20
  ## Postgres, one database per drive
17
21
 
18
22
  Name the database from `RUNCASTLE_ID`, create it if it is not there, migrate, and hand the URL back:
@@ -42,13 +46,13 @@ The compose file maps `"${PG_PORT}:5432"`. Stop is `docker compose down -v`, whi
42
46
 
43
47
  ## Redis
44
48
 
45
- Do not run a second server. Take a logical database index or a key prefix derived in-script from `RUNCASTLE_ID` — a small hash into 1..15 for the index — and leave db 0 to the human, whose own work is already in it. Write `REDIS_URL=redis://localhost:6379/7` or `REDIS_PREFIX=$RUNCASTLE_ID:` out, and have stop flush only that index or prefix.
49
+ Do not run a second server. Take a logical database index or a key prefix derived in-script from `myapp_$RUNCASTLE_ID` — a small hash into 1..15 for the index — and leave db 0 to the human, whose own work is already in it. Derive from the prefixed name, not the bare id: two projects' drives can share an id, and stop must flush only its own. Write `REDIS_URL=redis://localhost:6379/7` or `REDIS_PREFIX=myapp_$RUNCASTLE_ID:` out, and have stop flush only that index or prefix.
46
50
 
47
51
  ## Hosted databases
48
52
 
49
53
  Where the vendor has branches (Neon and its kin), setup creates one named for `RUNCASTLE_ID` with the vendor CLI and writes the connection string it prints to `drive.env`; stop deletes that branch.
50
54
 
51
- Where the role has no CREATEDB grant, take a schema per drive instead — `CREATE SCHEMA IF NOT EXISTS "$RUNCASTLE_ID"` plus a URL with the search path pinned to it, and `DROP SCHEMA ... CASCADE` at stop.
55
+ Where the role has no CREATEDB grant, take a schema per drive instead — `CREATE SCHEMA IF NOT EXISTS "myapp_$RUNCASTLE_ID"` plus a URL with the search path pinned to it, and `DROP SCHEMA ... CASCADE` at stop.
52
56
 
53
57
  Ask the human which they have: it is their bill and their production neighbour.
54
58