runcastle 1.4.8 → 1.5.4

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 (52) hide show
  1. package/bin/runcastle.js +10604 -7555
  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/0052_friendly_black_tarantula.sql +1 -0
  10. package/drizzle/0053_soft_karma.sql +12 -0
  11. package/drizzle/meta/0045_snapshot.json +1220 -0
  12. package/drizzle/meta/0046_snapshot.json +1227 -0
  13. package/drizzle/meta/0047_snapshot.json +1349 -0
  14. package/drizzle/meta/0048_snapshot.json +1356 -0
  15. package/drizzle/meta/0050_snapshot.json +1349 -0
  16. package/drizzle/meta/0051_snapshot.json +1356 -0
  17. package/drizzle/meta/0052_snapshot.json +1364 -0
  18. package/drizzle/meta/0053_snapshot.json +1283 -0
  19. package/drizzle/meta/_journal.json +63 -0
  20. package/hook-client.ts +110 -9
  21. package/index.js +10601 -7552
  22. package/package.json +1 -1
  23. package/skills/burner/implement-ticket.md +0 -1
  24. package/skills/burner/review-ticket.md +7 -5
  25. package/skills/burner/verify-fixes.md +4 -2
  26. package/skills/packs/README.md +24 -28
  27. package/skills/packs/runcastle/.claude-plugin/plugin.json +1 -1
  28. package/skills/packs/runcastle/skills/feature/SKILL.md +72 -0
  29. package/skills/packs/runcastle/skills/feature/references/design.md +132 -0
  30. package/skills/packs/runcastle/skills/feature/references/grilling.md +99 -0
  31. package/skills/packs/runcastle/skills/feature/references/planning-alone.md +70 -0
  32. package/skills/packs/runcastle/skills/{ideate → feature}/references/preview-starter.html +27 -17
  33. package/skills/packs/runcastle/skills/{ideate → feature}/references/preview.md +67 -8
  34. package/skills/packs/runcastle/skills/feature/references/rounds.md +91 -0
  35. package/skills/packs/runcastle/skills/feature/references/spec.md +54 -0
  36. package/skills/packs/runcastle/skills/feature/references/tickets.md +229 -0
  37. package/skills/packs/runcastle/skills/prepare/SKILL.md +19 -5
  38. package/skills/packs/runcastle/skills/prepare/references/recipes.md +6 -2
  39. package/skills/packs/runcastle/skills/project/SKILL.md +115 -14
  40. package/skills/packs/runcastle/skills/project/references/charter.md +3 -3
  41. package/skills/packs/runcastle/skills/project/references/health-sweeps.md +5 -5
  42. package/skills/packs/runcastle/skills/project/references/triage.md +21 -24
  43. package/skills/packs/runcastle/skills/qa/SKILL.md +1 -2
  44. package/web/assets/index-CT9k1kz0.css +1 -0
  45. package/web/assets/index-DVR7VbWW.js +186 -0
  46. package/web/index.html +2 -2
  47. package/skills/packs/runcastle/skills/ideate/SKILL.md +0 -128
  48. package/skills/packs/runcastle/skills/revisit/SKILL.md +0 -78
  49. package/skills/packs/runcastle/skills/spec/SKILL.md +0 -47
  50. package/skills/packs/runcastle/skills/tickets/SKILL.md +0 -119
  51. package/web/assets/index-CUoLAKY6.js +0 -188
  52. package/web/assets/index-bNgQedte.css +0 -1
@@ -16,9 +16,6 @@
16
16
  --color-primary:#eceef4;--color-on-primary:#0a0c11;
17
17
  --color-accent:#4f8ef7;--color-accent-text:#7aaafa;--color-accent-subtle:#16213a;
18
18
  --color-success:#46b67a;--color-warning:#e2a53b;--color-danger:#f06363;--color-danger-subtle:#2a1515;
19
- --color-phase-ideation:#ec7cb5;--color-phase-spec:#35c2b8;--color-phase-implementation:#f08a4b;
20
- --color-phase-planning:var(--color-phase-spec);--color-phase-building:var(--color-phase-implementation);
21
- --color-phase-review:var(--color-accent);--color-phase-shipped:var(--color-success);
22
19
  --color-scrim:rgb(0 0 0 / 0.6);
23
20
  --font-sans:"Geist Variable","Segoe UI Variable",system-ui,-apple-system,sans-serif;
24
21
  --font-mono:"Geist Mono Variable","Cascadia Code",ui-monospace,Consolas,monospace;
@@ -28,11 +25,10 @@
28
25
  color-scheme:light;
29
26
  --color-canvas:#f4f6f9;--color-surface:#ffffff;--color-surface-raised:#ffffff;--color-surface-hover:#eef1f6;--color-surface-selected:#e4e8f1;--color-surface-inset:#f4f6f9;
30
27
  --color-border:#dfe3ec;--color-border-subtle:rgb(22 30 50 / 0.07);--color-border-strong:#cdd2de;
31
- --color-text:#161a24;--color-text-secondary:#4c5366;--color-text-tertiary:#5c6377;--color-text-disabled:#a4aab9;--color-icon:#6a7185;
28
+ --color-text:#161a24;--color-text-secondary:#596075;--color-text-tertiary:#676e82;--color-text-disabled:#a4aab9;--color-icon:#7a8195;
32
29
  --color-primary:#161a24;--color-on-primary:#ffffff;
