@jaybeeuu/agent-cortex 2.6.0 → 2.7.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.
@@ -16,12 +16,19 @@ node <skill-scripts>/create-chores.mjs --parent <bead-id> [--priority <0-4>]
16
16
 
17
17
  ## Stage chore beads
18
18
 
19
- One per stage in `pipeline.json` (currently `code`, `verify`, `review`, `document`):
19
+ One per stage in `pipeline.json` (currently `code`, `verify`, `review`, `document`, `housekeeping`):
20
20
 
21
21
  - **Type**: `chore`
22
22
  - **Title**: `[<parent-id>] <stage title>` (e.g. `[abc-123] Code`)
23
- - **Labels**: `stage:<id>` (e.g. `stage:code`, `stage:verify`)
24
- - **Dependencies**: `parent-child` to the parent bead; `blocks` between stages per each stage's `dependsOn` (the script requires `dependsOn` targets to appear earlier in `pipeline.json`).
23
+ - **Labels**: `stage:<id>` (e.g. `stage:code`, `stage:verify`), plus any `labels` the stage declares — `housekeeping` adds `lifecycle:housekeeping`
24
+ - **Dependencies**: `parent-child` to the parent bead; `blocks` between stages per each stage's `dependsOn` (the script requires `dependsOn` targets to appear earlier in `pipeline.json`)
25
+
26
+ **A stage may wait on the gate instead of a stage.** The reserved `dependsOn` target
27
+ `featurePrReview` means "blocked behind the PR gate bead". The gate is not a stage — it is created
28
+ after them — so the script wires those stages to it in a second pass, once the gate exists. The
29
+ `housekeeping` stage uses it: its chore must not become ready until a human has merged the PR and
30
+ closed the gate, and the `blocks` edge keeps it out of `bd ready` while the feature is in flight.
31
+ The gate itself still waits on the last stage that does not wait on the gate (`document`).
25
32
 
26
33
  ## PR gate bead (`featurePrReview`)
27
34
 
@@ -87,6 +94,7 @@ The deliverable this feature produces and the problem it solves.
87
94
  | verify | Verify | code |
88
95
  | review | Review | verify |
89
96
  | document | Document | review |
97
+ | housekeeping | Housekeeping | `featurePrReview` — the PR gate bead, not a stage |
90
98
 
91
99
  `maxFixRounds: 4` — the maximum fix-retry rounds allowed for a stage's verify loop before the PR gate. Adjust stages only in `pipeline.json`; the script and templates follow it.
92
100
 
@@ -141,7 +141,7 @@ Steps 2–6 create beads; step 7 expands one. A bead that already exists but has
141
141
  Input: title `Add export API to invoices`, description `…`, priority `2`, parent `feature-456`. Output: the bead ID plus the script's JSON mapping stage IDs → chore bead IDs, with `featurePrReview` as the HITL PR gate:
142
142
 
143
143
  ```json
144
- {"code": "abc123", "verify": "def456", "review": "ghi789", "document": "jkl012", "featurePrReview": "bd-mno345"}
144
+ {"code": "abc123", "verify": "def456", "review": "ghi789", "document": "jkl012", "housekeeping": "mno345", "featurePrReview": "bd-pqr678"}
145
145
  ```
146
146
 
147
147
  Feature path input: "several endpoints, a UI, and a migration". Output: `feature-456` (`execution:hitl`), `plan-457` ("Plan: …", `execution:hitl`, owned by the feature, blocking every child), and one `execution:afk` impl child per deliverable, each expanded into its own stage chores plus PR gate.
@@ -28,6 +28,14 @@
28
28
  "description": "Review changes and update documentation if key decisions, behaviour changes, or constraints were introduced.",
29
29
  "template": "templates/document.md",
30
30
  "dependsOn": ["review"]
31
+ },
32
+ {
33
+ "id": "housekeeping",
34
+ "title": "Housekeeping",
35
+ "description": "Remove the worktrees of closed beads. Run `node <ralph-scripts>/prune-worktrees.mjs` from the main checkout and retry once if it exits non-zero. The exit status is 1 whenever any worktree survives — an open bead counts as survival — so fail this chore only when a closed-bead worktree is still refused after the retry, and leave it open. Never close it over a worktree the script did not remove.",
36
+ "template": "templates/housekeeping.md",
37
+ "labels": ["lifecycle:housekeeping"],
38
+ "dependsOn": ["featurePrReview"]
31
39
  }
32
40
  ]
33
41
  }
@@ -27,6 +27,7 @@ import { fileURLToPath } from 'node:url';
27
27
  * @property {string} description
28
28
  * @property {string} template
29
29
  * @property {string[]} dependsOn
30
+ * @property {string[]} [labels] Labels beside `stage:<id>`; e.g. `lifecycle:housekeeping`
30
31
  */
31
32
 
32
33
  /**
@@ -136,6 +137,10 @@ function wireBlocks(beadId, dependsOnId, blocks) {
136
137
 
137
138
  // ─── Main ─────────────────────────────────────────────────────────────────────
138
139
 
140
+ // Reserved `dependsOn` target: the PR gate bead. The gate is not a stage — it is
141
+ // created after them — so a stage that waits on it is wired in a second pass.
142
+ const GATE_ROLE = 'featurePrReview';
143
+
139
144
  const __filename = fileURLToPath(import.meta.url);
140
145
  const __dirname = dirname(__filename);
141
146
 
@@ -150,6 +155,10 @@ const adoptable = readAdoptable(parentId);
150
155
  /** @type {Map<string, string>} */
151
156
  const stageToBeadId = new Map();
152
157
 
158
+ /** Stages that wait on the gate rather than on another stage, held until it exists.
159
+ * @type {Array<{ beadId: string, blocks: Set<string> }>} */
160
+ const gateDependents = [];
161
+
153
162
  for (const stage of pipeline.stages) {
154
163
  const adopted = adoptable.get(`stage:${stage.id}`);
155
164
 
@@ -160,7 +169,7 @@ for (const stage of pipeline.stages) {
160
169
  '--type', 'chore',
161
170
  '--description', JSON.stringify(stage.description),
162
171
  '--priority', priority,
163
- '--labels', `stage:${stage.id}`,
172
+ '--labels', [`stage:${stage.id}`, ...(stage.labels ?? [])].join(','),
164
173
  '--parent', parentId,
165
174
  '--silent',
166
175
  ]);
