@jaybeeuu/agent-cortex 2.4.2 → 2.5.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jaybeeuu/agent-cortex",
3
- "version": "2.4.2",
3
+ "version": "2.5.1",
4
4
  "description": "Personal PI package with custom agents, skills, and extensions",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -11,7 +11,9 @@
11
11
  "url": "https://github.com/jaybeeuu/agent-cortex"
12
12
  },
13
13
  "bin": {
14
- "agent-cortex": "./bin/agent-cortex.mjs"
14
+ "agent-cortex": "./bin/agent-cortex.mjs",
15
+ "cortex": "./bin/agent-cortex.mjs",
16
+ "ctx": "./bin/agent-cortex.mjs"
15
17
  },
16
18
  "files": [
17
19
  "bin/",
@@ -40,6 +40,15 @@ const EXECUTION_LABEL_PREFIX = "execution:";
40
40
  */
41
41
  const V1_LABEL_VALUES = ["afk", "hitl"];
42
42
 
43
+ /**
44
+ * Ids per `bd label add` / `bd label remove` call. bd applies a label change id
45
+ * by id, so one call carrying every id in a large workspace takes minutes and
46
+ * can report success having persisted only part of the list — which is how a
47
+ * 710-bead rename silently left 280 beads unrenamed. A chunk keeps each call
48
+ * small enough to apply in full.
49
+ */
50
+ const LABEL_BATCH_SIZE = 20;
51
+
43
52
  /**
44
53
  * Where loose idea records live, relative to the working directory. This is the
45
54
  * one path the command assumes, and it is deliberately the directory every
@@ -262,9 +271,27 @@ async function findLegacyLabelBeads({ bd }) {
262
271
  return pending.sort((a, b) => a.id.localeCompare(b.id));
263
272
  }
264
273
 
274
+ /**
275
+ * Split `items` into consecutive runs of at most `size` — one `bd` call's worth.
276
+ *
277
+ * `size` must be positive: a size of 0 never advances the loop.
278
+ *
279
+ * @template T
280
+ * @param {T[]} items
281
+ * @param {number} size
282
+ * @returns {T[][]}
283
+ */
284
+ function chunkItems(items, size) {
285
+ /** @type {T[][]} */
286
+ const chunks = [];
287
+ for (let start = 0; start < items.length; start += size) chunks.push(items.slice(start, start + size));
288
+ return chunks;
289
+ }
290
+
265
291
  /**
266
292
  * Apply the label rename in batches: add the target label for a group of beads,
267
293
  * then remove the legacy one, so a bead is never left without its axis label.
294
+ * Both writes carry one chunk of ids, never the whole workspace.
268
295
  *
269
296
  * @param {StepContext} ctx
270
297
  * @param {Pending[]} pending
@@ -283,8 +310,10 @@ async function renameLegacyLabels({ bd }, pending) {
283
310
 
284
311
  for (const from of [...groups.keys()].sort()) {
285
312
  const { to, ids } = /** @type {{ to: string, ids: string[] }} */ (groups.get(from));
286
- await bd(["label", "add", ...ids, to]);
287
- await bd(["label", "remove", ...ids, from]);
313
+ for (const chunk of chunkItems(ids, LABEL_BATCH_SIZE)) {
314
+ await bd(["label", "add", ...chunk, to]);
315
+ await bd(["label", "remove", ...chunk, from]);
316
+ }
288
317
  }
289
318
  }
290
319
 
@@ -2,12 +2,15 @@
2
2
  // Validator for the composable agent directory layout: `agent.md` plus at
3
3
  // least one harness subdirectory with a valid `frontmatter.json`.
4
4
  //
5
- // Zero dependencies: import the pure functions, or run the test suite at
6
- // `test/validate-agent-dir.test.mjs`.
5
+ // Repo tooling, not a shipped skill: the composable layout exists only in this
6
+ // repository's `agents/` tree, and installs carry flat rendered agents. Run the
7
+ // test suite at `test/validate-agent-dir.test.mjs`.
7
8
 
8
- import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
9
+ import { readdir, readFile } from "node:fs/promises";
9
10
  import { join } from "node:path";
10
11
 
12
+ import { isDirectory, isFile } from "./lib/fs.mjs";
13
+
11
14
  // ─── Types ────────────────────────────────────────────────────────────────────
12
15
 