33
- --color-accent:#2f6fe4;--color-accent-text:#2463d6;--color-accent-subtle:#ecf2fe;
34
- --color-success:#1e8e53;--color-warning:#a86a06;--color-danger:#c42828;--color-danger-subtle:#fdeeee;
35
- --color-phase-ideation:#c13d7e;--color-phase-spec:#0b7f78;--color-phase-implementation:#b44f0c;
30
+ --color-accent:#3a7ef0;--color-accent-text:#2361d4;--color-accent-subtle:#ecf2fe;
31
+ --color-success:#23955a;--color-warning:#c06e00;--color-danger:#df5050;--color-danger-subtle:#fdeeee;
36
32
  --color-scrim:rgb(22 26 36 / 0.28);
37
33
  }
38
34
  @media (prefers-color-scheme:light){
@@ -40,11 +36,10 @@
40
36
  color-scheme:light;
41
37
  --color-canvas:#f4f6f9;--color-surface:#ffffff;--color-surface-raised:#ffffff;--color-surface-hover:#eef1f6;--color-surface-selected:#e4e8f1;--color-surface-inset:#f4f6f9;
42
38
  --color-border:#dfe3ec;--color-border-subtle:rgb(22 30 50 / 0.07);--color-border-strong:#cdd2de;
43
- --color-text:#161a24;--color-text-secondary:#4c5366;--color-text-tertiary:#5c6377;--color-text-disabled:#a4aab9;--color-icon:#6a7185;
39
+ --color-text:#161a24;--color-text-secondary:#596075;--color-text-tertiary:#676e82;--color-text-disabled:#a4aab9;--color-icon:#7a8195;
44
40
  --color-primary:#161a24;--color-on-primary:#ffffff;
45
- --color-accent:#2f6fe4;--color-accent-text:#2463d6;--color-accent-subtle:#ecf2fe;
46
- --color-success:#1e8e53;--color-warning:#a86a06;--color-danger:#c42828;--color-danger-subtle:#fdeeee;
47
- --color-phase-ideation:#c13d7e;--color-phase-spec:#0b7f78;--color-phase-implementation:#b44f0c;
41
+ --color-accent:#3a7ef0;--color-accent-text:#2361d4;--color-accent-subtle:#ecf2fe;
42
+ --color-success:#23955a;--color-warning:#c06e00;--color-danger:#df5050;--color-danger-subtle:#fdeeee;
48
43
  --color-scrim:rgb(22 26 36 / 0.28);
49
44
  }
50
45
  }
