runcastle 1.3.6 → 1.3.10
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 +4605 -2723
- package/drizzle/0037_amusing_steve_rogers.sql +7 -0
- package/drizzle/0038_project_docs_commit_prefix.sql +1 -0
- package/drizzle/0039_chat_session_kind.sql +3 -0
- package/drizzle/0039_review_verdict.sql +5 -0
- package/drizzle/0040_new_maestro.sql +10 -0
- package/drizzle/0041_round_harpoon.sql +2 -0
- package/drizzle/meta/0037_snapshot.json +1118 -0
- package/drizzle/meta/0040_snapshot.json +1212 -0
- package/drizzle/meta/0041_snapshot.json +1226 -0
- package/drizzle/meta/_journal.json +35 -0
- package/index.js +4599 -2718
- package/package.json +2 -1
- package/sandcastle-template/Containerfile +12 -3
- package/sandcastle-template/Dockerfile +12 -3
- package/skills/burner/implement-ticket.md +4 -1
- package/skills/burner/review-ticket.md +21 -3
- package/skills/burner/verify-fixes.md +11 -3
- package/skills/packs/README.md +42 -28
- package/skills/packs/runcastle/skills/code-review/SKILL.md +10 -0
- package/skills/packs/runcastle/skills/converge/SKILL.md +8 -6
- package/skills/packs/runcastle/skills/ideate/SKILL.md +5 -5
- package/skills/packs/runcastle/skills/prepare/SKILL.md +1 -1
- package/skills/packs/runcastle/skills/project/SKILL.md +26 -11
- package/skills/packs/runcastle/skills/project/references/health-sweeps.md +7 -4
- package/skills/packs/runcastle/skills/project/references/triage.md +183 -0
- package/skills/packs/runcastle/skills/qa/SKILL.md +4 -3
- package/skills/packs/runcastle/skills/revisit/SKILL.md +28 -15
- package/skills/packs/runcastle/skills/spec/SKILL.md +2 -2
- package/skills/packs/runcastle/skills/tickets/SKILL.md +22 -6
- package/skills/packs/runcastle/skills/waypoint/SKILL.md +2 -2
- package/web/assets/index-L11zqTP_.css +1 -0
- package/web/assets/index-x1_wkP9-.js +187 -0
- package/web/index.html +2 -2
- package/web/assets/index-D697YlOb.css +0 -1
- package/web/assets/index-jbVdnCzO.js +0 -186
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Notes triage — the pile the human jotted
|
|
2
|
+
|
|
3
|
+
Load this when triage actually starts: your briefing says to triage the open
|
|
4
|
+
project notes, or the human took up the offer you made because your prompt said
|
|
5
|
+
this project has open notes. Otherwise it is not your job — the notes sit in the
|
|
6
|
+
inbox and cost this conversation nothing.
|
|
7
|
+
|
|
8
|
+
## What a note is, and what triage is not
|
|
9
|
+
|
|
10
|
+
A **project note** is one line the human jotted in ten seconds from wherever they
|
|
11
|
+
were in the project, usually with a pasted screenshot. It belongs to the project,
|
|
12
|
+
not to any feature, and it records a *symptom*, not an intent: "crumbs overflow",
|
|
13
|
+
"this dialog feels wrong".
|
|
14
|
+
|
|
15
|
+
So triage is **not sorting**. Routing a ten-second note cold produces a brief
|
|
16
|
+
that restates the one-liner and a quick-change ticket nobody can implement. Your
|
|
17
|
+
job is to find the intent behind the notes — grilling for it, with the portfolio
|
|
18
|
+
in view the way §1a intake demands — and only then route. Three movements, in
|
|
19
|
+
order.
|
|
20
|
+
|
|
21
|
+
## Your three tools
|
|
22
|
+
|
|
23
|
+
- `mcp__runcastle__list_project_notes()` — the open notes, oldest first: `id`,
|
|
24
|
+
`text`, `createdAt`, and, when the note has a screenshot, `screenshotPath` (an
|
|
25
|
+
absolute host path — `Read` it) and `attachmentSentence` (ready-made prose
|
|
26
|
+
naming `.runcastle-attachments/<noteId>.png`, to paste into a ticket string).
|
|
27
|
+
A note taken during a project test drive also carries `driveBranch` and
|
|
28
|
+
`driveCommit` — it was noted while driving `<driveBranch>` @ `<driveCommit>`
|
|
29
|
+
(`+dirty` means uncommitted edits were driven too), so you can tell whether
|
|
30
|
+
it predates a fix that has landed since.
|
|
31
|
+
- `mcp__runcastle__triage_project_note({ noteIds, outcome, featureId? })` — marks
|
|
32
|
+
one or several notes triaged with the outcome line they are frozen with. It
|
|
33
|
+
takes several ids so a whole theme closes in one call.
|
|
34
|
+
- `mcp__runcastle__update_project_note({ noteId, text })` — rewrites an open
|
|
35
|
+
note's text. Only for a theme the human is deferring (movement 3).
|
|
36
|
+
|
|
37
|
+
The inbox and the rail's badge update live off these calls. There is no notes
|
|
38
|
+
file to write and nothing else to report.
|
|
39
|
+
|
|
40
|
+
## Movement 1 — read and cluster
|
|
41
|
+
|
|
42
|
+
1. `list_project_notes()`, and **`Read` every `screenshotPath`.** The screenshot
|
|
43
|
+
is usually most of what the note meant; triaging from the text alone is
|
|
44
|
+
guessing.
|
|
45
|
+
2. Group them into **themes** — notes about the same surface, the same want, or
|
|
46
|
+
the same misunderstanding. Most piles are a handful of themes and a couple of
|
|
47
|
+
strays.
|
|
48
|
+
3. **Play the grouping back, and let the human correct it before any grilling:**
|
|
49
|
+
|
|
50
|
+
> 9 notes, 4 themes: ticket-card density (4), the rail's empty states (2), two
|
|
51
|
+
> separate asks about the burn log, and one stray about Windows paths. Does
|
|
52
|
+
> that grouping look right?
|
|
53
|
+
|
|
54
|
+
Take their correction. A theme you split is often one thing, and one you
|
|
55
|
+
merged is often two — and the idea worth building usually lives in the theme,
|
|
56
|
+
not in any single note, so the grouping has to be right before the questions
|
|
57
|
+
start.
|
|
58
|
+
|
|
59
|
+
## Movement 2 — grill theme by theme
|
|
60
|
+
|
|
61
|
+
Take the themes one at a time and work each until its **intent** is clear enough
|
|
62
|
+
to route. This is the one place this session grills, and it grills about what the
|
|
63
|
+
human wanted, never about how it should be built — the design still belongs to
|
|
64
|
+
the feature's own `/runcastle:ideate` session.
|
|
65
|
+
|
|
66
|
+
- **One question at a time, always with your recommended answer attached.** A
|
|
67
|
+
bare question with no recommendation hands the work back to the human.
|
|
68
|
+
- **Consult the portfolio, as §1a requires**: `get_project_context` for the
|
|
69
|
+
feature index and the live ADRs, `read_adr` for the one that binds this
|
|
70
|
+
surface, `get_work_record({ featureSlug })` or `({ seam })` for what a
|
|
71
|
+
neighbour actually did and what it left undone, and `docs/features/<slug>/` on
|
|
72
|
+
disk for a merged feature's argument. "`ticket-cards` shipped a density pass in
|
|
73
|
+
August — is this a regression, or a new want?" is the question only this
|
|
74
|
+
session can ask.
|
|
75
|
+
- A note may change under grilling, and should:
|
|
76
|
+
- **Drop** it — already fixed, already decided, or no longer wanted. Triage it
|
|
77
|
+
on the spot with `dropped: <why>`.
|
|
78
|
+
- **Merge** it into its theme — it is the same want said twice, and the theme's
|
|
79
|
+
outcome closes it along with the rest.
|
|
80
|
+
- **Reframe** it — what it says and what it meant are different. Carry the
|
|
81
|
+
reframed understanding into the destination in movement 3.
|
|
82
|
+
- A theme is done when you can say what the human actually wants and which
|
|
83
|
+
destination it belongs in. One that is still ambiguous after a couple of
|
|
84
|
+
questions is a theme to defer, not one to route on a guess.
|
|
85
|
+
|
|
86
|
+
## Movement 3 — route, and carry the reasoning
|
|
87
|
+
|
|
88
|
+
Every theme goes to one of §2's five destinations. **Grilling that is not written
|
|
89
|
+
into the destination is lost the moment this terminal closes**, so what you just
|
|
90
|
+
worked out has to land in whatever that destination carries.
|
|
91
|
+
|
|
92
|
+
**Never create work the human has not confirmed.**
|
|
93
|
+
|
|
94
|
+
### New feature
|
|
95
|
+
|
|
96
|
+
A theme with real design questions. `create_feature({ title, oneLiner, brief,
|
|
97
|
+
baseBranch })` exactly as §1c describes — one call per feature, the base stated,
|
|
98
|
+
`draft: true` for the ones they want parked.
|
|
99
|
+
|
|
100
|
+
The `brief` carries the grilled reasoning, not the jotted line: why this exists,
|
|
101
|
+
what the human actually wants, what it must not swallow, what the portfolio
|
|
102
|
+
already settles. **Name screenshots by absolute path** — the `screenshotPath`
|
|
103
|
+
from `list_project_notes`, verbatim. Ideation runs on the host and `Read`s it
|
|
104
|
+
from there.
|
|
105
|
+
|
|
106
|
+
### Quick changes — one batch per triage session
|
|
107
|
+
|
|
108
|
+
Every quick-sized theme of **this** triage session goes into **one**
|
|
109
|
+
`create_feature({ title, oneLiner, tickets })` call: one feature, one rail row,
|
|
110
|
+
one **Burn**, one **Merge** for the whole pass. This is where §2's "one call per
|
|
111
|
+
quick change, not per ticket" widens — inside triage, the batch *is* the quick
|
|
112
|
+
change, and calling it once per theme would give you a feature each.
|
|
113
|
+
|
|
114
|
+
- Title it for the pass ("Triage batch — card density and empty states"), with a
|
|
115
|
+
one-liner the human would recognise a week from now.
|
|
116
|
+
- Every quick-sized theme contributes its ticket strings to that one `tickets`
|
|
117
|
+
array — the grilled version, carrying the repro or the expected/got the
|
|
118
|
+
grilling produced, never the ten-second line.
|
|
119
|
+
- A ticket whose note has a screenshot ends with that note's
|
|
120
|
+
`attachmentSentence`, **pasted verbatim**. That sentence is the whole contract:
|
|
121
|
+
the burner scans it back out of the ticket and puts the PNG in its sandbox.
|
|
122
|
+
- Pass `baseBranch` the way §1c requires. The batch's review is then an ordinary
|
|
123
|
+
feature test drive of its branch.
|
|
124
|
+
|
|
125
|
+
### A revisit, or another lap
|
|
126
|
+
|
|
127
|
+
Destinations 3 and 4 have no tool. Give the human a **handoff line** to paste
|
|
128
|
+
into that feature's own session, carrying the grilled reasoning — not "see the
|
|
129
|
+
notes":
|
|
130
|
+
|
|
131
|
+
> Paste this into `burn-log-readability`'s revisit: "Timestamps in the burn log
|
|
132
|
+
> are unreadable at a glance; we settled that the want is relative ages with the
|
|
133
|
+
> absolute time on hover, and that the run header keeps its absolute start time."
|
|
134
|
+
|
|
135
|
+
Then say which button opens it: **revisit** on the feature, or **Iterate** on its
|
|
136
|
+
review page for another lap.
|
|
137
|
+
|
|
138
|
+
### Nothing
|
|
139
|
+
|
|
140
|
+
Already decided, already built, or not worth doing. Say so with the address — the
|
|
141
|
+
ADR or the shipped feature that settles it — and triage the theme as
|
|
142
|
+
`dropped: <why>`.
|
|
143
|
+
|
|
144
|
+
## Marking the notes
|
|
145
|
+
|
|
146
|
+
As each theme resolves, close it in one call:
|
|
147
|
+
|
|
148
|
+
`triage_project_note({ noteIds: [ ...the theme's notes ], outcome, featureId? })`
|
|
149
|
+
|
|
150
|
+
The `outcome` is one line and it is the inbox's permanent record of where the
|
|
151
|
+
note went. Use these forms:
|
|
152
|
+
|
|
153
|
+
| Destination | Outcome line |
|
|
154
|
+
|---|---|
|
|
155
|
+
| New feature | `→ new feature <title>` |
|
|
156
|
+
| The batched quick change | `→ batch quick change <title>` |
|
|
157
|
+
| A revisit | `→ revisit of <slug> (handoff given)` |
|
|
158
|
+
| Another lap | `→ another lap of <slug> (handoff given)` |
|
|
159
|
+
| Dropped, or nothing | `dropped: <why>` |
|
|
160
|
+
|
|
161
|
+
**Pass `featureId` whenever the destination is a feature.** `create_feature`
|
|
162
|
+
returns the new feature's `id`: the batch's id goes on every note routed into it,
|
|
163
|
+
and a new feature's id on every note in its theme. That link is what lets the
|
|
164
|
+
inbox show which work a note became.
|
|
165
|
+
|
|
166
|
+
A triaged note is **frozen** — no edit, no delete, no re-triage (only the human
|
|
167
|
+
can reopen one from the inbox). Get the outcome line right the first time.
|
|
168
|
+
|
|
169
|
+
## Deferring and skipping
|
|
170
|
+
|
|
171
|
+
- **A theme the human wants to leave for later stays open** — but do not leave it
|
|
172
|
+
as the line it was jotted as. Offer the sharpened version and, **with their
|
|
173
|
+
OK**, write it with `update_project_note({ noteId, text })`, so the next triage
|
|
174
|
+
starts from what you both worked out instead of re-grilling it.
|
|
175
|
+
- **A note the human skips stays open.** There is no partial state and nothing to
|
|
176
|
+
record; it comes back next triage.
|
|
177
|
+
|
|
178
|
+
## Close
|
|
179
|
+
|
|
180
|
+
Tell them what this pass produced: the batch and how many tickets it carries,
|
|
181
|
+
each new feature, each handoff line still to paste, and how many notes are still
|
|
182
|
+
open. Then stop — you never launch what you created (§1c), and the cards
|
|
183
|
+
appearing in the rail are the handoff.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: qa
|
|
3
3
|
description: Answer questions about an existing (possibly shipped) feature from its docs and the codebase. Read-only — never advances phases, never emits tickets. Entry skill for kind=qa sessions.
|
|
4
|
-
disable-model-invocation:
|
|
4
|
+
disable-model-invocation: false
|
|
5
5
|
---
|
|
6
6
|
<!-- Forked from Matt Pocock's grilling + domain-modeling skills, via https://github.com/mattpocock/skills, 2026-07-14, adapted for runcastle -->
|
|
7
7
|
|
|
@@ -17,7 +17,8 @@ That is not a limitation to apologise for: it is the contract that makes this se
|
|
|
17
17
|
|
|
18
18
|
## Your tools
|
|
19
19
|
|
|
20
|
-
- `mcp__runcastle__get_feature_context` — the feature, its `phase` and `lap`, its canonical docs inlined in `docs[]` (brief, map, decisions, spec), an index of everything else in `moreDocs[]` (with `relPath` and byte counts), and the tickets.
|
|
20
|
+
- `mcp__runcastle__get_feature_context` — the feature, its `phase` and `lap`, its canonical docs inlined in `docs[]` (brief, map, decisions, spec), an index of everything else in `moreDocs[]` (with `relPath` and byte counts), and the tickets as summary rows (`seq`, `title`, `status`, `goal`, …). A canonical doc too large to inline is listed in `notInlined` instead — read it before you answer from it.
|
|
21
|
+
- `mcp__runcastle__get_ticket({ seq })` — one ticket in full: its context, acceptance criteria, and the burner's digest of what it actually did.
|
|
21
22
|
- `mcp__runcastle__read_feature_doc({ relPath })` — one of the `moreDocs[]` entries in full: test notes, research deliverables under `research/`, anything else the feature's sessions left behind.
|
|
22
23
|
- `mcp__runcastle__list_tickets({ status? })` — the ticket list on its own, when the question is about the queue and you do not need the whole feature.
|
|
23
24
|
- `mcp__runcastle__record_event({ type: "qa.note", message: "..." })` — a one-line note on the feature timeline. The only write you have, and the timeline is its only record, so use it when something surfaces that the record should keep: a clarification worth remembering, a discovered discrepancy between the docs and the code.
|
|
@@ -36,7 +37,7 @@ The injected system prompt carries the slug and the paths; trust `get_feature_co
|
|
|
36
37
|
A Q&A session is the wrong place to change the feature. If the conversation turns into "let's change X" or "this is a bug, fix it," name it and point them at the move that can:
|
|
37
38
|
|
|
38
39
|
- a **revisit** on this feature, for late information that amends its record or its ticket queue;
|
|
39
|
-
-
|
|
40
|
+
- **another lap** from review, if the feature is in Review and the drive taught them the spec was wrong — **Iterate** opens the conversation that amends the spec and cards the work, and **Burn** from Review starts the lap;
|
|
40
41
|
- a **new feature**, if it is really its own capability — created from the project session or the UI.
|
|
41
42
|
|
|
42
43
|
Say which one and why, then `record_event` a one-line note capturing the request so it is not lost. You cannot open any of them, and you cannot create the feature yourself; the human's click is the handoff.
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: revisit
|
|
3
|
-
description: Fold late-arriving information into a feature whose sessions are finished — amend the docs, then reconcile tickets (update/cancel/emit); an ordinary revisit never
|
|
4
|
-
disable-model-invocation:
|
|
3
|
+
description: Fold late-arriving information into a feature whose sessions are finished — amend the docs, then reconcile tickets (update/cancel/emit); an ordinary revisit never moves the feature. Also the lap session: digest the test drive, amend decisions + spec, emit the lap's tickets and report ideation → spec → tickets in the one session. Entry skill for kind=revisit sessions.
|
|
4
|
+
disable-model-invocation: false
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Revisit
|
|
8
8
|
|
|
9
9
|
The human came back: they remembered a constraint, changed their mind, or learned something that the grilling/spec didn't capture. This session's job is to make the **record** and the **ticket queue** match the new reality — nothing more. The pipeline does not move; whatever phase the feature is in, it stays in.
|
|
10
10
|
|
|
11
|
-
That is the ordinary revisit, and it is what the moves below describe. The one exception is a **lap
|
|
11
|
+
That is the ordinary revisit, and it is what the moves below describe. The one exception is a **lap**, and your kickoff line is what tells you: a lap briefing opens this same session with a bigger job — see **Lap mode** below, and work that section instead. Go by the line you were given, never by a guess from the feature's state.
|
|
12
12
|
|
|
13
13
|
## Order of operations
|
|
14
14
|
|
|
15
15
|
1. **Listen first.** The human opens with what changed. If this terminal resumed the previous conversation, use that context; do not re-grill what is already settled. Ask only the questions the NEW information raises.
|
|
16
|
-
2. **Context.** Call `mcp__runcastle__get_feature_context` — the feature, its `phase` and `lap`, the canonical docs inlined in `docs[]` (brief, map, decisions, spec), an index of everything else in `moreDocs[]` (test notes, `research/*.md`; fetch one with `mcp__runcastle__read_feature_doc({ relPath })`), and every ticket with its id and
|
|
16
|
+
2. **Context.** Call `mcp__runcastle__get_feature_context` — the feature, its `phase` and `lap`, the canonical docs inlined in `docs[]` (brief, map, decisions, spec), an index of everything else in `moreDocs[]` (test notes, `research/*.md`; fetch one with `mcp__runcastle__read_feature_doc({ relPath })`), and every ticket as a summary row with its id, `seq`, status and `goal` — its context, acceptance criteria and digest come from `mcp__runcastle__get_ticket({ seq })`. A canonical doc too large to inline is listed in `notInlined` instead; read it before acting. The injected system prompt carries the slug and paths; trust `get_feature_context` for the live state. When all you need is ticket ids to operate on, `mcp__runcastle__list_tickets({ status? })` is the cheap call — it returns the queue without the docs.
|
|
17
17
|
3. **Docs.** Capture the change as decision prose:
|
|
18
18
|
- Append to `docs/features/<slug>/decisions.md` under a dated `## Revisited <date>` heading — never rewrite old decisions, supersede them ("Supersedes: <old decision>").
|
|
19
19
|
- If `spec.md` exists and the change touches it, amend the affected sections in place.
|
|
@@ -23,36 +23,49 @@ That is the ordinary revisit, and it is what the moves below describe. The one e
|
|
|
23
23
|
- No longer needed → `mcp__runcastle__cancel_ticket({ id, reason })` (pending/failed only).
|
|
24
24
|
- New work required → `mcp__runcastle__emit_tickets({ tickets })` (batch, same shape as ideation).
|
|
25
25
|
- `done` work now wrong → emit a NEW ticket that corrects it. Never touch done/burning tickets.
|
|
26
|
-
5. **Close.** `mcp__runcastle__record_event({ type: "feature.revisited", message: "<one-line gist of what changed>" })`, then tell the human what state you left things in — especially whether a re-Burn is needed (pending tickets exist and the feature is at `
|
|
26
|
+
5. **Close.** `mcp__runcastle__record_event({ type: "feature.revisited", message: "<one-line gist of what changed>" })`, then tell the human what state you left things in — especially whether a re-Burn is needed (pending tickets exist and the feature is at `building` with no run behind them).
|
|
27
27
|
|
|
28
|
-
## Lap mode (
|
|
28
|
+
## Lap mode (another lap over the same feature)
|
|
29
29
|
|
|
30
|
-
Your kickoff line reads `LAP <n> REVIEW ITERATION`. The human burned the last lap, test-drove the branch, and
|
|
30
|
+
Your kickoff line reads `LAP <n> REVIEW ITERATION`. The human burned the last lap, test-drove the branch, and came back with what it taught them: the code was right, the *spec* wasn't. Nothing looped the feature backwards to get you here and nothing can — **Planning → Building → Review → Shipped** runs one way only — so read its `phase` off `get_feature_context` rather than assuming which one you are standing in. One trip round is a lap, and this session is the whole front half of lap `<n>` — you carry it from what-the-drive-taught to ticket cards waiting on the human's **Burn** click, in this one conversation:
|
|
31
31
|
|
|
32
|
-
1. **
|
|
33
|
-
2. **
|
|
32
|
+
1. **Context.** `mcp__runcastle__get_feature_context` — the feature (including its `lap`), the docs, `reviewEvidence`, and the full ticket history across every lap as summary rows (title, status, `goal`, `error`); each ticket's own `lap` is what separates them, and `mcp__runcastle__get_ticket({ seq })` gives one ticket's context, acceptance criteria and digest. A canonical doc listed in `notInlined` must be read before you plan. Skim what the last lap actually landed and what failed before you interview.
|
|
33
|
+
2. **Read the evidence before you plan.** Not after, and not instead of the interview. Your inputs, in reading order:
|
|
34
|
+
- **The previous lap's review evidence.** Your injected prompt names it by absolute path, and `get_feature_context` → `reviewEvidence` carries the same paths: `digestPath` is the review agent's own account of what it found, `dir` holds that pass's screenshots and `walkthrough.webm`, and `status` is how the pass itself ended — the review outcome. It reaches you through nothing else: the directory is host scratch space outside the repo, and the context payload's ticket rows carry no `digest` — a ticket's own digest is one `get_ticket({ seq })` away. An empty `reviewEvidence` means the previous lap finished no review pass — say that plainly rather than implying you read one.
|
|
35
|
+
- **What the human wrote down**: the previous lap's section of `docs/features/<slug>/test-notes.md` — it is in `moreDocs[]`, so read it with `mcp__runcastle__read_feature_doc({ relPath: "test-notes.md" })` (or off disk) — and the `## Later laps` section of `spec.md`, which is inlined in `docs[]`. **Both may be absent** — that is not an error and not something to go hunting for: interview the human from scratch instead. What did they hit, what surprised them, what do they want instead now they've used it?
|
|
36
|
+
- **What the docs say is not demonstrable yet.** Grep `spec.md` and this feature's tickets for `not demonstrable`, `do not demo` and `later laps`, and tell the human what you found **before** you offer a test drive. A human was once handed a lap-1 build whose own plan said "no UI, nobody should demo this before ticket X"; the drive taught nothing and cost them the time to find that out. Say which parts are drivable and which are not, then offer the drive.
|
|
34
37
|
3. **Never re-emit a promoted note.** Notes the human already promoted from the review checklist are tickets in *this* lap and arrive as ids in your context. However well such a note reads as a ticket, if its id is in front of you the work is already carded — emitting it again gives the burner the same job twice.
|
|
35
|
-
4. **Grill, briefly.** A small
|
|
38
|
+
4. **Grill, briefly.** A small lap is a short grilling: a few questions, one at a time, each with your recommended answer attached. Prune and promote `## Later laps` entries with the human as part of that conversation — what the drive taught is usually what decides which deferred scope this lap picks up and which stays parked.
|
|
36
39
|
5. **Amend the docs.** Append this lap's learning to `docs/features/<slug>/decisions.md` under a `## Lap <n>` heading (the convention) — never rewrite old decisions, supersede them. Then amend `spec.md` in place: the sections this lap changes, plus the pruned `## Later laps`.
|
|
37
|
-
6. **Emit this lap's tickets, dispositioning the defects as you do.** `mcp__runcastle__emit_tickets({ tickets })` — only the work this lap will burn; the rest stays in `## Later laps`. Reconcile stale pending tickets with `update_ticket` / `cancel_ticket`; `done` work that is now wrong gets a NEW ticket, as always.
|
|
40
|
+
6. **Emit this lap's tickets, dispositioning the defects as you do.** `mcp__runcastle__emit_tickets({ tickets })` — only the work this lap will burn; the rest stays in `## Later laps`. Every ticket that answers a bug the human reported obeys **Stand on the failure** below. Reconcile stale pending tickets with `update_ticket` / `cancel_ticket`; `done` work that is now wrong gets a NEW ticket, as always.
|
|
38
41
|
|
|
39
|
-
The defects an **earlier** lap's review left open arrive in your context as `openDefects` (`carriedDefects` are the ones a lap already parked — agenda, not obligation). Each open one needs exactly one of three answers
|
|
42
|
+
The defects an **earlier** lap's review left open arrive in your context as `openDefects` (`carriedDefects` are the ones a lap already parked — agenda, not obligation). Each open one needs exactly one of three answers. Nothing refuses over the ones you leave: `complete_phase({ phase: "tickets" })` names them back at you as a warning and the human reads the same list in the Burn dialog, so finishing this triage is yours, not a gate's:
|
|
40
43
|
|
|
41
44
|
- **Link** — this lap is carding the work that fixes it: emit that ticket with `originFindingId: "<the defect's id>"`. Nothing else to do; the burn closes the finding itself when the ticket lands.
|
|
42
45
|
- **Carry** — nobody is answering it this lap: `mcp__runcastle__resolve_finding({ findingId, disposition: "carry" })`. It leaves the open count, stays visible, and only the human reopens it.
|
|
43
46
|
- **Close as addressed** — this lap's work already answers it and no ticket names it: `mcp__runcastle__resolve_finding({ findingId, disposition: "addressed", note: "<what addressed it>" })`. The note is required and is your attestation, so make it specific ("lap 2's ticket 7 rewrote the endpoint").
|
|
44
47
|
|
|
45
48
|
Judge each against what you and the human just settled — you are the only party that knows whether this lap's tickets touch a defect, which is why this is yours and not a dialog. If one genuinely is not this lap's business, carry it; do not close what you have not established.
|
|
46
|
-
7. **
|
|
49
|
+
7. **Report the planning steps.** `mcp__runcastle__complete_phase` through `ideation` → `spec` → `tickets`, here, without opening another session. They are three steps inside Planning, not three gates: each records a milestone on the timeline and moves the feature nowhere. The `tickets` call comes back `ok: true` with `waitingOn: "human burn"` and a `warnings` array — the lap waits on the human's Burn click, which is the point. Whatever is in `warnings` is what they will read in the Burn dialog (un-dispositioned defects from move 6, no review ticket in the batch, no `spec.md` on disk): fix what you can while you are still the session that can, and call again.
|
|
47
50
|
8. **Hand to Burn.** Tell the human in a line or two what this lap does, then:
|
|
48
51
|
|
|
49
52
|
> Lap `<n>` is specced and carded. Review the ticket cards and click **Burn** — I'll stop here.
|
|
50
53
|
|
|
51
|
-
If
|
|
54
|
+
If this lap turns out to be genuinely big — whole branches of design reopened, decisions hanging on material nobody has read — say so and escalate the way ideation would (`/runcastle:ideate` §3, the map), rather than grinding it out here.
|
|
55
|
+
|
|
56
|
+
### Stand on the failure
|
|
57
|
+
|
|
58
|
+
Every bug the human reports gets exactly one of three answers before you card a fix for it:
|
|
59
|
+
|
|
60
|
+
- **Reproduced** — you drove it and saw the failure. Say what you did and what happened.
|
|
61
|
+
- **Traced** — you found it in the code. Name the file and line, and what there is wrong.
|
|
62
|
+
- **Unreproduced** — you could not do either. Say so plainly, and then make reproducing it the **first** acceptance criterion of the ticket that fixes it, so the burner stands on the failure before it changes anything.
|
|
63
|
+
|
|
64
|
+
Never card a fix for a failure nobody has stood on: a ticket written from a one-line report and a guess sends an agent to change the code it guessed at, and the next drive hits the same bug. This is what the human's report buys — it is the report, not a diagnosis.
|
|
52
65
|
|
|
53
66
|
## Do NOT
|
|
54
67
|
|
|
55
|
-
- **Never call `complete_phase` in an ordinary revisit.** It has no
|
|
68
|
+
- **Never call `complete_phase` in an ordinary revisit.** It has no planning steps of its own to report; the human drives the pipeline from the UI. **Lap mode is the one exception** — reporting ideation → spec → tickets is its job (move 7), and nothing else here licenses it.
|
|
56
69
|
- **Never resolve or reopen waypoints.** If a resolved waypoint's answer is now wrong, record the superseding decision in `decisions.md` — the map's history stays intact.
|
|
57
70
|
- **Code changes ride tickets** — the burner (or a human) implements them, never this session. (The no-code rule itself is in your injected prompt and enforced by the edit guard.)
|
|
58
71
|
|
|
@@ -11,7 +11,7 @@ Turn the ideation conversation plus `docs/features/<slug>/decisions.md` into a s
|
|
|
11
11
|
|
|
12
12
|
## Process
|
|
13
13
|
|
|
14
|
-
1. **Load state.** `mcp__runcastle__get_feature_context` for the feature, the locked `decisions.md`, and any existing `spec.md` (this may be a re-run). Explore the codebase for its current state if you have not already; use the project's domain vocabulary throughout and respect any ADRs in the area you touch.
|
|
14
|
+
1. **Load state.** `mcp__runcastle__get_feature_context` for the feature, the locked `decisions.md`, and any existing `spec.md` (this may be a re-run). A canonical doc too large to inline arrives in `notInlined` instead of `docs[]` — read it (`mcp__runcastle__read_feature_doc({ relPath })` or the file at its `absPath`) before writing a word; tickets arrive as summary rows with their `goal`, and `mcp__runcastle__get_ticket({ seq })` holds a ticket's context, acceptance criteria and digest. Explore the codebase for its current state if you have not already; use the project's domain vocabulary throughout and respect any ADRs in the area you touch.
|
|
15
15
|
|
|
16
16
|
2. **Sketch the seams** — the public boundaries the feature will be tested at. Prefer *existing* seams to new ones; use the *highest* seam possible; the fewer across the codebase, the better — the ideal is one. If new seams are needed, propose them at the highest point you can. These seams flow straight into the tickets and the burner, so get them right here.
|
|
17
17
|
|
|
@@ -42,6 +42,6 @@ Turn the ideation conversation plus `docs/features/<slug>/decisions.md` into a s
|
|
|
42
42
|
|
|
43
43
|
Do **not** hardcode specific file paths or code snippets — they go stale. Exception: if the ideation produced a snippet that pins a decision more precisely than prose can (a state machine, reducer, schema, type shape), inline the decision-rich parts and note it came from that discussion.
|
|
44
44
|
|
|
45
|
-
4. **
|
|
45
|
+
4. **Report the step.** `mcp__runcastle__complete_phase({ phase: "spec" })`. It does not refuse and it does not move the feature — spec is a step inside **Planning** — but it answers with `nextStep`, the next step still missing its artifact. If that comes back `"spec"`, `spec.md` is not on disk where the server looks: write it and call again. It logs its own timeline event and checkpoints `spec.md` into git; do not add an event of your own.
|
|
46
46
|
|
|
47
47
|
Return control to **the session skill that invoked you** — `/runcastle:ideate` for a linear feature, `/runcastle:converge` for a mapped one. It invokes `/runcastle:tickets` next. Do **not** invoke a session skill yourself to hand back: you are already inside one, and loading another session's entry skill would drop a whole procedure this session is not running (a converge session in particular is forbidden to grill) into the window.
|
|
@@ -7,7 +7,7 @@ disable-model-invocation: false
|
|
|
7
7
|
|
|
8
8
|
# Tickets
|
|
9
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.
|
|
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
11
|
|
|
12
12
|
## The one thing that makes a runcastle ticket different
|
|
13
13
|
|
|
@@ -42,7 +42,7 @@ Close the batch with **one review ticket** (`kind: "review"`), blocked by every
|
|
|
42
42
|
|
|
43
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
44
|
|
|
45
|
-
The server
|
|
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 Burn 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
46
|
|
|
47
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
48
|
|
|
@@ -51,6 +51,22 @@ Your job is to make that choice obvious. Prescribe a walkthrough only when there
|
|
|
51
51
|
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.
|
|
52
52
|
</review-ticket>
|
|
53
53
|
|
|
54
|
+
<parallelism-budget>
|
|
55
|
+
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.
|
|
56
|
+
|
|
57
|
+
So, after slicing and before the self-check below, **say it out loud in the session**, in these terms:
|
|
58
|
+
|
|
59
|
+
- the **critical path** — the longest chain of implementation tickets, named in order, and its length;
|
|
60
|
+
- **how many tickets can run at once**, stated against this project's `burnConcurrency`;
|
|
61
|
+
- 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.
|
|
62
|
+
|
|
63
|
+
**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.
|
|
64
|
+
|
|
65
|
+
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.
|
|
66
|
+
|
|
67
|
+
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.
|
|
68
|
+
</parallelism-budget>
|
|
69
|
+
|
|
54
70
|
<wide-refactor-exception>
|
|
55
71
|
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.
|
|
56
72
|
</wide-refactor-exception>
|
|
@@ -71,7 +87,7 @@ Each ticket:
|
|
|
71
87
|
|
|
72
88
|
### Assigning a model (only from the annotated roster)
|
|
73
89
|
|
|
74
|
-
`mcp__runcastle__get_feature_context` returns `annotatedModels
|
|
90
|
+
`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`.
|
|
75
91
|
|
|
76
92
|
- **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.
|
|
77
93
|
- **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.
|
|
@@ -81,17 +97,17 @@ The human sees the assignment on the ticket card and can change or clear it befo
|
|
|
81
97
|
|
|
82
98
|
## 3. Self-check, then emit
|
|
83
99
|
|
|
84
|
-
There is no in-session quiz — the human's review is the **Burn**
|
|
100
|
+
There is no in-session quiz — the human's review is the **Burn** click in the runcastle UI, reading these cards. So make them right before you emit. Check yourself, in this order:
|
|
85
101
|
|
|
86
102
|
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.
|
|
87
103
|
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.
|
|
88
104
|
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?
|
|
89
|
-
4. Are the blocking edges minimal and genuinely true gates? Where two are independent, do they touch different files — or will they collide on landing?
|
|
105
|
+
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?
|
|
90
106
|
5. Is every `context` self-sufficient for an agent that cannot ask — covering every file and pattern the ticket touches?
|
|
91
107
|
6. Does the batch close with exactly one `kind: "review"` ticket, blocked by every implementation ticket? It is not optional and there is no feature that skips it.
|
|
92
108
|
|
|
93
109
|
Then:
|
|
94
110
|
- `mcp__runcastle__emit_tickets({ tickets: [...] })` — **emit the array; do NOT write ticket files.** It returns `{ stored, ids }` and logs the timeline event itself; do not record one of your own. Emit the **whole batch, fully enriched, in one call** — large payloads are supported, so a batch of long `context` fields is fine. Never emit placeholder contexts ("Context follows via `update_ticket`.") to enrich ticket by ticket afterwards: a Burn landing mid-enrichment burns agents on the placeholders.
|
|
95
|
-
- `mcp__runcastle__complete_phase({ phase: "tickets" })`.
|
|
111
|
+
- `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 Burn 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.
|
|
96
112
|
|
|
97
113
|
Return control to **the session skill that invoked you** — `/runcastle:ideate` for a linear feature, `/runcastle:converge` for a mapped one — and let it close out the session. Do **not** invoke a session skill yourself to hand back: you are already inside one, and loading another session's entry skill would drop a whole procedure this session is not running (a converge session in particular is forbidden to grill) into the window.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: waypoint
|
|
3
3
|
description: The runcastle waypoint session. Work ONE waypoint on a feature's map — grill, prototype, or run a task checklist for its assigned question — write the decision prose to the docs, then resolve the waypoint. Entry skill for kind=waypoint sessions.
|
|
4
|
-
disable-model-invocation:
|
|
4
|
+
disable-model-invocation: false
|
|
5
5
|
---
|
|
6
6
|
<!-- runcastle mapped-ideation waypoint session (ADR-0001 §13.5) -->
|
|
7
7
|
|
|
@@ -15,7 +15,7 @@ This is a **mapped-ideation** feature: too big for one context window, so it was
|
|
|
15
15
|
- `waypoints` — every waypoint on the map, with its `title`, `type`, `question`, status and (once terminal) `summary`.
|
|
16
16
|
- `frontierIds` — the ids currently open, unclaimed and unblocked.
|
|
17
17
|
- `assignedWaypointId` — **your** waypoint's id, the one this session claimed. Find that id in `waypoints` and read its `title`, `type` and `question`. That is your whole assignment.
|
|
18
|
-
2. `docs[]` inlines the canonical docs in full — `brief.md`, `map.md`, `decisions.md`, `spec.md` if it exists. Read `map.md` (Destination, Notes, Not-yet-specified, Out-of-scope) so your answer fits the north star, and `decisions.md` for what is already locked — **do not re-litigate settled decisions.**
|
|
18
|
+
2. `docs[]` inlines the canonical docs in full — `brief.md`, `map.md`, `decisions.md`, `spec.md` if it exists. One too large to inline is listed in `notInlined` instead, with its `relPath` and `absPath`: read it with `mcp__runcastle__read_feature_doc({ relPath })` or off disk before acting. Any tickets arrive as summary rows with their `goal`; `mcp__runcastle__get_ticket({ seq })` holds one ticket's context, acceptance criteria and digest. Read `map.md` (Destination, Notes, Not-yet-specified, Out-of-scope) so your answer fits the north star, and `decisions.md` for what is already locked — **do not re-litigate settled decisions.**
|
|
19
19
|
3. **Read the research already done for this feature.** Earlier `research` waypoints wrote deliverables to `docs/features/<slug>/research/<seq>-<slug>.md`. They are *not* inlined — they appear in the `moreDocs` index with their `relPath` and `bytes`, and each resolved research waypoint's `summary` in `waypoints` names its file (`researched: <title> — see <path>`). Fetch the ones your question depends on with `mcp__runcastle__read_feature_doc({ relPath })`, or read them off disk in this worktree. A waypoint that re-derives a finding an earlier waypoint already paid an AFK run for is a wasted session.
|
|
20
20
|
|
|
21
21
|
## 1. Work it — mode by type
|