runcastle 1.2.3 → 1.2.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 +832 -368
- package/drizzle/0019_freezing_natasha_romanoff.sql +2 -0
- package/drizzle/0020_petite_ares.sql +2 -0
- package/drizzle/0021_tearful_skrulls.sql +2 -0
- package/drizzle/0022_yielding_iceman.sql +1 -0
- package/drizzle/meta/0019_snapshot.json +860 -0
- package/drizzle/meta/0020_snapshot.json +877 -0
- package/drizzle/meta/0021_snapshot.json +891 -0
- package/drizzle/meta/0022_snapshot.json +898 -0
- package/drizzle/meta/_journal.json +29 -1
- package/index.js +827 -363
- package/package.json +2 -2
- package/pty-host.cjs +10 -0
- package/skills/burner/implement-ticket.md +8 -1
- package/skills/burner/research-waypoint.md +2 -2
- package/skills/packs/README.md +10 -3
- package/skills/packs/runcastle/.claude-plugin/plugin.json +1 -1
- package/skills/packs/runcastle/skills/ideate/SKILL.md +1 -0
- package/skills/packs/runcastle/skills/project/SKILL.md +5 -3
- package/skills/packs/runcastle/skills/revisit/SKILL.md +1 -1
- package/skills/packs/runcastle/skills/spec/SKILL.md +4 -0
- package/skills/packs/runcastle/skills/tickets/SKILL.md +3 -4
- package/web/assets/{index-BhOja4Fc.js → index-D-c432xb.js} +42 -42
- package/web/assets/index-D92MdVG6.css +1 -0
- package/web/index.html +2 -2
- package/web/assets/index-o2brqt2b.css +0 -1
package/package.json
CHANGED
package/pty-host.cjs
CHANGED
|
@@ -138,6 +138,13 @@ function handle(msg) {
|
|
|
138
138
|
}
|
|
139
139
|
break
|
|
140
140
|
case 'kill':
|
|
141
|
+
// Deliberately NOT a tree kill. Teardown never relies on this: the server
|
|
142
|
+
// tree-kills the pid it owns — THIS host process — and `taskkill /T` sweeps
|
|
143
|
+
// host → cmd shim → dev server from the outside (pty-sidecar.ts's
|
|
144
|
+
// `killTree`), so by the time a `kill` frame could matter the host is
|
|
145
|
+
// already gone. This is the backstop for the paths that are not teardown
|
|
146
|
+
// (an explicit kill with the tree walk unavailable or failed), where
|
|
147
|
+
// killing node-pty's own child is the most this process can do.
|
|
141
148
|
if (proc) {
|
|
142
149
|
try {
|
|
143
150
|
proc.kill()
|
|
@@ -173,6 +180,9 @@ process.stdin.on('data', (chunk) => {
|
|
|
173
180
|
})
|
|
174
181
|
|
|
175
182
|
// When the server closes our stdin (it detached / crashed), tear down the child.
|
|
183
|
+
// This is the orphan path, not the teardown path — an orderly stop tree-kills
|
|
184
|
+
// this host from the outside and we never get here — so the same "kill what we
|
|
185
|
+
// own, no tree walk" rule as the `kill` frame applies.
|
|
176
186
|
process.stdin.on('end', () => {
|
|
177
187
|
if (proc) {
|
|
178
188
|
try {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# Implement this ticket — unattended
|
|
4
4
|
|
|
5
|
-
You are a single agent in a sandbox on branch `feature/<slug>`. You have **one ticket**. There is **no human to ask** — no follow-up questions are possible. Everything you need is in this prompt and in the repo. Work carefully, commit only green work, and stop when the ticket is done.
|
|
5
|
+
You are a single agent in a sandbox on **your ticket's own temp branch**, `runcastle/ticket/<slug>/<seq>-<unique>`, forked from the feature branch `feature/<slug>`. Commit there and only there — the run merges your branch back into the feature branch once you land, so never check out or commit to `feature/<slug>` yourself. You have **one ticket**. There is **no human to ask** — no follow-up questions are possible. Everything you need is in this prompt and in the repo. Work carefully, commit only green work, and stop when the ticket is done.
|
|
6
6
|
|
|
7
7
|
## How you run
|
|
8
8
|
|
|
@@ -74,6 +74,13 @@ Whatever the commands are, spend them well — a full suite on a monorepo is min
|
|
|
74
74
|
- **Spec = this ticket** — is every acceptance criterion actually met, and is there **nothing in the diff the ticket did not ask for**? Missing and extra both count.
|
|
75
75
|
Commit the fixes.
|
|
76
76
|
|
|
77
|
+
6. **Write your digest — the last thing you do.** Once every acceptance criterion passes and the self-review fixes are committed, write `DIGEST.md` (see "Where to work" for exactly where) as your final act before printing `<promise>COMPLETE</promise>`. Roughly 10–15 lines, three parts:
|
|
78
|
+
- **What was done** — past tense, what you actually built, *including where it deviated* from the approach the ticket described.
|
|
79
|
+
- **Surprises** — what the ticket or the spec did not anticipate: the coupling nobody mentioned, the test that was already red, the API that did not behave as documented.
|
|
80
|
+
- **Left undone** — adjacent work you noticed and deliberately did not do, so the next agent inherits the observation instead of re-finding it.
|
|
81
|
+
|
|
82
|
+
Write it in plain prose, for a reader who does not have your context. **Never commit `DIGEST.md`** — it is harvested from the workspace, not from the repo, and a committed one is diff noise. It is a **success artifact only**: if you are blocked and writing `BLOCKED.md`, write no digest — `BLOCKED.md` is your record.
|
|
83
|
+
|
|
77
84
|
## Hard rules
|
|
78
85
|
|
|
79
86
|
A few of these are enforced by a tool hook, not just stated here: `git stash`, test-runner concurrency flags, and rewriting files through interpreter heredocs are **denied** before they run. A denial is policy, not a broken environment — read its reason, take the alternative it names, and carry on. Do not try to route around it.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# Research this waypoint — unattended
|
|
4
4
|
|
|
5
|
-
You are a single agent in a sandbox on branch `feature/<slug>`. You have **one research question**. There is **no human to ask** — no follow-up questions are possible. Everything you need is in this prompt, on the web, and in the repo. Research carefully, write one focused summary, commit it, and stop.
|
|
5
|
+
You are a single agent in a sandbox on **this run's own temp branch**, `runcastle/research/<slug>/<seq>-<unique>`, forked from the feature branch `feature/<slug>`. Commit there and only there — the run merges your branch back into the feature branch when it lands, and the feature branch stays checked out elsewhere for the whole run, so never check it out or commit to it yourself. You have **one research question**. There is **no human to ask** — no follow-up questions are possible. Everything you need is in this prompt, on the web, and in the repo. Research carefully, write one focused summary, commit it, and stop.
|
|
6
6
|
|
|
7
7
|
## The waypoint
|
|
8
8
|
|
|
@@ -36,7 +36,7 @@ You are a single agent in a sandbox on branch `feature/<slug>`. You have **one r
|
|
|
36
36
|
|
|
37
37
|
Keep it tight and honest. If the question cannot be answered from available material, say so plainly in the doc and record what would be needed.
|
|
38
38
|
|
|
39
|
-
4. **Commit the doc to the
|
|
39
|
+
4. **Commit the doc to the branch you are on.** Stage only your new/changed doc under `docs/features/<slug>/` and commit with a clear message, e.g. `research(<seq>): <summary>`. The run reads your exit state from the commit — **a doc with no commit is read as no research landed** — and merges the commit onto the feature branch for you.
|
|
40
40
|
|
|
41
41
|
## Hard rules
|
|
42
42
|
|
package/skills/packs/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Skill packs
|
|
2
2
|
|
|
3
|
-
**Packs** are runcastle-owned Claude Code *plugin directories*
|
|
3
|
+
**Packs** are runcastle-owned Claude Code *plugin directories* wired to runcastle's MCP contracts. They are injected into each launched terminal via `--plugin-dir`, so the human's Claude Code needs nothing preinstalled, and upstream changes to Matt Pocock's skills can never break us. Six of the eight skills are adapted forks of his methodology skills and each keeps a provenance header crediting the original; `revisit` and `waypoint` are original runcastle work.
|
|
4
4
|
|
|
5
5
|
## The `runcastle` pack
|
|
6
6
|
|
|
@@ -13,6 +13,9 @@ Scope-specific skills, each namespaced `/runcastle:<skill>`:
|
|
|
13
13
|
| `/runcastle:tickets` | by ideate | emits session-sized vertical-slice tickets via MCP, completes the `tickets` phase |
|
|
14
14
|
| `/runcastle:qa` | entry for `kind=qa` | read-only Q&A over an existing feature; never advances phases |
|
|
15
15
|
| `/runcastle:project` | entry for `kind=project` | project scope, not feature scope: grills a lump of intent into N features and creates them, routes, answers portfolio questions, curates advisory-only, and owns `CONTEXT.md` |
|
|
16
|
+
| `/runcastle:waypoint` | entry for `kind=waypoint` | *original* — works ONE waypoint on a mapped feature, writes its decision prose, resolves the waypoint |
|
|
17
|
+
| `/runcastle:converge` | entry for `kind=converge` | closes a mapped feature: reads only the compressed knowledge, then drives spec + tickets from it |
|
|
18
|
+
| `/runcastle:revisit` | entry for `kind=revisit` | *original* — folds late information into a finished feature; on a Rethink, runs the whole front half of a lap |
|
|
16
19
|
|
|
17
20
|
Layout (the verified plugin format — only `plugin.json` lives inside `.claude-plugin/`; `skills/` is a sibling at the plugin root):
|
|
18
21
|
|
|
@@ -26,7 +29,11 @@ packs/
|
|
|
26
29
|
├── ideate/SKILL.md
|
|
27
30
|
├── spec/SKILL.md
|
|
28
31
|
├── tickets/SKILL.md
|
|
29
|
-
|
|
32
|
+
├── qa/SKILL.md
|
|
33
|
+
├── project/SKILL.md
|
|
34
|
+
├── waypoint/SKILL.md
|
|
35
|
+
├── converge/SKILL.md
|
|
36
|
+
└── revisit/SKILL.md
|
|
30
37
|
```
|
|
31
38
|
|
|
32
39
|
## How the launcher consumes a pack
|
|
@@ -37,7 +44,7 @@ The session launcher spawns Claude Code with the pack's **root** directory (the
|
|
|
37
44
|
claude ... --plugin-dir "<abs path>/packages/skills/packs/runcastle" ...
|
|
38
45
|
```
|
|
39
46
|
|
|
40
|
-
The `name` field in `plugin.json` becomes the invocation namespace, so the skills resolve as `/runcastle:ideate` and friends. The injected system prompt tells each session which entry skill to invoke for its kind (`/runcastle:ideate` for ideation, `/runcastle:qa` for Q&A). All
|
|
47
|
+
The `name` field in `plugin.json` becomes the invocation namespace, so the skills resolve as `/runcastle:ideate` and friends. The injected system prompt tells each session which entry skill to invoke for its kind (`/runcastle:ideate` for ideation, `/runcastle:qa` for Q&A). All eight skills carry `disable-model-invocation: false`, so the model may also reach for them by description.
|
|
41
48
|
|
|
42
49
|
## Adding a pack
|
|
43
50
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "runcastle",
|
|
3
|
-
"description": "Runcastle phase-scoped skills
|
|
3
|
+
"description": "Runcastle phase-scoped skills for the MCP feature pipeline: ideate, spec, tickets, qa, project, converge (forks of Matt Pocock's grilling, to-spec and to-tickets, adapted) plus revisit and waypoint (original).",
|
|
4
4
|
"version": "0.1.0",
|
|
5
5
|
"author": { "name": "runcastle" }
|
|
6
6
|
}
|
|
@@ -29,6 +29,7 @@ Interview the human about every aspect of the feature until you reach a genuinel
|
|
|
29
29
|
- **Model the domain as you go** (forked from domain-modeling): challenge terms that conflict with existing usage, sharpen fuzzy words to one canonical meaning ("you said 'account' — Customer or User?"), invent concrete edge-case scenarios that force precision, and cross-check claims against the actual code ("your code cancels whole Orders, but you just said partial — which is right?").
|
|
30
30
|
- **Probe the size early.** Within your first few questions, ask enough about scope to judge whether this is one-session-sized or map-sized — how many independent areas it touches, what has to be researched or prototyped before decisions can lock, how wide the design tree looks. Make that call up front, not thirty questions deep into one corner. If it smells map-sized, go to §3 *now* rather than rabbit-holing first.
|
|
31
31
|
- **Ask the slicing question: how sure are you this is what you want?** Put it to them early, plainly, with your recommendation attached. **Sure, and small** → spec the whole thing; it is one lap and will likely merge on lap 1 — the old linear flow, verbatim. **Unsure, or large** → recommend a thin **lap 1**: a walking skeleton of the uncertain part, or one sub-feature slice they can actually test-drive, with the consciously deferred scope parked in the spec's `## Later laps` section (from there it seeds the next lap's session alongside the test notes). **Say the recommendation out loud and let them answer** — this is a judgment you offer, not a rule you enforce, and the human decides. There is no mode and no flag: how many laps a feature takes is a description of how it went.
|
|
32
|
+
- **Park scope creep instead of swallowing it.** When something surfaces that is plainly its own feature, say so and offer to park it: `mcp__runcastle__create_feature({ title, oneLiner, brief, draft: true })` creates a parked draft — no branch, nothing written — with the `brief` carrying why you deferred it and what it must not swallow. Then get back to this feature. Drafts are the only thing you may create here; a full create belongs to the project session.
|
|
32
33
|
- **Size and certainty are orthogonal.** Mapping (§3) is for ideation too big to *think* through in one session; laps are for a feature too uncertain to *spec whole*. A feature can need both, one, or neither — a small but fuzzy feature gets a thin lap 1 and no map; a big but settled one gets a map and still merges on lap 1.
|
|
33
34
|
|
|
34
35
|
Do not move on until the human confirms you have reached shared understanding.
|
|
@@ -18,8 +18,8 @@ Everything else you do — portfolio Q&A, routing, curation, the charter — is
|
|
|
18
18
|
Four, and deliberately none of the feature pipeline's. A session with no feature has no business advancing one through a gate.
|
|
19
19
|
|
|
20
20
|
- `mcp__runcastle__get_project_context()` — the project, the charter in full, every live ADR in full, and a one-line index of every feature.
|
|
21
|
-
- `mcp__runcastle__get_work_record({ featureSlug? , seam? })` — what features actually **did**: tickets by status, seams, commits, errors, run summaries. Facts, never intent.
|
|
22
|
-
- `mcp__runcastle__create_feature({ title, oneLiner, baseBranch?, brief?, ticket? })` — the end of intake.
|
|
21
|
+
- `mcp__runcastle__get_work_record({ featureSlug? , seam? })` — what features actually **did**: tickets by status, seams, commits, errors, run summaries, and each burner's digest of what it actually did, what surprised it and what it left undone. Facts, never intent.
|
|
22
|
+
- `mcp__runcastle__create_feature({ title, oneLiner, baseBranch?, brief?, draft?, ticket? })` — the end of intake.
|
|
23
23
|
- `mcp__runcastle__record_event({ type, message })` — a note on the project timeline.
|
|
24
24
|
|
|
25
25
|
Every **merged** feature's docs are already on disk in this worktree — `docs/features/<slug>/`. Read them with your ordinary `Read`/`Grep`; the index says where. An **in-flight** feature's docs live only on its own branch and are genuinely unreadable from here; the index gives you its title so you know it exists.
|
|
@@ -48,6 +48,8 @@ This is a grilling, run the way `/runcastle:ideate` grills, at one level up: you
|
|
|
48
48
|
|
|
49
49
|
When it resolves: **one `create_feature` call per feature.**
|
|
50
50
|
|
|
51
|
+
**Ask, per feature: start it now, or park it as a draft?** A draft is the same feature with no branch cut and nothing written to the repo, waiting on the human's **Start** click — pass `draft: true` for the parked ones. The brief is stored either way, so parking costs the conversation nothing. Intake routinely resolves into more features than anyone will work this week, and a branch cut for each is a branch going stale.
|
|
52
|
+
|
|
51
53
|
Each one carries a real `brief` — the reasoning you just worked out, in prose: why this feature exists, what it is for, what it must not swallow, what is already settled about it. It is written into `brief.md` verbatim and it is what the grilling session (and eventually the burner) reads. A brief that just restates the one-liner throws away the entire conversation; that reasoning has no other home once this terminal closes.
|
|
52
54
|
|
|
53
55
|
**You never launch what you create.** No terminal, no session, no "shall I start on it?". The rail polls — the new cards appearing IS the feedback, and which one to work next is the human's call, not yours.
|
|
@@ -117,7 +119,7 @@ always describes the present, never the history.>
|
|
|
117
119
|
- Delete the line when the thread is done — this section is pruned, not appended to forever.
|
|
118
120
|
```
|
|
119
121
|
|
|
120
|
-
**`## Deferred / open threads` is
|
|
122
|
+
**`## Deferred / open threads` is the charter's home for a parked idea that is not yet a feature** — once it has resolved into one, park it as a draft (§1) instead. There is no backlog table and no `docs/backlog.md`, and you must not create one. Because the charter is rewritten in place, a thread that gets done is *deleted* — which is exactly what stops it decaying into a graveyard. Only put a line here if the idea is **not regenerable**: if re-reading the code would surface it again, it does not need remembering.
|
|
121
123
|
|
|
122
124
|
Term collisions are a rewrite, not an append: if a feature's vocabulary conflicts with a term already defined here, raise it with the human and settle on one meaning — never silently redefine.
|
|
123
125
|
|
|
@@ -35,7 +35,7 @@ Your kickoff line reads `LAP <n> REVIEW ITERATION`. The human burned the last la
|
|
|
35
35
|
4. **Grill, briefly.** A small rethink 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
36
|
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
37
|
6. **Emit this lap's tickets.** `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.
|
|
38
|
-
7. **Advance the pipeline.** `mcp__runcastle__complete_phase` through `ideation` → `spec` → `tickets`, here, without opening another session. It will
|
|
38
|
+
7. **Advance the pipeline.** `mcp__runcastle__complete_phase` through `ideation` → `spec` → `tickets`, here, without opening another session. It will not cross into implementation: the `tickets` call comes back `ok: true` with `waitingOn: "human burn"` and the feature parks at `tickets` instead of advancing — that gate (G3) is the human's Burn click, which is the point.
|
|
39
39
|
8. **Hand to Burn.** Tell the human in a line or two what this lap does, then:
|
|
40
40
|
|
|
41
41
|
> Lap `<n>` is specced and carded. Review the ticket cards and click **Burn** — I'll stop here.
|
|
@@ -34,6 +34,10 @@ Turn the ideation conversation plus `docs/features/<slug>/decisions.md` into a s
|
|
|
34
34
|
|
|
35
35
|
## Open questions
|
|
36
36
|
Anything still unresolved or deliberately deferred.
|
|
37
|
+
|
|
38
|
+
## Later laps
|
|
39
|
+
Optional — include it when this is a thin lap 1. Scope consciously deferred
|
|
40
|
+
to a later lap; it seeds the next lap's session alongside the test notes.
|
|
37
41
|
```
|
|
38
42
|
|
|
39
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.
|
|
@@ -35,7 +35,7 @@ Expect a **handful of substantial tickets** for a feature — roughly 4–8, eac
|
|
|
35
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
36
|
</vertical-slice-rules>
|
|
37
37
|
|
|
38
|
-
Give each ticket its **blocking edges** — the tickets that must finish before it can start.
|
|
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
39
|
|
|
40
40
|
<wide-refactor-exception>
|
|
41
41
|
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.
|
|
@@ -51,7 +51,7 @@ Each ticket:
|
|
|
51
51
|
- **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).
|
|
52
52
|
- **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.
|
|
53
53
|
- **seams** — `string[]`, the public interfaces to test at (carry them from the spec's Seams section; prefer existing, prefer the highest, prefer one).
|
|
54
|
-
- **blockedBy** — `number[]`, the seq numbers (from your 1..N ordering) of the tickets that *genuinely* gate this one. Only true gates.
|
|
54
|
+
- **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.
|
|
55
55
|
|
|
56
56
|
## 3. Self-check, then emit
|
|
57
57
|
|
|
@@ -64,8 +64,7 @@ There is no in-session quiz — the human's review is the **Burn** gate in the r
|
|
|
64
64
|
5. Is every `context` self-sufficient for an agent that cannot ask — covering every file and pattern the ticket touches?
|
|
65
65
|
|
|
66
66
|
Then:
|
|
67
|
-
- `mcp__runcastle__emit_tickets({ tickets: [...] })` — **emit the array; do NOT write ticket files.** It returns `{ stored, ids }
|
|
68
|
-
- `mcp__runcastle__record_event({ type: "tickets.emitted", message: "<n> tickets" })`.
|
|
67
|
+
- `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.
|
|
69
68
|
- `mcp__runcastle__complete_phase({ phase: "tickets" })`. If the gate returns `ok: false`, fix what it names and retry.
|
|
70
69
|
|
|
71
70
|
Return to `/runcastle:ideate` to close out the session.
|