13
16
  /**
@@ -96,28 +99,31 @@ export function validateFrontmatter(fm) {
96
99
 
97
100
  /**
98
101
  * @param {string} agentDirPath
99
- * @returns {ValidationResult}
102
+ * @returns {Promise<ValidationResult>}
100
103
  */
101
- export function validateAgentDir(agentDirPath) {
104
+ export async function validateAgentDir(agentDirPath) {
102
105
  /** @type {string[]} */
103
106
  const errors = [];
104
107
 
105
108
  // Check agent.md exists
106
109
  const agentMdPath = join(agentDirPath, "agent.md");
107
- if (!existsSync(agentMdPath)) {
110
+ if (!(await isFile(agentMdPath))) {
108
111
  errors.push("missing agent.md");
109
112
  }
110
113
 
111
114
  // Check for at least one harness subdirectory with frontmatter.json
112
- if (!existsSync(agentDirPath)) {
115
+ if (!(await isDirectory(agentDirPath))) {
113
116
  return { ok: false, errors: ["agent directory does not exist"] };
114
117
  }
115
118
 
116
- const entries = readdirSync(agentDirPath);
117
- const harnessDirs = entries.filter((entry) => {
118
- const fullPath = join(agentDirPath, entry);
119
- return statSync(fullPath).isDirectory() && entry !== "node_modules";
120
- });
119
+ const entries = await readdir(agentDirPath);
120
+ /** @type {string[]} */
121
+ const harnessDirs = [];
122
+ for (const entry of entries) {
123
+ if (entry !== "node_modules" && (await isDirectory(join(agentDirPath, entry)))) {
124
+ harnessDirs.push(entry);
125
+ }
126
+ }
121
127
 
122
128
  if (harnessDirs.length === 0) {
123
129
  errors.push("no harness subdirectories found (need at least one with frontmatter.json)");
@@ -126,12 +132,12 @@ export function validateAgentDir(agentDirPath) {
126
132
  // Validate each harness directory
127
133
  for (const harnessDir of harnessDirs) {
128
134
  const frontmatterPath = join(agentDirPath, harnessDir, "frontmatter.json");
129
- if (!existsSync(frontmatterPath)) {
135
+ if (!(await isFile(frontmatterPath))) {
130
136
  continue; // Skip directories without frontmatter.json
131
137
  }
132
138
 
133
139
  try {
134
- const content = readFileSync(frontmatterPath, "utf-8");
140
+ const content = await readFile(frontmatterPath, "utf-8");
135
141
  const fm = JSON.parse(content);
136
142
  const result = validateFrontmatter(fm);
137
143
  if (!result.ok) {
@@ -10,7 +10,9 @@ node <skill-scripts>/create-chores.mjs --parent <bead-id> [--priority <0-4>]
10
10
 
11
11
  - Reads `<skill>/pipeline.json`; creates each stage chore via `bd create` and wires dependencies via `bd dep add`.
12
12
  - Prints a JSON object mapping each stage ID to its chore bead ID, plus `featurePrReview` for the PR gate.
13
- - Expands a bead that already exists — it never creates a parent. It is also not idempotent: running it against a bead that already has stage chores creates a second chain, so callers check for existing `stage:*` children first (ralph's expansion guard).
13
+ - Expands a bead that already exists — it never creates a parent.
14
+ - **Idempotent**: it reads that parent's children once (`bd children <parent-id> --json`) and adopts an existing child by its role label — `stage:<id>` for a stage chore, `lifecycle:feature-pr` for the gate — creating only what is missing. Deps that are already wired are skipped. A re-run on an expanded bead creates nothing and prints the same mapping, so re-running after a failed expansion is the recovery path.
15
+ - Adoption ignores bead status and reads only labels: a chore a later stage already closed is still adopted and reprinted in the mapping, which keeps a re-run convergent instead of creating a duplicate. When a role label appears twice under one parent the first-listed child wins. Adoption is scoped to that parent's children and keyed on labels, never titles, so a stray chore from another bead is never adopted.
14
16
 
15
17
  ## Stage chore beads
16
18
 
@@ -86,4 +88,15 @@ The deliverable this feature produces and the problem it solves.
86
88
  | review | Review | verify |
87
89
  | document | Document | review |
88
90
 
89
- `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.
91
+ `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
+
93
+ ## Stage templates
94
+
95
+ `templates/` holds one prompt skeleton per stage; a dispatcher fills one to compose that stage's
96
+ chore prompt. The REPORT contract is not defined here —
97
+ `skills/workflow/run-pipeline-stage/prompts/stage-runner.md` owns it and every template mirrors it.
98
+ Drift between the two fails silently: a worker that follows a drifted template emits a well-formed
99
+ block with keys no one reads, so routing stops at that stage. Drift has already broken routing
100
+ twice. Compare a template with the authority whenever either changes. The intended guard is a lint
101
+ parity check beside `scripts/check-skill-frontmatter.mjs`, not a test — a test here would assert on
102
+ file text, which is a content inventory, and `style-tests` bans that shape.
@@ -81,7 +81,7 @@ Turn a piece of work into a tracked bead, classify it for autonomous (AFK) or hu
81
81
 
82
82
  The planning bead is cold-start ready — what and why, decisions already made, open questions and risks, references — and needs `## Acceptance Criteria`, which `--validate` requires for type `task` (template in `REFERENCE.md` → Hierarchy). A deliverable that genuinely needs a human — visual sign-off, credentials, hardware — is created `-l execution:hitl` and stops there.
83
83
 
84
- 7. **Expand into pipeline stage chores.** Run the `create-chores` script, which reads this skill's `pipeline.json` and deterministically creates all stage chores plus the PR gate bead in one shot:
84
+ 7. **Expand into pipeline stage chores.** Run the `create-chores` script, which reads this skill's `pipeline.json` and creates the stage chores plus the PR gate bead — adopting any the parent already has, so a re-run converges on the existing tree instead of duplicating it:
85
85
 
86
86
  ```bash
87
87
  # Run from the TARGET project's directory — bd resolves its beads DB from cwd;
@@ -92,13 +92,15 @@ Turn a piece of work into a tracked bead, classify it for autonomous (AFK) or hu
92
92
 
93
93
  `<skill-scripts>` is the absolute path to this skill's own `scripts/` directory, resolved from wherever this skill was loaded — it varies by harness and install location, so never hardcode one harness's path.
94
94
 
95
+ The script is idempotent: it adopts an existing child by its `stage:<id>` or `lifecycle:feature-pr` label, creates only what is missing, and skips deps that are already wired. If an expansion fails partway, re-run this command — it completes the partial chain and prints the full mapping.
96
+
95
97
  On the feature path, run it **once per AFK impl child**, with `--parent <impl-task-id>` — never the feature ID: stage chores inherit their parent's labels, the script checks only that it was handed a parent ID (`REFERENCE.md` → PR gate bead), so an `execution:hitl` feature parent yields a chore tree no executor routes. A deliverable created `execution:hitl` in step 6 is not expanded.
96
98
 
97
99
  8. **Report.** Return the bead ID (or, on the feature path, the feature ID plus every planning and impl child ID), the classification of each expanded bead (`AFK`), and the created stage chore IDs from the script output (including `featurePrReview`) — one mapping per expanded impl child on the feature path.
98
100
 
99
101
  ### Expanding an existing bead
100
102
 
101
- Steps 2–6 create beads; step 7 expands one. A bead that already exists but has no `stage:*` chore children — an AFK task filed before the pipeline existed, or one ralph found chore-less — is a supported entry point: run step 7's `create-chores.mjs --parent <existing-id> --priority <its-own-priority>` directly, then report the mapping. No create steps, no new description, no re-classification.
103
+ Steps 2–6 create beads; step 7 expands one. A bead that already exists but has no `stage:*` chore children — an AFK task filed before the pipeline existed, or one ralph found chore-less — is a supported entry point: run step 7's `create-chores.mjs --parent <existing-id> --priority <its-own-priority>` directly, then report the mapping. No create steps, no new description, no re-classification. A partial chain from a failed run is repaired the same way: re-running adopts the chores that exist, creates the missing ones, and reparents nothing.
102
104
 
103
105
  **Never re-run `create-task` for an existing bead.** It creates a bead first and expands second, so the run leaves a **second parent** beside the original, which stays chore-less — the duplicate ralph's expansion guard exists to prevent. `create-chores.mjs` expands a bead that already exists; only step 2 creates a bead.
104
106
 
@@ -110,6 +112,7 @@ Steps 2–6 create beads; step 7 expands one. A bead that already exists but has
110
112
  - **Skipping the classifier and classifying by hand.** The rubric is a subagent's job; a hand-rolled guess drifts from it.
111
113
  - **Expanding an HITL bead.** If classification is HITL, stop — creating a chore tree for human-driven work is wrong.
112
114
  - **Expanding an existing bead by re-running `create-task`.** That step creates a bead, so the original stays chore-less and gains a second parent beside it. Run `create-chores.mjs --parent <existing-id>` instead.
115
+ - **Treating a failed expansion as unrecoverable.** `create-chores.mjs` is idempotent: re-run it and it adopts the chores that exist, creates the rest, and reports the whole mapping.
113
116
  - **Hardcoding `<skill-scripts>`.** The install path differs per harness (plugin cache, installed-plugins, dev checkout); resolve it at runtime.
114
117
  - **Creating a feature with no planning bead.** A feature that goes straight to impl children is a feature nobody designed. The planning bead keeps the children blocked until the design is confirmed.
115
118
  - **Expressing ownership as a dependency.** `bd dep add <child> <parent>` mixes hierarchy into the readiness gate; pass `--parent` at create time instead.
@@ -2,6 +2,11 @@
2
2
  // Pre-create the pipeline stage chores for a task bead, plus the feature PR
3
3
  // review gate, from `pipeline.json`.
4
4
  //
5
+ // Idempotent: a re-run converges on the tree that already exists instead of adding a
6
+ // second chain beside it, so re-running after a failed expansion is the recovery path.
7
+ // Existing children are adopted by their role label (`stage:<id>`, `lifecycle:feature-pr`)
8
+ // under the given parent — never by title, and never across parents.
9
+ //
5
10
  // The parent is an `execution:afk` bead. Stage chores inherit that label from it, so
6
11
  // create-chores does not classify: a parent that is hitl or unlabelled silently yields
7
12
  // a chore tree no executor routes. Check the parent's labels before invoking this.
@@ -69,6 +74,66 @@ function run(args) {
69
74
  return execSync(cmd.join(' '), { encoding: 'utf-8' }).trim();
70
75
  }
71
76
 
77
+ // ─── Existing children ────────────────────────────────────────────────────────
78
+
79
+ /**
80
+ * @typedef {object} Dependency
81
+ * @property {string} depends_on_id
82
+ * @property {string} type
83
+ */
84
+
85
+ /**
86
+ * A child of the parent that already fills one of this script's roles.
87
+ * @typedef {object} Adoptable
88
+ * @property {string} id
89
+ * @property {Set<string>} blocks Beads it already depends on with a `blocks` dep
90
+ */
91
+
92
+ /**
93
+ * Read the parent's children once and index the ones this script owns by their role
94
+ * label. Scoped to that parent's children, so a stray chore belonging to another bead
95
+ * is never adopted. When a role label appears more than once the first-listed child
96
+ * wins, so a converged re-run is deterministic.
97
+ * @param {string} parentId
98
+ * @returns {Map<string, Adoptable>} Keyed by `stage:<id>` or `lifecycle:feature-pr`
99
+ */
100
+ function readAdoptable(parentId) {
101
+ const raw = run(['bd', 'children', parentId, '--json']);
102
+ /** @type {Array<{ id: string, labels?: string[], dependencies?: Dependency[] }>} */
103
+ const children = raw === '' ? [] : JSON.parse(raw);
104
+
105
+ /** @type {Map<string, Adoptable>} */
106
+ const adoptable = new Map();
107
+
108
+ for (const child of children) {
109
+ const blocks = new Set(
110
+ (child.dependencies ?? [])
111
+ .filter((dep) => dep.type === 'blocks')
112
+ .map((dep) => dep.depends_on_id),
113
+ );
114
+
115
+ for (const label of child.labels ?? []) {
116
+ if (label !== 'lifecycle:feature-pr' && !label.startsWith('stage:')) { continue; }
117
+ if (!adoptable.has(label)) { adoptable.set(label, { id: child.id, blocks }); }
118
+ }
119
+ }
120
+
121
+ return adoptable;
122
+ }
123
+
124
+ /**
125
+ * Wire a `blocks` dependency unless the bead already carries it. Re-adding an existing
126
+ * dep is the failure mode a re-run must not hit, so existing deps are skipped.
127
+ * @param {string} beadId
128
+ * @param {string} dependsOnId
129
+ * @param {Set<string>} blocks Dependencies the bead already has; extended in place
130
+ */
131
+ function wireBlocks(beadId, dependsOnId, blocks) {
132
+ if (blocks.has(dependsOnId)) { return; }
133
+ run(['bd', 'dep', 'add', beadId, dependsOnId, '--type', 'blocks']);
134
+ blocks.add(dependsOnId);
135
+ }
136
+
72
137
  // ─── Main ─────────────────────────────────────────────────────────────────────
73
138
 
74
139
  const __filename = fileURLToPath(import.meta.url);
@@ -80,31 +145,38 @@ const pipelinePath = resolve(__dirname, '..', 'pipeline.json');
80
145
  /** @type {Pipeline} */
81
146
  const pipeline = JSON.parse(readFileSync(pipelinePath, 'utf-8'));
82
147
 
148
+ const adoptable = readAdoptable(parentId);
149
+
83
150
  /** @type {Map<string, string>} */
84
151
  const stageToBeadId = new Map();
85
152
 
86
153
  for (const stage of pipeline.stages) {
87
- const title = `[${parentId}] ${stage.title}`;
88
-
89
- const beadId = run([
90
- 'bd', 'create', JSON.stringify(title),
91
- '--type', 'chore',
92
- '--description', JSON.stringify(stage.description),
93
- '--priority', priority,
94
- '--labels', `stage:${stage.id}`,
95
- '--parent', parentId,
96
- '--silent',
97
- ]);
154
+ const adopted = adoptable.get(`stage:${stage.id}`);
155
+
156
+ const beadId = adopted
157
+ ? adopted.id
158
+ : run([
159
+ 'bd', 'create', JSON.stringify(`[${parentId}] ${stage.title}`),
160
+ '--type', 'chore',
161
+ '--description', JSON.stringify(stage.description),
162
+ '--priority', priority,
163
+ '--labels', `stage:${stage.id}`,
164
+ '--parent', parentId,
165
+ '--silent',
166
+ ]);
98
167
 
99
168
  stageToBeadId.set(stage.id, beadId);
100
169
 
170
+ /** @type {Set<string>} */
171
+ const blocks = adopted ? adopted.blocks : new Set();
172
+
101
173
  for (const depStageId of stage.dependsOn) {
102
174
  const depBeadId = stageToBeadId.get(depStageId);
103
175
  if (!depBeadId) {
104
176
  console.error(`Stage '${stage.id}' depends on '${depStageId}', but it has not been created yet. Check dependsOn ordering in pipeline.json.`);
105
177
  process.exit(1);
106
178
  }
107
- run(['bd', 'dep', 'add', beadId, depBeadId, '--type', 'blocks']);
179
+ wireBlocks(beadId, depBeadId, blocks);
108
180
  }
109
181
  }
110
182
 
@@ -120,26 +192,33 @@ if (!documentStageBeadId) {
120
192
  process.exit(1);
121
193
  }
122
194
 
123
- const prGateBeadId = run([
124
- 'bd', 'create', JSON.stringify(`[${parentId}] PR Review and Merge`),
125
- '--type', 'task',
126
- '--description', JSON.stringify(
127
- 'Human review gate: review and merge the agent PR into the feature branch before closing this feature.\n\n## Acceptance Criteria\n\n- [ ] Agent PR reviewed and merged into the feature branch.',
128
- ),
129
- // The gate is type `task`, so `--validate` enforces its `## Acceptance Criteria`
130
- // section. Stage chores require no sections and deliberately omit the flag.
131
- '--validate',
132
- '--priority', priority,
133
- '--labels', 'execution:hitl,lifecycle:feature-pr',
134
- '--parent', parentId,
135
- // The gate declares its own execution class (it is human-owned review work), so opt
136
- // out of bd's create-time parent-label inheritance and its explicit :hitl is never
137
- // joined by an inherited :afk. Stage chores deliberately keep inheriting the parent's
138
- // :afk -- do not add this flag there.
139
- '--no-inherit-labels',
140
- '--silent',
141
- ]);
142
- run(['bd', 'dep', 'add', prGateBeadId, documentStageBeadId, '--type', 'blocks']);
195
+ const adoptedGate = adoptable.get('lifecycle:feature-pr');
196
+
197
+ const prGateBeadId = adoptedGate
198
+ ? adoptedGate.id
199
+ : run([
200
+ 'bd', 'create', JSON.stringify(`[${parentId}] PR Review and Merge`),
201
+ '--type', 'task',
202
+ '--description', JSON.stringify(
203
+ 'Human review gate: review and merge the agent PR into the feature branch before closing this feature.\n\n## Acceptance Criteria\n\n- [ ] Agent PR reviewed and merged into the feature branch.',
204
+ ),
205
+ // The gate is type `task`, so `--validate` enforces its `## Acceptance Criteria`
206
+ // section. Stage chores require no sections and deliberately omit the flag.
207
+ '--validate',
208
+ '--priority', priority,
209
+ '--labels', 'execution:hitl,lifecycle:feature-pr',
210
+ '--parent', parentId,
211
+ // The gate declares its own execution class (it is human-owned review work), so opt
212
+ // out of bd's create-time parent-label inheritance and its explicit :hitl is never
213
+ // joined by an inherited :afk. Stage chores deliberately keep inheriting the parent's
214
+ // :afk -- do not add this flag there.
215
+ '--no-inherit-labels',
216
+ '--silent',
217
+ ]);
218
+
219
+ /** @type {Set<string>} */
220
+ const gateBlocks = adoptedGate ? adoptedGate.blocks : new Set();
221
+ wireBlocks(prGateBeadId, documentStageBeadId, gateBlocks);
143
222
 
144
223
  /** @type {Record<string, string>} */
145
224
  const result = {
@@ -31,10 +31,21 @@ Implement the task described above using a test-driven approach:
31
31
 
32
32
  Make only the changes required to complete this task — do not refactor unrelated code.
33
33
 
34
- End your response with a ---REPORT--- block:
34
+ Commit and push the stage's work before reporting (see _Deliver before reporting_ in
35
+ `prompts/stage-runner.md`). A dirty worktree counts as a failed stage.
36
+
37
+ End your response with a ---REPORT--- block. The `run-pipeline-stage` skill's
38
+ `prompts/stage-runner.md` holds the authoritative REPORT contract — if this block and that
39
+ prompt differ, follow the prompt and correct this template.
35
40
  ---REPORT---
36
41
  BEAD_ID: <id>
37
- STAGE_COMPLETED: coding
42
+ STAGE_COMPLETED: code
38
43
  SUMMARY: <2–3 sentence summary of what was done>
39
44
  FILES_CHANGED: <comma-separated list, or "none">
45
+ COMMIT: <sha of the stage commit, or "none">
46
+ PUSHED: <yes|no>
47
+ PR: none
48
+ OUTCOME: <SUCCESS|BLOCKED>
49
+ BLOCKING_ISSUES: ← include only if OUTCOME is BLOCKED; one line per issue
50
+ - <what blocks the stage and what would unblock it>
40
51
  ---
@@ -31,10 +31,21 @@ Review the changes made and compare them against the existing documentation in t
31
31
  when no decisions or macro behaviour changed.
32
32
  6. Do not modify source code — documentation only.
33
33
 
34
- End your response with a ---REPORT--- block:
34
+ Commit and push the docs, and open or update the feature PR before reporting (see _Deliver
35
+ before reporting_ in `prompts/stage-runner.md`). A dirty worktree counts as a failed stage.
36
+
37
+ End your response with a ---REPORT--- block. The `run-pipeline-stage` skill's
38
+ `prompts/stage-runner.md` holds the authoritative REPORT contract — if this block and that
39
+ prompt differ, follow the prompt and correct this template.
35
40
  ---REPORT---
36
41
  BEAD_ID: <id>
37
- STAGE_COMPLETED: documenting
42
+ STAGE_COMPLETED: document
38
43
  SUMMARY: <2–3 sentence summary of what was documented, or "No documentation changes required">
39
44
  FILES_CHANGED: <comma-separated list of docs touched, or "none">
45
+ COMMIT: <sha of the stage commit, or "none">
46
+ PUSHED: <yes|no>
47
+ PR: <feature PR url>
48
+ OUTCOME: <SUCCESS|BLOCKED>
49
+ BLOCKING_ISSUES: ← include only if OUTCOME is BLOCKED; one line per issue
50
+ - <what blocks the stage and what would unblock it>
40
51
  ---
@@ -18,10 +18,21 @@ Apply only the changes described in the Task section above. Do not make any othe
18
18
 
19
19
  **Use context7** when working with any third-party library — fetch live docs to ensure you use the correct, up-to-date API for the version in this project.
20
20
 
21
- End your response with a ---REPORT--- block:
21
+ Commit and push the fix before reporting (see _Deliver before reporting_ in
22
+ `prompts/stage-runner.md`). A dirty worktree counts as a failed stage.
23
+
24
+ End your response with a ---REPORT--- block. The `run-pipeline-stage` skill's
25
+ `prompts/stage-runner.md` holds the authoritative REPORT contract — if this block and that
26
+ prompt differ, follow the prompt and correct this template.
22
27
  ---REPORT---
23
28
  BEAD_ID: <id>
24
- STAGE_COMPLETED: fixing
29
+ STAGE_COMPLETED: fix
25
30
  SUMMARY: <2–3 sentence summary of what was changed>
26
31
  FILES_CHANGED: <comma-separated list, or "none">
32
+ COMMIT: <sha of the stage commit, or "none">
33
+ PUSHED: <yes|no>
34
+ PR: none
35
+ OUTCOME: <SUCCESS|BLOCKED>
36
+ BLOCKING_ISSUES: ← include only if OUTCOME is BLOCKED; one line per issue
37
+ - <what remains unresolved and what would unblock it>
27
38
  ---
@@ -21,23 +21,29 @@ Discover and run the project's test and lint commands, then report the outcome.
21
21
  1. `package.json` `scripts` — look for keys named `test`, `lint`, `check`, `typecheck`, or similar.
22
22
  2. `Makefile` — look for targets named `test`, `lint`, `check`.
23
23
  3. Other well-known config files (e.g. `pyproject.toml`, `Cargo.toml`) — look for test/lint targets.
24
- 4. If no commands are found, report PASS with summary "No test or lint commands found".
24
+ 4. If no commands are found, report `OUTCOME: SUCCESS` with summary "No test or lint commands found".
25
25
 
26
26
  **Step 2 — Run each discovered command** in the project root. Capture stdout and stderr.
27
27
 
28
28
  **Step 3 — Evaluate**:
29
- - If all commands exit 0: `VERIFY_OUTCOME: PASS`.
30
- - If any command exits non-zero: `VERIFY_OUTCOME: FAIL`. List each failure concisely.
29
+ - If all commands exit 0: `OUTCOME: SUCCESS`.
30
+ - If any command exits non-zero: `OUTCOME: BLOCKED`. List each failure concisely under
31
+ `BLOCKING_ISSUES`.
31
32
 
32
33
  Do not fix failures — report them only.
33
34
 
34
- End your response with a ---REPORT--- block:
35
+ End your response with a ---REPORT--- block. The `run-pipeline-stage` skill's
36
+ `prompts/stage-runner.md` holds the authoritative REPORT contract — if this block and that
37
+ prompt differ, follow the prompt and correct this template.
35
38
  ---REPORT---
36
39
  BEAD_ID: <id>
37
- STAGE_COMPLETED: verifying
40
+ STAGE_COMPLETED: verify
38
41
  SUMMARY: <what commands were run and overall outcome>
39
42
  FILES_CHANGED: none
40
- VERIFY_OUTCOME: <PASS|FAIL>
41
- VERIFY_FAILURES: ← only if VERIFY_OUTCOME is FAIL
42
- - <test/lint failure summary>
43
+ COMMIT: <sha, or "none" if this stage added no commit>
44
+ PUSHED: <yes|no>
45
+ PR: none
46
+ OUTCOME: <SUCCESS|BLOCKED>
47
+ BLOCKING_ISSUES: ← include only if OUTCOME is BLOCKED; one line per failure
48
+ - <command, its exit status, and the failure summary>
43
49
  ---
@@ -77,10 +77,16 @@ Mechanics the plan reflects:
77
77
  2. Group bead ids by label value. A bead carrying both `afk` and `hitl` appears in both groups;
78
78
  that is a data problem worth inspecting, not a conflict the tool resolves for you.
79
79
  3. Per group, on `--apply`: `bd label add <ids…> execution:<value>` then
80
- `bd label remove <ids…> implementation-type:<value>`. Add-then-remove keeps the routing label
81
- present at every instant, so nothing is unroutable mid-flight.
80
+ `bd label remove <ids…> implementation-type:<value>`, at most 20 ids per call. Add-then-remove
81
+ keeps the routing label present at every instant, so nothing is unroutable mid-flight.
82
82
  4. Re-running after convergence finds zero targets — the no-op that proves idempotence.
83
83
 
84
+ The 20-id ceiling is a `bd` limit, not a preference. `bd label add|remove` applies a list id by
85
+ id and exits 0 even when it persisted only a prefix of it: one call carrying 710 ids left 280
86
+ beads unrenamed while reporting success. Every call in this skill therefore carries one chunk of
87
+ 20 ids, and the per-step verification re-detects from the export instead of trusting the exit
88
+ code. Any hand-run recipe below needs the same ceiling.
89
+
84
90
  Verification commands:
85
91
 
86
92
  ```bash
@@ -153,7 +159,8 @@ inversion.
153
159
 
154
160
  Every `bd label` call in this skill passes issue ids first and the label last, matching the CLI's
155
161
  `bd label add [issue-id...] [label[,label...]]`. Reversing that order fails: the ids are resolved
156
- before the trailing argument is read as a label.
162
+ before the trailing argument is read as a label. Keep each hand-run call to about 20 ids for the
163
+ same reason the command chunks: a longer list exits 0 having applied only part of itself.
157
164
 
158
165
  ```bash
159
166
  # Invert the v1 rename for a specific group
@@ -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. 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 — 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,11 @@ 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.
60
+ status. A bead with a full chain — one `stage:*` chore per stage in `pipeline.json` (four for the
61
+ shipped pipeline) plus the `lifecycle:feature-pr` gate — never needs an expansion. The check
62
+ answers "no chores at all", so a **partial** chain (a lower count from a failed run) passes it
63
+ and its missing stages never arrive. Nothing detects that automatically: re-run step 2's command
64
+ against that parent by hand, which is idempotent.
61
65
  2. **Dispatch an expansion worker** in the feature worktree (`.agent-cortex/worktrees/<parent-id>`) whose whole job is to run the pre-creator against the existing bead and report the JSON it printed:
62
66
  ```bash
63
67
  node <create-task-scripts>/create-chores.mjs \
@@ -93,8 +97,11 @@ On completion, remove the entry from `inflight` and confirm the chore tree exist
93
97
  bd children <parent-id> --json | grep -c '"stage:'
94
98
  ```
95
99
 
96
- Expect a non-zero count — one match per `stage:*` label in the chain. A printed `0` means the
97
- expansion failed. Surface the worker's output in chat and record the parent for the shutdown summary — **never re-dispatch the expansion**, because `create-chores.mjs` is not idempotent and a second run duplicates the chain beside the first. Then continue at the normal dispatch pass: either way nothing is created and nothing is closed by ralph.
100
+ Expect one match per `stage:*` label in the chain — four for the shipped `pipeline.json`; the
101
+ `featurePrReview` gate carries `lifecycle:feature-pr` instead, so it is not counted. A lower count
102
+ means the expansion did not finish: `0` created nothing, and a partial chain leaves stages that
103
+ will never become ready. Either way, surface the worker's output in chat, record the parent for the
104
+ shutdown summary, and re-dispatch the expansion: `create-chores.mjs` is idempotent, so the second run adopts the chores that already exist, creates only the missing ones, and prints the full mapping — re-running is the recovery path. Then continue at the normal dispatch pass: either way nothing is created and nothing is closed by ralph.
98
105
 
99
106
  ---
100
107
 
@@ -130,7 +137,7 @@ Decompose it:
130
137
  bd children <feature-id> --json
131
138
  ```
132
139
  Surface the created IDs in chat. An empty list means the decomposition failed: record the feature for the shutdown summary and continue — do not re-dispatch.
133
- 5. **Never re-dispatch a decomposition.** The bead creation it runs is not idempotent, so a second run duplicates the planning bead and every implementation child beside the first. This is the same hazard as re-running `create-chores.mjs`.
140
+ 5. **Never re-dispatch a decomposition.** The bead creation it runs is not idempotent, so a second run duplicates the planning bead and every implementation child beside the first. This hazard belongs to `create-task`'s bead creation alone — `create-chores.mjs` is idempotent, so re-running an expansion is its recovery path.
134
141
  6. **Go idle.** After decomposition nothing is dispatchable for that feature: `bd ready -l execution:afk` still lists the feature itself (it now has children, so it is skipped — never decomposed again) but none of its children until a human closes the planning bead. Returning to the event loop with *nothing* to dispatch is the correct outcome, not a failure to re-expand — that is Q5b working as designed.
135
142
 
136
143
  ---