@@ -171,6 +180,7 @@ for (const stage of pipeline.stages) {
171
180
  const blocks = adopted ? adopted.blocks : new Set();
172
181
 
173
182
  for (const depStageId of stage.dependsOn) {
183
+ if (depStageId === GATE_ROLE) { continue; }
174
184
  const depBeadId = stageToBeadId.get(depStageId);
175
185
  if (!depBeadId) {
176
186
  console.error(`Stage '${stage.id}' depends on '${depStageId}', but it has not been created yet. Check dependsOn ordering in pipeline.json.`);
@@ -178,15 +188,17 @@ for (const stage of pipeline.stages) {
178
188
  }
179
189
  wireBlocks(beadId, depBeadId, blocks);
180
190
  }
181
- }
182
191
 
183
- const finalStageId = pipeline.stages.at(-1)?.id;
184
- if (!finalStageId) {
185
- console.error('No pipeline stages found in pipeline.json.');
186
- process.exit(1);
192
+ if (stage.dependsOn.includes(GATE_ROLE)) { gateDependents.push({ beadId, blocks }); }
187
193
  }
188
194
 
189
- const documentStageBeadId = stageToBeadId.get('document') ?? stageToBeadId.get(finalStageId);
195
+ // The gate waits on the last stage that does not itself wait on the gate, so the
196
+ // chain stays acyclic once a stage can follow the gate.
197
+ const chainStageIds = pipeline.stages
198
+ .filter((stage) => !stage.dependsOn.includes(GATE_ROLE))
199
+ .map((stage) => stage.id);
200
+
201
+ const documentStageBeadId = stageToBeadId.get('document') ?? stageToBeadId.get(chainStageIds.at(-1) ?? '');
190
202
  if (!documentStageBeadId) {
191
203
  console.error('Could not resolve the final stage bead needed for PR gate dependency.');
192
204
  process.exit(1);
@@ -220,6 +232,12 @@ const prGateBeadId = adoptedGate
220
232
  const gateBlocks = adoptedGate ? adoptedGate.blocks : new Set();
221
233
  wireBlocks(prGateBeadId, documentStageBeadId, gateBlocks);
222
234
 
235
+ // Second pass: the stages that wait on the gate finally get their dep, now that the
236
+ // gate bead exists.
237
+ for (const { beadId, blocks } of gateDependents) {
238
+ wireBlocks(beadId, prGateBeadId, blocks);
239
+ }
240
+
223
241
  /** @type {Record<string, string>} */
224
242
  const result = {
225
243
  ...Object.fromEntries(stageToBeadId),
@@ -0,0 +1,65 @@
1
+ ## Project context
2
+ <bd prime output — verbatim>
3
+
4
+ ## Task
5
+ <bd show <id> output — verbatim>
6
+
7
+ ## Progress log
8
+ Write progress to: `.agent-cortex/ralph/ralph-<bead-id>.log`
9
+ Log stage start, each pruner run with its exit status, and stage complete (see Progress Logging in
10
+ run-pipeline-stage skill).
11
+
12
+ ## Instructions
13
+ This stage removes worktrees, so **do not work in the bead worktree**. Run every command from the
14
+ repository's main checkout — the directory that holds `.agent-cortex/worktrees/` — because the
15
+ worktree this run may remove can be your own.
16
+
17
+ 1. **Run the pruner once** from the main checkout:
18
+ ```bash
19
+ node <ralph-scripts>/prune-worktrees.mjs
20
+ ```
21
+ `<ralph-scripts>` is the absolute path to `skills/workflow/ralph/scripts/`, resolved from wherever
22
+ that skill was loaded. Read the per-worktree report it prints — it lists every scanned worktree,
23
+ its classification (`closed-bead`, `open-bead`, `orphan`, `unclassified`) and its outcome
24
+ (`removed`, `kept`, `refused`).
25
+
26
+ 2. **Retry once on a non-zero exit.** Exit status `1` means at least one scanned worktree survived
27
+ the run, and an open bead counts as survival — a normal run against a live backlog exits `1`. So
28
+ run the same command a second time when the first run exits non-zero. One retry, never more.
29
+
30
+ 3. **Decide the outcome from the report, not the exit status.** After the retry:
31
+ - **SUCCESS** — no worktree classified `closed-bead` is left in `refused` state. Report every
32
+ worktree the run removed and every one it kept, with the printed reason.
33
+ - **BLOCKED** — a `closed-bead` worktree is still `refused`, or the pruner could not run at all
34
+ (missing node, unreadable directory). A refused closed-bead worktree is a worktree this run did
35
+ not remove, whatever the reason printed.
36
+
37
+ 4. **Never close this chore over a worktree the script did not remove.** This stage is the one
38
+ exception to the close-on-both-outcomes rule, because this stage is last in the chain and nothing
39
+ waits on its close. Close it only when every `closed-bead` worktree is gone; the worktree removal
40
+ is the only thing this stage completes.
41
+
42
+ On BLOCKED, **do not close the chore**. Leave it open — return it to `open` status if your claim
43
+ moved it to `in_progress` — report the surviving worktrees with the reasons the pruner printed,
44
+ and hand the chore back to a human. Create no `stage:fix` chore and no fresh `stage:housekeeping`
45
+ chore: there is nothing for a fix round to repair, and no later stage to re-block. A `kept` or
46
+ `refused` entry for an open bead is a normal report, not a failure.
47
+
48
+ Do not delete a worktree by hand, do not pass `--remove-orphans`, and do not edit the pruner. This
49
+ stage reports what the script did.
50
+
51
+ End your response with a ---REPORT--- block. The `run-pipeline-stage` skill's
52
+ `prompts/stage-runner.md` holds the authoritative REPORT contract — if this block and that prompt
53
+ differ, follow the prompt and correct this template.
54
+ ---REPORT---
55
+ BEAD_ID: <id>
56
+ STAGE_COMPLETED: housekeeping
57
+ SUMMARY: <2–3 sentences: worktrees removed, worktrees kept and why>
58
+ FILES_CHANGED: none
59
+ COMMIT: none
60
+ PUSHED: no
61
+ PR: none
62
+ OUTCOME: <SUCCESS|BLOCKED>
63
+ BLOCKING_ISSUES: ← include only if OUTCOME is BLOCKED; one line per issue
64
+ - <worktree path and the reason the pruner printed>
65
+ ---
@@ -25,7 +25,7 @@ Clarify the skill goal with the user. Ask what triggers users would actually say
25
25
 
26
26
  ### 2. Draft from the anatomy template
27
27
 
28
- Read `docs/skills/skill-anatomy.md` as the source of truth. Follow the annotated template for section ordering and front-matter fields. Target ~150 lines for `SKILL.md` but stay under the 150-line hard cap. When the workflow branches or has decision points, include one inline ASCII workflow diagram per the anatomy doc's diagram style guide — ASCII only, never Mermaid.
28
+ Read `{{PATH:skill_anatomy}}` as the source of truth. Follow the annotated template for section ordering and front-matter fields. Target ~150 lines for `SKILL.md` but stay under the 150-line hard cap. When the workflow branches or has decision points, include one inline ASCII workflow diagram per the anatomy doc's diagram style guide — ASCII only, never Mermaid.
29
29
 
30
30
  ### 3. Apply voice and tone rules
31
31
 
@@ -62,7 +62,7 @@ Output: A new `skills/review-security/SKILL.md` with front matter, When to use,
62
62
 
63
63
  ## Verification checklist
64
64
 
65
- - [ ] `docs/skills/skill-anatomy.md` was read before drafting
65
+ - [ ] `{{PATH:skill_anatomy}}` was read before drafting
66
66
  - [ ] Required sections present: `## When to use`, `## Workflow`, `## Verification checklist`
67
67
  - [ ] Section order follows the anatomy template
68
68
  - [ ] Workflow diagram (if present) is ASCII-only and follows the anatomy diagram style guide
@@ -8,7 +8,7 @@ description: Refactor an existing SKILL.md to the canonical anatomy template thr
8
8
  ## When to use
9
9
 
10
10
  - A skill's SKILL.md predates the anatomy template and needs upgrading.
11
- - You want to align a skill with `docs/skills/skill-anatomy.md` before the refactor wave touches it.
11
+ - You want to align a skill with `{{PATH:skill_anatomy}}` before the refactor wave touches it.
12
12
  - You want to compare a skill's structure to the current template standard.
13
13
  - Trigger phrases: "refactor this skill", "restructure this skill", "review this skill", "upgrade X to the template", "align with anatomy".
14
14
 
@@ -27,7 +27,7 @@ Read the target skill's `skills/<name>/SKILL.md` and the canonical anatomy refer
27
27
 
28
28
  ```bash
29
29
  cat skills/<name>/SKILL.md
30
- cat docs/skills/skill-anatomy.md
30
+ cat {{PATH:skill_anatomy}}
31
31
  ```
32
32
 
33
33
  If the target has no `SKILL.md` yet, abort — this skill upgrades existing files; use `write-a-skill` for new creation.
@@ -47,7 +47,7 @@ are fine.
47
47
 
48
48
  ## Expansion Guard
49
49
 
50
- Stage chores are pre-created, never created on demand. `create-chores.mjs` (in `skills/planning/create-task/`) reads `pipeline.json` and creates the whole chain in one shot — `code` → `verify` → `review` → `document`, plus the `featurePrReview` HITL gate — with `blocks` deps that encode the order, and with the parent's priority and inherited labels. It is idempotent: a re-run adopts the chores that already exist under the parent, creates only the missing ones, and skips deps that are already wired. Ralph's dispatch is therefore always "claim what `bd ready -l execution:afk` returns".
50
+ Stage chores are pre-created, never created on demand. `create-chores.mjs` (in `skills/planning/create-task/`) reads `pipeline.json` and creates the whole chain in one shot — `code` → `verify` → `review` → `document`, plus the `featurePrReview` HITL gate, plus the `housekeeping` chore blocked behind that gate — with `blocks` deps that encode the order, and with the parent's priority and inherited labels. It is idempotent: a re-run adopts the chores that already exist under the parent, creates only the missing ones, and skips deps that are already wired. Ralph's dispatch is therefore always "claim what `bd ready -l execution:afk` returns".
51
51
 
52
52
  A ready `execution:afk` task bead with **no** `stage:*` chore children has nothing to dispatch. Expand it, then dispatch the resulting `stage:code` chore:
53
53
 
@@ -57,7 +57,7 @@ A ready `execution:afk` task bead with **no** `stage:*` chore children has nothi
57
57
  ```
58
58
  Expect `0` printed. `grep -c` exits `1` when it counts zero matches, so add `|| true` when the
59
59
  check runs under `set -e` or inside an `&&` chain — the printed count is the answer, not the exit
60
- status. A bead with a full chain — one `stage:*` chore per stage in `pipeline.json` (four for the
60
+ status. A bead with a full chain — one `stage:*` chore per stage in `pipeline.json` (five for the
61
61
  shipped pipeline) plus the `lifecycle:feature-pr` gate — never needs an expansion. The check
62
62
  answers "no chores at all", so a **partial** chain (a lower count from a failed run) passes it
63
63
  and its missing stages never arrive. Nothing detects that automatically: re-run step 2's command
@@ -72,7 +72,7 @@ A ready `execution:afk` task bead with **no** `stage:*` chore children has nothi
72
72
  ```json
73
73
  { "kind": "expand", "choreId": null, "parentId": "<parent-id>", "title": "<parent title>", "agentId": "<agent-id>", "logLine": 1 }
74
74
  ```
75
- 4. **Report the mapping** the worker returns — `code`, `verify`, `review`, `document`, `featurePrReview` — and close nothing. The worker owns no bead on this run; the expansion is bookkeeping, not a stage.
75
+ 4. **Report the mapping** the worker returns — `code`, `verify`, `review`, `document`, `housekeeping`, `featurePrReview` — and close nothing. The worker owns no bead on this run; the expansion is bookkeeping, not a stage.
76
76
  5. **Dispatch normally.** The next `bd ready -l execution:afk` pass picks up the `stage:code` chore, and the pre-created chain carries the rest.
77
77
  6. **Handle the completion as an expansion completion**, not a stage completion — see below.
78
78
 
@@ -97,7 +97,7 @@ On completion, remove the entry from `inflight` and confirm the chore tree exist
97
97
  bd children <parent-id> --json | grep -c '"stage:'
98
98
  ```
99
99
 
100
- Expect one match per `stage:*` label in the chain — four for the shipped `pipeline.json`; the
100
+ Expect one match per `stage:*` label in the chain — five for the shipped `pipeline.json`; the
101
101
  `featurePrReview` gate carries `lifecycle:feature-pr` instead, so it is not counted. A lower count
102
102
  means the expansion did not finish: `0` created nothing, and a partial chain leaves stages that
103
103
  will never become ready. Either way, surface the worker's output in chat, record the parent for the
@@ -223,6 +223,16 @@ node skills/workflow/ralph/scripts/prune-worktrees.mjs --remove-orphans # also r
223
223
 
224
224
  **An unregistered stale directory is refused forever.** Earlier git runs may have dropped its registration, so every scan reports it and never removes it. Clean it up by hand: `git worktree prune`, then `rm -rf <path>`.
225
225
 
226
+ ### Housekeeping chore
227
+
228
+ Pruning is a pipeline stage, not a habit: `create-chores.mjs` pre-creates a `stage:housekeeping` chore for every expanded task, carrying `lifecycle:housekeeping` and blocked behind that task's `lifecycle:feature-pr` gate. It stays out of `bd ready` until a human closes the gate, so it never competes with pipeline work.
229
+
230
+ - **Executor.** A dispatched worker — the `stage:housekeeping` chore in `bd ready -l execution:afk` — runs `prune-worktrees.mjs` from the **main checkout**, never from the feature worktree: the worktree it removes can be the parent's own. Ralph dispatches it with the main checkout as cwd for that reason.
231
+ - **Retry once.** Exit status `1` means some scanned worktree survived, and an open bead counts as survival, so a live backlog always exits non-zero. The worker therefore retries the run once. The retry is the whole allowance — never a third run.
232
+ - **Never close over a worktree the script did not remove.** The outcome comes from the per-worktree report, not the exit status: the chore succeeds when no `closed-bead` worktree is left `refused`. A closed-bead worktree still `refused` after the retry fails the chore, and the chore is left **open** with the printed reasons in its notes. A `kept` or `refused` entry for an open bead is a normal report, not a failure.
233
+
234
+ Ralph creates nothing for this stage and closes nothing: the worker owns the chore, as it owns every leaf bead.
235
+
226
236
  ---
227
237
 
228
238
  ## Feature PR Gate
@@ -240,15 +250,15 @@ When the `document` chore for a parent task completes:
240
250
  4. Find the child HITL task bead for this parent with label `lifecycle:feature-pr` and `execution:hitl` (created by `create-task`).
241
251
  5. Update that HITL bead with the PR URL/status (comment or note) so the reviewer has the link.
242
252
  6. Do not schedule new parent features while this gate is **unblocked** — an unblocked gate is a feature genuinely awaiting a human merge (see _Gate predicate_ below).
243
- 7. Once the PR is merged, a human closes the HITL PR gate bead, which unblocks the scheduling brake. After that, close the parent feature bead:
253
+ 7. Once the PR is merged, a human closes the HITL PR gate bead, which unblocks the scheduling brake **and** the pre-created `stage:housekeeping` chore. After that, close the parent feature bead:
244
254
  ```bash
245
255
  bd close <parent-id>
246
256
  ```
247
- Only then continue with next parent features.
257
+ The housekeeping run that follows removes the parent's worktree, because its bead now reads `closed`. Only then continue with next parent features.
248
258
 
249
259
  ### Gate predicate — parked vs pending
250
260
 
251
- Ralph's only scheduling brake is a gate that is **unblocked**. `create-chores` pre-creates a `lifecycle:feature-pr` gate for *every* planned feature, blocked behind that feature's own `document` chore; such a gate is **parked** — the feature has not reached its PR stage — and must never stop dispatch. Only when the `document` chore closes does the gate unblock, and that is the state in which a human must merge.
261
+ Ralph's only scheduling brake is a gate that is **unblocked**. `create-chores` pre-creates a `lifecycle:feature-pr` gate for *every* planned feature, blocked behind that feature's own `document` chore; such a gate is **parked** — the feature has not reached its PR stage — and must never stop dispatch. Only when the `document` chore closes does the gate unblock, and that is the state in which a human must merge. The feature's `stage:housekeeping` chore waits on the gate bead itself, so it becomes ready only after that human closes it.
252
262
 
253
263
  Resolve the question with one command instead of ad-hoc `bd` queries:
254
264
 
@@ -9,7 +9,7 @@ Ralph is a parallel orchestration loop. It advances `execution:afk` beads throug
9
9
 
10
10
  One query decides what ralph works on: **`bd ready -l execution:afk`**. Features, tasks and chores all arrive through it, and each bead's *shape* — not a second `bd` filter — decides which procedure carries it: a childless feature is decomposed, a bead with no stage chores is expanded, a stage chore is dispatched. An `execution:afk` bead is therefore never invisible to ralph for lacking a `stage:*` label, and ralph never invents a narrower "only stage chores" predicate that would hide one.
11
11
 
12
- Each leaf execution bead belongs to the worker that ran it: the worker appends its milestone notes, closes the bead on both outcomes, and builds its own `stage:fix` + fresh-stage remediation chain when it fails. Ralph reads beads and routes — it never closes a leaf bead and never creates a fix bead. Its only retry control is a read-only cap backstop.
12
+ Each leaf execution bead belongs to the worker that ran it: the worker appends its milestone notes, closes the bead on both outcomes, and builds its own `stage:fix` + fresh-stage remediation chain when it fails. The `housekeeping` worker is the exception — it never closes its chore over a worktree the pruner did not remove, and it builds no chain. Ralph reads beads and routes — it never closes a leaf bead and never creates a fix bead. Its only retry control is a read-only cap backstop.
13
13
 
14
14
  State model: task state lives in beads (ready/in_progress via `bd`); the only local file is `.agent-cortex/ralph/state.json`, holding the agent-ID→bead mapping. Pipeline configuration comes from `skills/planning/create-task/pipeline.json`, and stage execution belongs to the `run-pipeline-stage` skill. Stage chores are pre-created from that pipeline when a task is expanded — with `blocks` deps encoding the order — so ralph's dispatch is always "claim what `bd ready -l execution:afk` returns"; it never creates a stage chore.
15
15
 
@@ -45,7 +45,7 @@ State model: task state lives in beads (ready/in_progress via `bd`); the only lo
45
45
 
46
46
  ### 2. Expansion guard (stage chores are pre-created)
47
47
 
48
- A ready `execution:afk` task bead with no `stage:*` chore children has nothing to dispatch, so it is not dispatched as work. Expand it first: dispatch a worker in the feature worktree that runs `create-chores.mjs --parent <id> --priority <parent-priority>` against the existing bead and reports the returned `code`/`verify`/`review`/`document`/`featurePrReview` mapping — the next `bd ready -l execution:afk` pass then picks up `stage:code` (see REFERENCE.md, _Expansion Guard_).
48
+ A ready `execution:afk` task bead with no `stage:*` chore children has nothing to dispatch, so it is not dispatched as work. Expand it first: dispatch a worker in the feature worktree that runs `create-chores.mjs --parent <id> --priority <parent-priority>` against the existing bead and reports the returned `code`/`verify`/`review`/`document`/`housekeeping`/`featurePrReview` mapping — the next `bd ready -l execution:afk` pass then picks up `stage:code` (see REFERENCE.md, _Expansion Guard_).
49
49
 
50
50
  The expansion is idempotent and re-running it is the recovery path: the script adopts the chores that already exist under the parent, creates only the missing ones, and reports the whole mapping. The auto-recovery fires only when a re-check finds no `stage:*` chore at all; a **partial** chain (a count below the stage total) needs a manual re-run of that same command.
51
51
 
@@ -74,7 +74,8 @@ On each completion notification (a background agent finished):
74
74
  | `verify` | `OUTCOME: BLOCKED` | Create nothing — the worker built `stage:fix` + a fresh `stage:verify`; dispatch the ready fix chore |
75
75
  | `review` | `OUTCOME: SUCCESS` | Closing the review chore unblocks the document chore |
76
76
  | `review` | `OUTCOME: BLOCKED` | Create nothing — the worker built `stage:fix` + a fresh `stage:review`; dispatch the ready fix chore |
77
- | `document` | `OUTCOME: SUCCESS` | Open/update the feature PR now (`feature/<parent-id>` → its base branch), report the URL, and update the child HITL gate bead (`lifecycle:feature-pr`). Wait for a human to close that bead after merge, then `bd close <parent-id>` |
77
+ | `document` | `OUTCOME: SUCCESS` | Open/update the feature PR now (`feature/<parent-id>` → its base branch), report the URL, and update the child HITL gate bead (`lifecycle:feature-pr`). Wait for a human to close that bead after merge, then `bd close <parent-id>` — closing the gate unblocks the pre-created `stage:housekeeping` chore, which removes the worktrees of closed beads |
78
+ | `housekeeping` | `OUTCOME: SUCCESS` | Nothing further — it is the last stage; its closed chore ends the pipeline |
78
79
  | any | parent status `blocked` | Nothing — the worker hit `maxFixRounds`; record the parent for the shutdown summary |
79
80
 
80
81
  5. Run `bd ready -l execution:afk` and dispatch new ready work while the in-flight count is under 5 — max 5 parent features, counted from `state.json`'s `inflight` (a parent counts while any of its workers — stage, `kind: "decompose"` or `kind: "expand"` — sits in `inflight`; REFERENCE.md, _State Files_). A ready childless feature is decomposed, not staged, and a ready bead with no chore tree is expanded (see _Feature decomposition_ and _Expansion guard_ above). Before each dispatch run the read-only cap backstop (REFERENCE.md, _Cap Backstop_): at `maxFixRounds` block the parent and create nothing. Do not schedule new parent features while `pending-gates.mjs` reports a non-empty `pendingHumanAction` — that is, while a `lifecycle:feature-pr` gate is **unblocked**. Gates still blocked behind their own pipeline (pre-created for a feature that has not reached its PR stage) are parked and never stop dispatch.
@@ -96,7 +97,7 @@ bd show <id> # full detail, including the worker's notes
96
97
  - **Multi-feature epics** (two or more feature task children): the epic runs on `epic/<epic-id>`, based on `origin/main` — never local `main`. Each `execution:afk` parent task runs on `feature/<parent-id>`, based from its epic branch, in the dedicated worktree `.agent-cortex/worktrees/<parent-id>`.
97
98
  - Feature PR gate: `feature/<parent-id>` → `epic/<epic-id>` (or → `main` for single-feature epics). Epic PR gate: `epic/<epic-id>` → `main`.
98
99
  - **Never stack branches.** `feature/<parent-id>` is based on `epic/<epic-id>` (or `origin/main`), never on another `feature/*`. A dependency between two tasks lives in the bead graph (`bd dep add <waiting> <blocked-on>`) so the dependent task stays out of `bd ready` until its dependency has merged — stacking the branch defeats that gate by hand and costs a rebase-on-squash later (see `git-workflow` rule 3).
99
- - Branch/worktree setup and the PR commands live in REFERENCE.md (_Feature branches and worktrees_, _Feature PR gate_). Closed-bead worktrees are reclaimed by `skills/workflow/ralph/scripts/prune-worktrees.mjs`, which removes worktrees whose bead is closed, reports the rest, and removes an orphan only with `--remove-orphans` (REFERENCE.md, _Pruning Worktrees_).
100
+ - Branch/worktree setup and the PR commands live in REFERENCE.md (_Feature branches and worktrees_, _Feature PR gate_). Closed-bead worktrees are reclaimed by `skills/workflow/ralph/scripts/prune-worktrees.mjs`, which removes worktrees whose bead is closed, reports the rest, and removes an orphan only with `--remove-orphans` (REFERENCE.md, _Pruning Worktrees_). The `stage:housekeeping` chore runs it: dispatch that chore with the **main checkout** as cwd, not the feature worktree, because the worktree it removes may be the parent's own. A non-zero exit gets one retry; a closed-bead worktree still refused after it fails the chore, which stays open.
100
101
 
101
102
  ### 7. HITL pause and shutdown
102
103
 
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: run-pipeline-stage
3
- description: Execute a single pipeline stage for a bead (code, verify, review, document) via its playbook and read-only routing, reporting the outcome in a fixed REPORT shape. Use when you want to "work through beads inline", "pick up a specific stage bead", or run one stage "without the full ralph orchestrator".
3
+ description: Execute a single pipeline stage for a bead (code, verify, review, document, housekeeping) via its playbook and read-only routing, reporting the outcome in a fixed REPORT shape. Use when you want to "work through beads inline", "pick up a specific stage bead", or run one stage "without the full ralph orchestrator".
4
4
  ---
5
5
 
6
6
  # Run Pipeline Stage
7
7
 
8
- Stage beads (`stage:code`, `stage:verify`, `stage:review`, `stage:document`, `stage:fix`) come from the pipeline definition in `skills/planning/create-task/pipeline.json`. This skill executes one stage and owns that bead's lifecycle: the runner that does the work appends its own notes, closes its own leaf execution bead on **both** outcomes, and builds its own remediation children when the stage fails. The orchestrator dispatches and routes — it never edits or closes a leaf bead.
8
+ Stage beads (`stage:code`, `stage:verify`, `stage:review`, `stage:document`, `stage:housekeeping`, `stage:fix`) come from the pipeline definition in `skills/planning/create-task/pipeline.json`. This skill executes one stage and owns that bead's lifecycle: the runner that does the work appends its own notes, closes its own leaf execution bead on **both** outcomes, and builds its own remediation children when the stage fails. `housekeeping` is the single exception: it never closes its bead over a worktree the pruner did not remove, and it builds no remediation children. The orchestrator dispatches and routes — it never edits or closes a leaf bead.
9
9
 
10
10
  ## When to use
11
11
 
@@ -24,7 +24,7 @@ Stage beads (`stage:code`, `stage:verify`, `stage:review`, `stage:document`, `st
24
24
 
25
25
  | Actor | Owns |
26
26
  |---|---|
27
- | Stage runner (worker) | Milestone notes, progress-log lines, closing its leaf bead on both outcomes, remediation children, the REPORT |
27
+ | Stage runner (worker) | Milestone notes, progress-log lines, closing its leaf bead on both outcomes (`housekeeping` excepted: it leaves its bead open), remediation children, the REPORT |
28
28
  | Orchestrator (ralph or you) | Claim, dispatch, forward routing, the read-only cap backstop, the PR gate |
29
29
 
30
30
  A leaf bead is the worker's record of what happened. The orchestrator reads it; it does not write it.
@@ -58,7 +58,9 @@ The worker built anything this stage needed; routing picks up whatever `bd ready
58
58
  | `review` | `OUTCOME: SUCCESS` | Dispatch the ready `stage:document` chore |
59
59
  | `fix` | `OUTCOME: SUCCESS` | Dispatch the ready re-run chore that was blocked on the fix |
60
60
  | `document` | `OUTCOME: SUCCESS` | Open/update the feature PR and the HITL gate bead |
61
+ | `housekeeping` | `OUTCOME: SUCCESS` | Create nothing — this is the last stage; its closed chore ends the pipeline |
61
62
  | `code` / `verify` / `review` / `fix` / `document` | `OUTCOME: BLOCKED` | Create nothing — the worker built `stage:fix` + a fresh instance of its own stage; dispatch the ready fix chore |
63
+ | `housekeeping` | `OUTCOME: BLOCKED` | Create nothing — the worker left its chore **open**; hand the surviving worktrees to a human. Do not re-dispatch it automatically |
62
64
  | any | parent status `blocked` | Nothing — the worker or the cap backstop blocked it; record it for shutdown |
63
65
 
64
66
  Cap backstop (read-only): before dispatching a `stage:*` chore, count the parent's fix rounds and, if already at `maxFixRounds` (`skills/planning/create-task/pipeline.json`, currently `4`), block the parent and create nothing:
@@ -69,7 +71,7 @@ fix_rounds=$(bd children <parent-id> --json | grep -c '"stage:fix"')
69
71
 
70
72
  ## Remediation Children (worker-built)
71
73
 
72
- On `OUTCOME: BLOCKED` the worker creates its own chain — a `stage:fix` chore carrying the feedback, plus a **fresh instance of the stage that failed**, blocked on the fix. A fresh instance is not optional: the failed chore is already closed, so the pre-created chain cannot re-verify a fix.
74
+ On `OUTCOME: BLOCKED` the worker creates its own chain — a `stage:fix` chore carrying the feedback, plus a **fresh instance of the stage that failed**, blocked on the fix. A fresh instance is not optional: the failed chore is already closed, so the pre-created chain cannot re-verify a fix. `housekeeping` is the exception — it changes no files, its bead stays open, and nothing downstream waits on it, so it builds no chain.
73
75
 
74
76
  | Failed stage | Chain |
75
77
  |---|---|
@@ -78,6 +80,7 @@ On `OUTCOME: BLOCKED` the worker creates its own chain — a `stage:fix` chore c
78
80
  | `review` | `stage:fix` → fresh `stage:review` |
79
81
  | `document` | `stage:fix` → fresh `stage:document` |
80
82
  | `fix` | `stage:fix` → fresh `stage:fix` |
83
+ | `housekeeping` | none — the worker leaves its chore open, reports `BLOCKED`, and hands the surviving worktrees to a human. No `stage:fix` and no fresh `stage:housekeeping`, because the stage repairs no files and sits last in the chain |
81
84
 
82
85
  Webbing, at cap, and the exact commands live in `prompts/stage-runner.md` and each playbook's _Remediation and close_ section. In short: both beads are children of the parent task (`--parent <parent-id>`), the fresh stage is `bd dep add <fresh> <fix> --type blocks`, and any not-yet-run later stage is re-blocked behind the fresh bead. Chores carry no required sections, so the feedback description points at the parent/requirements bead (`<parent-id>`) rather than restating its acceptance criteria.
83
86
 
@@ -112,7 +115,7 @@ Every stage-runner prompt ends with the literal REPORT instruction (see `prompts
112
115
  ```
113
116
  ---REPORT---
114
117
  BEAD_ID: <id>
115
- STAGE_COMPLETED: <code|verify|review|document|fix>
118
+ STAGE_COMPLETED: <code|verify|review|document|housekeeping|fix>
116
119
  SUMMARY: <2-3 sentence summary of what was done>
117
120
  FILES_CHANGED: <comma-separated list, or "none">
118
121
  COMMIT: <sha of the stage commit, or "none">
@@ -0,0 +1,58 @@
1
+ # Stage Playbook: housekeeping
2
+
3
+ This stage runs after the human merge gate and is last in the chain. It removes the worktrees of
4
+ closed beads and changes no files.
5
+
6
+ ## Stage Instructions
7
+
8
+ 1. **Work from the main checkout.** Run every command from the repository's main checkout — the
9
+ directory that holds `.agent-cortex/worktrees/` — never from a bead worktree. The worktree this
10
+ run removes can be the one you are standing in.
11
+
12
+ 2. **Run the pruner once:**
13
+ ```bash
14
+ node <ralph-scripts>/prune-worktrees.mjs
15
+ ```
16
+ `<ralph-scripts>` is the absolute path to `skills/workflow/ralph/scripts/`, resolved from wherever
17
+ that skill was loaded. Read the per-worktree report it prints: every scanned worktree, its
18
+ classification (`closed-bead`, `open-bead`, `orphan`, `unclassified`) and its outcome (`removed`,
19
+ `kept`, `refused`).
20
+
21
+ 3. **Retry once on a non-zero exit.** Exit status `1` means at least one scanned worktree survived,
22
+ and an open bead counts as survival, so a normal run against a live backlog exits `1`. Run the
23
+ same command a second time when the first run exits non-zero. One retry, never more.
24
+
25
+ 4. **Decide from the per-worktree report, not from the exit status.** After the retry:
26
+ - **SUCCESS** — no `closed-bead` worktree is left in `refused` state. Report every worktree the
27
+ run removed and every one it kept, with the printed reason.
28
+ - **BLOCKED** — a `closed-bead` worktree is still `refused` after the retry, or the pruner could
29
+ not run at all (missing node, unreadable directory). A refused `closed-bead` worktree is a
30
+ worktree this run did not remove, whatever reason it printed.
31
+
32
+ 5. **Never close over a worktree the script did not remove.** Do not delete a worktree by hand, do
33
+ not pass `--remove-orphans`, and do not edit the pruner. This stage reports what the script did.
34
+
35
+ ## Stage Outcome
36
+
37
+ - **No commit and no push.** This stage changes no files, so report `COMMIT: none` and
38
+ `PUSHED: no` on both outcomes. Do not create an empty commit.
39
+ - Set `OUTCOME: SUCCESS` when every `closed-bead` worktree is gone after the retry. `FILES_CHANGED`
40
+ is `none`.
41
+ - Set `OUTCOME: BLOCKED` when a `closed-bead` worktree survived the retry, or the pruner could not
42
+ run. List each surviving worktree with the printed reason under `BLOCKING_ISSUES`.
43
+ - A `kept` or `refused` entry for an open bead is a normal report, not a failure.
44
+
45
+ ## Remediation and close
46
+
47
+ - **This stage is the exception to close-on-both-outcomes.** Nothing downstream waits on its close,
48
+ so the bead's own state is the report.
49
+ - `OUTCOME: SUCCESS` — append the notes and the progress log, then
50
+ `bd close <id> --reason="SUCCESS: <removed/kept summary>"`. Create nothing: this is the last stage
51
+ and its closed chore ends the pipeline.
52
+ - `OUTCOME: BLOCKED` — **do not close the bead.** Leave it open: return it to `open` status
53
+ (`bd update <id> --status open`) if your claim moved it to `in_progress`. Report the surviving
54
+ worktrees and hand the chore back to a human. Create no `stage:fix` chore and no fresh
55
+ `stage:housekeeping` chore — a fix round repairs files, and this stage repairs none.
56
+ - **No downstream re-block.** This stage sits behind the PR gate rather than behind another stage, so
57
+ no later stage needs wiring behind a fresh instance.
58
+ - Report `COMMIT: none`, `PUSHED: no`, and `PR: none`.
@@ -38,10 +38,19 @@ not a transcript.
38
38
  ```
39
39
  The outcome lives in the close reason, the milestone notes, and the REPORT. A closed bead
40
40
  with a `BLOCKED` reason is a completed stage, not abandoned work.
41
+
42
+ **Exception — `housekeeping`.** It closes its bead only when the pruner removed every
43
+ `closed-bead` worktree. On `BLOCKED` it leaves the bead **open** and builds nothing (see
44
+ _Remediation_ below).
41
45
  3. **Do not create the next stage on success.** Routing forward is the orchestrator's job.
42
46
 
43
47
  ## Remediation (only when your outcome is BLOCKED)
44
48
 
49
+ **Exception — `housekeeping` builds nothing.** It changes no files and sits last in the chain, so a
50
+ `stage:fix` chore and a fresh instance have nothing to repair and nothing to re-block. It leaves its
51
+ bead open, reports `OUTCOME: BLOCKED` with the surviving worktrees, and hands the chore to a human.
52
+ Every step below applies to every other stage.
53
+
45
54
  A fresh instance of this stage is required: the bead you are closing cannot re-run itself.
46
55
 
47
56
  1. **Cap check.** Read `maxFixRounds` from `skills/planning/create-task/pipeline.json` and count
@@ -89,6 +98,9 @@ dirty worktree counts as a failed stage whatever the outcome line claims.
89
98
  3. Report `COMMIT`, `PUSHED`, and `PR`. The `document` stage also opens or updates the feature PR
90
99
  and reports its URL; every earlier stage reports `PR: none`.
91
100
 
101
+ **Exception — `housekeeping`.** It changes no files, so it commits and pushes nothing. Report
102
+ `COMMIT: none` and `PUSHED: no` with `OUTCOME: SUCCESS`; never create an empty commit.
103
+
92
104
  ## Instructions
93
105
  1. Read and follow the playbook file listed above.
94
106
  2. Treat the bead as the source-of-truth specification (not a prompt script).
@@ -101,7 +113,7 @@ End your response with a `---REPORT---` block in exactly this format:
101
113
  ```
102
114
  ---REPORT---
103
115
  BEAD_ID: <id>
104
- STAGE_COMPLETED: <code|verify|review|document|fix>
116
+ STAGE_COMPLETED: <code|verify|review|document|housekeeping|fix>
105
117
  SUMMARY: <2-3 sentence summary of what was done>
106
118
  FILES_CHANGED: <comma-separated list, or "none">
107
119
  COMMIT: <sha of the stage commit, or "none">
@@ -51,10 +51,20 @@ rather than inventing a dotted form:
51
51
  `ask_user`) — and `task` / `read_agent` / `wait_for_agents` subagent tools, the last three
52
52
  supplied by this repo's own `extensions/subagent` rather than by pi; `skill` maps
53
53
  to `read` (skills load by reading the listed SKILL.md path).
54
- - **`paths`** — `plugin_root`, `agents_dir`, and `skills_dir`. These are the install-time
55
- variables that differ per harness. `plugin_root` is the anchor; the other two resolve
56
- relative to it (`{base, relative}` specs) so a harness only overrides the single root it
57
- actually installs into.
54
+ - **`paths`** — `plugin_root`, `agents_dir`, `skills_dir`, and `skill_anatomy`. These are the
55
+ install-time variables that differ per harness. `plugin_root` is the anchor; the others
56
+ resolve relative to it (`{base, relative}` specs) so a harness only overrides the single
57
+ root it actually installs into.
58
+ A path entry whose relative path sits under `docs/` names a document the install ships:
59
+ both installers copy it from the package root into the staged tree at that relative path
60
+ (`scripts/lib/docs.mjs`), so a skill can read it on a machine with no checkout. One copy
61
+ per install serves every skill that references it, and the token map decides both the
62
+ location and the shipment — `skill_anatomy` is read by both `write-a-skill` and
63
+ `refactor-skill`. The repo's `docs/` stays the single source of truth; the staged file is
64
+ a verbatim copy, never a second authored version.
65
+ A second shipped doc is declared twice: the `paths` entry sets its install location, and
66
+ `package.json` `files` must list the same path. The test stages from the checkout, so only
67
+ the `files` whitelist catches a doc that the published package omits.
58
68
  Claude's `plugin_root` stays the `${CLAUDE_PLUGIN_ROOT}` env-var token, never a literal
59
69
  `~/.agent-cortex/claude`: Claude Code **copies** marketplace-installed plugins — including
60
70
  local directory sources like `~/.agent-cortex/claude` — into its own cache, so only the
package/token-map.json CHANGED
@@ -39,6 +39,10 @@
39
39
  "skills_dir": {
40
40
  "claude": { "base": "plugin_root", "relative": "skills" },
41
41
  "pi": { "base": "plugin_root", "relative": "skills" }
42
+ },
43
+ "skill_anatomy": {
44
+ "claude": { "base": "plugin_root", "relative": "docs/skills/skill-anatomy.md" },
45
+ "pi": { "base": "plugin_root", "relative": "docs/skills/skill-anatomy.md" }
42
46
  }
43
47
  },
44
48
  "agents": {
@@ -75,7 +79,8 @@
75
79
  },
76
80
  "paths": {
77
81
  "pi": "plugin_root is the staged package `agent-cortex install pi` materialises at ~/.agent-cortex/pi; pi loads that directory by path and the shipped extensions resolve their own resources relative to it (agent-modes computes plugin_root from its actual install location). A checkout or symlinked install differs — installers compute plugin_root from the actual resolution and can override it (--plugin-root).",
78
- "claude": "plugin_root stays ${CLAUDE_PLUGIN_ROOT} (env var) for the materialised plugin — never a literal ~/.agent-cortex/claude. Claude Code copies marketplace-installed plugins (including local directory sources like ~/.agent-cortex/claude) into its own cache and expands the env var per context, so a literal home path in substituted files would break under cache-copy; the installer passes the env form through verbatim."
82
+ "claude": "plugin_root stays ${CLAUDE_PLUGIN_ROOT} (env var) for the materialised plugin — never a literal ~/.agent-cortex/claude. Claude Code copies marketplace-installed plugins (including local directory sources like ~/.agent-cortex/claude) into its own cache and expands the env var per context, so a literal home path in substituted files would break under cache-copy; the installer passes the env form through verbatim.",
83
+ "skill_anatomy": "A path entry whose relative path sits under docs/ names a document the install ships: both installers copy it from the package root into the staged tree at that relative path (scripts/lib/docs.mjs), so a skill can read it on a machine with no checkout. One copy per install serves every skill that references it — write-a-skill and refactor-skill both do. The repo's docs/ stays the single source of truth; the staged copy is a verbatim copy, never a second authored version."
79
84
  },
80
85
  "agents": {
81
86
  "prefix": "Claude strips the agent-cortex: name prefix during conversion (NAME_PREFIX in bin/installers/claude.mjs): pi agents are agent-cortex:<slug>, claude agents are <slug>."