@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/CHANGELOG.md +59 -0
- package/README.md +18 -6
- package/agents/README.md +3 -1
- package/agents/ralph/agent.md +6 -4
- package/bin/agent-cortex.mjs +12 -7
- package/bin/installers/claude.mjs +55 -11
- package/bin/installers/pi.mjs +35 -1
- package/extensions/agent-guardrails/package.json +1 -1
- package/extensions/agent-modes/index.test.ts +13 -0
- package/extensions/agent-modes/package.json +1 -1
- package/extensions/lib/tiny-model.ts +7 -6
- package/extensions/output-style/package.json +1 -1
- package/extensions/session-start/package.json +1 -1
- package/extensions/skill-stats/package.json +1 -1
- package/extensions/subagent/package.json +3 -3
- package/lib/cli.mjs +29 -7
- package/package.json +4 -2
- package/scripts/lib/migrate.mjs +31 -2
- package/{skills/productivity/validate-agent-dir/scripts → scripts}/validate-agent-dir.mjs +20 -14
- package/skills/planning/create-task/REFERENCE.md +15 -2
- package/skills/planning/create-task/SKILL.md +5 -2
- package/skills/planning/create-task/scripts/create-chores.mjs +111 -32
- package/skills/planning/create-task/templates/code.md +13 -2
- package/skills/planning/create-task/templates/document.md +13 -2
- package/skills/planning/create-task/templates/fix.md +13 -2
- package/skills/planning/create-task/templates/verify.md +14 -8
- package/skills/productivity/migration/REFERENCE.md +10 -3
- package/skills/workflow/ralph/REFERENCE.md +12 -5
- package/skills/workflow/ralph/SKILL.md +6 -3
- package/skills/workflow/run-pipeline-stage/SKILL.md +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,64 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.5.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- e87b2d0: Align the code, verify, document, and fix REPORT templates on the authoritative
|
|
8
|
+
`run-pipeline-stage` contract. Each template now emits `STAGE_COMPLETED` with its stage name
|
|
9
|
+
(`code`, `verify`, `document`, `fix`) plus `COMMIT`, `PUSHED`, `PR`, and
|
|
10
|
+
`OUTCOME: SUCCESS|BLOCKED`, and lists blocked work under `BLOCKING_ISSUES`. The unparsed
|
|
11
|
+
`VERIFY_OUTCOME`/`VERIFY_FAILURES` keys are gone, so an orchestrator can route every stage's
|
|
12
|
+
report.
|
|
13
|
+
- 83ed526: Stop `agent-cortex install claude` erasing Claude Code's `enabledPlugins` entry.
|
|
14
|
+
|
|
15
|
+
The settings merge now asserts only the `enabledPlugins` and `extraKnownMarketplaces` entries the
|
|
16
|
+
committed `claude/settings.json` template declares; entries it does not declare survive, so a
|
|
17
|
+
same-version re-run keeps the entry Claude Code writes when the plugin is enabled. Registration
|
|
18
|
+
also reports an installed-but-not-enabled plugin, naming `claude plugin enable <plugin>@<marketplace>`.
|
|
19
|
+
|
|
20
|
+
- 34bcea7: Type-check the extensions against the pi version that is actually installed.
|
|
21
|
+
|
|
22
|
+
The extension workspaces pinned `@earendil-works/pi-coding-agent`, `pi-agent-core`, and `pi-ai` at
|
|
23
|
+
`^0.78.1` for `import type` only, while `pi --version` reported 1.5.2. `pnpm typecheck` therefore
|
|
24
|
+
validated against an API that no installed pi provides. The pins now follow the running CLI at
|
|
25
|
+
`^1.5.2`, and the `agent-modes` `ExtensionAPI` test double gains the members 1.5.x declares:
|
|
26
|
+
`registerMarkdownTransformer`, `registerEntryRenderer`, `registerToolRenderer`, `registerVirtualModel`,
|
|
27
|
+
`registerMcpServer`, `unregisterMcpServer`, `getMcpServers`, `unregisterVirtualModel`, and
|
|
28
|
+
`getSettings`, plus the unsubscribe return value of `on`.
|
|
29
|
+
|
|
30
|
+
- fa84942: Move the composable agent-directory validator from
|
|
31
|
+
`skills/productivity/validate-agent-dir/scripts/` to `scripts/validate-agent-dir.mjs`. The
|
|
32
|
+
directory had no `SKILL.md`, so neither installer shipped it while it still looked like a shipped
|
|
33
|
+
skill, and the module validates this repository's own `agents/` layout — repo tooling, not a
|
|
34
|
+
portable skill. Its filesystem probes are now async (`validateAgentDir` returns a promise),
|
|
35
|
+
matching the no-`*Sync` rule for `scripts/`.
|
|
36
|
+
|
|
37
|
+
`agents/README.md` (Validation) and `test/validate-agent-dir.test.mjs` point at the new path. No
|
|
38
|
+
skill, agent, hook, or installer behaviour changes.
|
|
39
|
+
|
|
40
|
+
## 2.5.0
|
|
41
|
+
|
|
42
|
+
### Minor Changes
|
|
43
|
+
|
|
44
|
+
- 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`.
|
|
45
|
+
- 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.
|
|
46
|
+
|
|
47
|
+
### Patch Changes
|
|
48
|
+
|
|
49
|
+
- 2cab55e: Chunk the `migrate` v1 label rename: each `bd label add` and `bd label remove` call
|
|
50
|
+
now carries at most 20 bead ids. One call carrying every id in a large workspace
|
|
51
|
+
applied only part of the list and still reported success, so v1 never converged and a
|
|
52
|
+
workspace could report itself migrated with hundreds of legacy labels attached.
|
|
53
|
+
- dd4e69c: Fail `agent-cortex install pi` when a skill sits outside `skills/<group>/<name>`.
|
|
54
|
+
|
|
55
|
+
pi advertises a skill by the absolute path of its `SKILL.md`, and the installer copies exactly
|
|
56
|
+
`skills/<group>/<name>`. A skill authored deeper was skipped without a trace, so no session was
|
|
57
|
+
ever told about it. The install now stops and names the offending path.
|
|
58
|
+
|
|
59
|
+
`docs/dev-loop.md` records the related limit that needs no code: a session lists its skills when
|
|
60
|
+
it starts, so a session already open across a re-stage holds the paths it was given.
|
|
61
|
+
|
|
3
62
|
## 2.4.2
|
|
4
63
|
|
|
5
64
|
### 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
|
|
@@ -287,7 +292,8 @@ symlinks), `SessionStart` + `Notification` hooks, and 2 MCP servers — writes a
|
|
|
287
292
|
manifest at `~/.agent-cortex/.claude-plugin/marketplace.json` exposing `./claude`, and
|
|
288
293
|
registers it with Claude Code, and merges the committed `claude/settings.json` template into
|
|
289
294
|
`~/.claude/settings.json`: the template supplies defaults, personal values win, and the
|
|
290
|
-
installer owns exactly `enabledPlugins` + `extraKnownMarketplaces`
|
|
295
|
+
installer owns exactly the `enabledPlugins` + `extraKnownMarketplaces` entries the template
|
|
296
|
+
declares — entries it does not declare are left alone — while every other key
|
|
291
297
|
(`permissions`, `hooks`, `env`, `statusLine`, unknown keys) is preserved. It also writes the
|
|
292
298
|
managed section of `~/.claude/CLAUDE.md` — the reply-shape block the pi `output-style`
|
|
293
299
|
extension appends, taken from the same source file — and leaves every other line of that
|
|
@@ -355,10 +361,15 @@ agent-cortex install claude --require-register # register or fail the inst
|
|
|
355
361
|
|
|
356
362
|
The install also merges the committed `claude/settings.json` template into
|
|
357
363
|
`~/.claude/settings.json` (the settings file Claude Code reads at user scope). The template
|
|
358
|
-
supplies defaults and personal values win; the installer owns exactly `enabledPlugins` +
|
|
359
|
-
`extraKnownMarketplaces
|
|
360
|
-
|
|
361
|
-
|
|
364
|
+
supplies defaults and personal values win; the installer owns exactly the `enabledPlugins` +
|
|
365
|
+
`extraKnownMarketplaces` entries the template declares, so entries it does not declare (including
|
|
366
|
+
the one Claude Code writes when you enable the plugin) survive, and `permissions`, `hooks`, `env`,
|
|
367
|
+
`statusLine` and unknown keys are never touched. A legacy symlinked `settings.json` is replaced
|
|
368
|
+
with a real file rather than written through, and `--output` leaves the user's Claude config
|
|
369
|
+
alone. When registration finds the plugin installed but the settings do not enable it, the install
|
|
370
|
+
warns and names `claude plugin enable <plugin>@<marketplace>` — a disabled plugin loads nothing.
|
|
371
|
+
That check reads the user-scope file only, so a plugin enabled at project or local scope still
|
|
372
|
+
warns; `claude plugin list` reports the effective state.
|
|
362
373
|
|
|
363
374
|
It also writes the managed section of `~/.claude/CLAUDE.md` (Claude's user memory), delimited
|
|
364
375
|
by `<!-- BEGIN agent-cortex:output-style -->` / `<!-- END agent-cortex:output-style -->`. The
|
|
@@ -480,7 +491,8 @@ is an empty legacy namespace, not just a clean live one (see
|
|
|
480
491
|
A bare `agent-cortex migrate` is a **dry run** — it prints the per-bead plan and writes
|
|
481
492
|
nothing. `--apply` runs every pending step in order and verifies each before moving on. It
|
|
482
493
|
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.
|
|
494
|
+
half-applying, when `bd` is missing or a `bd` subcommand errors. Label writes carry at most 20
|
|
495
|
+
ids per `bd` call, because `bd` can exit 0 having applied only part of a longer list.
|
|
484
496
|
|
|
485
497
|
Detection reads `bd export --all` and cross-checks `bd label list-all`, never a
|
|
486
498
|
label-filtered `bd list`: on bd 1.3.1 that query can omit closed beads, which once let a
|
package/agents/README.md
CHANGED
|
@@ -245,7 +245,9 @@ agents, token-substituted skills, extensions) at `~/.agent-cortex/pi`.
|
|
|
245
245
|
|
|
246
246
|
## Validation
|
|
247
247
|
|
|
248
|
-
|
|
248
|
+
The composable layout is validated by the repo tooling at `scripts/validate-agent-dir.mjs` — a
|
|
249
|
+
module, not a shipped skill, with no CLI. Import `validateAgentDir` to check a directory, or run
|
|
250
|
+
its test suite:
|
|
249
251
|
|
|
250
252
|
```bash
|
|
251
253
|
node --test test/validate-agent-dir.test.mjs
|
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
|
|
|
@@ -118,8 +120,8 @@ if (parsed.command === "install") {
|
|
|
118
120
|
// materialises the plugin into the home install root (~/.agent-cortex/claude)
|
|
119
121
|
// with copied, token-substituted skills, writes the marketplace manifest at
|
|
120
122
|
// ~/.agent-cortex/.claude-plugin/marketplace.json, merges the committed
|
|
121
|
-
// claude/settings.json template into ~/.claude/settings.json (
|
|
122
|
-
// enabledPlugins + extraKnownMarketplaces), and registers it with Claude Code
|
|
123
|
+
// claude/settings.json template into ~/.claude/settings.json (asserting only
|
|
124
|
+
// the enabledPlugins + extraKnownMarketplaces entries the template declares), and registers it with Claude Code
|
|
123
125
|
// (marketplace add → install → update); `--output <dir>` is the generate-only
|
|
124
126
|
// form (tests/CI validation), and the generated claude/ plugin subtree is
|
|
125
127
|
// never committed — only the settings template is.
|
|
@@ -145,6 +147,9 @@ if (parsed.command === "install") {
|
|
|
145
147
|
root: /** @type {string} */ (result.marketplaceRoot),
|
|
146
148
|
manifest: /** @type {object} */ (result.marketplaceManifest),
|
|
147
149
|
pluginVersion: result.pluginVersion,
|
|
150
|
+
// The enablement check reads the same file this install just merged:
|
|
151
|
+
// a plugin Claude Code does not enable loads nothing.
|
|
152
|
+
...(result.settings ? { settingsPath: result.settings.path } : {}),
|
|
148
153
|
dryRun: parsed.dryRun ?? false,
|
|
149
154
|
requireRegister: parsed.requireRegister ?? false,
|
|
150
155
|
});
|
|
@@ -85,13 +85,16 @@ const PACKAGE_ROOT = join(MODULE_DIR, "..", "..");
|
|
|
85
85
|
*/
|
|
86
86
|
|
|
87
87
|
/**
|
|
88
|
-
*
|
|
88
|
+
* Register plan/outcome returned by registerClaude.
|
|
89
89
|
* @typedef {object} RegisterClaudeResult
|
|
90
90
|
* @property {string} marketplace
|
|
91
91
|
* @property {string} plugin
|
|
92
92
|
* @property {string[][]} commands
|
|
93
93
|
* @property {boolean} dryRun
|
|
94
94
|
* @property {boolean} registered
|
|
95
|
+
* @property {boolean | null} enabled Whether the user's settings enable the plugin;
|
|
96
|
+
* null when the settings could not be read or
|
|
97
|
+
* its state is unconfirmed.
|
|
95
98
|
* @property {string[]} skipped
|
|
96
99
|
*/
|
|
97
100
|
|
|
@@ -400,9 +403,13 @@ async function buildHandAuthored(root) {
|
|
|
400
403
|
* - template keys fill in defaults the live file lacks; every value already
|
|
401
404
|
* present in the live file wins (deeply), and unknown keys are preserved,
|
|
402
405
|
* so personal config survives re-install;
|
|
403
|
-
* - `enabledPlugins` and `extraKnownMarketplaces` are CLI-owned
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
+
* - `enabledPlugins` and `extraKnownMarketplaces` are CLI-owned in the other
|
|
407
|
+
* direction: the entries the template declares are asserted (the template
|
|
408
|
+
* value wins for those), and entries it does not declare are kept. A
|
|
409
|
+
* template key that is empty or absent declares nothing and therefore owns
|
|
410
|
+
* nothing — silence never deletes, so a re-install cannot erase the entry
|
|
411
|
+
* Claude Code writes when the user enables the plugin. `permissions`,
|
|
412
|
+
* `hooks`, `env`, `statusLine` and every other key are never touched.
|
|
406
413
|
* @param {{ root: string, target: string, dryRun: boolean, warn: Warn }} options
|
|
407
414
|
* @returns {Promise<ManagedFileResult | null>}
|
|
408
415
|
*/
|
|
@@ -410,16 +417,32 @@ async function installSettings({ root, target, dryRun, warn }) {
|
|
|
410
417
|
const template = await readTemplateJson(join(root, SETTINGS_TEMPLATE), warn);
|
|
411
418
|
if (template === null) return null;
|
|
412
419
|
|
|
413
|
-
const
|
|
420
|
+
const existing = await readJsonObject(target, warn);
|
|
421
|
+
const merged = mergeSettings(template, existing);
|
|
414
422
|
for (const key of SETTINGS_OWNED_KEYS) {
|
|
415
|
-
|
|
416
|
-
|
|
423
|
+
const value = mergeOwnedSetting(template[key], existing[key]);
|
|
424
|
+
if (value === undefined) delete merged[key];
|
|
425
|
+
else merged[key] = value;
|
|
417
426
|
}
|
|
418
427
|
|
|
419
428
|
const content = `${JSON.stringify(merged, null, 2)}\n`;
|
|
420
429
|
return writeManagedFile({ target, content, dryRun });
|
|
421
430
|
}
|
|
422
431
|
|
|
432
|
+
/**
|
|
433
|
+
* Merge one CLI-owned settings key. The template asserts the entries it
|
|
434
|
+
* declares — its value wins for exactly those — and every entry it does not
|
|
435
|
+
* declare survives from the live file. An empty or absent template key declares
|
|
436
|
+
* nothing, so the live value passes through untouched: silence never deletes.
|
|
437
|
+
* @param {unknown} template
|
|
438
|
+
* @param {unknown} existing
|
|
439
|
+
* @returns {unknown}
|
|
440
|
+
*/
|
|
441
|
+
function mergeOwnedSetting(template, existing) {
|
|
442
|
+
if (!isPlainObject(template) || !isPlainObject(existing)) return existing === undefined ? template : existing;
|
|
443
|
+
return { ...existing, ...template };
|
|
444
|
+
}
|
|
445
|
+
|
|
423
446
|
/**
|
|
424
447
|
* Merge the committed template over the live file. Template keys fill in
|
|
425
448
|
* defaults, live values win (deeply), and keys the template does not know about
|
|
@@ -1045,6 +1068,11 @@ async function pluginLoadVerdict(claudeBin, pluginId) {
|
|
|
1045
1068
|
* When that list cannot be read, or does not list the plugin, the load state is
|
|
1046
1069
|
* unconfirmed: the install warns and skips the success line.
|
|
1047
1070
|
*
|
|
1071
|
+
* A loaded plugin that the user's settings do not enable loads nothing either:
|
|
1072
|
+
* Claude Code reads `enabledPlugins` from that file, and registration skips the
|
|
1073
|
+
* install as "already installed", so the run warns with the command that fixes
|
|
1074
|
+
* it rather than reporting a working install.
|
|
1075
|
+
*
|
|
1048
1076
|
* @param {object} options
|
|
1049
1077
|
* @param {string} [options.root] Marketplace root carrying .claude-plugin/…
|
|
1050
1078
|
* (defaults to the repo containing this module)
|
|
@@ -1052,6 +1080,8 @@ async function pluginLoadVerdict(claudeBin, pluginId) {
|
|
|
1052
1080
|
* @param {boolean} [options.dryRun] Plan and report without running or writing
|
|
1053
1081
|
* @param {object} [options.manifest] Pre-parsed marketplace manifest (skips the file read)
|
|
1054
1082
|
* @param {string} [options.pluginVersion] Materialised plugin version (default: read from plugin.json)
|
|
1083
|
+
* @param {string} [options.settingsPath] Claude user settings the enablement check reads
|
|
1084
|
+
* (default ~/.claude/settings.json)
|
|
1055
1085
|
* @param {boolean} [options.requireRegister] Exit non-zero when registration cannot run
|
|
1056
1086
|
* @param {(msg: string) => void} [options.warn] Warning sink (default: console.warn)
|
|
1057
1087
|
* @returns {Promise<RegisterClaudeResult>}
|
|
@@ -1062,6 +1092,7 @@ export async function registerClaude({
|
|
|
1062
1092
|
dryRun = false,
|
|
1063
1093
|
manifest,
|
|
1064
1094
|
pluginVersion,
|
|
1095
|
+
settingsPath = DEFAULT_SETTINGS_PATH,
|
|
1065
1096
|
requireRegister = false,
|
|
1066
1097
|
warn = DEFAULT_WARN,
|
|
1067
1098
|
} = {}) {
|
|
@@ -1072,7 +1103,7 @@ export async function registerClaude({
|
|
|
1072
1103
|
const version = pluginVersion ?? (await readMaterialisedPluginVersion(root, manifestObj));
|
|
1073
1104
|
const home = resolve(root);
|
|
1074
1105
|
|
|
1075
|
-
const summary = /** @type {RegisterClaudeResult} */ ({ marketplace, plugin, commands: [], dryRun, registered: true, skipped: [] });
|
|
1106
|
+
const summary = /** @type {RegisterClaudeResult} */ ({ marketplace, plugin, commands: [], dryRun, registered: true, enabled: null, skipped: [] });
|
|
1076
1107
|
console.log(`Registering plugin "${pluginId}" with Claude Code (via ${claudeBin})…`);
|
|
1077
1108
|
|
|
1078
1109
|
const manualCommands = [
|
|
@@ -1156,9 +1187,22 @@ export async function registerClaude({
|
|
|
1156
1187
|
`verify with "${claudeBin} plugin list" (restart Claude Code to apply)`,
|
|
1157
1188
|
);
|
|
1158
1189
|
} else {
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1190
|
+
// Installed but disabled is a plugin that loads nothing: Claude Code gates
|
|
1191
|
+
// every skill and agent on the user's enabledPlugins entry, and the install
|
|
1192
|
+
// path above skips a same-version re-install as "already installed", so
|
|
1193
|
+
// this is the only place that state is reported.
|
|
1194
|
+
const enabled = (await readJsonObject(settingsPath, warn)).enabledPlugins?.[pluginId] === true;
|
|
1195
|
+
summary.enabled = enabled;
|
|
1196
|
+
if (enabled) {
|
|
1197
|
+
console.log(
|
|
1198
|
+
`Installed and enabled — verify with "claude plugin list" / "claude plugin details ${plugin}" (restart Claude Code to apply).`,
|
|
1199
|
+
);
|
|
1200
|
+
} else {
|
|
1201
|
+
warn(
|
|
1202
|
+
`"${pluginId}" is installed but not enabled in ${settingsPath} — Claude Code loads nothing from a disabled plugin; ` +
|
|
1203
|
+
`enable it with "${claudeBin} plugin enable ${pluginId}" and restart Claude Code`,
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1162
1206
|
}
|
|
1163
1207
|
return summary;
|
|
1164
1208
|
}
|
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
|
/**
|
|
@@ -64,6 +64,8 @@ function createFakePi(flagValues: Record<string, boolean | string> = {}): FakePi
|
|
|
64
64
|
const list = handlers.get(event) ?? [];
|
|
65
65
|
list.push(handler);
|
|
66
66
|
handlers.set(event, list);
|
|
67
|
+
// `ExtensionAPI.on` returns an unsubscribe function.
|
|
68
|
+
return () => {};
|
|
67
69
|
},
|
|
68
70
|
registerFlag: (name, options) => {
|
|
69
71
|
flags.push([name, options]);
|
|
@@ -77,8 +79,16 @@ function createFakePi(flagValues: Record<string, boolean | string> = {}): FakePi
|
|
|
77
79
|
registerShortcut: () => {},
|
|
78
80
|
registerTool: () => {},
|
|
79
81
|
registerMessageRenderer: () => {},
|
|
82
|
+
registerMarkdownTransformer: () => {},
|
|
83
|
+
registerEntryRenderer: () => {},
|
|
84
|
+
registerToolRenderer: () => {},
|
|
80
85
|
registerProvider: () => {},
|
|
81
86
|
unregisterProvider: () => {},
|
|
87
|
+
registerMcpServer: () => {},
|
|
88
|
+
unregisterMcpServer: () => {},
|
|
89
|
+
unregisterVirtualModel: () => {},
|
|
90
|
+
registerVirtualModel: () => {},
|
|
91
|
+
getMcpServers: () => [],
|
|
82
92
|
sendMessage: () => {},
|
|
83
93
|
sendUserMessage: () => {},
|
|
84
94
|
appendEntry: () => {},
|
|
@@ -89,6 +99,9 @@ function createFakePi(flagValues: Record<string, boolean | string> = {}): FakePi
|
|
|
89
99
|
throw new Error("not implemented");
|
|
90
100
|
},
|
|
91
101
|
getAllTools: () => [],
|
|
102
|
+
getSettings: () => {
|
|
103
|
+
throw new Error("not implemented");
|
|
104
|
+
},
|
|
92
105
|
getCommands: () => [],
|
|
93
106
|
setModel: async () => false,
|
|
94
107
|
getThinkingLevel: () => "off",
|
|
@@ -16,12 +16,13 @@
|
|
|
16
16
|
* The model call itself is gone. It went through pi-ai's `complete()`, a value
|
|
17
17
|
* import of pi's runtime, and that import stopped the extension loading outside
|
|
18
18
|
* the agent-cortex checkout (see test/pi-extensions-load.test.mjs). The
|
|
19
|
-
* ExtensionAPI promises no completion call.
|
|
20
|
-
* `ModelRegistry.complete
|
|
21
|
-
* would
|
|
22
|
-
* Extensions also make no internal LLM calls (AGENTS.md), so this
|
|
23
|
-
* off. Both callers fall back to the truncation they already used
|
|
24
|
-
* failed. The `model` key is no longer read and is ignored if
|
|
19
|
+
* ExtensionAPI promises no completion call. The installed host (pi 1.5.2) does
|
|
20
|
+
* expose `ModelRegistry.complete` through `ExtensionContext.modelRegistry`, but
|
|
21
|
+
* calling it would make the extensions depend on a method that exists on later
|
|
22
|
+
* hosts only. Extensions also make no internal LLM calls (AGENTS.md), so this
|
|
23
|
+
* feature stays off. Both callers fall back to the truncation they already used
|
|
24
|
+
* when the model failed. The `model` key is no longer read and is ignored if
|
|
25
|
+
* present.
|
|
25
26
|
*/
|
|
26
27
|
|
|
27
28
|
import { homedir } from "node:os";
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
"typecheck": "tsc --noEmit"
|
|
9
9
|
},
|
|
10
10
|
"devDependencies": {
|
|
11
|
-
"@earendil-works/pi-agent-core": "^
|
|
12
|
-
"@earendil-works/pi-ai": "^
|
|
13
|
-
"@earendil-works/pi-coding-agent": "^
|
|
11
|
+
"@earendil-works/pi-agent-core": "^1.5.2",
|
|
12
|
+
"@earendil-works/pi-ai": "^1.5.2",
|
|
13
|
+
"@earendil-works/pi-coding-agent": "^1.5.2",
|
|
14
14
|
"@types/node": "^22.0.0",
|
|
15
15
|
"tsx": "^4.0.0",
|
|
16
16
|
"typebox": "^1.1.38",
|
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
|