@@ -89,6 +84,12 @@ button{font:inherit;color:inherit;background:none;border:0;padding:0;cursor:poin
89
84
  /* the framed panel a mocked screen sits in; inside it, use the project's own styling */
90
85
  .screen{border:1px solid var(--color-border);border-radius:var(--radius-lg);overflow:hidden;background:var(--color-surface);position:relative}
91
86
 
87
+ /* a screen designed in design mode: a name, one line, and a button that opens it in the app's design panel */
88
+ .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)}
89
+ .designed p{color:var(--color-text-secondary)}
90
+ .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}
91
+ .designed button[data-open-design]:hover{background:var(--color-accent);color:#fff}
92
+
92
93
  /* decided / left out / under the hood */
93
94
  .dlist{border-top:1px solid var(--color-border-subtle);margin-top:12px}
94
95
  .dlist details{border-bottom:1px solid var(--color-border-subtle)}
@@ -135,7 +136,7 @@ button{font:inherit;color:inherit;background:none;border:0;padding:0;cursor:poin
135
136
  <header id="what">
136
137
  <h1>[Feature title]</h1>
137
138
  <p class="lede">[One or two sentences: what this feature is, in the human's words.]</p>
138
- <div class="meta"><span>Lap 1</span><span><span data-dcount></span> decisions</span><span class="mono">docs/features/[slug]/preview.html</span></div>
139
+ <div class="meta"><span><span data-dcount></span> decisions</span><span class="mono">docs/features/[slug]/preview.html</span></div>
139
140
  <nav class="jump" aria-label="Parts of this preview">
140
141
  <a href="#show">The show</a><a href="#decided">Decided <b data-dcount></b></a><a href="#left-out">Left out</a><a href="#hood">Under the hood</a>
141
142
  </nav>
@@ -152,7 +153,10 @@ button{font:inherit;color:inherit;background:none;border:0;padding:0;cursor:poin
152
153
  inside a .screen panel, wrapped in a screen block.
153
154
 
154
155
  Example screen block (copy it, rename it in kebab case, uncomment the
155
- panel; delete it if the show has no screen):
156
+ panel; delete it if the show has no screen). A screen designed in design
157
+ mode gets a designed-screen entry instead of a mock; see preview.md:
158
+ <div class="designed"><h3>Name</h3><p>What is real, what is sample.</p>
159
+ <button data-open-design="/preview/path">Open in design</button></div>
156
160
  -->
157
161
  <!-- screen:example-name -->
158
162
  <!--
@@ -174,8 +178,8 @@ button{font:inherit;color:inherit;background:none;border:0;padding:0;cursor:poin
174
178
  <section id="left-out">
175
179
  <h2>What is left out</h2>
176
180
  <div class="out2">
177
- <div><h3>Later laps</h3><ul>
178
- <li><i class="dot"></i>[Deferred to a later lap]</li>
181
+ <div><h3>Later</h3><ul>
182
+ <li><i class="dot"></i>[Deferred to a backlog feature]</li>
179
183
  </ul></div>
180
184
  <div><h3>Not this feature</h3><ul>
181
185
  <li><i class="dot"></i>[Out of scope]</li>
@@ -250,13 +254,19 @@ button{font:inherit;color:inherit;background:none;border:0;padding:0;cursor:poin
250
254
 
251
255
  // a pin opens its decision; in-page links scroll by script, because hash
252
256
  // navigation is unreliable inside the app's sandboxed frame
253
- function go(el) { el.scrollIntoView({ behavior: 'smooth', block: 'center' }) }
257
+ function reveal(el) { el.scrollIntoView({ behavior: 'smooth', block: 'center' }) }
254
258
  document.addEventListener('click', function (e) {
259
+ // Open in design: the app listens for exactly this message and opens the design panel
260
+ var od = e.target.closest('[data-open-design]')
261
+ if (od) {
262
+ parent.postMessage({ type: 'runcastle:open-design', path: od.dataset.openDesign }, '*')
263
+ return
264
+ }
255
265
  var pin = e.target.closest('a.pin[data-d]')
256
266
  if (pin) {
257
267
  e.preventDefault()
258
268
  var d = $('#d' + pin.dataset.d)
259
- d.open = true; go(d); d.classList.add('flash')
269
+ d.open = true; reveal(d); d.classList.add('flash')
260
270
  setTimeout(function () { d.classList.remove('flash') }, 1400)
261
271
  return
262
272
  }
@@ -1,12 +1,36 @@
1
1
  # The plan preview — the page the human looks at
2
2
 
3
- Load this when the decisions are locked and the preview step (§3 of the ideate
4
- skill) arrives, or when a decision edit at planning means the preview has to be
5
- redrawn. Otherwise it costs this conversation nothing.
3
+ Load this when the decisions are settled and you are about to show the human
4
+ the plan, or when a decision edit means the preview has to be redrawn.
5
+ Otherwise it costs this conversation nothing.
6
6
 
7
7
  The preview is the one page the human reads the feature by. They confirm the
8
8
  plan by looking at it, and when tired it is the only thing they read before
9
- Burn. Draw it for that reader.
9
+ they say yes to the first round. Draw it for that reader.
10
+
11
+ ## Show it, then get the yes
12
+
13
+ 1. **Draw it** from `./preview-starter.html`, following the rules below.
14
+ 2. **Check it against `decisions.md`** (the self-check at the end). Fix the
15
+ preview before anyone sees it.
16
+ 3. **Show it**: `show_panel({ view: 'plan' })` opens it beside the chat. Tell
17
+ the human what you propose — tickets and rounds, or work you will do
18
+ yourself — and ask for their explicit yes. Then wait for the answer.
19
+ 4. **A correction is a decision edit plus a redraw.** Edit the decision in
20
+ `decisions.md` and redraw the preview in the same move, then ask them to
21
+ look again.
22
+
23
+ With no human present, steps 1 and 2 are the whole of this; `./planning-alone.md`
24
+ says how that run asks for the yes.
25
+
26
+ **Never skip the preview on your own judgement** — not because the feature is
27
+ small, not because the grill went quickly. A small feature gets a small
28
+ preview. **If the human says to skip it**, write no file. The yes is still
29
+ needed.
30
+
31
+ **The redraw rule.** Any later edit to `decisions.md` updates `preview.html` in
32
+ the same move, before anything else, while the plan is still being agreed or
33
+ built.
10
34
 
11
35
  ## The file
12
36
 
@@ -30,13 +54,14 @@ Burn. Draw it for that reader.
30
54
  title, with the reason available on demand (the starter's `D` list builds
31
55
  this). Where a decision is visible in the show, pin its number on that spot:
32
56
  `<a class="pin" data-d="3">3</a>`.
33
- **A self-planned feature** (§1a of the ideate skill) opens this part with a
57
+ **A self-planned feature** (`./planning-alone.md`) opens this part with a
34
58
  **Check first** group of at most five decisions, the ones the planner picked.
35
59
  Each entry shows the decision's number and title, its alternative, and why it
36
60
  is uncertain (the starter's `C` list builds this). The remaining decisions
37
61
  follow in the list, each showing that it was assumed (a third element on its
38
62
  `D` entry). **A grilled feature has neither**: no group and no assumed marks.
39
- 4. **What is left out.** Out of scope, and anything deferred to a later lap.
63
+ 4. **What is left out.** Out of scope, and under **Later** anything deferred
64
+ to a backlog feature.
40
65
  5. **Under the hood.** The technical shape for a reader who wants it,
41
66
  **collapsed by default**, drawn where it can be.
42
67
 
@@ -65,10 +90,41 @@ Each screen in the show is a **screen block**: the lines between two markers,
65
90
  ```
66
91
 
67
92
  with `<name>` in kebab case and unique in the file. A ticket that builds that
68
- screen names the block, and its burn agent reads only those lines — so put
93
+ screen names the block, and its worker reads only those lines — so put
69
94
  everything the screen needs to be understood between the markers, and nothing
70
95
  else. Delete the starter's `example-name` block if the show has no screen.
71
96
 
97
+ ## Designed screens
98
+
99
+ A screen designed in design mode (`./design.md`) is **not mocked
100
+ again**. Its place in the show is a **designed-screen entry**:
101
+
102
+ - its name;
103
+ - one line saying what is real and what is sample data;
104
+ - an **Open in design** button: `<button data-open-design="/preview/path">Open in design</button>`,
105
+ where the value is the preview page's path on the design server. The starter
106
+ wires it; the app answers by opening the design panel at that path, starting
107
+ the server first if needed.
108
+
109
+ Wrap the entry in a screen block, but put no markup in it. The block names the
110
+ **committed component file and the preview page path**, so the ticket that
111
+ connects the screen has something to point at. Use the starter's `.designed`
112
+ entry:
113
+
114
+ ```html
115
+ <!-- screen:invoice-list -->
116
+ <div class="designed">
117
+ <h3>Invoice list</h3>
118
+ <p>Layout and states are real components; the rows are sample data.</p>
119
+ <p class="mono">src/components/InvoiceList.tsx · preview /preview/invoice-list</p>
120
+ <button data-open-design="/preview/invoice-list">Open in design</button>
121
+ </div>
122
+ <!-- /screen:invoice-list -->
123
+ ```
124
+
125
+ Hand mocks remain for screens nobody designed live, for server-built pages and
126
+ for native mobile, which design mode cannot preview.
127
+
72
128
  ## Inline only
73
129
 
74
130
  **Styles, scripts and images are inline. The file makes no network request** —
@@ -77,7 +133,8 @@ or inline SVG. This is enforced, not a preference: the app renders the preview
77
133
  in a sandboxed frame that runs scripts and refuses every fetch, external image
78
134
  and font, form post and popup. Anything hosted elsewhere simply does not load.
79
135
  In-page links scroll by script (the starter does this), because hash navigation
80
- is unreliable inside that frame.
136
+ is unreliable inside that frame. The Open in design button posts a message to
137
+ the app, which is not a request, so it stays within this rule.
81
138
 
82
139
  ## Sizing
83
140
 
@@ -95,4 +152,6 @@ minute for a small feature, a few minutes for a large one.**
95
152
  For a grilled feature, `C` is empty and no decision is marked.
96
153
  - Every pin points at the decision the spot actually shows.
97
154
  - The five parts are in order; **Under the hood** is closed.
155
+ - Every screen designed in design mode is a designed-screen entry that names its
156
+ committed component and preview page, not a redrawn mock.
98
157
  - The file is still fully inline.
@@ -0,0 +1,91 @@
1
+ # Running rounds
2
+
3
+ Load this when you are about to dispatch a round, or you have just been woken by
4
+ one coming back.
5
+
6
+ A round is how workers build: each gets one ticket, a clean sandbox and the full
7
+ brief, and works on its own branch that lands on the feature branch. You
8
+ dispatch it, you are woken when it comes back, you check what it did, and you
9
+ plan the next move.
10
+
11
+ Start the first round only after the human's explicit yes in chat, and ask again
12
+ before any round the agreed plan did not settle (SKILL.md, "The human's yes").
13
+
14
+ ## What you run on
15
+
16
+ `get_feature_context` carries:
17
+
18
+ - `rounds` — every round so far, numbered from 1: the tickets it held, how it ended, its gates.
19
+ - `latestReady` — your newest ready declaration, if any.
20
+ - `driveActive` — the human is test-driving this feature right now.
21
+ - `stopped` — the human pressed Stop and has not replied since.
22
+ - `waitingReason` — why the feature is waiting on the human, if it is.
23
+ - `burnConcurrency` — how many workers the project runs at once.
24
+
25
+ ## Plan a round
26
+
27
+ 1. **Write the tickets** with `emit_tickets`, or reshape pending ones with
28
+ `update_ticket` / `cancel_ticket`, following `./tickets.md`. Then **dispatch**
29
+ with `dispatch_round({ seqs })`, naming exactly the pending tickets this round
30
+ covers. Tickets you do not name wait for a later round. A ticket may stay
31
+ blocked by tickets that landed or were cancelled in an earlier round: those
32
+ count as satisfied. A blocker that is still pending or failed must be named
33
+ in the same round.
34
+ 2. **Every worker task names its deliverable and how it is checked.** A worker
35
+ with no stated check reports success on its own terms. Say what is built and
36
+ what proves it: the test that goes green, the command that passes, the screen
37
+ that shows it.
38
+ 3. **Keep the round to `burnConcurrency` workers or fewer.** A soft cap: workers
39
+ past it only queue, and a wider round has more to reconcile when it returns.
40
+ 4. **Contract first when the work spans layers.** When workers on different
41
+ layers must agree on a shape — types, an API, a schema — make one ticket land
42
+ that contract in code and make the others `blockedBy` it in the same round.
43
+ **Contracts live in code, never in a doc**: workers trust the code.
44
+ 5. **UI work names the design references.** A ticket that touches UI tells its
45
+ worker which design docs to follow — the project's own design system docs and
46
+ any design references in this feature's docs — and asks it to plan the
47
+ screen, build it, then critique it against those docs with screenshots.
48
+
49
+ Workers do not talk to each other or to you. They report through their digests,
50
+ their ticket status and their commits. A worker that finds the contract wrong
51
+ stops and says so in its digest; reconciling it is your job, between rounds.
52
+
53
+ ## When a round comes back
54
+
55
+ You are woken with the round's outcome, each ticket's status, the verify-gate
56
+ result, any review findings and where the digests are.
57
+
58
+ 1. **Trust the record over the prose.** Ticket status, the gates and the commits
59
+ are what happened; a digest is a worker's account of it. Read each digest
60
+ with `get_ticket({ seq })`, and where the two disagree, believe the record.
61
+ 2. **Reconcile.** Did a worker report the contract wrong, or change something
62
+ another relied on? Did a decision turn out wrong? Fix the contract in code,
63
+ amend `decisions.md` and `spec.md` when what was decided changed, and update
64
+ the pending tickets. A contract reported wrong is a reason to re-plan.
65
+ 3. **Handle review findings.** A finding stays open until you resolve it: fix it
66
+ in a round or yourself, or dismiss it and say why.
67
+ 4. **Decide the next move**:
68
+ - small leftovers → fix them yourself;
69
+ - more work, or a failed ticket to retry → plan the next round;
70
+ - a question only the human can answer → ask it in chat and call `wait_for_you({ why })`;
71
+ - the feature does what the plan says → run the checks and declare ready (SKILL.md, "Ready for the human").
72
+
73
+ A failed round is just what came back. Re-plan, retry or fix it yourself.
74
+ `declare_ready` warns when the last round had failures; pass that on.
75
+
76
+ ## While and after the human drives
77
+
78
+ Never dispatch while the feature is being driven (`driveActive`). Your own
79
+ commits wait until the drive stops. When the human reports back, follow
80
+ SKILL.md, "After a drive"; when you are satisfied again, declare ready with a
81
+ new scenario.
82
+
83
+ ## What the server refuses
84
+
85
+ `dispatch_round` is refused, with the reason, for a draft or shipped feature,
86
+ while a round is already running, while the human is driving this feature, and
87
+ after the human pressed Stop until they have replied. It is also refused when a
88
+ ticket you name is blocked by a ticket outside the round that is still pending
89
+ or failed (or does not exist): the refusal lists each such edge, and the fix is
90
+ to name that blocker in the round too. `stop_round` stops the running round;
91
+ work that already landed stays.
@@ -0,0 +1,54 @@
1
+ # Writing the spec
2
+
3
+ Load this once what the feature does is settled — whether you will build it
4
+ yourself or hand it to workers — or when a later decision changes what
5
+ `spec.md` says.
6
+
7
+ The spec is synthesis, not an interview: write it from the conversation and
8
+ `docs/features/<slug>/decisions.md`. Do not re-ask the human anything that is
9
+ settled.
10
+
11
+ ## Process
12
+
13
+ 1. **Load state.** `mcp__runcastle__get_feature_context` for the decisions and
14
+ any existing `spec.md` (this may be a rewrite). Read any canonical doc listed
15
+ in `notInlined` before writing a word. Explore the codebase for its current
16
+ state if you have not already; use the project's domain vocabulary and
17
+ respect any ADRs in the area you touch.
18
+
19
+ 2. **Sketch the seams** — the public boundaries the feature will be tested at.
20
+ Prefer *existing* seams to new ones; use the *highest* seam possible; the
21
+ fewer across the codebase, the better — the ideal is one. If new seams are
22
+ needed, propose them at the highest point you can. They flow straight into
23
+ the tickets and the tests, so get them right here.
24
+
25
+ 3. **Write `docs/features/<slug>/spec.md`** with exactly these sections:
26
+
27
+ ```markdown
28
+ # <Feature title>
29
+
30
+ ## Problem
31
+ The problem the user faces, from the user's perspective.
32
+
33
+ ## Approach
34
+ The solution from the user's perspective, then the shape of it: modules built/modified, their interfaces, schema/API/contract decisions, key interactions. Prose, not a file-by-file plan.
35
+
36
+ ## Seams
37
+ The interfaces to test at (from step 2) — each named, with what it lets you observe. Flag which are existing vs. new.
38
+
39
+ ## Out of scope
40
+ What this feature explicitly does not cover, including scope parked as backlog features.
41
+
42
+ ## Open questions
43
+ Anything still unresolved or deliberately deferred.
44
+ ```
45
+
46
+ Do **not** hardcode file paths or code snippets — they go stale. Exception:
47
+ when the conversation produced a snippet that pins a decision more precisely
48
+ than prose can (a state machine, a reducer, a schema, a type shape), inline
49
+ the decision-rich parts and say where it came from.
50
+
51
+ 4. **Commit it** with the decisions it was written from, as a docs commit.
52
+
53
+ When a later decision changes what the feature does, amend the affected
54
+ sections in place rather than appending.
@@ -0,0 +1,229 @@
1
+ <!-- Forked from Matt Pocock's to-tickets skill, via https://github.com/mattpocock/skills, 2026-07-14, adapted for runcastle -->
2
+
3
+ # Writing tickets
4
+
5
+ Load this when work is worth handing to sandboxed workers and you are about to
6
+ write, reshape or cancel tickets.
7
+
8
+ Break the work into **tickets** — vertical slices, each cutting end-to-end
9
+ through every layer it needs, each big enough to be worth a whole agent session
10
+ and small enough to finish inside one — and emit them with
11
+ `mcp__runcastle__emit_tickets`. Synthesize from `spec.md` and the conversation.
12
+ The feature's existing tickets come as summary rows in `get_feature_context`;
13
+ `mcp__runcastle__get_ticket({ seq })` holds one ticket's context, acceptance
14
+ criteria and digest.
15
+
16
+ ## The one thing that makes a runcastle ticket different
17
+
18
+ Each ticket is executed by a **single fresh agent, alone, in a sandbox, with
19
+ ~100k tokens and NO way to ask a follow-up question.** It sees only what you put
20
+ in the ticket plus the code already in the repo. So — *inverting* the usual
21
+ "avoid file paths, they go stale" rule — the ticket's `context` field **must be
22
+ concrete and self-sufficient**: name the files to touch, the existing patterns
23
+ to copy, the type/schema shapes, the gotchas, the commands to run and verify
24
+ with. A ticket that assumes the agent can ask "where does X live?" is a broken
25
+ ticket. Over-specify context; under-specify nothing.
26
+
27
+ ## The second thing: a ticket costs ~10 minutes before it does any work — and pays it twice if it overruns
28
+
29
+ That fresh agent is fresh *every time*. Each ticket gets its own container: cold
30
+ build, a full dependency install (measured 70–507s, ~2.5 min average), then an
31
+ agent that re-reads the spec, the docs digest, and every file your `context`
32
+ names — reading the repo was 16% of all agent time across 42 measured attempts.
33
+ **This overhead is paid per ticket and is the same whether the ticket is twenty
34
+ lines or a day's work.** Splitting one coherent piece of work into three tickets
35
+ triples it, and adds two seams where a new agent has to reconstruct what the
36
+ previous one had just finished learning.
37
+
38
+ But that meter runs both ways. A ticket too big to finish in one pass gets no
39
+ cheap second wind: an iteration is not a retry (ADR-0008) — the container is
40
+ rebuilt, the install runs again, and a brand-new agent re-reads everything from
41
+ the last commit, having lost every conclusion its predecessor reached.
42
+
43
+ So size a ticket to **earn its container and still land in one pass**:
44
+ substantial, with room left at the end for the tests, the self-review and the
45
+ commits. Fine-grained slicing belongs *inside* a ticket, as the
46
+ `acceptanceCriteria` list: the worker goes red→green one criterion at a time and
47
+ commits each green slice, so one ticket with six criteria orients once; six
48
+ one-criterion tickets orient six times.
49
+
50
+ Expect a **handful of substantial tickets** for a feature — roughly 4–8, each
51
+ worth a session. A dozen thin ones means you sliced by layer or by file, not by
52
+ behaviour. Two or three giant ones means you will pay back the containers you
53
+ saved, with interest, when they overrun.
54
+
55
+ ## 1. Draft vertical slices
56
+
57
+ <vertical-slice-rules>
58
+ - Each slice cuts a narrow but COMPLETE path through every layer it needs (schema, API, UI, tests) — vertical, NOT a horizontal slice of one layer.
59
+ - A completed slice is demoable or verifiable on its own.
60
+ - Each slice is worth one fresh ~100k-token agent session working alone and **lands inside it** — leave room for the orientation at the front and the tests, self-review and commits at the back.
61
+ - **Merge when a merge test holds.** Two drafts are one ticket if they touch the same files, share a type or module, or the second one's first act would be re-reading what the first just wrote. Same for the obvious siblings of one behaviour: create/edit/delete of a resource, an endpoint and the UI that calls it, code and the tests that cover it.
62
+ - **Split only for a real reason:** (a) a genuine gate — the later work cannot be written until the earlier work's interface exists; (b) the combined work would not land in one session; (c) a prefactor, which is its own slice done first ("make the change easy, then make the easy change"); (d) parallel width, under the rule below; (e) the wide-refactor sequence below.
63
+ - **Not reasons to split:** one ticket per file, layer, endpoint or component; keeping each ticket easy; keeping diffs small; separating tests from the code they test.
64
+ - **Parallelism pays when the halves are file-disjoint.** Tickets with no blocking edge burn concurrently, each on its own temp branch, landing one at a time (ADR-0002) — so two substantial slices touching *different* files buy real wall-clock. Two touching the *same* files race to land, and the loser fails on a merge conflict: that width costs a re-burn. Overlapping files → merge.
65
+ </vertical-slice-rules>
66
+
67
+ Give each ticket its **blocking edges** — the tickets that must finish before it
68
+ can start. The field is required: a ticket with no blockers gets an empty array.
69
+
70
+ <review-ticket>
71
+ **A review ticket, when you choose to run one.** Whether a batch gets a review
72
+ is your call (SKILL.md, "Review is your call"). When it does, add **one** ticket
73
+ with `kind: "review"`, blocked by every implementation ticket whose work it
74
+ checks. It is not a slice: it is the brief for the agent that exercises the
75
+ *integrated* work on the feature branch and reports what it finds as findings.
76
+ Its `goal` says what to verify and its `acceptanceCriteria` are the things to
77
+ check off, one per behaviour worth confirming — the review agent walks them in
78
+ order. `emit_tickets` refuses a ticket *titled* like a review but carrying
79
+ another kind: set the kind (or retitle it) and re-emit.
80
+
81
+ The review agent works in **exactly one of two modes**, never both — a browser
82
+ **Drive** of the app against your acceptance criteria, or **Gates**: the
83
+ project's verify commands plus a two-axis read of the diff. Reviews that
84
+ attempted both ran long or died having delivered neither.
85
+
86
+ Make the choice obvious. Prescribe a walkthrough only when there is genuinely a
87
+ surface to walk, in plain prose ("open the settings flow, add a project, check
88
+ the empty state"), and write the `acceptanceCriteria` as the steps of that walk.
89
+ When the work changed nothing a human could drive — prompt contracts, docs, an
90
+ internal refactor, a backend-only change — say so in the ticket, and write
91
+ criteria the gates-and-diff read can check off.
92
+
93
+ **Say what to walk, never which mode.** A review ticket never directs the agent
94
+ to Gates — no "Mode: Gates", no "skip the drive" — and the mode is the review
95
+ agent's call from the surface you describe. Least of all give as the reason that
96
+ the change "isn't deployed or installed yet": the drive boots **the branch** —
97
+ the project's drive setup points it at the checkout's own code, migrations and
98
+ assets precisely so a branch is visible in its own drive. A review ticket once
99
+ prescribed Gates on exactly that reasoning, and a UI feature reached the human
100
+ without ever being driven. If the state to check does not exist yet in a fresh
101
+ drive, the `acceptanceCriteria` say how to produce it inside the drive, as far as
102
+ the project's drive instructions allow, and then what to check.
103
+
104
+ The review agent never edits code, so its ticket needs no `seams` beyond the
105
+ surface it exercises, and finding bugs is a *successful* review. Its digest
106
+ carries a prose summary of what landed, which the Review view leads with, so
107
+ `goal` asks for the verification, not for the summary.
108
+ </review-ticket>
109
+
110
+ <parallelism-budget>
111
+ **Budget the parallelism explicitly, before you emit.** A round runs up to
112
+ `burnConcurrency` tickets at once (from `get_feature_context`), but a ticket
113
+ cannot start until every ticket in its `blockedBy` has landed, so a round's
114
+ wall-clock is set by its **critical path**: the longest chain of blocking edges.
115
+ A chain of N tickets is N sequential sandboxes however wide the pool is.
116
+ Measured at `burnConcurrency: 3`, three consecutive batches whose tickets each
117
+ chained end to end came in at **1.09x, 1.16x and 1.00x** — the width was there
118
+ and never spent.
119
+
120
+ So, after slicing, **say it out loud in the session**:
121
+
122
+ - the **critical path** — the longest chain of implementation tickets, named in order, and its length;
123
+ - **how many tickets can run at once**, against this project's `burnConcurrency`;
124
+ - whether that clears the bar: T implementation tickets at concurrency C want a chain no longer than roughly **T / C**.
125
+
126
+ **When it does not clear the bar, reslice.** **A blocking edge is true only when
127
+ the later ticket reads the earlier one's output**: a file it writes, a type or
128
+ interface it defines, a schema it migrates, a stub it fills. "Related", "same
129
+ area", "logical reading order" are not blocking edges — drop them. Where the
130
+ edge is real, merge the two into one ticket, or pull the shared foundation
131
+ forward into the blocker so everything else goes wide behind it.
132
+
133
+ A review ticket is the tail by definition: measure the critical path over the
134
+ implementation tickets and add it. Width only counts if it lands: parallel
135
+ tickets touching the same files collide on landing.
136
+ </parallelism-budget>
137
+
138
+ <ship-a-scenario>
139
+ **Ship a scenario when the state is hard to reach.** A scenario is a project
140
+ command under `.runcastle/scenarios/` that seeds a state and prints the URL to
141
+ land on, so a test drive starts where the feature is instead of on a blank app.
142
+ Read `get_feature_context`'s `scenarios` field — `{ setUp, runnerConfigured,
143
+ scenarios, fixtures }` — before you decide anything.
144
+
145
+ **When `setUp` is true**, ask of each ticket: will the person driving this have
146
+ to build state by hand to see it — an account, a cart with an expired coupon?
147
+
148
+ - If yes, give the implementation ticket that builds that state an acceptance criterion: "Ship `.runcastle/scenarios/<group>/<name>.<ext>` that seeds <state> (reusing `_fixtures`) and prints `ENTRY_URL=<where>`". Name the fixtures it should reuse. When the change is an API call, the criterion also ships a sibling `.http` file at the same path with the request, and the scenario prints `TOKEN=` when the call is authenticated.
149
+ - Name that scenario in the review ticket's `context`, if there is one, so the reviewer replays it rather than improvising the setup; and use it as the `scenario` you declare ready with.
150
+ - Copy changes and settings toggles get none. A scenario earns its place only where reaching the state is the chore.
151
+ - The worker writes and typechecks the scenario but cannot run it — a sandbox has no drive.
152
+
153
+ **When `setUp` is false**, card nothing about scenarios. Tell the human once
154
+ that + New scenario in a test drive sets scenarios up for this project.
155
+ </ship-a-scenario>
156
+
157
+ <wide-refactor-exception>
158
+ A **wide refactor** — one mechanical change (rename a column, retype a shared symbol) whose blast radius breaks thousands of call sites at once — cannot land green as a vertical slice. Sequence it **expand → migrate → contract**. Expand: add the new form beside the old so nothing breaks (one ticket). Migrate: move call sites over in batches sized by blast radius (per package/dir), each a ticket blocked by the expand, green throughout because the old form still exists. Contract: delete the old form once no caller remains, blocked by every migrate batch. If even the batches cannot stay green alone, let them all block a final integrate-and-verify ticket — green is promised only there.
159
+ </wide-refactor-exception>
160
+
161
+ <preview-screen-block>
162
+ When the feature has a `preview.html` and a ticket **builds a screen that appears in it as a screen block**, point that ticket at **one named screen block**, never the file. Its **context** names the path (`docs/features/<slug>/preview.html`) and the block name, and tells the worker to find the two markers, `<!-- screen:<name> -->` and `<!-- /screen:<name> -->`, and **read only the lines between them**. Say plainly that the block shows the approved look and behaviour and **is not the implementation**: build it with the project's real components and never copy the mock's markup. **Never tell a worker to read the whole file.**
163
+
164
+ A ticket with no screen does not mention the preview.
165
+
166
+ A **designed-screen block** holds no mock. It names a component committed in design mode and its preview page. Point the ticket at that component file and that preview page path, not at markup.
167
+ </preview-screen-block>
168
+
169
+ <connect-designed-screens>
170
+ A ticket that wires a **designed screen** (one committed in design mode, `design: …` commits on the feature branch) names the committed component and its preview page in its `context`, and says plainly that **the look is approved**. Its job is to **connect, never restyle**:
171
+
172
+ - swap the sample data for real data (queries, API calls);
173
+ - add the behaviour the design left out;
174
+ - change markup only where the wiring needs it.
175
+
176
+ When wiring forces a visible change (a field the design lacked, say), the worker calls it out in its digest.
177
+ </connect-designed-screens>
178
+
179
+ ## 2. Build each ticket to the schema
180
+
181
+ Emit an array of `TicketInput`. Number them from 1 in the array in **dependency
182
+ order (blockers first)**; `blockedBy` holds those numbers.
183
+
184
+ Each ticket:
185
+ - **title** — `{group}: {task}`, in the domain vocabulary: `Checkout: apply the coupon at payment`.
186
+ - **goal** — the end-to-end behaviour this ticket makes work, from the user's/caller's perspective. Not a layer list.
187
+ - **context** — everything the sandboxed agent needs and cannot ask for: file paths, patterns to follow, type/schema shapes, gotchas, how to run and verify. Be concrete.
188
+ - **acceptanceCriteria** — `string[]`, each independently *verifiable* (a behaviour you can observe, a command that passes). Four to eight on a substantial ticket is right; a list running well past that will not land in one pass.
189
+ - **seams** — `string[]`, the public interfaces to test at (from the spec's Seams; prefer existing, prefer the highest, prefer one).
190
+ - **blockedBy** — `number[]`, **required**, only true gates; `[]` when there are none.
191
+ - **kind** — `"implementation"` (the default, omit it) or `"review"`.
192
+ - **model** — optional, and usually omitted. See below.
193
+
194
+ ### Assigning a model (only from the annotated roster)
195
+
196
+ `get_feature_context` returns `annotatedModels`: `{ id, runtime, note }` for the
197
+ models the human wrote a use-case note against. That list is the *entire* set
198
+ of ids you may use.
199
+
200
+ - **Empty list → never set `model`.**
201
+ - **Non-empty → set it only where a note plainly fits the ticket.** Unset is the good default.
202
+ - **Never invent an id**, and never write one not in `annotatedModels` — the store rejects it and the whole batch fails to emit.
203
+
204
+ ## 3. Self-check, then emit
205
+
206
+ Check yourself, in this order:
207
+
208
+ 1. **Can any two of these be one ticket?** Take them pair by pair; merging changes the rest.
209
+ 2. **Would any one overrun?** Split it where its files stop overlapping.
210
+ 3. Is each survivor a demoable vertical slice, worth a session?
211
+ 4. Are the blocking edges minimal and true? State the critical path and the width against `burnConcurrency`, and reslice if the chain runs longer than roughly total / concurrency. Do independent tickets touch different files?
212
+ 5. Is every `context` self-sufficient for an agent that cannot ask?
213
+ 6. If you chose a review, is there exactly one `kind: "review"` ticket, blocked by the work it checks?
214
+ 7. If `scenarios.setUp`, does every ticket that builds hard-to-reach state ship a scenario for it?
215
+
216
+ Then `mcp__runcastle__emit_tickets({ tickets: [...] })` — **emit the array; do
217
+ NOT write ticket files.** It returns `{ stored, ids }`, logs the timeline event
218
+ itself, and opens the **Work** view beside the chat. Emit the **whole batch,
219
+ fully enriched, in one call**; never emit placeholder contexts to enrich
220
+ afterwards — a round dispatched mid-enrichment burns workers on placeholders.
221
+
222
+ ## Reshaping tickets later
223
+
224
+ When something changes after tickets exist, walk the ticket list against it:
225
+
226
+ - Stale but still needed → `update_ticket({ id, ...changed fields })` (pending or failed only).
227
+ - No longer needed → `cancel_ticket({ id, reason })` (pending or failed only).
228
+ - New work → `emit_tickets` again; tickets accumulate on the feature.
229
+ - Landed work now wrong → a NEW ticket that corrects it. Never touch landed or running tickets.