runcastle 1.5.3 → 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.
- package/bin/runcastle.js +1716 -2625
- package/drizzle/0052_friendly_black_tarantula.sql +1 -0
- package/drizzle/0053_soft_karma.sql +12 -0
- package/drizzle/meta/0052_snapshot.json +1364 -0
- package/drizzle/meta/0053_snapshot.json +1283 -0
- package/drizzle/meta/_journal.json +14 -0
- package/index.js +1539 -2448
- package/package.json +1 -1
- package/skills/packs/README.md +24 -29
- package/skills/packs/runcastle/.claude-plugin/plugin.json +1 -1
- package/skills/packs/runcastle/skills/feature/SKILL.md +72 -0
- package/skills/packs/runcastle/skills/feature/references/design.md +132 -0
- package/skills/packs/runcastle/skills/feature/references/grilling.md +99 -0
- package/skills/packs/runcastle/skills/feature/references/planning-alone.md +70 -0
- package/skills/packs/runcastle/skills/{ideate → feature}/references/preview-starter.html +11 -16
- package/skills/packs/runcastle/skills/{ideate → feature}/references/preview.md +33 -8
- package/skills/packs/runcastle/skills/feature/references/rounds.md +91 -0
- package/skills/packs/runcastle/skills/feature/references/spec.md +54 -0
- package/skills/packs/runcastle/skills/feature/references/tickets.md +229 -0
- package/skills/packs/runcastle/skills/project/SKILL.md +18 -16
- package/skills/packs/runcastle/skills/project/references/health-sweeps.md +5 -5
- package/skills/packs/runcastle/skills/project/references/triage.md +20 -23
- package/skills/packs/runcastle/skills/qa/SKILL.md +1 -2
- package/web/assets/index-CT9k1kz0.css +1 -0
- package/web/assets/index-DVR7VbWW.js +186 -0
- package/web/index.html +2 -2
- package/skills/packs/runcastle/skills/design/SKILL.md +0 -73
- package/skills/packs/runcastle/skills/ideate/SKILL.md +0 -131
- package/skills/packs/runcastle/skills/lead/SKILL.md +0 -104
- package/skills/packs/runcastle/skills/revisit/SKILL.md +0 -55
- package/skills/packs/runcastle/skills/spec/SKILL.md +0 -47
- package/skills/packs/runcastle/skills/tickets/SKILL.md +0 -147
- package/web/assets/index-BYSs5Hdy.js +0 -186
- package/web/assets/index-bpL_iO_C.css +0 -1
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: tickets
|
|
3
|
-
description: Break the spec into vertical slices sized to earn a fresh sandboxed agent session and land inside it, and emit them via MCP. Then complete the tickets phase. Invoked by /runcastle:ideate.
|
|
4
|
-
disable-model-invocation: false
|
|
5
|
-
---
|
|
6
|
-
<!-- Forked from Matt Pocock's to-tickets skill, via https://github.com/mattpocock/skills, 2026-07-14, adapted for runcastle -->
|
|
7
|
-
|
|
8
|
-
# Tickets
|
|
9
|
-
|
|
10
|
-
Break the work into **tickets** — vertical slices, each cutting end-to-end through every layer it needs, each big enough to be worth a whole agent session and small enough to finish inside one — and emit them to the runcastle store via `mcp__runcastle__emit_tickets`. You are still in the unbroken ideation window: synthesize from the spec (`docs/features/<slug>/spec.md`). Do not compact or clear. If `get_feature_context` lists a canonical doc in `notInlined`, read it before you slice. Its existing tickets are summary rows (`seq`, `title`, `status`, `goal`, …); `mcp__runcastle__get_ticket({ seq })` holds one ticket's context, acceptance criteria and digest.
|
|
11
|
-
|
|
12
|
-
## The one thing that makes a runcastle ticket different
|
|
13
|
-
|
|
14
|
-
Each ticket is executed by a **single fresh agent, alone, in a sandbox, with ~100k tokens and NO way to ask a follow-up question.** It sees only what you put in the ticket plus the code already in the repo. So — *inverting* the usual "avoid file paths, they go stale" rule — the ticket's `context` field **must be concrete and self-sufficient**: name the files to touch, the existing patterns to copy, the type/schema shapes, the gotchas, the commands to run and verify with. A ticket that assumes the agent can ask "where does X live?" is a broken ticket. Over-specify context; under-specify nothing.
|
|
15
|
-
|
|
16
|
-
## The second thing: a ticket costs ~10 minutes before it does any work — and pays it twice if it overruns
|
|
17
|
-
|
|
18
|
-
That fresh agent is fresh *every time*. Each ticket gets its own container: cold build, a full dependency install (measured 70–507s, ~2.5 min average), then an agent that re-reads the spec, the docs digest, and every file your `context` names — reading the repo was 16% of all agent time across 42 measured attempts. **This overhead is paid per ticket and is the same whether the ticket is twenty lines or a day's work.** Splitting one coherent piece of work into three tickets triples it, and adds two seams where a new agent has to reconstruct what the previous one had just finished learning.
|
|
19
|
-
|
|
20
|
-
But that meter runs both ways. A ticket too big to finish in one pass gets no cheap second wind: an iteration is not a retry (ADR-0008) — the container is rebuilt, the install runs again, and a brand-new agent re-reads everything from the last commit, having lost every conclusion its predecessor reached. An overstuffed ticket pays the exact overhead it was merged to avoid, on top of the orientation it already paid once, and it pays it *after* the work is half-done rather than before.
|
|
21
|
-
|
|
22
|
-
So size a ticket to **earn its container and still land in one pass**: substantial, with room left at the end for the tests, the self-review and the commits — not the smallest slice you can name, and not the largest you can imagine fitting. Fine-grained slicing still matters — it just belongs *inside* a ticket, as the `acceptanceCriteria` list. The burner works red→green one criterion at a time and commits each green slice, so one ticket with six criteria produces six small commits and orients once; six one-criterion tickets produce the same commits and orient six times.
|
|
23
|
-
|
|
24
|
-
Expect a **handful of substantial tickets** for a feature — roughly 4–8, each worth a session. A dozen thin ones means you sliced by layer or by file, not by behaviour. Two or three giant ones means you will pay back the containers you saved, with interest, when they overrun.
|
|
25
|
-
|
|
26
|
-
## 1. Draft vertical slices
|
|
27
|
-
|
|
28
|
-
<vertical-slice-rules>
|
|
29
|
-
- 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.
|
|
30
|
-
- A completed slice is demoable or verifiable on its own.
|
|
31
|
-
- Each slice is worth one fresh ~100k-token agent session working alone and **lands inside it** — the orientation at the front and the tests, self-review and commits at the back are fixed tax, so leave them room instead of filling the window to the brim.
|
|
32
|
-
- **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. Two drafts that share none of that are not one ticket merely because both would fit.
|
|
33
|
-
- **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.
|
|
34
|
-
- **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.
|
|
35
|
-
- **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 for one container. Two touching the *same* files race to land, and the loser fails on a merge conflict: that width costs a re-burn, which is worse than the container it bought. Overlapping files → merge.
|
|
36
|
-
</vertical-slice-rules>
|
|
37
|
-
|
|
38
|
-
Give each ticket its **blocking edges** — the tickets that must finish before it can start. Every ticket carries them: the field is required, so a ticket with no blockers gets an empty array and can start immediately.
|
|
39
|
-
|
|
40
|
-
<review-ticket>
|
|
41
|
-
Close the batch with **one review ticket** (`kind: "review"`), blocked by every implementation ticket in it. It is not a slice: it is the prose brief for the agent that exercises the *integrated* feature at the tail of the burn, on the feature branch, and writes what it finds as test notes for the human's review phase. Its `goal` says what to verify and its `acceptanceCriteria` are the things to check off, one per behaviour worth confirming — the review agent walks them in order.
|
|
42
|
-
|
|
43
|
-
**Every batch closes with one, unconditionally.** Not "when there is something to exercise" — every batch, no exceptions, exactly one (more per feature comes later). Whatever the lap turned out to be, there is always a review the agent can run: a diff is the one thing every batch produces by definition. A lap that ships without a review ticket is a lap nobody looked at, and its absence is invisible on the review page — which is precisely the silence this rule exists to end.
|
|
44
|
-
|
|
45
|
-
The server says so too, and the rule is yours to keep either way: a batch with no `kind: "review"` ticket comes back from `complete_phase({ phase: "tickets" })` as a **warning**, and the human sees the same sentence in the Go dialog — it never refuses, so nothing but you stops the lap shipping unlooked-at. `emit_tickets` does still refuse a batch outright when a ticket is *titled* like the review but carries another kind — set the kind (or retitle it) and re-emit.
|
|
46
|
-
|
|
47
|
-
The agent reviews in **exactly one of two modes**, never both — a browser **Drive** of the app against your acceptance criteria, or **Gates**: the project's verify commands plus a two-axis read of the diff. Reviews that attempted both ran long or died having delivered neither, so the burner prompt makes the agent choose up front and spend the whole review inside the choice.
|
|
48
|
-
|
|
49
|
-
Your job is to make that choice obvious. Prescribe a walkthrough only when there is genuinely a surface to walk, in plain prose ("open the settings flow, add a project, check the empty state"), and write the `acceptanceCriteria` as the steps of that walk. When the lap changed nothing a human could drive — prompt contracts, docs, an internal refactor, a backend-only change — say so in the ticket, and write criteria the gates-and-diff mode can actually check off. Do not ask for both; do not ask for a browser walk of something with no UI.
|
|
50
|
-
|
|
51
|
-
**Say what to walk, never which mode.** A review ticket never directs the agent to Gates — no "Mode: Gates", no "skip the drive" — and the mode is the review agent's call from the surface you describe. Least of all give as the reason that the change "isn't deployed or installed yet", or that the machinery running the review doesn't have this branch's code. That mixes up two things: the review pass itself (its prompt, its gates, its build steps) runs on whatever is installed, but the drive boots **the branch** — the project's drive setup points it at the checkout's own code, migrations and assets precisely so a branch is visible in its own drive. On a real lap a review ticket prescribed Gates on exactly that reasoning, the agent obeyed it, and a UI feature shipped to review without ever being driven. If the state to check does not exist yet in a fresh drive — it is produced by a later step, a background job, another run — the `acceptanceCriteria` say how to produce it inside the drive, as far as the project's drive instructions allow, and then what to check once it is there. Only a lap with genuinely no surface a human could operate gets criteria written for the gates-and-diff read.
|
|
52
|
-
|
|
53
|
-
The review agent never edits code, so its ticket needs no `seams` beyond the surface it exercises, and finding bugs is a *successful* review — the notes are the deliverable. Its digest is the other one: the agent writes the lap's "What landed this lap" prose summary there, and the review page renders it as the lead of the review card, so `goal` should ask for the verification, not for the summary — that part is the burner's standing contract.
|
|
54
|
-
</review-ticket>
|
|
55
|
-
|
|
56
|
-
<parallelism-budget>
|
|
57
|
-
The other thing that is true of the batch as a whole: **budget the parallelism explicitly, before you emit.** The burn runs up to `burnConcurrency` tickets at once — the project's setting, served to you in `get_feature_context` as `burnConcurrency` — but a ticket cannot start until every ticket in its `blockedBy` has landed, so the lap's wall-clock is set by its **critical path**: the longest chain of blocking edges through the batch. A chain of N tickets is N sequential sandboxes however wide the pool is. Measured on a real project at `burnConcurrency: 3`, three consecutive laps whose tickets each chained end to end came in at **1.09x, 1.16x and 1.00x** — the width was there all three times and was never once spent.
|
|
58
|
-
|
|
59
|
-
So, after slicing and before the self-check below, **say it out loud in the session**, in these terms:
|
|
60
|
-
|
|
61
|
-
- the **critical path** — the longest chain of implementation tickets, named in order, and its length;
|
|
62
|
-
- **how many tickets can run at once**, stated against this project's `burnConcurrency`;
|
|
63
|
-
- whether that clears the bar: a batch of T implementation tickets at concurrency C wants a chain no longer than roughly **T / C**. Longer than that and the pool sits idle for most of the burn.
|
|
64
|
-
|
|
65
|
-
**When it does not clear the bar, reslice.** The usual cause is habit rather than necessity — tickets sequenced because they *feel* ordered, or because one "sets up" the other. **A blocking edge is true only when the later ticket reads the earlier one's output**: a file it writes, a type or interface it defines, a schema it migrates, a stub it fills. "Related", "same area", "logical reading order", "easier to review in sequence" are not blocking edges — drop them and those tickets burn side by side. Where the edge is genuinely real, the fix runs the other way: merge the two into one ticket (the merge test above has usually already said so), or pull the shared foundation forward into the blocker so everything else goes wide behind it.
|
|
66
|
-
|
|
67
|
-
Two things are not chain problems. The **review ticket** is blocked by every implementation ticket by definition and is always the tail: measure the critical path over the implementation tickets and add it. The **expand → migrate → contract** sequence below is the shape this rule is asking for — its migrate batch is the wide step.
|
|
68
|
-
|
|
69
|
-
And width only counts if it lands: parallel tickets touching the same files collide on landing (the file-disjoint rule above), so a reslice that buys width by overlapping files has bought a re-burn instead.
|
|
70
|
-
</parallelism-budget>
|
|
71
|
-
|
|
72
|
-
<ship-a-scenario>
|
|
73
|
-
**Ship a scenario when the state is hard to reach.** A scenario is a project command under `.runcastle/scenarios/` that seeds a state and prints the URL to land on, so a test drive starts where the feature is instead of on a blank app. Read `get_feature_context`'s `scenarios` field — `{ setUp, runnerConfigured, scenarios, fixtures }` — before you decide anything.
|
|
74
|
-
|
|
75
|
-
**When `setUp` is true**, ask of each ticket: will the person driving this have to build state by hand to see it — an account, a cart with an expired coupon, a feature sitting at review?
|
|
76
|
-
|
|
77
|
-
- 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 from `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.
|
|
78
|
-
- Name that scenario in the review ticket's `context`, so the reviewer replays it rather than improvising the setup.
|
|
79
|
-
- Copy changes and settings toggles get none. A scenario is never required; it earns its place only where reaching the state is the chore.
|
|
80
|
-
- The burn agent writes and typechecks the scenario but cannot run it — a sandbox has no drive. Its first run is the review drive, where a failure is a defect.
|
|
81
|
-
|
|
82
|
-
**When `setUp` is false**, card nothing about scenarios. Say once in your closing message that + New scenario in a test drive sets scenarios up for this project.
|
|
83
|
-
</ship-a-scenario>
|
|
84
|
-
|
|
85
|
-
<wide-refactor-exception>
|
|
86
|
-
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** instead. 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 batch a ticket blocked by the expand, CI 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 share an integration branch that all block a final integrate-and-verify ticket — green is promised only there.
|
|
87
|
-
</wide-refactor-exception>
|
|
88
|
-
|
|
89
|
-
<preview-screen-block>
|
|
90
|
-
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 burn agent 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. The mock's styles live in the page head and are deliberately not read. **Never tell an agent to read the whole file** — the rest repeats docs it already has.
|
|
91
|
-
|
|
92
|
-
A ticket with no screen does not mention the preview.
|
|
93
|
-
|
|
94
|
-
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.
|
|
95
|
-
</preview-screen-block>
|
|
96
|
-
|
|
97
|
-
<connect-designed-screens>
|
|
98
|
-
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**:
|
|
99
|
-
|
|
100
|
-
- swap the sample data for real data (queries, API calls);
|
|
101
|
-
- add the behaviour the design left out;
|
|
102
|
-
- change markup only where the wiring needs it.
|
|
103
|
-
|
|
104
|
-
It does not rebuild the component or change its look. When wiring forces a visible change (a field the design lacked, say), the burner calls it out in its digest so the review sees it. No new check exists: the project's typecheck keeps preview pages compiling, and nothing else enforces them.
|
|
105
|
-
</connect-designed-screens>
|
|
106
|
-
|
|
107
|
-
## 2. Build each ticket to the schema
|
|
108
|
-
|
|
109
|
-
Emit an array of `TicketInput`. Number them from 1 in the array in **dependency order (blockers first)**; `blockedBy` holds those numbers (they are the seq numbers the store assigns).
|
|
110
|
-
|
|
111
|
-
Each ticket:
|
|
112
|
-
- **title** — short, descriptive, in the domain vocabulary.
|
|
113
|
-
- **goal** — the end-to-end behaviour this ticket makes work, from the user's/caller's perspective. Not a layer list.
|
|
114
|
-
- **context** — everything the sandboxed agent needs and cannot ask for: file paths to touch, existing patterns/modules to follow, type/schema shapes, gotchas, how to run and verify. Be concrete (see above).
|
|
115
|
-
- **acceptanceCriteria** — `string[]`, each independently *verifiable* (a behaviour you can observe, a command that passes). These are what the burner works red-green against, one at a time, committing each green one — so this is where the fine granularity lives. A substantial ticket having four to eight criteria is right, not a smell; a list running well past that is a ticket that will not land in one pass.
|
|
116
|
-
- **seams** — `string[]`, the public interfaces to test at (carry them from the spec's Seams section; prefer existing, prefer the highest, prefer one).
|
|
117
|
-
- **blockedBy** — `number[]`, **required**, the seq numbers (from your 1..N ordering) of the tickets that *genuinely* gate this one. Only true gates; pass `[]` when there are none — omitting it fails validation.
|
|
118
|
-
- **kind** — `"implementation"` (the default, omit it) or `"review"` for the one review ticket described above.
|
|
119
|
-
- **model** — optional, and usually omitted. See below.
|
|
120
|
-
|
|
121
|
-
### Assigning a model (only from the annotated roster)
|
|
122
|
-
|
|
123
|
-
`mcp__runcastle__get_feature_context` returns `annotatedModels` in its header: `{ id, runtime, note }` for the models the human wrote a use-case note against, and only those. That list is the *entire* set of ids you may put in a ticket's `model`.
|
|
124
|
-
|
|
125
|
-
- **Empty list → never set `model`.** The human annotated nothing, so there is no stated reason to prefer one model over another. Omit the field on every ticket.
|
|
126
|
-
- **Non-empty → set it only where a note actually fits the ticket.** The note is the human's own vocabulary for what that model is good at ("UI/UX taste", "mechanical refactors"); assign it when this ticket's work is plainly that, and leave the field unset when nothing in the list speaks to it. Unset is the good default — it falls through to the model the project already burns on.
|
|
127
|
-
- **Never invent an id**, never copy one from these docs or from memory, and never write a model that is not in `annotatedModels` — the store rejects it and the whole batch fails to emit.
|
|
128
|
-
|
|
129
|
-
The human sees the assignment on the ticket card and can change or clear it before Go, so a wrong-but-reasonable choice is cheap; a fabricated id is not.
|
|
130
|
-
|
|
131
|
-
## 3. Self-check, then emit
|
|
132
|
-
|
|
133
|
-
There is no in-session quiz — the human's review is the **Go** click in the runcastle UI, reading these cards. So make them right before you emit. Check yourself, in this order:
|
|
134
|
-
|
|
135
|
-
1. **Can any two of these be one ticket?** Go pair by pair. Same files, shared type, sibling behaviours, or one whose blocker is the only thing it waits on and together they'd still land in one session → merge them. Do this first, because merging changes the rest.
|
|
136
|
-
2. **Would any one of these overrun?** A criteria list longer than one agent can take red→green — with tests, self-review and commits still to come — costs a whole second container to finish. Split it where its files stop overlapping.
|
|
137
|
-
3. Is each survivor a demoable vertical slice rather than a horizontal layer, and is it worth a session rather than rattling around in one?
|
|
138
|
-
4. Are the blocking edges minimal and genuinely true gates — each one a ticket that reads its blocker's output? State the critical path and the width it leaves against `burnConcurrency`, and reslice if the chain runs longer than roughly total / concurrency. Where two are independent, do they touch different files — or will they collide on landing?
|
|
139
|
-
5. Is every `context` self-sufficient for an agent that cannot ask — covering every file and pattern the ticket touches?
|
|
140
|
-
6. Does the batch close with exactly one `kind: "review"` ticket, blocked by every implementation ticket? It is not optional and there is no feature that skips it.
|
|
141
|
-
7. If `scenarios.setUp`, does every ticket that builds hard-to-reach state ship a scenario for it, and does the review ticket name it? If not set up, have you carded none and kept the one + New scenario line for your close?
|
|
142
|
-
|
|
143
|
-
Then:
|
|
144
|
-
- `mcp__runcastle__emit_tickets({ tickets: [...] })` — **emit the array; do NOT write ticket files.** It returns `{ stored, ids }` and logs the timeline event itself; do not record one of your own. Emit the **whole batch, fully enriched, in one call** — large payloads are supported, so a batch of long `context` fields is fine. Never emit placeholder contexts ("Context follows via `update_ticket`.") to enrich ticket by ticket afterwards: a round dispatched mid-enrichment burns agents on the placeholders.
|
|
145
|
-
- `mcp__runcastle__complete_phase({ phase: "tickets" })`. It comes back `ok: true` with `waitingOn: "human burn"` and a `warnings` array — the things the human will otherwise read in the Go dialog (no review ticket in the batch, earlier-lap defects still un-dispositioned, no `spec.md` on disk). Nothing refuses, so anything in that array is yours to fix now, while you are still the session that can: fix it and call again.
|
|
146
|
-
|
|
147
|
-
Return control to **the session skill that invoked you** — `/runcastle:ideate` — and let it close out the session. Do **not** invoke a session skill yourself to hand back: you are already inside one, and loading another session's entry skill would drop a whole procedure this session is not running into the window.
|