runcastle 1.2.7 → 1.2.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 +6098 -4370
- package/drizzle/0027_dashing_texas_twister.sql +1 -0
- package/drizzle/0028_tired_deadpool.sql +2 -0
- package/drizzle/0029_damp_stellaris.sql +1 -0
- package/drizzle/0030_productive_marvex.sql +1 -0
- package/drizzle/0031_backfill_feature_base_branch.sql +10 -0
- package/drizzle/0032_gigantic_tempest.sql +1 -0
- package/drizzle/meta/0027_snapshot.json +928 -0
- package/drizzle/meta/0028_snapshot.json +942 -0
- package/drizzle/meta/0029_snapshot.json +949 -0
- package/drizzle/meta/0030_snapshot.json +956 -0
- package/drizzle/meta/0031_snapshot.json +956 -0
- package/drizzle/meta/0032_snapshot.json +949 -0
- package/drizzle/meta/_journal.json +42 -0
- package/index.js +6075 -4358
- package/package.json +1 -1
- package/sandcastle-template/Containerfile +9 -1
- package/sandcastle-template/Dockerfile +9 -1
- package/skills/burner/implement-ticket.md +32 -47
- package/skills/burner/resolve-conflict.md +3 -3
- package/skills/burner/review-ticket.md +24 -25
- package/skills/packs/README.md +95 -5
- package/skills/packs/runcastle/.claude-plugin/plugin.json +1 -1
- package/skills/packs/runcastle/skills/converge/SKILL.md +26 -21
- package/skills/packs/runcastle/skills/ideate/SKILL.md +14 -8
- package/skills/packs/runcastle/skills/prepare/SKILL.md +96 -0
- package/skills/packs/runcastle/skills/prepare/references/recipes.md +72 -0
- package/skills/packs/runcastle/skills/project/SKILL.md +32 -48
- package/skills/packs/runcastle/skills/project/references/charter.md +79 -0
- package/skills/packs/runcastle/skills/project/references/health-sweeps.md +44 -0
- package/skills/packs/runcastle/skills/qa/SKILL.md +29 -13
- package/skills/packs/runcastle/skills/revisit/SKILL.md +8 -8
- package/skills/packs/runcastle/skills/spec/SKILL.md +3 -3
- package/skills/packs/runcastle/skills/tickets/SKILL.md +13 -2
- package/skills/packs/runcastle/skills/waypoint/SKILL.md +14 -9
- package/web/assets/index-BCAA3_oq.css +1 -0
- package/web/assets/index-BlVLhZP1.js +142 -0
- package/web/index.html +2 -2
- package/web/assets/index-B3xwZ1FR.css +0 -1
- package/web/assets/index-C0S4i8wY.js +0 -142
|
@@ -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: true
|
|
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,9 +15,17 @@ 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 returns the feature (`slug`, `title`, `oneLiner`), current `phase`, the docs
|
|
18
|
+
1. Call `mcp__runcastle__get_feature_context`. It returns the feature (`slug`, `title`, `oneLiner`), current `phase` and `lap`, 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**), an index of every other doc in `moreDocs[]` (fetch one with `mcp__runcastle__read_feature_doc({ relPath })`), and any tickets.
|
|
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
|
+
**Your tools**, and there are not many — this session's real work is the conversation:
|
|
22
|
+
|
|
23
|
+
- `mcp__runcastle__get_feature_context` / `mcp__runcastle__read_feature_doc` / `mcp__runcastle__list_tickets` — the record.
|
|
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
|
+
- `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
|
+
- `mcp__runcastle__complete_phase` — the phase boundaries (§4), and the ones `/runcastle:spec` and `/runcastle:tickets` cross after you.
|
|
27
|
+
- `mcp__runcastle__emit_tickets` — used by `/runcastle:tickets`, not by you directly.
|
|
28
|
+
- `mcp__runcastle__record_event` — the feature timeline. Most state changes already emit their own event; only reach for this when the timeline would otherwise have no record at all.
|
|
21
29
|
|
|
22
30
|
## 1. Grill relentlessly
|
|
23
31
|
|
|
@@ -44,9 +52,7 @@ The moment a decision locks, append it to `docs/features/<slug>/decisions.md`
|
|
|
44
52
|
**Why:** <the reason / the trade-off chosen over the alternatives>
|
|
45
53
|
```
|
|
46
54
|
|
|
47
|
-
Keep `decisions.md` free of implementation minutiae — it is the record of *what was decided and why*, the raw material spec and tickets are built from. `complete_phase` checkpoints the file into git for you; you just write it.
|
|
48
|
-
|
|
49
|
-
Every few locked decisions, `mcp__runcastle__record_event({ type: "decision.locked", message: "<n> decisions locked" })`.
|
|
55
|
+
Keep `decisions.md` free of implementation minutiae — it is the record of *what was decided and why*, the raw material spec and tickets are built from. `complete_phase` checkpoints the file into git for you; you just write it. The file is the record — do not narrate it onto the timeline as you go.
|
|
50
56
|
|
|
51
57
|
## 3. Escalation branch — when the feature outgrows this window
|
|
52
58
|
|
|
@@ -67,7 +73,7 @@ Some features are too big for one unbroken context. The tells: you are rabbit-ho
|
|
|
67
73
|
|
|
68
74
|
When the human confirms shared understanding and the open questions are answered or explicitly deferred:
|
|
69
75
|
|
|
70
|
-
`mcp__runcastle__complete_phase({ phase: "ideation" })` → returns `{ ok, nextPhase }`. If `ok: false`, it names what the gate wants (e.g. `decisions.md` missing) — fix it and retry.
|
|
76
|
+
`mcp__runcastle__complete_phase({ phase: "ideation" })` → returns `{ ok, nextPhase }`. If `ok: false`, it names what the gate wants (e.g. `decisions.md` missing) — fix it and retry. It logs its own timeline event; do not add one.
|
|
71
77
|
|
|
72
78
|
## 5. Spec → tickets — stay in this window
|
|
73
79
|
|
|
@@ -81,4 +87,4 @@ Do not open a new session for these. They run here, on top of everything you jus
|
|
|
81
87
|
|
|
82
88
|
> Tickets are in the runcastle UI. Review the ticket cards and click **Burn** to start the AFK agents. That is the next step — I will stop here.
|
|
83
89
|
|
|
84
|
-
Do **not**
|
|
90
|
+
Do **not** start burning. The two human clicks (Burn, then Merge after test-drive) are the only gates left, and they belong to the human. (You do not write code here either — that rule is in your injected prompt and enforced by the edit guard, not restated as advice.)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: prepare
|
|
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: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Prepare — establish this project's host settings
|
|
8
|
+
|
|
9
|
+
A preparation session runs in the developer's **real checkout on their own machine**, not in a sandbox. That is the whole capability it exists to use: every setting here can be *run* instead of guessed at, and a value with an account of itself is worth more than a value that merely looks plausible.
|
|
10
|
+
|
|
11
|
+
Your system prompt carries the per-session facts — the repo path, which keys are still open, what has already been established and how stale it is. This skill is the method.
|
|
12
|
+
|
|
13
|
+
## The order of the conversation
|
|
14
|
+
|
|
15
|
+
1. Open by naming what is still open and what you need from the human for each. Do not survey the repo first; the prompt already told you the gap.
|
|
16
|
+
2. Work the keys **one at a time**: propose, ask, run it if they agree, record it.
|
|
17
|
+
3. If the open keys touch the drive loop, author the machinery (below) before you record the two hook commands — the settings are invocation lines for scripts that must exist.
|
|
18
|
+
4. Close by proposing a dry-run drive (below). Values that have never been driven look exactly as trustworthy as values that run perfectly.
|
|
19
|
+
|
|
20
|
+
**Ask before you act.** Anything that starts or stops a service, creates or migrates a database, installs software, or writes outside the repo needs the human to agree first: say what you are about to run and why, then wait. Their own stack is running next to yours.
|
|
21
|
+
|
|
22
|
+
## The seven keys
|
|
23
|
+
|
|
24
|
+
Four describe the drive loop and can only be settled here:
|
|
25
|
+
|
|
26
|
+
- **`devCommand`** — spawned in a drive-owned terminal pane for the length of a test drive. The first localhost URL it prints becomes the "Open app" link.
|
|
27
|
+
- **`driveSetupCommand`** / **`driveStopCommand`** — run on the host, in the project repo, before the dev pane starts and after the drive stops. They are the **invocation lines** of scripts you write and commit, not the machinery itself.
|
|
28
|
+
- **`dbResetCommand`** — **not** part of the drive loop. Its only consumer is the migration-drift banner after a drive stops: when the drive branch and the branch you returned to disagree about migration files, this is offered as the one-click dev-database rebuild.
|
|
29
|
+
|
|
30
|
+
Three describe the **sandbox** a burn agent works in, and no amount of running things on this host settles them — they are judgement calls about the repo, so treat them as human-supplied unless the human asks you to work one out with them:
|
|
31
|
+
|
|
32
|
+
- **`setupCommand`** — what a fresh sandbox runs before the agent starts (install, plus any codegen or build step every ticket needs). Setting it *replaces* install detection, so the install command must be included explicitly.
|
|
33
|
+
- **`verifyCommands`** — the exact typecheck/test/lint lines a burn agent should use, one per line. Unset, agents guess workspace filter names and burn whole suite runs discovering the right one.
|
|
34
|
+
- **`knownFailures`** — what already fails on the main branch, so an agent can tell its own breakage from the repo's. Free text; a count plus the suite names is enough.
|
|
35
|
+
|
|
36
|
+
## The drive contract
|
|
37
|
+
|
|
38
|
+
A drive is a script **you** write, committed to the project, plus the two things only the server can do. Everything else — ports, database names, redis indexes, compose project names, URLs — the script computes for itself. Runcastle mandates these seven points and nothing else:
|
|
39
|
+
|
|
40
|
+
1. **The machinery lives in `.runcastle/`, committed to the repo.** Write the steps as real scripts and commit them. Versioning them with the code they prepare is the load-bearing part: a branch that adds a package, a service or a migration amends its own script, and the drive on that branch runs the amended version. Nothing inspects a diff, ever.
|
|
41
|
+
2. **`driveSetupCommand` / `driveStopCommand` are one line each** — how to invoke those scripts. Logic in the setting instead of the script is logic no branch can amend.
|
|
42
|
+
3. **Identity comes in as `RUNCASTLE_*`.** Every drive hook and the dev pane are handed `RUNCASTLE_SLUG` (the feature slug), `RUNCASTLE_BRANCH` (the branch under the wheel) and `RUNCASTLE_ID` — that slug made identifier-safe (lowercase `[a-z0-9_]`, never leading with a digit, length-capped), so it is legal as a database, schema or container name. Derive every per-drive name from `RUNCASTLE_ID`. Never derive one from `git rev-parse`: the dry run drives under a synthetic identity on whatever branch is checked out.
|
|
43
|
+
4. **Computed values go back out through `.runcastle/drive.env`.** Setup appends plain `KEY=VALUE` lines there; when it exits, the server parses that file and overlays it verbatim onto the dev pane and the stop hook, and shows the variable NAMES on the timeline. It is the only way a value your script computed reaches the dev server — a variable exported inside the script dies with the script. Truncate the file at the top of setup so a rerun does not accumulate stale lines.
|
|
44
|
+
5. **`.runcastle/drive.env` MUST be gitignored.** Add the entry yourself. The server deletes the file when a drive ends, but a scratch file holding a connection string must never be one `git add -A` away from a commit.
|
|
45
|
+
6. **Every step is unconditionally idempotent.** Install, migrate, seed, compose up — run them every time, never behind a "has anything changed?" check. A no-op on a clean tree is cheap; a skipped install on a branch that added a package is a dead drive. This is how the loop absorbs whatever a feature branch changed with no delta detection anywhere.
|
|
46
|
+
7. **Exit 0 means the services are actually up.** The waits belong INSIDE the script — `docker compose up --wait`, a `pg_isready` loop, curl-until-healthy — because the dev pane starts the instant setup returns. The server waits for the app itself; it will not wait for your database.
|
|
47
|
+
|
|
48
|
+
Stop undoes what setup made, **for this identity only**: drop the database it created, take its compose project down with its volumes, free its ports. The human's own stack is never yours.
|
|
49
|
+
|
|
50
|
+
Writing those files is the one exception to a preparation session not editing the repo: you may write `.runcastle/` and `.gitignore` in this checkout and nothing else — a hook enforces it. Show the human the script before you commit it; it is their repo and their PR.
|
|
51
|
+
|
|
52
|
+
## Discover the shape before you author anything
|
|
53
|
+
|
|
54
|
+
Projects differ in every dimension this touches, and a script fitted to a project you imagined is worse than no script at all. Your system prompt reports what the server could probe for itself — the platform, whether there is a compose file, whether `.runcastle/` already exists, which lockfile is present. Take those as read and find out the rest, by reading the repo and running things here:
|
|
55
|
+
|
|
56
|
+
- **Workspace layout** — one package or a monorepo with workspaces, and which package the app actually is. Install, migrate and seed each run from somewhere specific.
|
|
57
|
+
- **The services the app needs to boot** — database, redis, queues, object storage, a second process. Read the config and the env example, then confirm with the human.
|
|
58
|
+
- **Whether the human actually uses docker for this project.** A compose file in the repo is not consent to run it.
|
|
59
|
+
- **Hosted or local data stores** — a local postgres you may freely `createdb` on is a very different recipe from a hosted one where you may only branch or add a schema.
|
|
60
|
+
- **How the app loads its environment** — see the audit below.
|
|
61
|
+
|
|
62
|
+
Then write the smallest script that brings THAT shape up. For worked shapes to adapt — postgres-per-drive, docker compose, redis, hosted databases, deterministic ports — read `references/recipes.md`. They are illustrations, not a library: take the idea and fit it to what you found, in the language this host actually runs.
|
|
63
|
+
|
|
64
|
+
## Audit how the app loads its environment
|
|
65
|
+
|
|
66
|
+
The overlay is process environment, which beats a `.env` file in dotenv, Prisma and Next by default. One pattern defeats it: a loader told to clobber what is already exported — `dotenv.config({ override: true })` and its equivalents — ignores everything the script computed and quietly keeps the app on the shared database. That is a drive that looks perfect while testing the wrong data. No machinery can detect it; you are the detector.
|
|
67
|
+
|
|
68
|
+
Grep the app's entry points and config for env loading and decide, for each, which side wins. Where the process environment loses: get it fixed — you may not edit app code from this session, so propose the change (usually dropping `override`) and let the human make it — or, if it must stay, record the finding with `record_event` naming the file and what it breaks, so the first confusing drive is not a mystery.
|
|
69
|
+
|
|
70
|
+
## Recording what you establish
|
|
71
|
+
|
|
72
|
+
One `record_finding` call per key, and `evidence` is not optional in spirit: record what you ran and what it printed, or what the human told you. A value with no account of itself is a guess with a source field.
|
|
73
|
+
|
|
74
|
+
`userSupplied` is the one flag worth being careful with. **True** means the human GAVE you this value or confirmed it verbatim — that marks it as theirs and permanently stops automatic runs from overwriting it. Leave it **false** for anything you worked out yourself, even with them watching; that stays improvable by a later run. Getting this backwards silently retires a field from preparation forever, and the only way back is the human clearing it.
|
|
75
|
+
|
|
76
|
+
Secrets: this is a development environment and the human has agreed to supply real connection strings and credentials here. Store them as given. Do not paste a secret into a timeline note or a commit message — `record_finding` is the only place a value belongs.
|
|
77
|
+
|
|
78
|
+
## Closing move: propose a dry-run drive
|
|
79
|
+
|
|
80
|
+
Once the open drive keys are recorded, end the session by proposing one. Recorded values that have never been driven look exactly as trustworthy as values that run perfectly, and the first person to find out otherwise is someone mid-feature with a broken environment.
|
|
81
|
+
|
|
82
|
+
**Ask first.** This starts services and creates a database on their machine. Name what will run — the setup command, the dev command, the stop command — then wait. If they decline, end the session normally: the keys simply stay unverified, and the drive UI will keep saying so.
|
|
83
|
+
|
|
84
|
+
On a yes, `dry_run_drive({ action })` runs it in two halves and you inspect between them:
|
|
85
|
+
|
|
86
|
+
1. **`start`** — the server runs `driveSetupCommand` with the identity variables, reads back the `.runcastle/drive.env` it wrote (the reply lists the variable NAMES it parsed) and spawns `devCommand` in a real drive pane under that overlay. Identity is the reserved slug `prep-dry-run`, so `RUNCASTLE_ID` is `prep_dry_run` and a script deriving from it makes e.g. `myapp_prep_dry_run`, on the current branch. Nothing is checked out.
|
|
87
|
+
2. **While it is up**, check what the server cannot: the variable names came back as you meant them, the temp database exists and is FRESH, the migrations applied, and the app actually RESPONDS at the sniffed URL — the server waits for it to answer before "Open app" goes live, but only you can say the page is the right one. `status` gives you the pane and the URL while you work.
|
|
88
|
+
3. **`stop`** — the server runs `driveStopCommand` under the same overlay. Then check the cleanup: temp database gone, no orphaned process, container or volume left behind.
|
|
89
|
+
|
|
90
|
+
Anything off at any step, fix it — amend the script, or `record_finding` for a key — and run the WHOLE thing again until a pass is clean. Watch especially for a `myapp_prep_dry_run` left standing after the stop: an idempotent setup will happily reuse it on the next start, so a teardown that never worked reads exactly like a drive that did.
|
|
91
|
+
|
|
92
|
+
The verified stamps are computed server-side from what the machinery observed, and only on a clean full pass. You cannot mark your own homework: your deeper checks decide whether to retry, never what gets stamped.
|
|
93
|
+
|
|
94
|
+
## When nothing is open
|
|
95
|
+
|
|
96
|
+
If your system prompt says every key is already set, this session is a **confirmation**, not a preparation. Say which values are recorded and how stale they are, ask the human whether they still hold, and stop. Do not re-derive a settled value, and do not propose a dry run unasked — replacing a measured value with a fresh guess makes preparation worse, not better.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Drive recipes — shapes to adapt, never to copy
|
|
2
|
+
|
|
3
|
+
Each of these is one shape that has worked. Take the idea and fit it to the project you actually found.
|
|
4
|
+
|
|
5
|
+
## Read this first: the snippets are sketches, not scripts
|
|
6
|
+
|
|
7
|
+
Every fragment below is written in POSIX shell because it is the most compact way to show the *idea*. **They are pseudocode.** They have not been run, they omit error handling, and two of them (`pick_port`, `port_in_use`) call helpers that do not exist anywhere — you have to write them.
|
|
8
|
+
|
|
9
|
+
Write the real thing in **whatever language this host actually runs**, which the system prompt reports as `process.platform`:
|
|
10
|
+
|
|
11
|
+
- **`win32`** — PowerShell (`.ps1`), or a cross-platform `node`/`bun` script. Do not hand a Windows developer a bash script and a `cksum` pipeline; `.runcastle/` in runcastle's own repo is TypeScript for exactly this reason.
|
|
12
|
+
- **`darwin` / `linux`** — bash is fine, but a `node`/`bun` script is still the better default in a JS project: the runtime is already installed and it is the only version of the script that survives the project gaining a Windows contributor.
|
|
13
|
+
|
|
14
|
+
The one thing that must NOT change when you translate: the contract (identity in via `RUNCASTLE_*`, computed values out via `.runcastle/drive.env`, unconditional idempotence, exit 0 means up).
|
|
15
|
+
|
|
16
|
+
## Postgres, one database per drive
|
|
17
|
+
|
|
18
|
+
Name the database from `RUNCASTLE_ID`, create it if it is not there, migrate, and hand the URL back:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
DB="myapp_$RUNCASTLE_ID"
|
|
22
|
+
createdb "$DB" 2>/dev/null || true # idempotent: already-there is success
|
|
23
|
+
DATABASE_URL="postgres://localhost/$DB" npm run migrate
|
|
24
|
+
echo "DATABASE_URL=postgres://localhost/$DB" >> .runcastle/drive.env
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Stop drops exactly that database, and nothing else: `dropdb --if-exists "myapp_$RUNCASTLE_ID"`.
|
|
28
|
+
|
|
29
|
+
## docker compose
|
|
30
|
+
|
|
31
|
+
Isolate the whole stack per drive with a project name derived from the identity, map host ports from variables the script chose, and let compose do the waiting:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
export COMPOSE_PROJECT_NAME="myapp_$RUNCASTLE_ID"
|
|
35
|
+
export PG_PORT=$(pick_port "$RUNCASTLE_SLUG-pg") # you write pick_port — see below
|
|
36
|
+
docker compose up --wait -d
|
|
37
|
+
echo "COMPOSE_PROJECT_NAME=$COMPOSE_PROJECT_NAME" >> .runcastle/drive.env
|
|
38
|
+
echo "DATABASE_URL=postgres://localhost:$PG_PORT/app" >> .runcastle/drive.env
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The compose file maps `"${PG_PORT}:5432"`. Stop is `docker compose down -v`, which needs the same `COMPOSE_PROJECT_NAME` — which is why it goes into `drive.env` too.
|
|
42
|
+
|
|
43
|
+
## Redis
|
|
44
|
+
|
|
45
|
+
Do not run a second server. Take a logical database index or a key prefix derived in-script from `RUNCASTLE_ID` — a small hash into 1..15 for the index — and leave db 0 to the human, whose own work is already in it. Write `REDIS_URL=redis://localhost:6379/7` or `REDIS_PREFIX=$RUNCASTLE_ID:` out, and have stop flush only that index or prefix.
|
|
46
|
+
|
|
47
|
+
## Hosted databases
|
|
48
|
+
|
|
49
|
+
Where the vendor has branches (Neon and its kin), setup creates one named for `RUNCASTLE_ID` with the vendor CLI and writes the connection string it prints to `drive.env`; stop deletes that branch.
|
|
50
|
+
|
|
51
|
+
Where the role has no CREATEDB grant, take a schema per drive instead — `CREATE SCHEMA IF NOT EXISTS "$RUNCASTLE_ID"` plus a URL with the search path pinned to it, and `DROP SCHEMA ... CASCADE` at stop.
|
|
52
|
+
|
|
53
|
+
Ask the human which they have: it is their bill and their production neighbour.
|
|
54
|
+
|
|
55
|
+
## Deterministic ports
|
|
56
|
+
|
|
57
|
+
Every lap of a feature should keep the same URL, and no drive should collide with the human's own running stack. Hash the SLUG — not the branch, so laps agree — into a high range, then probe upward for a free one.
|
|
58
|
+
|
|
59
|
+
The two helpers here are the part you must actually write:
|
|
60
|
+
|
|
61
|
+
- **`pick_port(seed)`** — hash `seed` into a high range and return the first free port at or above it.
|
|
62
|
+
- **`port_in_use(port)`** — true when something is already bound. There is no portable one-liner: bind a socket and catch the error (`net.createServer().listen()` in node), or shell out to `ss`/`lsof` on linux/macOS and `Get-NetTCPConnection` on Windows.
|
|
63
|
+
|
|
64
|
+
The sketch, with `cksum` standing in for "any stable hash you have to hand" (it is not on a stock Windows box):
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
base=$(( 20000 + $(printf %s "$RUNCASTLE_SLUG" | cksum | cut -d" " -f1) % 10000 ))
|
|
68
|
+
port=$base; while port_in_use "$port"; do port=$((port + 1)); done
|
|
69
|
+
echo "PORT=$port" >> .runcastle/drive.env
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The dev pane inherits `PORT` from the overlay, so the URL it prints — the "Open app" link — is the port the script picked.
|
|
@@ -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: true
|
|
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,14 +17,17 @@ Everything else you do — portfolio Q&A, routing, curation, the charter — is
|
|
|
17
17
|
|
|
18
18
|
## Your tools
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
Five, and deliberately none of the feature pipeline's. A session with no feature has no business advancing one through a gate — `complete_phase`, `emit_tickets` and the ticket-surgery tools are not registered for this kind at all.
|
|
21
21
|
|
|
22
|
-
- `mcp__runcastle__get_project_context()` — the project, the charter in full, every live ADR
|
|
23
|
-
- `
|
|
22
|
+
- `mcp__runcastle__get_project_context()` — the project row, the charter (`CONTEXT.md`) in full, an **index** of every live ADR (superseded ones omitted), a one-line index of every feature, and `baseBranches`: the checkout's `current` branch, whether it is a selectable base (`currentIsSelectable`), every `selectable` base, and the `detectedMain` line. ADR bodies are *not* inlined.
|
|
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
|
+
- `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. Send exactly one of the two arguments.
|
|
24
25
|
- `mcp__runcastle__create_feature({ title, oneLiner, baseBranch?, brief?, draft?, tickets? })` — the end of intake.
|
|
25
26
|
- `mcp__runcastle__record_event({ type, message })` — a note on the project timeline.
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
**What the feature index makes readable.** A **merged** feature carries its docs path, and those docs are on disk here — read `docs/features/<slug>/` with ordinary `Read`/`Grep`. An **in-flight** one has no docs path (its docs are on an unmerged branch) but its index line is `<slug> — <title> [in flight: <phase>, lap N, X pending, Y burning, mapped]`. **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
|
+
|
|
30
|
+
**Two 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) and `./references/health-sweeps.md` (running a sweep — §6).
|
|
28
31
|
|
|
29
32
|
## 0. Open by asking
|
|
30
33
|
|
|
@@ -34,9 +37,7 @@ Your first visible move is a **question**, not a lookup. Greet them and put it:
|
|
|
34
37
|
|
|
35
38
|
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.
|
|
36
39
|
|
|
37
|
-
Lazy is about *timing*, not about skipping
|
|
38
|
-
|
|
39
|
-
When you do need it, size the read to the question. `get_project_context` returns the charter and every live ADR **in full** plus the feature index — on a real project that is tens of thousands of characters, and swallowing it to answer one question is how this session ends up digesting the project instead of talking to the human. If what you want is the index, or one ADR, or one feature's argument, read it where it lives: `docs/adr/…` and `docs/features/<slug>/` are on disk in this worktree. Call `get_project_context` when you genuinely want the whole picture.
|
|
40
|
+
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.
|
|
40
41
|
|
|
41
42
|
If it turns out there is **no charter**, note it and read on — see §5. Do not scaffold one.
|
|
42
43
|
|
|
@@ -48,12 +49,13 @@ A feature idea has landed. The order is fixed, and it is the whole point of this
|
|
|
48
49
|
|
|
49
50
|
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.
|
|
50
51
|
|
|
51
|
-
- `get_project_context` — the feature index (what exists at all, shipped and in flight
|
|
52
|
-
- `
|
|
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.
|
|
53
|
+
- `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
|
+
- `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.
|
|
53
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.
|
|
54
|
-
- `docs/features/<slug>/` on disk —
|
|
56
|
+
- `docs/features/<slug>/` on disk — a **merged** feature's unabridged argument: its `decisions.md` says what was settled and why.
|
|
55
57
|
|
|
56
|
-
Size the read to the idea —
|
|
58
|
+
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.
|
|
57
59
|
|
|
58
60
|
### 1b. Advise — recommend, ask, propose the split
|
|
59
61
|
|
|
@@ -73,6 +75,16 @@ When they have confirmed the cut: **one `create_feature` call per feature.** Not
|
|
|
73
75
|
|
|
74
76
|
**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.
|
|
75
77
|
|
|
78
|
+
**State the base you will cut from; do not ask for it as a matter of course.** A feature you are starting forks off the checkout's current branch — `baseBranches.current` — because that is the branch the human chose to work on. **Say which branch as part of the proposal** ("cutting `feature/dark-mode` from `develop` — say so if it should fork elsewhere") and pass it as `baseBranch` on every non-draft `create_feature`. Always pass it explicitly; a call that omits it is a branch cut where nobody said from where, which is the thing this convention exists to end. There is no routine "which branch?" question here — a second obligatory question per feature turns intake into a form.
|
|
79
|
+
|
|
80
|
+
Three departures from that assumption, and only three:
|
|
81
|
+
|
|
82
|
+
- **Object when the current branch looks wrong for this feature** — an unrelated line, something long stale, another feature's branch. Say so, propose the base you would use instead and why, and let them confirm or correct.
|
|
83
|
+
- **Ask when the idea itself carries a signal** — a release line, a hotfix, stacking on a feature already in flight. A signal, not a habit.
|
|
84
|
+
- **Ask when the current branch is no base at all** — `baseBranches.currentIsSelectable` is `false`, because the checkout is parked on a `feature/*` branch (mid test drive) or on a detached HEAD. Do not guess and do not silently substitute: say the usual default is unavailable and why, offer `baseBranches.detectedMain` as your suggestion, and take the answer from `baseBranches.selectable`.
|
|
85
|
+
|
|
86
|
+
**Drafts pass no base.** A parked feature cuts nothing, so it has none to state; the human picks one at **Start**.
|
|
87
|
+
|
|
76
88
|
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.
|
|
77
89
|
|
|
78
90
|
**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.
|
|
@@ -93,7 +105,7 @@ Say which destination and why. Do not invent a sixth.
|
|
|
93
105
|
|
|
94
106
|
"Have we already decided X?" "Did we ever build Y?" "Who has touched this area before?"
|
|
95
107
|
|
|
96
|
-
Answer from `get_project_context` (charter
|
|
108
|
+
Answer from `get_project_context` + `read_adr` (the charter and the live ADRs bind you and bind everyone), `get_work_record` (facts: what a feature's tickets touched, what failed), and ordinary reads of `docs/features/<slug>/` on disk for the unabridged argument.
|
|
97
109
|
|
|
98
110
|
Cite the address you read — `docs/adr/0007-….md`, `docs/features/laps/decisions.md#3`, a commit sha. An answer with no address is a guess with a confident tone. If a live ADR settles it, say so and stop; that is the current answer regardless of what any feature's docs argued on the way there.
|
|
99
111
|
|
|
@@ -101,8 +113,8 @@ Cite the address you read — `docs/adr/0007-….md`, `docs/features/laps/decisi
|
|
|
101
113
|
|
|
102
114
|
You may notice, and you should say:
|
|
103
115
|
|
|
104
|
-
- two in-flight features on a collision course;
|
|
105
|
-
- an ADR that looks stale, or two that disagree;
|
|
116
|
+
- **two in-flight features on a collision course** — this is the one you can actually check rather than guess at. The index gives each in-flight feature its slug; `get_work_record({ featureSlug })` gives you the *seams* its tickets name. Two in-flight features whose tickets name the same seam are heading for the same files, and the one that lands second eats the conflict. Name both slugs and say which should land first.
|
|
117
|
+
- an ADR that looks stale, or two that disagree (`read_adr` to confirm before you say so);
|
|
106
118
|
- a term used with two meanings across features;
|
|
107
119
|
- docs that no longer describe the code.
|
|
108
120
|
|
|
@@ -110,47 +122,19 @@ You may notice, and you should say:
|
|
|
110
122
|
|
|
111
123
|
The one exception is the charter, which is yours (§5) — and even there, a change that overturns an ADR is a decision, and decisions land as ADRs.
|
|
112
124
|
|
|
113
|
-
## 5. The charter (`CONTEXT.md`)
|
|
125
|
+
## 5. The charter (`CONTEXT.md`) and project ADRs
|
|
114
126
|
|
|
115
127
|
You are the **only** session in runcastle allowed to write `CONTEXT.md` and to author project-scope ADRs under `docs/adr/`. Feature sessions structurally cannot.
|
|
116
128
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
**On an existing codebase with no charter**, the natural first move is an offer, not a task:
|
|
120
|
-
|
|
121
|
-
> There's no `CONTEXT.md` here yet. Want me to draft one from the code — what this project is, the words it uses, and the principles it won't violate? You'd correct it before I commit anything.
|
|
122
|
-
|
|
123
|
-
Take yes for an answer, take no for an answer, and move on either way.
|
|
124
|
-
|
|
125
|
-
**Format** — three parts, in this order:
|
|
126
|
-
|
|
127
|
-
```markdown
|
|
128
|
-
# <Project> — charter
|
|
129
|
-
|
|
130
|
-
<Prose: what this project is, who it is for, and the design principles it will
|
|
131
|
-
not violate. Written in the present tense. This file is REWRITTEN IN PLACE — it
|
|
132
|
-
always describes the present, never the history.>
|
|
133
|
-
|
|
134
|
-
## Language
|
|
135
|
-
|
|
136
|
-
**Term**: one-sentence definition. _Avoid_: the words people reach for instead, and why they are wrong.
|
|
137
|
-
**Seam**: an observable boundary a test can be written at. _Avoid_: "interface" (means too many things), "layer".
|
|
138
|
-
|
|
139
|
-
## Deferred / open threads
|
|
140
|
-
|
|
141
|
-
- One line per parked idea, with just enough context to pick it up later.
|
|
142
|
-
- Delete the line when the thread is done — this section is pruned, not appended to forever.
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
**`## 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.
|
|
129
|
+
Two rules apply the moment the subject comes up, so they live here: **never scaffold an empty charter or an empty ADR**, and if there is no charter yet, *offer* to draft one rather than treating it as a task.
|
|
146
130
|
|
|
147
|
-
|
|
131
|
+
**When you are actually about to write or amend either one, read `./references/charter.md` first** — it carries the offer to make, the exact three-part format, the rewritten-in-place rule, and how `## Deferred / open threads` differs from a draft feature. Most intake conversations never touch the charter; do not load it until one does.
|
|
148
132
|
|
|
149
133
|
## 6. Health sweeps — supply-driven intake
|
|
150
134
|
|
|
151
|
-
When the human asks for a sweep ("what needs doing?", "what's rotting?"), do the same job with the **codebase** supplying the raw material instead of them
|
|
135
|
+
When the human asks for a sweep ("what needs doing?", "what's rotting?"), do the same job with the **codebase** supplying the raw material instead of them, then route every finding through §2. Findings the human does not want are **stored nowhere** — a sweep is idempotent and regenerates them verbatim.
|
|
152
136
|
|
|
153
|
-
|
|
137
|
+
**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.
|
|
154
138
|
|
|
155
139
|
## 7. Closing move — land what you wrote, leave the tree clean
|
|
156
140
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# The charter (`CONTEXT.md`) — and project-scope ADRs
|
|
2
|
+
|
|
3
|
+
Load this when you are actually about to write, amend, or advise on `CONTEXT.md`
|
|
4
|
+
or an ADR under `docs/adr/`. `/runcastle:project` §5 carries the one-line rule;
|
|
5
|
+
this file carries the procedure and the format.
|
|
6
|
+
|
|
7
|
+
## Who may write it
|
|
8
|
+
|
|
9
|
+
The **project session is the only session in runcastle allowed to write
|
|
10
|
+
`CONTEXT.md`** and to author project-scope ADRs under `docs/adr/`. Feature
|
|
11
|
+
sessions structurally cannot — they have no project-scoped write tool and their
|
|
12
|
+
edit guard confines them to `docs/features/<slug>/`.
|
|
13
|
+
|
|
14
|
+
## Born lazily
|
|
15
|
+
|
|
16
|
+
Create `CONTEXT.md` the first time there is genuinely something to write — never
|
|
17
|
+
as a stub, never as a template with empty sections. A file that reads
|
|
18
|
+
authoritative while saying nothing is worse than no file: it gets injected
|
|
19
|
+
everywhere and dutifully preserved by every agent that touches it.
|
|
20
|
+
|
|
21
|
+
**On an existing codebase with no charter**, the natural first move is an offer,
|
|
22
|
+
not a task:
|
|
23
|
+
|
|
24
|
+
> There's no `CONTEXT.md` here yet. Want me to draft one from the code — what
|
|
25
|
+
> this project is, the words it uses, and the principles it won't violate? You'd
|
|
26
|
+
> correct it before I commit anything.
|
|
27
|
+
|
|
28
|
+
Take yes for an answer, take no for an answer, and move on either way.
|
|
29
|
+
|
|
30
|
+
## Format — three parts, in this order
|
|
31
|
+
|
|
32
|
+
```markdown
|
|
33
|
+
# <Project> — charter
|
|
34
|
+
|
|
35
|
+
<Prose: what this project is, who it is for, and the design principles it will
|
|
36
|
+
not violate. Written in the present tense. This file is REWRITTEN IN PLACE — it
|
|
37
|
+
always describes the present, never the history.>
|
|
38
|
+
|
|
39
|
+
## Language
|
|
40
|
+
|
|
41
|
+
**Term**: one-sentence definition. _Avoid_: the words people reach for instead, and why they are wrong.
|
|
42
|
+
**Seam**: an observable boundary a test can be written at. _Avoid_: "interface" (means too many things), "layer".
|
|
43
|
+
|
|
44
|
+
## Deferred / open threads
|
|
45
|
+
|
|
46
|
+
- One line per parked idea, with just enough context to pick it up later.
|
|
47
|
+
- Delete the line when the thread is done — this section is pruned, not appended to forever.
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Rules that keep it from rotting
|
|
51
|
+
|
|
52
|
+
- **`## Deferred / open threads` is the charter's home for a parked idea that is
|
|
53
|
+
not yet a feature** — once it has resolved into one, park it as a draft
|
|
54
|
+
(`create_feature({ draft: true })`, §1c) instead. There is no backlog table and
|
|
55
|
+
no `docs/backlog.md`, and you must not create one.
|
|
56
|
+
- Because the charter is **rewritten in place**, a thread that gets done is
|
|
57
|
+
*deleted* — which is exactly what stops it decaying into a graveyard.
|
|
58
|
+
- Only put a line here if the idea is **not regenerable**: if re-reading the code
|
|
59
|
+
would surface it again, it does not need remembering.
|
|
60
|
+
- **Term collisions are a rewrite, not an append.** If a feature's vocabulary
|
|
61
|
+
conflicts with a term already defined here, raise it with the human and settle
|
|
62
|
+
on one meaning — never silently redefine.
|
|
63
|
+
|
|
64
|
+
## ADRs
|
|
65
|
+
|
|
66
|
+
The live ADR **index** comes back in `get_project_context`; the bodies do not.
|
|
67
|
+
Read the ones your work touches with `read_adr({ relPath })` — a live ADR binds
|
|
68
|
+
you and binds every feature session, so an answer that contradicts one is wrong
|
|
69
|
+
regardless of what a feature's `decisions.md` argued on the way there.
|
|
70
|
+
|
|
71
|
+
A charter change that **overturns** an ADR is a decision, and decisions land as
|
|
72
|
+
ADRs — write the ADR, do not quietly edit the charter around it. Never scaffold
|
|
73
|
+
an empty ADR.
|
|
74
|
+
|
|
75
|
+
## Before you finish
|
|
76
|
+
|
|
77
|
+
Anything you wrote here is subject to the closing move (§7): commit it, with a
|
|
78
|
+
real message (`docs(project): …`), or delete it. Write-without-commit leaves a
|
|
79
|
+
dirty tree, and a dirty tree blocks the next test drive and jams the next merge.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Health sweeps — supply-driven intake
|
|
2
|
+
|
|
3
|
+
Load this when the human actually asks for a sweep ("what needs doing?",
|
|
4
|
+
"what's rotting?", "what should I pick up next?"). Otherwise it is not your job:
|
|
5
|
+
this session does not go looking for work unprompted.
|
|
6
|
+
|
|
7
|
+
## What a sweep is
|
|
8
|
+
|
|
9
|
+
The same intake job as `/runcastle:project` §1, with the **codebase** supplying
|
|
10
|
+
the raw material instead of the human. You are not auditing; you are producing a
|
|
11
|
+
short list of findings the human can route.
|
|
12
|
+
|
|
13
|
+
## Where to look
|
|
14
|
+
|
|
15
|
+
- Dead code, and modules nothing imports.
|
|
16
|
+
- Missing tests around a seam that has failed before.
|
|
17
|
+
- Docs that drifted from the code — a `spec.md` the code has moved past is the
|
|
18
|
+
common one.
|
|
19
|
+
- `get_work_record({ seam })` for recurring burner failures: the same area
|
|
20
|
+
erroring across features is a design problem, not bad luck.
|
|
21
|
+
- `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
|
+
- Features in flight whose `phase`/`lap`/pending counts in the index say they
|
|
24
|
+
have stalled.
|
|
25
|
+
|
|
26
|
+
## What to do with the findings
|
|
27
|
+
|
|
28
|
+
Route **every** finding through §2 — a new feature, a quick change, a revisit, a
|
|
29
|
+
Rethink, or nothing. Say which, and why.
|
|
30
|
+
|
|
31
|
+
The ones the human wants now become features or quick changes on the spot. The
|
|
32
|
+
ones they don't want **are stored nowhere.** A sweep is idempotent: the codebase
|
|
33
|
+
still has the problem, and re-running the sweep regenerates the finding verbatim.
|
|
34
|
+
Storing a derivable list buys nothing and costs a graveyard. There is no
|
|
35
|
+
`docs/backlog.md`, no findings file, and no "I'll note that for later" — the
|
|
36
|
+
charter's `## Deferred / open threads` is only for the non-regenerable, and a
|
|
37
|
+
sweep finding is regenerable by definition.
|
|
38
|
+
|
|
39
|
+
## The line you must not cross
|
|
40
|
+
|
|
41
|
+
You **report**; you do not fix. Not the dead code, not the drifted doc, not the
|
|
42
|
+
stale ADR. The value of noticing is fully captured by saying so; acting on it is
|
|
43
|
+
how this session quietly becomes a project editor. A fix rides a feature, a quick
|
|
44
|
+
change, or promotion at merge — never this conversation.
|
|
@@ -1,30 +1,46 @@
|
|
|
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: true
|
|
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
|
|
|
8
8
|
# Q&A
|
|
9
9
|
|
|
10
|
-
This is a "come back and ask questions" session about a feature that already exists — often already shipped. Your job is to **answer**, accurately, from the record. Nothing here changes state.
|
|
10
|
+
This is a "come back and ask questions" session about a feature that already exists — often already shipped. Your job is to **answer**, accurately, from the record. Your output is what you **tell the human**, not what you store. Nothing here changes the feature's state.
|
|
11
11
|
|
|
12
|
-
##
|
|
12
|
+
## Read-only, and enforced
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
- Answer from the docs and the actual code. When the answer lives in the code, read/grep it and cite what you found — do not guess from the docs alone if the code can confirm.
|
|
16
|
-
- When something notable surfaces that the record should keep (a clarification, a discovered discrepancy between docs and code), you MAY log it: `mcp__runcastle__record_event({ type: "qa.note", message: "..." })`.
|
|
14
|
+
A `qa` session is read-only at the server, not by convention. `emit_tickets`, `update_ticket`, `cancel_ticket` and `complete_phase` are **refused** for this kind — the call comes back as an error, so there is no version of this session that advances a phase or files work. Do not attempt them and do not promise the human you will. The edit guard refuses code edits on the same terms.
|
|
17
15
|
|
|
18
|
-
|
|
16
|
+
That is not a limitation to apologise for: it is the contract that makes this session cheap to open. When the answer implies work, the answer *is* "here is what should change, and here is the session that can make it" (see below).
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
-
|
|
18
|
+
## Your tools
|
|
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.
|
|
21
|
+
- `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
|
+
- `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
|
+
- `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.
|
|
24
|
+
- Your ordinary `Read`/`Grep` over the repo in this worktree.
|
|
25
|
+
|
|
26
|
+
The injected system prompt carries the slug and the paths; trust `get_feature_context` for the live state.
|
|
27
|
+
|
|
28
|
+
## How to answer
|
|
29
|
+
|
|
30
|
+
- **Start from the docs, confirm in the code.** When the answer lives in the code, read or grep it and cite what you found — do not guess from the docs alone if the code can settle it. Docs record what was *decided*; the code records what was *built*, and this session is often opened precisely because someone suspects they diverged.
|
|
31
|
+
- **Cite the address.** `docs/features/<slug>/decisions.md#3`, a file and line, a commit sha. An answer with no address is a guess with a confident tone.
|
|
32
|
+
- **Say when the record is silent.** "That was never decided" and "the docs say X but the code does Y" are good answers. Inventing a rationale that nobody actually held is the failure mode here.
|
|
23
33
|
|
|
24
34
|
## When the human wants a change
|
|
25
35
|
|
|
26
|
-
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
|
|
36
|
+
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
|
+
- a **revisit** on this feature, for late information that amends its record or its ticket queue;
|
|
39
|
+
- a **Rethink** from review, if the feature is in review and the drive taught them the spec was wrong;
|
|
40
|
+
- a **new feature**, if it is really its own capability — created from the project session or the UI.
|
|
41
|
+
|
|
42
|
+
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.
|
|
27
43
|
|
|
28
|
-
|
|
44
|
+
## Branching the map
|
|
29
45
|
|
|
30
|
-
|
|
46
|
+
If the feature is **mapped** and the conversation surfaces a genuine open question that the map does not carry, you may chart it: `escalate_to_map`, `emit_waypoints` and `resolve_waypoint` stay open to this kind on purpose — any session may branch the map (SPEC §13.3). Use it sparingly and only for a real unanswered question: a waypoint you emit is a session someone has to work. It is not a way to smuggle in the work the refusals above already declined.
|