runcastle 1.3.8 → 1.4.0
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 +3059 -1817
- package/drizzle/0040_new_maestro.sql +10 -0
- package/drizzle/0041_round_harpoon.sql +2 -0
- package/drizzle/meta/0040_snapshot.json +1212 -0
- package/drizzle/meta/0041_snapshot.json +1226 -0
- package/drizzle/meta/_journal.json +15 -1
- package/index.js +3055 -1814
- package/package.json +2 -1
- package/sandcastle-template/Containerfile +12 -3
- package/sandcastle-template/Dockerfile +12 -3
- package/skills/packs/README.md +41 -27
- package/skills/packs/runcastle/skills/converge/SKILL.md +2 -2
- package/skills/packs/runcastle/skills/ideate/SKILL.md +3 -3
- package/skills/packs/runcastle/skills/prepare/SKILL.md +1 -1
- package/skills/packs/runcastle/skills/project/SKILL.md +24 -9
- package/skills/packs/runcastle/skills/project/references/health-sweeps.md +5 -2
- package/skills/packs/runcastle/skills/project/references/triage.md +183 -0
- package/skills/packs/runcastle/skills/qa/SKILL.md +3 -2
- package/skills/packs/runcastle/skills/revisit/SKILL.md +4 -4
- package/skills/packs/runcastle/skills/spec/SKILL.md +1 -1
- package/skills/packs/runcastle/skills/tickets/SKILL.md +2 -2
- package/skills/packs/runcastle/skills/waypoint/SKILL.md +2 -2
- package/web/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/web/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/web/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/web/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/web/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
- package/web/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
- package/web/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
- package/web/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
- package/web/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
- package/web/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
- package/web/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/web/assets/index-CSp0YX_G.css +1 -0
- package/web/assets/index-DMGQBgZN.js +187 -0
- package/web/index.html +15 -2
- package/web/assets/index-BpZNJf3C.css +0 -1
- package/web/assets/index-BpmuhqzB.js +0 -187
- package/web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "runcastle",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Burn tickets into shipped features with Claude Code.",
|
|
5
5
|
"license": "FSL-1.1-ALv2",
|
|
6
6
|
"type": "module",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"sandcastle-template"
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.281",
|
|
32
33
|
"@clack/prompts": "^1.1.0",
|
|
33
34
|
"@hono/mcp": "0.3.1",
|
|
34
35
|
"@hono/trpc-server": "0.4.2",
|
|
@@ -18,7 +18,12 @@ RUN curl -fsSL https://bun.sh/install | bash
|
|
|
18
18
|
# Codex CLI (the second agent runtime) — authenticated at runtime via CODEX_API_KEY.
|
|
19
19
|
# Installed as root, before the user switch, so it lands in the system-wide npm
|
|
20
20
|
# prefix: after `USER` a global install has nowhere writable to go.
|
|
21
|
-
|
|
21
|
+
# Pinned to the host's `codex --version`, passed in by Rebuild. The ARG sits
|
|
22
|
+
# directly above its RUN on purpose: a changed version busts the cache for this
|
|
23
|
+
# layer and the ones after it, and nothing before it. Empty installs latest.
|
|
24
|
+
ARG CODEX_VERSION
|
|
25
|
+
RUN npm install -g @openai/codex@${CODEX_VERSION:-latest} \
|
|
26
|
+
|| { echo "runcastle: Codex CLI ${CODEX_VERSION:-latest} install failed — is that version published?" >&2; exit 1; }
|
|
22
27
|
|
|
23
28
|
# Rootless-podman invariant (research #32): the in-container agent user MUST be at
|
|
24
29
|
# UID/GID 1000 so bind-mounted host files share an owner. Sandcastle passes the host
|
|
@@ -32,8 +37,12 @@ USER ${AGENT_UID}:${AGENT_GID}
|
|
|
32
37
|
# Claude Code CLI (the other agent runtime) — authenticated at runtime via
|
|
33
38
|
# CLAUDE_CODE_OAUTH_TOKEN. Installed per-user, so it must follow the switch.
|
|
34
39
|
# One image serves both runtimes: which CLI a burn invokes is decided by the
|
|
35
|
-
# resolved model's runtime, not by the image.
|
|
36
|
-
|
|
40
|
+
# resolved model's runtime, not by the image. Pinned to the host's
|
|
41
|
+
# `claude --version` the same way as Codex above; `install.sh` takes the version
|
|
42
|
+
# as its first argument, and none installs latest.
|
|
43
|
+
ARG CLAUDE_CODE_VERSION
|
|
44
|
+
RUN curl -fsSL https://claude.ai/install.sh | bash -s ${CLAUDE_CODE_VERSION} \
|
|
45
|
+
|| { echo "runcastle: Claude Code CLI ${CLAUDE_CODE_VERSION:-latest} install failed — is that version published?" >&2; exit 1; }
|
|
37
46
|
ENV PATH="/home/agent/.local/bin:$PATH"
|
|
38
47
|
|
|
39
48
|
# Sandcastle bind-mounts the git worktree here and runs the agent in it.
|
|
@@ -18,7 +18,12 @@ RUN curl -fsSL https://bun.sh/install | bash
|
|
|
18
18
|
# Codex CLI (the second agent runtime) — authenticated at runtime via CODEX_API_KEY.
|
|
19
19
|
# Installed as root, before the user switch, so it lands in the system-wide npm
|
|
20
20
|
# prefix: after `USER` a global install has nowhere writable to go.
|
|
21
|
-
|
|
21
|
+
# Pinned to the host's `codex --version`, passed in by Rebuild. The ARG sits
|
|
22
|
+
# directly above its RUN on purpose: a changed version busts the cache for this
|
|
23
|
+
# layer and the ones after it, and nothing before it. Empty installs latest.
|
|
24
|
+
ARG CODEX_VERSION
|
|
25
|
+
RUN npm install -g @openai/codex@${CODEX_VERSION:-latest} \
|
|
26
|
+
|| { echo "runcastle: Codex CLI ${CODEX_VERSION:-latest} install failed — is that version published?" >&2; exit 1; }
|
|
22
27
|
|
|
23
28
|
# Rootless-podman invariant (research #32): the in-container agent user MUST be at
|
|
24
29
|
# UID/GID 1000 so bind-mounted host files share an owner. Sandcastle passes the host
|
|
@@ -32,8 +37,12 @@ USER ${AGENT_UID}:${AGENT_GID}
|
|
|
32
37
|
# Claude Code CLI (the other agent runtime) — authenticated at runtime via
|
|
33
38
|
# CLAUDE_CODE_OAUTH_TOKEN. Installed per-user, so it must follow the switch.
|
|
34
39
|
# One image serves both runtimes: which CLI a burn invokes is decided by the
|
|
35
|
-
# resolved model's runtime, not by the image.
|
|
36
|
-
|
|
40
|
+
# resolved model's runtime, not by the image. Pinned to the host's
|
|
41
|
+
# `claude --version` the same way as Codex above; `install.sh` takes the version
|
|
42
|
+
# as its first argument, and none installs latest.
|
|
43
|
+
ARG CLAUDE_CODE_VERSION
|
|
44
|
+
RUN curl -fsSL https://claude.ai/install.sh | bash -s ${CLAUDE_CODE_VERSION} \
|
|
45
|
+
|| { echo "runcastle: Claude Code CLI ${CLAUDE_CODE_VERSION:-latest} install failed — is that version published?" >&2; exit 1; }
|
|
37
46
|
ENV PATH="/home/agent/.local/bin:$PATH"
|
|
38
47
|
|
|
39
48
|
# Sandcastle bind-mounts the git worktree here and runs the agent in it.
|
package/skills/packs/README.md
CHANGED
|
@@ -73,36 +73,50 @@ claude ... --plugin-dir "<abs path>/packages/skills/packs/runcastle" ...
|
|
|
73
73
|
|
|
74
74
|
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).
|
|
75
75
|
|
|
76
|
-
###
|
|
76
|
+
### Entry-skill isolation — settings deny rules, not `disable-model-invocation`
|
|
77
77
|
|
|
78
78
|
Every skill's `description` is loaded into every session in the pack, whether or
|
|
79
79
|
not that session can use it. That is cheap for a skill the model might genuinely
|
|
80
|
-
need and
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
`
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
`
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
80
|
+
need and risky for one it must not run: a session carrying the descriptions of
|
|
81
|
+
every other kind's entry skill is one description-match away from running
|
|
82
|
+
another session's procedure — `project/SKILL.md` says "**Never run an ideation
|
|
83
|
+
grilling**" while `ideate`'s description sits in the same list offering exactly
|
|
84
|
+
that.
|
|
85
|
+
|
|
86
|
+
The seven **session-entry** skills — `ideate`, `qa`, `converge`, `revisit`,
|
|
87
|
+
`waypoint`, `project`, `prepare` — used to carry `disable-model-invocation:
|
|
88
|
+
true` for that reason, on the build-era understanding that explicit invocation
|
|
89
|
+
kept working (CC-INTEGRATION-NOTES §1). It no longer does: Claude Code now
|
|
90
|
+
hard-blocks the **Skill tool** for such a skill even when the prompt names it,
|
|
91
|
+
and only a *user-typed* `/runcastle:<skill>` gets through. The launcher's
|
|
92
|
+
kickoff lines are prose ("Proceed with your task: invoke the /runcastle:project
|
|
93
|
+
skill…"), so every fresh session was directed to do the one thing its skill's
|
|
94
|
+
frontmatter forbade — and a `/clear` left the session with no path back to its
|
|
95
|
+
own entry skill at all (recorded in `docs/research/CORRECTIONS.md`).
|
|
96
|
+
|
|
97
|
+
So all skills in the pack are now `disable-model-invocation: false`, and the
|
|
98
|
+
cross-kind prohibition moved to where the other standing prohibitions already
|
|
99
|
+
live: **per-session settings**. The launcher launches each session with
|
|
100
|
+
`permissions.deny` rules — `Skill(runcastle:<x>)` plus `Skill(runcastle:<x> *)`
|
|
101
|
+
— for every entry skill that is not that kind's own (`entrySkillsFor` /
|
|
102
|
+
`entrySkillDenyRules` in `packages/server/src/launcher`), and each injected
|
|
103
|
+
prompt states the policy in one line. Same layering as the edit guard: the
|
|
104
|
+
prompt says it, the harness enforces it. `chat` keeps both of its openings
|
|
105
|
+
(`ideate` + `revisit`); `qa` — whose session kind was retired — is denied
|
|
106
|
+
everywhere; `drive-fix` has no entry skill and denies all seven.
|
|
107
|
+
|
|
108
|
+
The chained/reached skills — `spec` and `tickets` (invoked by name from inside
|
|
109
|
+
`ideate` and `converge`) and `code-review` (reached by description) — are in no
|
|
110
|
+
deny list.
|
|
111
|
+
|
|
112
|
+
The rule, for anything added later: **a session-entry skill goes into
|
|
113
|
+
`ENTRY_SKILLS` and the per-kind table in
|
|
114
|
+
`packages/server/src/launcher/runtimes/skills.ts`; a chained or genuinely
|
|
115
|
+
description-reached one does not.** The question is not "might this be useful?"
|
|
116
|
+
but "is there a session whose *whole job* is something else that could pick this
|
|
117
|
+
up by mistake?" If yes, it is an entry skill and every other kind denies it.
|
|
118
|
+
`disable-model-invocation` stays `false` across the pack — setting it `true`
|
|
119
|
+
breaks the prose kickoff.
|
|
106
120
|
|
|
107
121
|
## Layering: prompt, skill, hook — one home each
|
|
108
122
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: converge
|
|
3
3
|
description: Close a mapped feature. Read the compressed knowledge — map.md, decisions.md and the research deliverables — never the waypoint transcripts, then drive spec and tickets out of that one unbroken window. Entry skill for kind=converge sessions.
|
|
4
|
-
disable-model-invocation:
|
|
4
|
+
disable-model-invocation: false
|
|
5
5
|
---
|
|
6
6
|
<!-- Forked from Matt Pocock's grilling/to-spec/to-tickets discipline, via https://github.com/mattpocock/skills, 2026-07-15, adapted for runcastle mapped ideation -->
|
|
7
7
|
|
|
@@ -15,7 +15,7 @@ Everything happens in this one window. **Never compact. Never `/clear`. Never su
|
|
|
15
15
|
|
|
16
16
|
## 0. Read the compressed knowledge — and only that
|
|
17
17
|
|
|
18
|
-
Call `mcp__runcastle__get_feature_context`. The injected system prompt carries the slug and the paths; trust `get_feature_context` for the live state. It returns the feature, the current `phase`, the map, the tickets, and its docs in two parts — `docs[]`, inlined in full, and `moreDocs[]`, an index of everything else with a `relPath` and a byte count. Read, in this order:
|
|
18
|
+
Call `mcp__runcastle__get_feature_context`. The injected system prompt carries the slug and the paths; trust `get_feature_context` for the live state. It returns the feature, the current `phase`, the map, the tickets as summary rows (`seq`, `title`, `status`, `goal`; `mcp__runcastle__get_ticket({ seq })` has a ticket's context, acceptance criteria and digest), and its docs in two parts — `docs[]`, inlined in full, and `moreDocs[]`, an index of everything else with a `relPath` and a byte count. A canonical doc too large to inline is listed in `notInlined` instead of `docs[]`: read it with `mcp__runcastle__read_feature_doc({ relPath })` or off disk before acting. Read, in this order:
|
|
19
19
|
|
|
20
20
|
1. **`map.md`** (inlined in `docs[]`) — the destination, the notes, and what was ruled out of scope.
|
|
21
21
|
2. **`decisions.md`** (inlined in `docs[]`) — every decision the waypoint sessions locked. This is the spine of the spec.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ideate
|
|
3
3
|
description: The runcastle ideation session. Grill the human relentlessly about a feature, lock decisions incrementally into decisions.md, then drive spec and tickets out of the same unbroken context. Entry skill for kind=ideation sessions.
|
|
4
|
-
disable-model-invocation:
|
|
4
|
+
disable-model-invocation: false
|
|
5
5
|
---
|
|
6
6
|
<!-- Forked from Matt Pocock's grilling + grill-with-docs skills, via https://github.com/mattpocock/skills, 2026-07-14, adapted for runcastle -->
|
|
7
7
|
|
|
@@ -15,12 +15,12 @@ Everything happens in this one window. **Never compact. Never `/clear`. Never su
|
|
|
15
15
|
|
|
16
16
|
## 0. Orient
|
|
17
17
|
|
|
18
|
-
1. Call `mcp__runcastle__get_feature_context`. It
|
|
18
|
+
1. Call `mcp__runcastle__get_feature_context`. It opens with a header — the feature (`slug`, `title`, `oneLiner`), current `phase` and `lap`, `annotatedModels`, `burnConcurrency` — then any tickets as summary rows (`seq`, `title`, `status`, `goal`, …; a ticket's context, acceptance criteria and digest come from `mcp__runcastle__get_ticket({ seq })`), then the canonical docs inlined in `docs[]` (`brief.md`, and any `decisions.md` — **this may be a resumed session; read what is already locked and do not re-ask it**), and an index of every other doc in `moreDocs[]` (fetch one with `mcp__runcastle__read_feature_doc({ relPath })`). A canonical doc too large to inline is listed in `notInlined` with its `relPath` and `absPath` instead — **read it before acting**, with `read_feature_doc` or off disk.
|
|
19
19
|
2. Decisions live at `docs/features/<slug>/decisions.md` in this worktree. The injected system prompt carries the slug and paths; trust `get_feature_context` for the live state.
|
|
20
20
|
|
|
21
21
|
**Your tools**, and there are not many — this session's real work is the conversation:
|
|
22
22
|
|
|
23
|
-
- `mcp__runcastle__get_feature_context` / `mcp__runcastle__read_feature_doc` / `mcp__runcastle__list_tickets` — the record.
|
|
23
|
+
- `mcp__runcastle__get_feature_context` / `mcp__runcastle__read_feature_doc` / `mcp__runcastle__list_tickets` / `mcp__runcastle__get_ticket` — the record.
|
|
24
24
|
- `mcp__runcastle__create_feature({ title, oneLiner, brief, draft: true })` — park scope creep as a draft feature (§1). `draft: true` is the only door open to you; a full create belongs to the project session.
|
|
25
25
|
- `mcp__runcastle__escalate_to_map` + `mcp__runcastle__emit_waypoints` — the escalation branch (§3). This is the entire entry point into mapped ideation: no other surface charts a map from a linear feature.
|
|
26
26
|
- `mcp__runcastle__complete_phase` — the phase boundaries (§4), and the ones `/runcastle:spec` and `/runcastle:tickets` cross after you.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: prepare
|
|
3
3
|
description: Establish a project's host settings with evidence — the dev command, the drive setup/stop scripts, the dev-database reset — by running them on the developer's own machine rather than guessing them from config. Covers the drive contract, discovering a project's shape, and the closing dry-run drive. Entry skill for kind=prepare sessions.
|
|
4
|
-
disable-model-invocation:
|
|
4
|
+
disable-model-invocation: false
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Prepare — establish this project's host settings
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: project
|
|
3
3
|
description: The runcastle project session. Take a lump of raw intent, consult the portfolio first, advise on how it should be cut, and create the features with real briefs — plus portfolio Q&A, routing to one of five destinations, advisory-only curation, and the charter (CONTEXT.md), which this is the only session allowed to write. Entry skill for kind=project 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's project-level session -->
|
|
7
7
|
|
|
@@ -17,17 +17,21 @@ Everything else you do — portfolio Q&A, routing, curation, the charter — is
|
|
|
17
17
|
|
|
18
18
|
## Your tools
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
Nine, and deliberately none of the feature pipeline's. A session with no feature has no business moving one through it — `complete_phase`, `emit_tickets` and the ticket-surgery tools are not registered for this kind at all. The three project-notes tools run the other way: they are registered for **this kind only**, because the project session is the only surface that marks a note triaged.
|
|
21
21
|
|
|
22
|
-
- `mcp__runcastle__get_project_context()` — the project row, the charter (`CONTEXT.md`) in full, an **index** of every live ADR (superseded ones omitted),
|
|
22
|
+
- `mcp__runcastle__get_project_context()` — the project row, the charter (`CONTEXT.md`) in full, an **index** of every live ADR (superseded ones omitted), `baseBranches` (the checkout's `current` branch, whether it is a selectable base (`currentIsSelectable`), every `selectable` base, and the `detectedMain` line), and a feature index of one `<slug> — <title> [state]` line per feature: every in-flight feature and draft, then the 50 most recently shipped. Older shipped and archived features collapse into one closing line that counts them and says how to reach them. ADR bodies and one-liners are *not* inlined.
|
|
23
23
|
- `mcp__runcastle__read_adr({ relPath })` — one ADR in full, from the index. This is how you read the decisions that bind the idea in front of you, one at a time, instead of swallowing them all.
|
|
24
|
-
- `
|
|
24
|
+
- `mcp__runcastle__read_feature_brief({ slug })` — one feature's title, one-liner, status/phase/lap and brief: off disk for a shipped feature, from the database for an in-flight or draft one. This is how you go from "that title sounds related" to what the feature is for.
|
|
25
|
+
- `mcp__runcastle__get_work_record({ featureSlug? , seam? , seq? })` — what features actually **did**. Send exactly one of `featureSlug` or `seam`. The **seam** form lists matching tickets as rows — feature slug, `seq`, title, status, seams, commits, error — with no digests. The **featureSlug** form tells the whole story: 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; a digest too large to fit is moved out and its ticket marked `digestNotInlined: true`. **`featureSlug` + `seq`** returns that one ticket with its digest. Facts, never intent.
|
|
25
26
|
- `mcp__runcastle__create_feature({ title, oneLiner, baseBranch?, brief?, draft?, tickets? })` — the end of intake.
|
|
26
27
|
- `mcp__runcastle__record_event({ type, message })` — a note on the project timeline.
|
|
28
|
+
- `mcp__runcastle__list_project_notes()` — the project's **open notes**, oldest first: id, text, createdAt, and for a note with a screenshot its absolute host path (`Read` it) plus a ready-made attachment sentence to paste into a ticket string. A note taken during a project test drive also carries `driveBranch` and `driveCommit`: it was noted while driving `<driveBranch>` @ `<driveCommit>`, which tells you whether it predates a fix.
|
|
29
|
+
- `mcp__runcastle__triage_project_note({ noteIds, outcome, featureId? })` — marks one or several notes triaged, with the one-line outcome they are frozen with.
|
|
30
|
+
- `mcp__runcastle__update_project_note({ noteId, text })` — rewrites an open note's text.
|
|
27
31
|
|
|
28
|
-
**What the feature index makes readable.** A **
|
|
32
|
+
**What the feature index makes readable.** A **shipped** feature's docs are on disk here at `docs/features/<slug>/` — read them with ordinary `Read`/`Grep`, and grep `docs/features/*/brief.md` for the older ones the index collapsed into its closing line. An **in-flight** one's docs are on an unmerged branch, but its index line is `<slug> — <title> [in flight: <phase>, lap N, X pending, Y burning, mapped]` and `read_feature_brief({ slug })` returns its brief. **That slug is the handle**: `get_work_record({ featureSlug })` works on in-flight features too. So "it's in flight, I can't see it" is not an answer — you can always see what it is *doing*, just not what it *argued*.
|
|
29
33
|
|
|
30
|
-
**
|
|
34
|
+
**Three procedures load on demand**, beside this file — read one only when that job actually arrives: `./references/charter.md` (writing or amending `CONTEXT.md` and project ADRs — §5), `./references/health-sweeps.md` (running a sweep — §6) and `./references/triage.md` (triaging the project's jotted notes — **Notes triage**).
|
|
31
35
|
|
|
32
36
|
## 0. Open by asking
|
|
33
37
|
|
|
@@ -35,6 +39,11 @@ Your first visible move is a **question**, not a lookup. Greet them and put it:
|
|
|
35
39
|
|
|
36
40
|
> What are we cutting into features today?
|
|
37
41
|
|
|
42
|
+
**Two things displace that question**, and both come from what you were launched with:
|
|
43
|
+
|
|
44
|
+
- **A briefing that says to triage the open project notes.** Then triage is why this terminal exists: read `./references/triage.md` and start its first movement instead of asking the opening question.
|
|
45
|
+
- **A prompt line saying "This project has N open notes."** The count was taken at launch and the notes are still waiting. Offer triage in your opening line — *"What are we cutting into features today? (There are 7 open notes waiting to be triaged, if you'd rather start there.)"* — and if they take it, read `./references/triage.md` then.
|
|
46
|
+
|
|
38
47
|
Then orient **lazily**: reach for context when intake, routing, or a portfolio question actually needs it, never as an opening ritual. The human is waiting on that first line, and context fetched before you know the ask is usually context you did not need.
|
|
39
48
|
|
|
40
49
|
Lazy is about *timing*, not about skipping — the moment a feature idea arrives, intake genuinely needs the portfolio, and §1a says so as a requirement. Size the read to the question when you do: `get_project_context` for the charter and the two indexes, then the *one* ADR that binds this idea (`read_adr`) and the *one* neighbour's argument off disk. Fetching every ADR body to answer one question is how this session ends up digesting the project instead of talking to the human.
|
|
@@ -49,10 +58,10 @@ A feature idea has landed. The order is fixed, and it is the whole point of this
|
|
|
49
58
|
|
|
50
59
|
Your first move on a feature idea is a **lookup, not a question**. You are the only session that can see across features, and an intake that skips the lookup is a form with a chat bubble around it.
|
|
51
60
|
|
|
52
|
-
- `get_project_context` — the feature index (what exists at all, shipped and in flight, each in-flight one with its slug, phase, lap and ticket counts) and the index of live ADRs.
|
|
61
|
+
- `get_project_context` — the feature index (what exists at all, shipped and in flight, each in-flight one with its slug, phase, lap and ticket counts) and the index of live ADRs. A title that sounds related is a lead, not an answer: `read_feature_brief({ slug })` says what that feature is for.
|
|
53
62
|
- `read_adr({ relPath })` — the one or two ADRs the index says already bind this area. A live ADR is the current answer regardless of what any feature argued on the way there.
|
|
54
63
|
- `get_work_record({ featureSlug })` — what a neighbouring feature actually **did**: its status, its ship date, its run summaries, and its tickets each carrying the burner's own digest of what it built, what surprised it, and what it left undone. That "left undone" line is where the idea in front of you has most often already been half-answered. **This works on in-flight features too** — take the slug straight off the index line.
|
|
55
|
-
- `get_work_record({ seam })` — the same, asked sideways: *who has touched this area before, and what happened to them?* Use it whenever the idea names a surface rather than a feature.
|
|
64
|
+
- `get_work_record({ seam })` — the same, asked sideways: *who has touched this area before, and what happened to them?* Use it whenever the idea names a surface rather than a feature. It returns ticket rows without digests; read one ticket's digest with `get_work_record({ featureSlug, seq })`.
|
|
56
65
|
- `docs/features/<slug>/` on disk — a **merged** feature's unabridged argument: its `decisions.md` says what was settled and why.
|
|
57
66
|
|
|
58
67
|
Size the read to the idea — but **do not skip it**. "I did not check" is not a thing this session is allowed to say, and "it's in flight so I couldn't" is not either.
|
|
@@ -94,7 +103,7 @@ Each one carries a real `brief` — the reasoning you just worked out, in prose:
|
|
|
94
103
|
Anything that arrives (from the human, or from a sweep in §6) goes to exactly one of:
|
|
95
104
|
|
|
96
105
|
1. **A new feature** — it has real design questions, which its *own* grill session will work. `create_feature` with a brief.
|
|
97
|
-
2. **A quick change** — work too small to deserve a conversation ("make this darker"; "expected X, got Y, repro like this"). `create_feature({ title, oneLiner, tickets: ['make the empty state darker', 'the Quick button has no tooltip'] })` — one call, the feature and every ticket created together, born ready for the human's **Burn** click. **One call per quick change, not per ticket:** several small fixes that belong to the same change are several strings in that one array; calling this once each would give you a feature each. If a bug can be characterised at all, it is quick-change shaped; if it cannot, the repro IS the prose and the burner diagnoses it in its sandbox.
|
|
106
|
+
2. **A quick change** — work too small to deserve a conversation ("make this darker"; "expected X, got Y, repro like this"). `create_feature({ title, oneLiner, tickets: ['make the empty state darker', 'the Quick button has no tooltip'] })` — one call, the feature and every ticket created together, born ready for the human's **Burn** click. **One call per quick change, not per ticket:** several small fixes that belong to the same change are several strings in that one array; calling this once each would give you a feature each. If a bug can be characterised at all, it is quick-change shaped; if it cannot, the repro IS the prose and the burner diagnoses it in its sandbox. **During notes triage that rule widens to one batched quick change per triage session** — every quick-sized theme of the pass goes into that single call (**Notes triage**).
|
|
98
107
|
3. **An existing feature's revisit** — it belongs to a feature already in flight. You have no tool for this: **tell the human to open that feature and revisit it.**
|
|
99
108
|
4. **Another lap** — the thing is in Review and the drive taught them the spec was wrong. Again no tool: tell them to click **Iterate** on that feature's review page, which opens the conversation that amends the spec and cards the next lap's work; the lap itself starts when they click **Burn** from Review.
|
|
100
109
|
5. **Nothing** — it is already decided, already built, or not worth doing. Say so plainly, with the ADR or the shipped feature that settles it.
|
|
@@ -136,6 +145,12 @@ When the human asks for a sweep ("what needs doing?", "what's rotting?"), do the
|
|
|
136
145
|
|
|
137
146
|
**Read `./references/health-sweeps.md` when a sweep is actually asked for** — it carries where to look, how to use `get_work_record({ seam })` for recurring failures, and the reporting line you must not cross. Do not run a sweep unprompted.
|
|
138
147
|
|
|
148
|
+
## Notes triage — the notes the human jotted
|
|
149
|
+
|
|
150
|
+
The human jots one-line notes (often with a screenshot) from any screen in the project, and they pile up in the project's inbox. Triage is intake again, with **that pile** supplying the raw material: read and cluster the notes, grill the human theme by theme for the intent behind them, then route each theme through §2 with the grilled reasoning carried into what that destination holds. A ten-second note records a symptom, so routing one cold is not triage.
|
|
151
|
+
|
|
152
|
+
**Read `./references/triage.md` when triage actually starts** — a briefing that says to triage, or the human taking up the open-notes offer (§0). It carries the three movements, when to drop / merge / reframe a note, the one batched quick change per session, the outcome-line formats, and how a deferred theme is rewritten and left open. Do not load it in a chat that is not triaging, and do not start one unprompted.
|
|
153
|
+
|
|
139
154
|
## 7. Closing move — land what you wrote, leave the tree clean
|
|
140
155
|
|
|
141
156
|
You work in a **runcastle-owned worktree on a runcastle-owned branch**, never the human's checkout. Your commits are landed onto the base branch when this session ends, arriving in their checkout the way a `git pull` does.
|
|
@@ -17,9 +17,12 @@ short list of findings the human can route.
|
|
|
17
17
|
- Docs that drifted from the code — a `spec.md` the code has moved past is the
|
|
18
18
|
common one.
|
|
19
19
|
- `get_work_record({ seam })` for recurring burner failures: the same area
|
|
20
|
-
erroring across features is a design problem, not bad luck.
|
|
20
|
+
erroring across features is a design problem, not bad luck. Its rows carry
|
|
21
|
+
each ticket's error but no digest.
|
|
21
22
|
- `get_work_record({ featureSlug })` digests, read for their "left undone" line —
|
|
22
|
-
the cheapest source of real, already-scoped work in the whole project.
|
|
23
|
+
the cheapest source of real, already-scoped work in the whole project. A
|
|
24
|
+
ticket marked `digestNotInlined: true` had its digest moved out for size;
|
|
25
|
+
read it with `get_work_record({ featureSlug, seq })`.
|
|
23
26
|
- Features in flight whose `phase`/`lap`/pending counts in the index say they
|
|
24
27
|
have stalled.
|
|
25
28
|
|
|
@@ -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.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: revisit
|
|
3
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:
|
|
4
|
+
disable-model-invocation: false
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Revisit
|
|
@@ -13,7 +13,7 @@ That is the ordinary revisit, and it is what the moves below describe. The one e
|
|
|
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.
|
|
@@ -29,9 +29,9 @@ That is the ordinary revisit, and it is what the moves below describe. The one e
|
|
|
29
29
|
|
|
30
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. **Context.** `mcp__runcastle__get_feature_context` — the feature (including its `lap`), the docs, `reviewEvidence`, and the full ticket history across every lap; each ticket's own `lap` is what separates them. Skim what the last lap actually landed and what failed before you interview.
|
|
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
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
|
|
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
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
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.
|
|
37
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.
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -87,7 +87,7 @@ Each ticket:
|
|
|
87
87
|
|
|
88
88
|
### Assigning a model (only from the annotated roster)
|
|
89
89
|
|
|
90
|
-
`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`.
|
|
91
91
|
|
|
92
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.
|
|
93
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.
|