@jaybeeuu/agent-cortex 2.4.1 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -0
- package/README.md +7 -1
- package/agents/ralph/agent.md +6 -4
- package/bin/agent-cortex.mjs +7 -5
- package/bin/installers/pi.mjs +35 -1
- package/lib/cli.mjs +29 -7
- package/package.json +4 -2
- package/scripts/lib/migrate.mjs +31 -2
- package/skills/engineering/design-an-interface/SKILL.md +1 -1
- package/skills/planning/create-task/REFERENCE.md +3 -1
- package/skills/planning/create-task/SKILL.md +5 -2
- package/skills/planning/create-task/scripts/create-chores.mjs +111 -32
- package/skills/productivity/migration/REFERENCE.md +10 -3
- package/skills/workflow/ralph/REFERENCE.md +12 -5
- package/skills/workflow/ralph/SKILL.md +7 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,60 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.5.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 8ca9462: New `cortex` and `ctx` commands: both run the same CLI as `agent-cortex`. The help text and the error hints now name the command that was invoked, so `ctx --help` prints `Usage: ctx <command>`; any other script name falls back to `agent-cortex`.
|
|
8
|
+
- 45992c7: `create-chores.mjs` is now idempotent: a re-run adopts the stage chores and PR gate that already exist under the parent, creates only what is missing, and skips deps that are already wired. Re-running it after a failed chore expansion is now the recovery path, so ralph no longer refuses to re-dispatch an expansion.
|
|
9
|
+
|
|
10
|
+
### Patch Changes
|
|
11
|
+
|
|
12
|
+
- 2cab55e: Chunk the `migrate` v1 label rename: each `bd label add` and `bd label remove` call
|
|
13
|
+
now carries at most 20 bead ids. One call carrying every id in a large workspace
|
|
14
|
+
applied only part of the list and still reported success, so v1 never converged and a
|
|
15
|
+
workspace could report itself migrated with hundreds of legacy labels attached.
|
|
16
|
+
- dd4e69c: Fail `agent-cortex install pi` when a skill sits outside `skills/<group>/<name>`.
|
|
17
|
+
|
|
18
|
+
pi advertises a skill by the absolute path of its `SKILL.md`, and the installer copies exactly
|
|
19
|
+
`skills/<group>/<name>`. A skill authored deeper was skipped without a trace, so no session was
|
|
20
|
+
ever told about it. The install now stops and names the offending path.
|
|
21
|
+
|
|
22
|
+
`docs/dev-loop.md` records the related limit that needs no code: a session lists its skills when
|
|
23
|
+
it starts, so a session already open across a re-stage holds the paths it was given.
|
|
24
|
+
|
|
25
|
+
## 2.4.2
|
|
26
|
+
|
|
27
|
+
### Patch Changes
|
|
28
|
+
|
|
29
|
+
- 015ede5: Stop the Claude install from dropping `{{TOOL:read_agent}}` and `{{TOOL:wait_for_agents}}`
|
|
30
|
+
into dangling prose. Both tokens are null on the claude column (Claude reports a subagent's
|
|
31
|
+
result inline, so there is no separate read or wait step), so the installer replaced them with
|
|
32
|
+
an empty string and warned three times — leaving `Read the completed agent's output with and
|
|
33
|
+
parse…` and two similar broken sentences in the installed `ralph` and `design-an-interface`
|
|
34
|
+
skills. Those sentences are now harness-neutral, so both installs read correctly and the
|
|
35
|
+
installer emits no null-mapping warning. A regression guard fails the installer test if a
|
|
36
|
+
shipped skill names a tool the claude column cannot map.
|
|
37
|
+
- 199cefe: Point the shipped pi settings at models that still exist. `tinyModel.model` was
|
|
38
|
+
`opencode/mimo-v2.5-free`, which the provider catalogue has dropped, so the
|
|
39
|
+
setting resolved to nothing and pi fell back to another free model. That fallback
|
|
40
|
+
returns `429 FreeUsageLimitError` once the free quota is spent, and the 429 JSON
|
|
41
|
+
became the entire output of the background call that runs auto-naming and
|
|
42
|
+
notifications — so a subagent run died at session start while the agent itself was
|
|
43
|
+
on a paid model. `tinyModel.model` is now `opencode-go/deepseek-v4.1-flash`.
|
|
44
|
+
`enabledModels` also drops three ids that do not resolve — `opencode-go/deepseek-v4-flash`,
|
|
45
|
+
`opencode/deepseek-v4-flash` and `opencode/mimo-v2.5-free` — which made pi print a
|
|
46
|
+
`No models match pattern` warning on every startup.
|
|
47
|
+
- 6b6080b: Stop the test suite from leaking temp workspaces into `/tmp`. Each test file that
|
|
48
|
+
creates workspaces under the system temp dir now shares one temp root that a
|
|
49
|
+
file-level `after()` hook removes, so the removal happens whether the tests
|
|
50
|
+
passed, failed, or threw before reaching their own cleanup — previously a single
|
|
51
|
+
green run of `test/installer.test.mjs` left 21 workspaces behind (7.4 MB apparent,
|
|
52
|
+
13 MB on disk: each is a materialised plugin tree of skills, agents and hooks),
|
|
53
|
+
plus one from `test/cli.test.mjs`'s `install claude --output` case. Repeated runs
|
|
54
|
+
could fill a small tmpfs and make unrelated tests fail with ENOSPC. A regression
|
|
55
|
+
guard runs a deliberately failing test file and asserts the run leaves nothing
|
|
56
|
+
behind.
|
|
57
|
+
|
|
3
58
|
## 2.4.1
|
|
4
59
|
|
|
5
60
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -151,6 +151,11 @@ long-lived credential in repository secrets to save one click per release.
|
|
|
151
151
|
|
|
152
152
|
## Installation
|
|
153
153
|
|
|
154
|
+
The CLI installs under three names — `agent-cortex`, `cortex` and `ctx` — that run the
|
|
155
|
+
same command, and the help and error text names the one you invoked. That naming is POSIX
|
|
156
|
+
only: the npm and pnpm shims on Windows call `bin/agent-cortex.mjs` directly, so the text
|
|
157
|
+
falls back to `agent-cortex` there.
|
|
158
|
+
|
|
154
159
|
### Pi package staging (`agent-cortex install pi`)
|
|
155
160
|
|
|
156
161
|
`agent-cortex install pi` materialises agent-cortex as a self-contained pi
|
|
@@ -480,7 +485,8 @@ is an empty legacy namespace, not just a clean live one (see
|
|
|
480
485
|
A bare `agent-cortex migrate` is a **dry run** — it prints the per-bead plan and writes
|
|
481
486
|
nothing. `--apply` runs every pending step in order and verifies each before moving on. It
|
|
482
487
|
shells out to the `bd` CLI on `PATH` (`BD_PATH` overrides) and fails loudly, rather than
|
|
483
|
-
half-applying, when `bd` is missing or a `bd` subcommand errors.
|
|
488
|
+
half-applying, when `bd` is missing or a `bd` subcommand errors. Label writes carry at most 20
|
|
489
|
+
ids per `bd` call, because `bd` can exit 0 having applied only part of a longer list.
|
|
484
490
|
|
|
485
491
|
Detection reads `bd export --all` and cross-checks `bd label list-all`, never a
|
|
486
492
|
label-filtered `bd list`: on bd 1.3.1 that query can omit closed beads, which once let a
|
package/agents/ralph/agent.md
CHANGED
|
@@ -77,7 +77,7 @@ A feature carries `execution:hitl` by default, so this whole procedure only runs
|
|
|
77
77
|
|
|
78
78
|
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, then let the normal dispatch pick up the resulting `stage:code` chore:
|
|
79
79
|
|
|
80
|
-
1. Confirm the bead is chore-less: `bd children <parent-id> --json | grep -c '"stage:'` prints `0` — add `|| true` under `set -e` or inside an `&&` chain, since `grep -c` exits `1` on a zero count and the printed number is the answer. A bead
|
|
80
|
+
1. Confirm the bead is chore-less: `bd children <parent-id> --json | grep -c '"stage:'` prints `0` — add `|| true` under `set -e` or inside an `&&` chain, since `grep -c` exits `1` on a zero count and the printed number is the answer. A bead with a full chain — one `stage:*` chore per stage in `pipeline.json`, plus the `lifecycle:feature-pr` gate — never needs an expansion. This check only sees a count of zero, so a **partial** chain is not detected here and its missing stages never arrive: re-run step 2's command against that parent by hand, which is idempotent.
|
|
81
81
|
2. Dispatch an **expansion worker** in the feature worktree (`.agent-cortex/worktrees/<parent-id>`, created per the branching model if it is missing) whose whole job is to run the pre-creator against the existing bead and report the JSON it printed:
|
|
82
82
|
|
|
83
83
|
```bash
|
|
@@ -86,6 +86,8 @@ A ready `execution:afk` task bead with no `stage:*` chore children has nothing t
|
|
|
86
86
|
```
|
|
87
87
|
|
|
88
88
|
`<create-task-scripts>` is the absolute path to the `create-task` skill's `scripts/` directory, resolved from wherever the skill was loaded. `<parent-priority>` is the parent bead's own priority from `bd show <parent-id>`.
|
|
89
|
+
|
|
90
|
+
The command is idempotent: it adopts an existing child by its `stage:<id>` or `lifecycle:feature-pr` label, creates only what is missing, skips deps that are already wired, and prints the resolved mapping either way. Re-running it is the recovery path for a failed expansion.
|
|
89
91
|
3. Record the worker in `inflight` as an **expansion** entry — it has no chore to key on, so `choreId` is `null`:
|
|
90
92
|
```json
|
|
91
93
|
{ "kind": "expand", "choreId": null, "parentId": "<parent-id>", "title": "<parent title>", "agentId": "<agent-id>", "logLine": 1 }
|
|
@@ -94,7 +96,7 @@ A ready `execution:afk` task bead with no `stage:*` chore children has nothing t
|
|
|
94
96
|
4. The worker reports the returned mapping — `code`, `verify`, `review`, `document`, `featurePrReview` — and closes nothing. It emits **no** `---REPORT---` block and owns no bead, so there is nothing to parse and no close to confirm.
|
|
95
97
|
5. Its completion means the parent now has a `stage:*` chain: the event loop's next `bd ready -l execution:afk` pass picks up the `stage:code` chore (event loop step 7).
|
|
96
98
|
|
|
97
|
-
**Expansion completion contract.** An expansion worker is dispatched like a stage worker but is not one: no chore claim, no `---REPORT---`, no leaf-bead close. Handle its completion with the expansion branch in event-loop step 3, which confirms the chain exists (`bd children <parent-id> --json | grep -c '"stage:'` prints
|
|
99
|
+
**Expansion completion contract.** An expansion worker is dispatched like a stage worker but is not one: no chore claim, no `---REPORT---`, no leaf-bead close. Handle its completion with the expansion branch in event-loop step 3, which confirms the chain exists (`bd children <parent-id> --json | grep -c '"stage:'` prints one match per stage chore — the gate carries `lifecycle:feature-pr` and is not counted) and dispatches the ready `stage:code` chore. A count below the stage total in `pipeline.json` is a partial chain, whose missing stages never arrive, so re-dispatch the expansion instead of dispatching on. If the parent still has no `stage:*` children the expansion failed: surface the worker's output in chat, record the parent for the shutdown summary, and re-dispatch the expansion — `create-chores.mjs` is idempotent, so a second run adopts the chores that exist, creates only the missing ones, and reports the whole mapping. Either way, create nothing and close nothing yourself.
|
|
98
100
|
|
|
99
101
|
**Never expand an existing bead by running `create-task`** (or any other command that creates a bead). `create-task` creates a bead first and expands second, so running it for an existing bead leaves a **second parent** beside the original, which stays chore-less and never gets dispatched. `create-chores.mjs` expands a bead that already exists; it never creates parents.
|
|
100
102
|
|
|
@@ -165,8 +167,8 @@ After initialization, Ralph waits for background agents to complete. Completions
|
|
|
165
167
|
1. **Flush** that bead's log using the procedure in the wake section to pick up any final lines for the chat summary (a mechanism that delivers the log with the completion needs no flush).
|
|
166
168
|
2. **Read** the completed agent's full output the way the wake section describes.
|
|
167
169
|
3. **Re-read** `.agent-cortex/ralph/state.json` to identify the entry for that agent ID, then branch on its `kind`:
|
|
168
|
-
- **`kind: "expand"` — expansion completion branch.** The worker claimed no chore, emits no `---REPORT---`, and owns no bead to confirm, so steps 4-6 **do not apply**: remove the entry from `inflight`, confirm the parent now has `stage:*` chore children (`bd children <parent-id> --json | grep -c '"stage:'`) and surface the reported mapping in chat, then go straight to step 7 — the new `stage:code` chore is ready to dispatch. If the
|
|
169
|
-
- **`kind: "decompose"` — decomposition completion branch.** Same shape as expansion: no chore, no `---REPORT---`, so steps 4-6 **do not apply**: remove the entry from `inflight`, confirm the feature now has children (`bd children <feature-id> --json` returns a non-empty list) and surface the created IDs in chat, then go straight to step 7. Nothing is dispatchable for that feature until a human closes the planning bead, so finding no ready work for it is the correct outcome — do **not** re-dispatch the decomposition (the bead creation it ran is not idempotent). If the feature still has no children, the worker failed: surface its output, record it for the shutdown summary, remove the entry, and continue at step 7. Never create or close a bead here.
|
|
170
|
+
- **`kind: "expand"` — expansion completion branch.** The worker claimed no chore, emits no `---REPORT---`, and owns no bead to confirm, so steps 4-6 **do not apply**: remove the entry from `inflight`, confirm the parent now has `stage:*` chore children (`bd children <parent-id> --json | grep -c '"stage:'`) and surface the reported mapping in chat, then go straight to step 7 — the new `stage:code` chore is ready to dispatch. If the count is `0` — or below the stage total, which is a partial chain whose later stages never arrive — the expansion failed: surface the worker's output, record the parent for the shutdown summary, remove the entry, and re-dispatch the expansion — re-running `create-chores.mjs` is idempotent and is the recovery path, so it repairs the tree instead of duplicating it. Never create or close a bead for an expansion.
|
|
171
|
+
- **`kind: "decompose"` — decomposition completion branch.** Same shape as expansion: no chore, no `---REPORT---`, so steps 4-6 **do not apply**: remove the entry from `inflight`, confirm the feature now has children (`bd children <feature-id> --json` returns a non-empty list) and surface the created IDs in chat, then go straight to step 7. Nothing is dispatchable for that feature until a human closes the planning bead, so finding no ready work for it is the correct outcome — do **not** re-dispatch the decomposition (the bead creation it ran is not idempotent; that hazard is `create-task`'s, not the expansion's). If the feature still has no children, the worker failed: surface its output, record it for the shutdown summary, remove the entry, and continue at step 7. Never create or close a bead here.
|
|
170
172
|
- **`kind: "stage"`** — a stage worker: continue with steps 4-6.
|
|
171
173
|
4. **Parse** the agent's `---REPORT---` block (see report format in the `run-pipeline-stage` skill).
|
|
172
174
|
5. **Confirm** the worker closed its own leaf execution bead: `bd show <chore-id>` should read `closed`. If it is still open the worker died mid-stage — read its notes and re-dispatch. Never close it yourself.
|
package/bin/agent-cortex.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { parseArgs, buildHelpText, validateHarness } from "../lib/cli.mjs";
|
|
3
|
+
import { parseArgs, buildHelpText, resolveProgramName, validateHarness } from "../lib/cli.mjs";
|
|
4
4
|
import { join, dirname } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { readFile } from "node:fs/promises";
|
|
@@ -10,10 +10,12 @@ import { installPi, registerPi, sweepLegacyPiUserDirs } from "./installers/pi.mj
|
|
|
10
10
|
/** @typedef {import("./installers/pi.mjs").RegisterPiResult} RegisterPiResult */
|
|
11
11
|
|
|
12
12
|
const argv = process.argv.slice(2);
|
|
13
|
-
|
|
13
|
+
// `agent-cortex`, `cortex` and `ctx` all run this file; the text names the one used.
|
|
14
|
+
const programName = resolveProgramName(process.argv[1]);
|
|
15
|
+
const parsed = parseArgs(argv, programName);
|
|
14
16
|
|
|
15
17
|
if (parsed.command === "help") {
|
|
16
|
-
process.stdout.write(buildHelpText() + "\n");
|
|
18
|
+
process.stdout.write(buildHelpText(programName) + "\n");
|
|
17
19
|
process.exit(0);
|
|
18
20
|
}
|
|
19
21
|
|
|
@@ -53,7 +55,7 @@ if (parsed.command === "version") {
|
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
if (parsed.command === "unknown") {
|
|
56
|
-
process.stderr.write(`Unknown command: ${parsed.name}\nRun "
|
|
58
|
+
process.stderr.write(`Unknown command: ${parsed.name}\nRun "${programName} --help" for usage.\n`);
|
|
57
59
|
process.exit(1);
|
|
58
60
|
}
|
|
59
61
|
|
|
@@ -65,7 +67,7 @@ if (parsed.command === "install") {
|
|
|
65
67
|
|
|
66
68
|
const check = validateHarness(parsed.harness);
|
|
67
69
|
if (!check.ok) {
|
|
68
|
-
process.stderr.write(`${check.error}\nRun "
|
|
70
|
+
process.stderr.write(`${check.error}\nRun "${programName} --help" for usage.\n`);
|
|
69
71
|
process.exit(1);
|
|
70
72
|
}
|
|
71
73
|
|
package/bin/installers/pi.mjs
CHANGED
|
@@ -395,7 +395,19 @@ async function installSkills({ root, skillsDir, dryRun, pluginRoot, tokenMap, wa
|
|
|
395
395
|
const groupDir = join(skillsSrc, group);
|
|
396
396
|
if (!(await isDirectory(groupDir))) continue;
|
|
397
397
|
for (const name of (await readdir(groupDir)).sort()) {
|
|
398
|
-
if (!(await isFile(join(groupDir, name, "SKILL.md"))))
|
|
398
|
+
if (!(await isFile(join(groupDir, name, "SKILL.md")))) {
|
|
399
|
+
// pi advertises a skill by the absolute path of its SKILL.md, and this
|
|
400
|
+
// installer carries exactly skills/<group>/<name>. A skill authored
|
|
401
|
+
// below that depth would be dropped without a trace, so fail instead.
|
|
402
|
+
const nested = await findSkillFiles(join(groupDir, name));
|
|
403
|
+
if (nested.length > 0) {
|
|
404
|
+
throw new Error(
|
|
405
|
+
`skill "${nested[0]}" is not at skills/<group>/<name>/SKILL.md — ` +
|
|
406
|
+
"the installer copies exactly that depth, so pi would never advertise this skill",
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
399
411
|
skills += 1;
|
|
400
412
|
const stats = await copyTree(join(groupDir, name), join(skillsDir, group, name), { dryRun, transform });
|
|
401
413
|
md += stats.md;
|
|
@@ -405,6 +417,28 @@ async function installSkills({ root, skillsDir, dryRun, pluginRoot, tokenMap, wa
|
|
|
405
417
|
return { skills, md, files, dir: skillsDir };
|
|
406
418
|
}
|
|
407
419
|
|
|
420
|
+
/**
|
|
421
|
+
* Every `SKILL.md` below `dir`, recursively — the skills pi would advertise if
|
|
422
|
+
* its discovery walked the repository tree instead of the staged one.
|
|
423
|
+
* @param {string} dir
|
|
424
|
+
* @returns {Promise<string[]>}
|
|
425
|
+
*/
|
|
426
|
+
async function findSkillFiles(dir) {
|
|
427
|
+
/** @type {string[]} */
|
|
428
|
+
const found = [];
|
|
429
|
+
let entries;
|
|
430
|
+
try {
|
|
431
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
432
|
+
} catch {
|
|
433
|
+
return found;
|
|
434
|
+
}
|
|
435
|
+
for (const entry of entries) {
|
|
436
|
+
if (entry.isDirectory()) found.push(...(await findSkillFiles(join(dir, entry.name))));
|
|
437
|
+
else if (entry.isFile() && entry.name === "SKILL.md") found.push(join(dir, entry.name));
|
|
438
|
+
}
|
|
439
|
+
return found;
|
|
440
|
+
}
|
|
441
|
+
|
|
408
442
|
// ─── Extensions ──────────────────────────────────────────────────────────────
|
|
409
443
|
|
|
410
444
|
/**
|
package/lib/cli.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Result of parsing the CLI argv. Every branch returns `command`; the rest are
|
|
3
5
|
* populated only for the commands that accept them.
|
|
@@ -18,6 +20,24 @@
|
|
|
18
20
|
|
|
19
21
|
const SUPPORTED_HARNESSES = ["claude", "pi"];
|
|
20
22
|
|
|
23
|
+
// The published bin names. `package.json` maps all three onto bin/agent-cortex.mjs,
|
|
24
|
+
// so the help and error text names whichever shim the user typed.
|
|
25
|
+
const PROGRAM_NAMES = ["agent-cortex", "cortex", "ctx"];
|
|
26
|
+
const DEFAULT_PROGRAM_NAME = "agent-cortex";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolve the program name to print from the path the CLI was invoked as. Only
|
|
30
|
+
* the published command names are recognised; anything else (a bare `node
|
|
31
|
+
* bin/agent-cortex.mjs`, a renamed copy) falls back to `agent-cortex`, so the
|
|
32
|
+
* text always names a command that exists.
|
|
33
|
+
* @param {string | undefined} invokedPath `process.argv[1]`, or a bare name in tests
|
|
34
|
+
* @returns {string}
|
|
35
|
+
*/
|
|
36
|
+
export function resolveProgramName(invokedPath) {
|
|
37
|
+
const name = basename(invokedPath ?? "");
|
|
38
|
+
return PROGRAM_NAMES.includes(name) ? name : DEFAULT_PROGRAM_NAME;
|
|
39
|
+
}
|
|
40
|
+
|
|
21
41
|
// Options accepted by `install <harness>`. `--output`/`--dry-run` are shared by
|
|
22
42
|
// the claude and pi installers; `--plugin-root` is pi-specific. Unknown or
|
|
23
43
|
// malformed options surface as `optionError` on the parsed result; the CLI
|
|
@@ -33,9 +53,10 @@ const INSTALL_OPTIONS = {
|
|
|
33
53
|
/**
|
|
34
54
|
* Parse the CLI argv.
|
|
35
55
|
* @param {string[]} argv
|
|
56
|
+
* @param {string} [programName] The resolved command name, for usage hints
|
|
36
57
|
* @returns {ParsedArgs}
|
|
37
58
|
*/
|
|
38
|
-
export function parseArgs(argv) {
|
|
59
|
+
export function parseArgs(argv, programName = DEFAULT_PROGRAM_NAME) {
|
|
39
60
|
if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
|
|
40
61
|
return { command: "help" };
|
|
41
62
|
}
|
|
@@ -64,7 +85,7 @@ export function parseArgs(argv) {
|
|
|
64
85
|
const inlineValue = arg.startsWith("--output=") ? arg.slice("--output=".length) : null;
|
|
65
86
|
const spec = inlineValue !== null ? INSTALL_OPTIONS["--output"] : INSTALL_OPTIONS[arg];
|
|
66
87
|
if (!spec) {
|
|
67
|
-
return { command: "install", harness, optionError: `Unknown option "${arg}". Run "
|
|
88
|
+
return { command: "install", harness, optionError: `Unknown option "${arg}". Run "${programName} --help" for usage.` };
|
|
68
89
|
}
|
|
69
90
|
if (spec.boolean) {
|
|
70
91
|
options[spec.key] = true;
|
|
@@ -76,7 +97,7 @@ export function parseArgs(argv) {
|
|
|
76
97
|
}
|
|
77
98
|
const value = rest[i + 1];
|
|
78
99
|
if (value == null || value.startsWith("--")) {
|
|
79
|
-
return { command: "install", harness, optionError: `Missing value for "${arg}". Run "
|
|
100
|
+
return { command: "install", harness, optionError: `Missing value for "${arg}". Run "${programName} --help" for usage.` };
|
|
80
101
|
}
|
|
81
102
|
options[spec.key] = value;
|
|
82
103
|
i += 1;
|
|
@@ -116,13 +137,13 @@ export function parseArgs(argv) {
|
|
|
116
137
|
if (arg === "--dir" || inlineDir !== null) {
|
|
117
138
|
const value = rest[i + 1];
|
|
118
139
|
if (inlineDir !== null || value == null || value.startsWith("--")) {
|
|
119
|
-
return { command: "migrate", optionError: `Missing value for "--dir". Run "
|
|
140
|
+
return { command: "migrate", optionError: `Missing value for "--dir". Run "${programName} --help" for usage.` };
|
|
120
141
|
}
|
|
121
142
|
dir = value;
|
|
122
143
|
i += 1;
|
|
123
144
|
continue;
|
|
124
145
|
}
|
|
125
|
-
return { command: "migrate", optionError: `Unknown option "${arg}". Run "
|
|
146
|
+
return { command: "migrate", optionError: `Unknown option "${arg}". Run "${programName} --help" for usage.` };
|
|
126
147
|
}
|
|
127
148
|
if (sawApply && sawDryRun) {
|
|
128
149
|
return { command: "migrate", optionError: '"--apply" and "--dry-run" are mutually exclusive.' };
|
|
@@ -134,10 +155,11 @@ export function parseArgs(argv) {
|
|
|
134
155
|
}
|
|
135
156
|
|
|
136
157
|
/**
|
|
158
|
+
* @param {string} [programName] The resolved command name, for the usage line
|
|
137
159
|
* @returns {string}
|
|
138
160
|
*/
|
|
139
|
-
export function buildHelpText() {
|
|
140
|
-
return `Usage:
|
|
161
|
+
export function buildHelpText(programName = DEFAULT_PROGRAM_NAME) {
|
|
162
|
+
return `Usage: ${programName} <command> [options]
|
|
141
163
|
|
|
142
164
|
Commands:
|
|
143
165
|
install <harness> Install agent-cortex for a harness
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jaybeeuu/agent-cortex",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
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/",
|
package/scripts/lib/migrate.mjs
CHANGED
|
@@ -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
|
-
|
|
287
|
-
|
|
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
|
|
|
@@ -55,7 +55,7 @@ description: "Generate multiple radically different interface designs for a modu
|
|
|
55
55
|
4. Trade-offs of this approach
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
Collect each agent's output
|
|
58
|
+
Collect each agent's output as it finishes.
|
|
59
59
|
|
|
60
60
|
3. **Present designs.** Show each design sequentially — signature, usage examples, what it hides — so the user absorbs each approach before any comparison happens.
|
|
61
61
|
|
|
@@ -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.
|
|
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
|
|
|
@@ -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
|
|
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
|
|
88
|
-
|
|
89
|
-
const beadId =
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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 = {
|
|
@@ -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
|
|
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
|
|
97
|
-
|
|
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
|
|
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
|
---
|
|
@@ -41,26 +41,28 @@ State model: task state lives in beads (ready/in_progress via `bd`); the only lo
|
|
|
41
41
|
- **Bead that already has its children** (a decomposed feature, an expanded task parent): skip it — it is not dispatchable itself, and its `stage:*` chore is dispatched on its own.
|
|
42
42
|
`execution:hitl` beads are not in this query and are not ralph's to touch: record them for the Pending Human Action summary. That includes every feature still at its default `execution:hitl` label.
|
|
43
43
|
7. Dispatch each unit from its parent's worktree, up to 5 in flight: a stage chore via the event loop, a childless feature via a decomposition worker (_Feature decomposition_), a chore-less task via an expansion worker (_Expansion guard_). Create branches and worktrees first (REFERENCE.md, _Feature Branches and Worktrees_).
|
|
44
|
-
8. Hand over to your harness's completion-detection mechanism: PI keeps a
|
|
44
|
+
8. Hand over to your harness's completion-detection mechanism: PI keeps a blocking wait outstanding over every in-flight agent ID, and Claude re-invokes you as each worker completes. If no **agent work** was dispatched — `state.json`'s `inflight` holds no entries, so no stage, decomposition or expansion worker is running — and `skills/workflow/ralph/scripts/pending-gates.mjs` reports a non-empty `pendingHumanAction` (an unblocked `lifecycle:feature-pr` gate, or an epic tagged `awaiting-epic-pr-merge`), go straight to HITL Pause (REFERENCE.md). A decomposition- or expansion-only init is in flight, not idle: keep the completion mechanism running and continue at the event loop.
|
|
45
45
|
|
|
46
46
|
### 2. Expansion guard (stage chores are pre-created)
|
|
47
47
|
|
|
48
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_).
|
|
49
49
|
|
|
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
|
+
|
|
50
52
|
**Never expand an existing bead with `create-task`.** It creates a bead first and expands second, so the original would stay chore-less and gain a **second parent** beside it. `create-chores.mjs` expands; only `create-task` creates parents.
|
|
51
53
|
|
|
52
54
|
### 3. Feature decomposition
|
|
53
55
|
|
|
54
56
|
A ready `execution:afk` bead of type `feature` with no children is not dispatchable work — there is nothing to stage and nothing to expand. Decompose it: dispatch a worker in `.agent-cortex/worktrees/<feature-id>` that turns the feature into one `execution:hitl` planning bead plus `execution:afk` implementation children that depend on it, then continue (REFERENCE.md, _Feature Decomposition_).
|
|
55
57
|
|
|
56
|
-
A feature carries `execution:hitl` by default, so this only runs when a human has deliberately relabelled a feature `execution:afk` to make its delivery agent-routable. Decomposition is then the last thing ralph does with it for a while: the children depend on the planning bead, so once the worker reports, `bd ready -l execution:afk` returns only the feature itself — no dispatchable child — and skipping that row is the *correct* behaviour, not re-expanding. **Never re-dispatch a decomposition**: the bead creation it runs is not idempotent, so a second run duplicates the whole hierarchy.
|
|
58
|
+
A feature carries `execution:hitl` by default, so this only runs when a human has deliberately relabelled a feature `execution:afk` to make its delivery agent-routable. Decomposition is then the last thing ralph does with it for a while: the children depend on the planning bead, so once the worker reports, `bd ready -l execution:afk` returns only the feature itself — no dispatchable child — and skipping that row is the *correct* behaviour, not re-expanding. **Never re-dispatch a decomposition**: the bead creation it runs is not idempotent, so a second run duplicates the whole hierarchy. That hazard is `create-task`'s, not `create-chores.mjs`'s — the expansion is idempotent.
|
|
57
59
|
|
|
58
60
|
### 4. Event loop
|
|
59
61
|
|
|
60
62
|
On each completion notification (a background agent finished):
|
|
61
63
|
|
|
62
64
|
1. Poll that parent's log for new lines — `tail -n +<nextLine> .agent-cortex/ralph/ralph-<parent-id>.log` (logs are keyed by parent bead ID, so every stage of a feature shares one file) — and post only new key events (stage transitions, test/lint results), never empty updates.
|
|
63
|
-
2. Read the completed agent's output
|
|
65
|
+
2. Read the completed agent's output and parse its `---REPORT---` block for `BEAD_ID`, `STAGE_COMPLETED`, and the outcome. A **decomposition** (`kind: "decompose"`) or **expansion** (`kind: "expand"`) worker has no REPORT and no leaf bead to confirm. An **expansion worker** owns no bead — confirm the parent now has `stage:*` chores (`bd children <parent-id> --json | grep -c '"stage:'`) and skip to step 5 to dispatch its ready `stage:code` chore. A count of `0` means the expansion produced no chores at all; a count below the stage total means the chain is **partial**. Both are failures: surface the worker's output and re-run `create-chores.mjs --parent <id>`, which adopts the chores already present and creates only the missing ones, so the re-run is the recovery path and it never needs a second chain. A **decomposition worker** — confirm the feature now has children (`bd children <feature-id> --json` returns a non-empty list), record that nothing is dispatchable for it until a human closes the planning bead, and skip to step 5; never re-decompose. If a decomposition's confirmation comes back empty, the worker failed: surface its output, record the parent for the shutdown summary, and do not re-dispatch.
|
|
64
66
|
3. Confirm the stage worker closed its chore bead: `bd show <bead-id>` should read `closed` with the outcome in its close reason. If it is still open the worker died mid-stage — read its notes and re-dispatch; never close it yourself.
|
|
65
67
|
4. Route the parent per the dispatch table — nothing is created here: the pre-created chain already encodes the order, and the worker owns any remediation it needs.
|
|
66
68
|
|
|
@@ -124,6 +126,7 @@ Both paths end in a **gated** `bd dolt push`: on a non-zero exit, retry once, th
|
|
|
124
126
|
| "The review passed, so I can merge" | Feature and epic PR merges are human-controlled; the gate exists so a person sees every change. |
|
|
125
127
|
| "The worker left the chore open — I'll close it" | The worker owns that bead; an open one means it died mid-stage, so read its notes and re-dispatch instead of closing over the missing record. |
|
|
126
128
|
| "This bead has no chores — I'll create them as I go" | The whole chain is pre-created with `blocks` deps; hand-creating stages duplicates chores and loses the order. Expand with `create-chores.mjs` instead. |
|
|
129
|
+
| "The expansion failed, so I'll build the missing chores by hand" | Re-run `create-chores.mjs --parent <id>`: it is idempotent, so it adopts the chores that exist, creates only the missing ones, and reports the whole mapping. |
|
|
127
130
|
| "That feature came back ready again, so I'll decompose it a second time" | The implementation children depend on the planning bead, so nothing is ready until a human closes it. Re-decomposing duplicates the hierarchy; idling *is* the correct outcome. |
|
|
128
131
|
|
|
129
132
|
## Philosophy / rationale
|
|
@@ -156,6 +159,6 @@ Do not invoke `classify-bead` from this loop — classification was applied at p
|
|
|
156
159
|
- [ ] In-flight count never exceeded 5, and the read-only cap backstop ran before every `stage:*` dispatch.
|
|
157
160
|
- [ ] Every in-flight, dispatchable-work, HITL-pause, and shutdown predicate was read from `state.json`'s `inflight` (stage, decomposition **and** expansion workers) plus `bd ready -l execution:afk`, with the ready, claimable scope of a chore-less `execution:afk` task bead — never from a chore-only `bd` query, and never counting a bead an open `lifecycle:feature-pr` gate blocks from being claimed.
|
|
158
161
|
- [ ] Ralph closed no leaf execution bead and created no fix bead; every closed chore carries its worker's close reason.
|
|
159
|
-
- [ ] Any chore-less `execution:afk` task bead was expanded with `create-chores.mjs` (one parent bead), never with `create-task
|
|
162
|
+
- [ ] Any chore-less `execution:afk` task bead was expanded with `create-chores.mjs` (one parent bead), never with `create-task`; a failed expansion was recovered by re-running that same idempotent command.
|
|
160
163
|
- [ ] Feature and epic PRs were pushed and reported immediately at their gates, and nothing was auto-merged.
|
|
161
164
|
- [ ] Every `bd dolt push` before HITL Pause/Shutdown exited 0 — a non-zero exit was retried once, then surfaced as a blocker (command, exit code, stderr) with no completion or Pending Human Action summary printed; status was reported from `bd` commands rather than a generated file, and the chat received no empty poll updates.
|