runcastle 1.5.3 → 1.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/runcastle.js +2047 -2825
- package/drizzle/0052_friendly_black_tarantula.sql +1 -0
- package/drizzle/0053_soft_karma.sql +12 -0
- package/drizzle/0054_careless_stellaris.sql +3 -0
- package/drizzle/meta/0052_snapshot.json +1364 -0
- package/drizzle/meta/0053_snapshot.json +1283 -0
- package/drizzle/meta/0054_snapshot.json +1268 -0
- package/drizzle/meta/_journal.json +21 -0
- package/index.js +2045 -2823
- 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 +158 -0
- package/skills/packs/runcastle/skills/feature/references/grilling.md +101 -0
- package/skills/packs/runcastle/skills/feature/references/planning-alone.md +120 -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 +93 -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 +106 -63
- package/skills/packs/runcastle/skills/project/references/health-sweeps.md +11 -10
- package/skills/packs/runcastle/skills/project/references/triage.md +32 -30
- package/skills/packs/runcastle/skills/qa/SKILL.md +1 -2
- package/web/assets/index-B1MyomR6.css +1 -0
- package/web/assets/index-CPgKzYyE.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
|
@@ -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:#
|
|
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:#
|
|
34
|
-
--color-success:#
|
|
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:#
|
|
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:#
|
|
46
|
-
--color-success:#
|
|
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
|
}
|
|
@@ -141,7 +136,7 @@ button{font:inherit;color:inherit;background:none;border:0;padding:0;cursor:poin
|
|
|
141
136
|
<header id="what">
|
|
142
137
|
<h1>[Feature title]</h1>
|
|
143
138
|
<p class="lede">[One or two sentences: what this feature is, in the human's words.]</p>
|
|
144
|
-
<div class="meta"><span
|
|
139
|
+
<div class="meta"><span><span data-dcount></span> decisions</span><span class="mono">docs/features/[slug]/preview.html</span></div>
|
|
145
140
|
<nav class="jump" aria-label="Parts of this preview">
|
|
146
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>
|
|
147
142
|
</nav>
|
|
@@ -183,8 +178,8 @@ button{font:inherit;color:inherit;background:none;border:0;padding:0;cursor:poin
|
|
|
183
178
|
<section id="left-out">
|
|
184
179
|
<h2>What is left out</h2>
|
|
185
180
|
<div class="out2">
|
|
186
|
-
<div><h3>Later
|
|
187
|
-
<li><i class="dot"></i>[Deferred to a
|
|
181
|
+
<div><h3>Later</h3><ul>
|
|
182
|
+
<li><i class="dot"></i>[Deferred to a backlog feature]</li>
|
|
188
183
|
</ul></div>
|
|
189
184
|
<div><h3>Not this feature</h3><ul>
|
|
190
185
|
<li><i class="dot"></i>[Out of scope]</li>
|
|
@@ -259,7 +254,7 @@ button{font:inherit;color:inherit;background:none;border:0;padding:0;cursor:poin
|
|
|
259
254
|
|
|
260
255
|
// a pin opens its decision; in-page links scroll by script, because hash
|
|
261
256
|
// navigation is unreliable inside the app's sandboxed frame
|
|
262
|
-
function
|
|
257
|
+
function reveal(el) { el.scrollIntoView({ behavior: 'smooth', block: 'center' }) }
|
|
263
258
|
document.addEventListener('click', function (e) {
|
|
264
259
|
// Open in design: the app listens for exactly this message and opens the design panel
|
|
265
260
|
var od = e.target.closest('[data-open-design]')
|
|
@@ -271,7 +266,7 @@ button{font:inherit;color:inherit;background:none;border:0;padding:0;cursor:poin
|
|
|
271
266
|
if (pin) {
|
|
272
267
|
e.preventDefault()
|
|
273
268
|
var d = $('#d' + pin.dataset.d)
|
|
274
|
-
d.open = true;
|
|
269
|
+
d.open = true; reveal(d); d.classList.add('flash')
|
|
275
270
|
setTimeout(function () { d.classList.remove('flash') }, 1400)
|
|
276
271
|
return
|
|
277
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
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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** (
|
|
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
|
|
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,13 +90,13 @@ 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
|
|
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
|
|
|
72
97
|
## Designed screens
|
|
73
98
|
|
|
74
|
-
A screen designed in design mode (
|
|
99
|
+
A screen designed in design mode (`./design.md`) is **not mocked
|
|
75
100
|
again**. Its place in the show is a **designed-screen entry**:
|
|
76
101
|
|
|
77
102
|
- its name;
|
|
@@ -0,0 +1,93 @@
|
|
|
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
|
+
If designs wait uncommitted in the worktree, the human's yes commits them first;
|
|
14
|
+
an orchestrator relay never does (`./design.md`, section 8).
|
|
15
|
+
|
|
16
|
+
## What you run on
|
|
17
|
+
|
|
18
|
+
`get_feature_context` carries:
|
|
19
|
+
|
|
20
|
+
- `rounds` — every round so far, numbered from 1: the tickets it held, how it ended, its gates.
|
|
21
|
+
- `latestReady` — your newest ready declaration, if any.
|
|
22
|
+
- `driveActive` — the human is test-driving this feature right now.
|
|
23
|
+
- `stopped` — the human pressed Stop and has not replied since.
|
|
24
|
+
- `waitingReason` — why the feature is waiting on the human, if it is.
|
|
25
|
+
- `burnConcurrency` — how many workers the project runs at once.
|
|
26
|
+
|
|
27
|
+
## Plan a round
|
|
28
|
+
|
|
29
|
+
1. **Write the tickets** with `emit_tickets`, or reshape pending ones with
|
|
30
|
+
`update_ticket` / `cancel_ticket`, following `./tickets.md`. Then **dispatch**
|
|
31
|
+
with `dispatch_round({ seqs })`, naming exactly the pending tickets this round
|
|
32
|
+
covers. Tickets you do not name wait for a later round. A ticket may stay
|
|
33
|
+
blocked by tickets that landed or were cancelled in an earlier round: those
|
|
34
|
+
count as satisfied. A blocker that is still pending or failed must be named
|
|
35
|
+
in the same round.
|
|
36
|
+
2. **Every worker task names its deliverable and how it is checked.** A worker
|
|
37
|
+
with no stated check reports success on its own terms. Say what is built and
|
|
38
|
+
what proves it: the test that goes green, the command that passes, the screen
|
|
39
|
+
that shows it.
|
|
40
|
+
3. **Keep the round to `burnConcurrency` workers or fewer.** A soft cap: workers
|
|
41
|
+
past it only queue, and a wider round has more to reconcile when it returns.
|
|
42
|
+
4. **Contract first when the work spans layers.** When workers on different
|
|
43
|
+
layers must agree on a shape — types, an API, a schema — make one ticket land
|
|
44
|
+
that contract in code and make the others `blockedBy` it in the same round.
|
|
45
|
+
**Contracts live in code, never in a doc**: workers trust the code.
|
|
46
|
+
5. **UI work names the design references.** A ticket that touches UI tells its
|
|
47
|
+
worker which design docs to follow — the project's own design system docs and
|
|
48
|
+
any design references in this feature's docs — and asks it to plan the
|
|
49
|
+
screen, build it, then critique it against those docs with screenshots.
|
|
50
|
+
|
|
51
|
+
Workers do not talk to each other or to you. They report through their digests,
|
|
52
|
+
their ticket status and their commits. A worker that finds the contract wrong
|
|
53
|
+
stops and says so in its digest; reconciling it is your job, between rounds.
|
|
54
|
+
|
|
55
|
+
## When a round comes back
|
|
56
|
+
|
|
57
|
+
You are woken with the round's outcome, each ticket's status, the verify-gate
|
|
58
|
+
result, any review findings and where the digests are.
|
|
59
|
+
|
|
60
|
+
1. **Trust the record over the prose.** Ticket status, the gates and the commits
|
|
61
|
+
are what happened; a digest is a worker's account of it. Read each digest
|
|
62
|
+
with `get_ticket({ seq })`, and where the two disagree, believe the record.
|
|
63
|
+
2. **Reconcile.** Did a worker report the contract wrong, or change something
|
|
64
|
+
another relied on? Did a decision turn out wrong? Fix the contract in code,
|
|
65
|
+
amend `decisions.md` and `spec.md` when what was decided changed, and update
|
|
66
|
+
the pending tickets. A contract reported wrong is a reason to re-plan.
|
|
67
|
+
3. **Handle review findings.** A finding stays open until you resolve it: fix it
|
|
68
|
+
in a round or yourself, or dismiss it and say why.
|
|
69
|
+
4. **Decide the next move**:
|
|
70
|
+
- small leftovers → fix them yourself;
|
|
71
|
+
- more work, or a failed ticket to retry → plan the next round;
|
|
72
|
+
- a question only the human can answer → ask it in chat and call `wait_for_you({ why })`;
|
|
73
|
+
- the feature does what the plan says → run the checks and declare ready (SKILL.md, "Ready for the human").
|
|
74
|
+
|
|
75
|
+
A failed round is just what came back. Re-plan, retry or fix it yourself.
|
|
76
|
+
`declare_ready` warns when the last round had failures; pass that on.
|
|
77
|
+
|
|
78
|
+
## While and after the human drives
|
|
79
|
+
|
|
80
|
+
Never dispatch while the feature is being driven (`driveActive`). Your own
|
|
81
|
+
commits wait until the drive stops. When the human reports back, follow
|
|
82
|
+
SKILL.md, "After a drive"; when you are satisfied again, declare ready with a
|
|
83
|
+
new scenario.
|
|
84
|
+
|
|
85
|
+
## What the server refuses
|
|
86
|
+
|
|
87
|
+
`dispatch_round` is refused, with the reason, for a draft or shipped feature,
|
|
88
|
+
while a round is already running, while the human is driving this feature, and
|
|
89
|
+
after the human pressed Stop until they have replied. It is also refused when a
|
|
90
|
+
ticket you name is blocked by a ticket outside the round that is still pending
|
|
91
|
+
or failed (or does not exist): the refusal lists each such edge, and the fix is
|
|
92
|
+
to name that blocker in the round too. `stop_round` stops the running round;
|
|
93
|
+
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.
|