@jaybeeuu/agent-cortex 2.6.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/README.md +44 -6
- package/agents/ralph/agent.md +4 -2
- package/bin/agent-cortex.mjs +156 -66
- package/bin/installers/claude.mjs +33 -8
- package/bin/installers/pi.mjs +11 -1
- package/docs/skills/skill-anatomy.md +212 -0
- package/lib/cli.mjs +47 -1
- package/lib/harness-update.mjs +254 -0
- package/lib/harness-versions.mjs +36 -2
- package/package.json +2 -1
- package/scripts/lib/docs.mjs +67 -0
- package/skills/planning/create-task/REFERENCE.md +11 -3
- package/skills/planning/create-task/SKILL.md +1 -1
- package/skills/planning/create-task/pipeline.json +8 -0
- package/skills/planning/create-task/scripts/create-chores.mjs +25 -7
- package/skills/planning/create-task/templates/housekeeping.md +65 -0
- package/skills/productivity/write-a-skill/SKILL.md +2 -2
- package/skills/review/refactor-skill/SKILL.md +2 -2
- package/skills/workflow/ralph/REFERENCE.md +17 -7
- package/skills/workflow/ralph/SKILL.md +5 -4
- package/skills/workflow/run-pipeline-stage/SKILL.md +8 -5
- package/skills/workflow/run-pipeline-stage/playbooks/housekeeping.md +58 -0
- package/skills/workflow/run-pipeline-stage/prompts/stage-runner.md +13 -1
- package/token-map.README.md +14 -4
- package/token-map.json +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 464c761: Add `ctx update`, which refreshes installed harnesses by re-running their installers. Bare
|
|
8
|
+
`ctx update` lists every harness with its installed version and the CLI version, then asks which
|
|
9
|
+
to refresh; a non-interactive shell prints the same table plus the flags and exits non-zero
|
|
10
|
+
instead of blocking. `ctx update pi claude` refreshes exactly the named harnesses, `ctx update -a`
|
|
11
|
+
refreshes every harness that is not at the CLI version, and `ctx update -a -f` re-runs every
|
|
12
|
+
installer regardless of version. The report gives each harness its previous and new version and
|
|
13
|
+
the artifacts the installer wrote. Selection and reporting are the only additions: every refresh
|
|
14
|
+
goes through the existing `bin/installers/` entry points, so update-vs-install stays the
|
|
15
|
+
installer's decision.
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- bfce679: Add a `housekeeping` pipeline stage that runs after the human merge gate. `create-chores.mjs` now
|
|
20
|
+
creates a `stage:housekeeping` chore labelled `lifecycle:housekeeping` and blocked behind the
|
|
21
|
+
`lifecycle:feature-pr` gate bead, so it stays out of `bd ready` until a human merges the PR and
|
|
22
|
+
closes the gate. A dispatched worker then runs `prune-worktrees.mjs` from the main checkout, retries
|
|
23
|
+
once on a non-zero exit, and fails the chore without closing it when a closed-bead worktree is still
|
|
24
|
+
refused. The four existing stages and the gate are unchanged.
|
|
25
|
+
- 8d11154: Ship the skill anatomy doc into both installs. `write-a-skill` and `refactor-skill` read
|
|
26
|
+
`docs/skills/skill-anatomy.md`, but neither install contained it, so the reference was dead on any
|
|
27
|
+
machine without the checkout. Both installers now copy the document into the staged tree once per
|
|
28
|
+
install (`scripts/lib/docs.mjs`), and the skills name it through a new `{{PATH:skill_anatomy}}` entry
|
|
29
|
+
in `token-map.json`, so the token map stays the single source of the per-harness path. A new test
|
|
30
|
+
stages both installs and fails when a rendered skill references a repository doc the install does
|
|
31
|
+
not contain.
|
|
32
|
+
|
|
3
33
|
## 2.6.0
|
|
4
34
|
|
|
5
35
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -209,6 +209,7 @@ The staged package contains:
|
|
|
209
209
|
| `package.json` | pi manifest — `keywords: ["pi-package"]`, `pi.extensions` + `pi.skills`; the version tracks the package |
|
|
210
210
|
| `agents/` | composed flat agents (ralph, plan, ralph-plan, strategy) with `{{TOOL:...}}` / `{{PATH:...}}` substituted — the shipped `agent-modes` extension resolves them from the package root |
|
|
211
211
|
| `skills/` | token-substituted skill copies (real files, never symlinks) |
|
|
212
|
+
| `docs/` | the repository documents those skills read (currently `docs/skills/skill-anatomy.md`), copied verbatim once per install so the reference resolves on a machine without the checkout |
|
|
212
213
|
| `extensions/` | copy of the repo `extensions/` tree — imports nothing from pi's runtime except the host-aliased `typebox`, so pi loads it from any directory |
|
|
213
214
|
| `token-map.json` | the token map `agent-modes` reads from the package root |
|
|
214
215
|
| `node_modules/` | the required third-party pi packages and their transitive closure |
|
|
@@ -243,9 +244,9 @@ never removes an entry you added. Each report gives the `pi remove` command that
|
|
|
243
244
|
resolves it, with the resolved path, because `pi remove` resolves a relative
|
|
244
245
|
argument against the current directory.
|
|
245
246
|
|
|
246
|
-
Re-run `agent-cortex install pi` after every `git pull` to refresh
|
|
247
|
-
package; registration is already in place, so that re-run changes nothing in
|
|
248
|
-
|
|
247
|
+
Re-run `agent-cortex install pi` (or `agent-cortex update pi`) after every `git pull` to refresh
|
|
248
|
+
the staged package; registration is already in place, so that re-run changes nothing in pi's
|
|
249
|
+
settings. Verify with `pi list` and `pi config` (restart pi to apply).
|
|
249
250
|
|
|
250
251
|
Refreshing the installed runtime after a release, and trying uncommitted work without
|
|
251
252
|
overwriting it, are recorded in [`docs/dev-loop.md`](docs/dev-loop.md).
|
|
@@ -408,8 +409,9 @@ from a different root is refreshed in place rather than re-pointed at the fresh
|
|
|
408
409
|
install. Re-point it with
|
|
409
410
|
`claude plugin marketplace remove jaybeeuu` and re-run the install.
|
|
410
411
|
|
|
411
|
-
The plain install IS the update — there is no separate build step
|
|
412
|
-
|
|
412
|
+
The plain install IS the update — there is no separate build step, and `agent-cortex update claude`
|
|
413
|
+
is the same materialise-and-register run selected by name. (`--output <dir>` only generates a
|
|
414
|
+
preview and never refreshes an installed plugin.)
|
|
413
415
|
|
|
414
416
|
#### Uninstall
|
|
415
417
|
|
|
@@ -474,7 +476,7 @@ version — picked up at install time) — then run
|
|
|
474
476
|
`agent-cortex install claude` (re-materialises the home plugin, merges settings, and
|
|
475
477
|
re-registers it; `--output <dir>` generates a preview only). The only committed file under
|
|
476
478
|
`claude/` is the settings template; `~/.agent-cortex/claude` is generated in full
|
|
477
|
-
(`.claude-plugin/plugin.json`, `agents/`, `skills/`, `hooks.json`, `.mcp.json`, `scripts/`)
|
|
479
|
+
(`.claude-plugin/plugin.json`, `agents/`, `skills/`, `docs/`, `hooks.json`, `.mcp.json`, `scripts/`)
|
|
478
480
|
so never edit anything under it. CI validates the Claude materialiser (structural checks on
|
|
479
481
|
a temp-dir install) so sources and installer can never silently diverge.
|
|
480
482
|
|
|
@@ -502,6 +504,42 @@ Only plain release versions are compared, so a prerelease on either side also re
|
|
|
502
504
|
The command is read-only: it stats and reads those manifests, and never installs, writes, or
|
|
503
505
|
reaches the network.
|
|
504
506
|
|
|
507
|
+
## Refreshing installed harnesses (`agent-cortex update`)
|
|
508
|
+
|
|
509
|
+
`agent-cortex update` re-runs a harness installer and reports what changed. It installs nothing
|
|
510
|
+
itself: every refresh goes through the same path as `agent-cortex install <harness>`, so update and
|
|
511
|
+
install can never disagree about what is out of date.
|
|
512
|
+
|
|
513
|
+
```sh
|
|
514
|
+
agent-cortex update # list the harnesses, then ask which to refresh
|
|
515
|
+
agent-cortex update pi claude # refresh exactly these, no prompt
|
|
516
|
+
agent-cortex update -a # refresh every harness not at the CLI version
|
|
517
|
+
agent-cortex update -a -f # re-run every installer regardless of version
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
The prompt lists every supported harness with its installed version, the CLI version, and a status
|
|
521
|
+
marker (`current`, `stale`, `ahead`, `unknown`, or `missing`) — the data `agent-cortex version`
|
|
522
|
+
reports, plus a row for a harness with no install. Answer with row numbers counted from the top
|
|
523
|
+
row, `a` for every harness, or blank to cancel.
|
|
524
|
+
|
|
525
|
+
A harness named on the command line always refreshes, whatever version it reports, and takes
|
|
526
|
+
precedence over `-a`. `-a` refreshes every harness that does not report `current`, so an
|
|
527
|
+
uninstalled, stale, ahead, or unreadable harness is refreshed. `-f` requires `-a`: alone it says
|
|
528
|
+
nothing about which harnesses to touch. An unknown name fails before any installer runs.
|
|
529
|
+
|
|
530
|
+
A shell that is not a terminal cannot answer the prompt, so a bare `agent-cortex update` there
|
|
531
|
+
prints the same table plus the flags above and exits non-zero — a script can never mistake
|
|
532
|
+
"nothing chosen" for "nothing to do".
|
|
533
|
+
|
|
534
|
+
The report gives each refreshed harness its previous version, its new version, and the artifacts
|
|
535
|
+
the installer wrote; harnesses already at the CLI version are listed as skipped. A harness's new
|
|
536
|
+
version is read back from the manifest its installer wrote, so a refresh that failed to
|
|
537
|
+
materialise reports `unknown` rather than claiming the CLI version.
|
|
538
|
+
|
|
539
|
+
This refreshes the installs, not the CLI. Bump the global package first
|
|
540
|
+
(`pnpm add -g @jaybeeuu/agent-cortex@latest`), then update the harnesses — see
|
|
541
|
+
[`docs/dev-loop.md`](docs/dev-loop.md).
|
|
542
|
+
|
|
505
543
|
## Migrating a beads workspace
|
|
506
544
|
|
|
507
545
|
`agent-cortex migrate` brings the `.beads` workspace it runs in — any repo, not just this
|
package/agents/ralph/agent.md
CHANGED
|
@@ -21,7 +21,7 @@ Each pipeline stage is a separate **chore bead**, pre-created by `create-chores.
|
|
|
21
21
|
- **Single-feature epics** (epic has exactly one feature task child): skip the epic branch entirely. The feature branch (`feature/<parent-id>`) is based directly from `origin/main`. The HITL PR targets `main` directly. No epic PR is required.
|
|
22
22
|
- **Multi-feature epics** (epic has two or more feature task children): each epic runs on `epic/<epic-id>` (base: `origin/main`, never local `main`). Each feature branch is based from its epic branch. Completed features PR into `epic/<epic-id>`; the completed epic PRs into `main`.
|
|
23
23
|
- Each parent feature task runs on `feature/<parent-id>` in `.agent-cortex/worktrees/<parent-id>`.
|
|
24
|
-
- Each implemented bead owns a child HITL PR-gate task bead (`lifecycle:feature-pr`, blocked by its own `document` chore) for PR review/merge.
|
|
24
|
+
- Each implemented bead owns a child HITL PR-gate task bead (`lifecycle:feature-pr`, blocked by its own `document` chore) for PR review/merge, plus a `stage:housekeeping` chore blocked behind that gate.
|
|
25
25
|
- Completed features must be reviewed by PR merge from the agent branch (`feature/<parent-id>`) into its base (epic branch for multi-feature epics, `main` for single-feature epics), then the HITL PR task bead must be closed by a human before Ralph continues feature scheduling.
|
|
26
26
|
- Completed multi-feature epics must be reviewed by PR merge into `main`.
|
|
27
27
|
|
|
@@ -93,11 +93,13 @@ A ready `execution:afk` task bead with no `stage:*` chore children has nothing t
|
|
|
93
93
|
{ "kind": "expand", "choreId": null, "parentId": "<parent-id>", "title": "<parent title>", "agentId": "<agent-id>", "logLine": 1 }
|
|
94
94
|
```
|
|
95
95
|
That entry is what keeps it in the wait set and makes its completion attributable to the parent.
|
|
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.
|
|
96
|
+
4. The worker reports the returned mapping — `code`, `verify`, `review`, `document`, `housekeeping`, `featurePrReview` — and closes nothing. It emits **no** `---REPORT---` block and owns no bead, so there is nothing to parse and no close to confirm.
|
|
97
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).
|
|
98
98
|
|
|
99
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.
|
|
100
100
|
|
|
101
|
+
**Housekeeping chore.** One stage runs outside the pipeline's ordering: `stage:housekeeping`, labelled `lifecycle:housekeeping` and blocked behind the PR gate bead, so it becomes ready only after a human merges the PR and closes the gate. Dispatch it with the **main checkout** as cwd — not the feature worktree — because the worktree it removes can be the parent's own. The worker runs `prune-worktrees.mjs`, retries once on a non-zero exit (exit `1` is normal: an open bead counts as survival), and then decides from the per-worktree report. It fails the chore and leaves it **open** when a `closed-bead` worktree is still `refused`; a `kept` or `refused` entry for an open bead is a normal report. Never close a chore over a worktree the script did not remove.
|
|
102
|
+
|
|
101
103
|
**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.
|
|
102
104
|
|
|
103
105
|
---
|
package/bin/agent-cortex.mjs
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { parseArgs, buildHelpText, resolveProgramName, validateHarness } from "../lib/cli.mjs";
|
|
3
|
+
import { parseArgs, buildHelpText, resolveProgramName, validateHarness, SUPPORTED_HARNESSES } from "../lib/cli.mjs";
|
|
4
4
|
import {
|
|
5
5
|
collectVersionReport,
|
|
6
|
+
collectHarnessStatus,
|
|
6
7
|
formatVersionReport,
|
|
7
8
|
serialiseVersionReport,
|
|
8
9
|
} from "../lib/harness-versions.mjs";
|
|
10
|
+
import {
|
|
11
|
+
describeClaudeArtifacts,
|
|
12
|
+
describePiArtifacts,
|
|
13
|
+
formatHarnessTable,
|
|
14
|
+
formatUpdateFlags,
|
|
15
|
+
formatUpdateReport,
|
|
16
|
+
promptHarnessSelection,
|
|
17
|
+
runUpdate,
|
|
18
|
+
selectHarnesses,
|
|
19
|
+
} from "../lib/harness-update.mjs";
|
|
9
20
|
import { join, dirname } from "node:path";
|
|
10
21
|
import { fileURLToPath } from "node:url";
|
|
11
22
|
import { readFile } from "node:fs/promises";
|
|
@@ -90,50 +101,76 @@ if (parsed.command === "install") {
|
|
|
90
101
|
process.exit(1);
|
|
91
102
|
}
|
|
92
103
|
|
|
93
|
-
|
|
104
|
+
try {
|
|
105
|
+
await installHarness(/** @type {"pi" | "claude"} */ (parsed.harness), {
|
|
106
|
+
dryRun: parsed.dryRun ?? false,
|
|
107
|
+
...(parsed.output ? { output: parsed.output } : {}),
|
|
108
|
+
...(parsed.pluginRoot ? { pluginRoot: parsed.pluginRoot } : {}),
|
|
109
|
+
requireRegister: parsed.requireRegister ?? false,
|
|
110
|
+
});
|
|
111
|
+
} catch (err) {
|
|
112
|
+
process.stderr.write(`Install failed: ${/** @type {Error} */ (err).message}\n`);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
process.exit(0);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (parsed.command === "update") {
|
|
119
|
+
process.exitCode = await runUpdateCommand(parsed, programName);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Run one harness's installer and, for a plain install, its registration.
|
|
124
|
+
* `install <harness>` and `update` both come through here, so update-vs-install
|
|
125
|
+
* stays the installer's decision and neither command grows a second install path.
|
|
126
|
+
* @param {"pi" | "claude"} harness
|
|
127
|
+
* @param {object} [options]
|
|
128
|
+
* @param {boolean} [options.dryRun]
|
|
129
|
+
* @param {string} [options.output]
|
|
130
|
+
* @param {string} [options.pluginRoot]
|
|
131
|
+
* @param {boolean} [options.requireRegister]
|
|
132
|
+
* @returns {Promise<string[]>} What the installer wrote, for the update report
|
|
133
|
+
*/
|
|
134
|
+
async function installHarness(harness, { dryRun = false, output, pluginRoot, requireRegister = false } = {}) {
|
|
135
|
+
if (harness === "pi") {
|
|
94
136
|
// A plain `agent-cortex install pi` stages the package into the home
|
|
95
137
|
// install root (~/.agent-cortex/pi) and registers that directory with pi
|
|
96
138
|
// through pi's own `pi install` (idempotent by state: a re-run is a no-op
|
|
97
139
|
// update); `--output <dir>` is the stage-only form (tests/CI) and prints
|
|
98
140
|
// the registration command instead.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
141
|
+
const result = await installPi({
|
|
142
|
+
dryRun,
|
|
143
|
+
warn: () => {}, // warnings surface once in the printed summary
|
|
144
|
+
...(output ? { output } : {}),
|
|
145
|
+
...(pluginRoot ? { pluginRoot } : {}),
|
|
146
|
+
});
|
|
147
|
+
printPiInstall(result);
|
|
148
|
+
if (output === undefined) {
|
|
149
|
+
const registration = await registerPi({
|
|
150
|
+
output: result.output,
|
|
151
|
+
dryRun,
|
|
152
|
+
requireRegister,
|
|
153
|
+
warn: (msg) => process.stdout.write(` ⚠ ${msg}\n`),
|
|
105
154
|
});
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
// depend on this guard staying in place.
|
|
118
|
-
if (registration.registered && !(parsed.dryRun ?? false)) {
|
|
119
|
-
printPiSweep(
|
|
120
|
-
await sweepLegacyPiUserDirs({
|
|
121
|
-
piUserDir: dirname(registration.settingsPath),
|
|
122
|
-
dryRun: parsed.dryRun ?? false,
|
|
123
|
-
}),
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
} else {
|
|
127
|
-
process.stdout.write(` → register it with pi: pi install ${result.output}\n`);
|
|
155
|
+
// The old installer's rendered copies are only stale once the staged
|
|
156
|
+
// package is what pi loads — and nothing is deleted on a dry run. The
|
|
157
|
+
// flag is passed through as well, so the sweep's own guarantee does not
|
|
158
|
+
// depend on this guard staying in place.
|
|
159
|
+
if (registration.registered && !dryRun) {
|
|
160
|
+
printPiSweep(
|
|
161
|
+
await sweepLegacyPiUserDirs({
|
|
162
|
+
piUserDir: dirname(registration.settingsPath),
|
|
163
|
+
dryRun,
|
|
164
|
+
}),
|
|
165
|
+
);
|
|
128
166
|
}
|
|
129
|
-
}
|
|
130
|
-
process.
|
|
131
|
-
process.exit(1);
|
|
167
|
+
} else {
|
|
168
|
+
process.stdout.write(` → register it with pi: pi install ${result.output}\n`);
|
|
132
169
|
}
|
|
133
|
-
|
|
170
|
+
return describePiArtifacts(result);
|
|
134
171
|
}
|
|
135
172
|
|
|
136
|
-
process.stdout.write(`Installing agent-cortex for "${
|
|
173
|
+
process.stdout.write(`Installing agent-cortex for "${harness}" harness…\n`);
|
|
137
174
|
|
|
138
175
|
// claude is the first wired harness: a plain `agent-cortex install claude`
|
|
139
176
|
// materialises the plugin into the home install root (~/.agent-cortex/claude)
|
|
@@ -144,40 +181,90 @@ if (parsed.command === "install") {
|
|
|
144
181
|
// (marketplace add → install → update); `--output <dir>` is the generate-only
|
|
145
182
|
// form (tests/CI validation), and the generated claude/ plugin subtree is
|
|
146
183
|
// never committed — only the settings template is.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
184
|
+
|
|
185
|
+
// Avoid loading the installer on the help/summary paths and for other harnesses.
|
|
186
|
+
const { installClaude, registerClaude } = await import("./installers/claude.mjs");
|
|
187
|
+
const result = await installClaude({
|
|
188
|
+
dryRun,
|
|
189
|
+
warn: () => {}, // warnings surface once via result.warnings
|
|
190
|
+
...(output ? { output } : {}),
|
|
191
|
+
});
|
|
192
|
+
for (const warning of result.warnings) {
|
|
193
|
+
process.stdout.write(` ⚠ ${warning}\n`);
|
|
194
|
+
}
|
|
195
|
+
// Runtime registration happens only for the plain install — `--output`
|
|
196
|
+
// keeps generating only (documented in --help).
|
|
197
|
+
if (output === undefined) {
|
|
198
|
+
await registerClaude({
|
|
199
|
+
// A default install always materialises the marketplace manifest; the
|
|
200
|
+
// generate-only form (--output) is the only path where it is null, and
|
|
201
|
+
// this branch is not that path.
|
|
202
|
+
root: /** @type {string} */ (result.marketplaceRoot),
|
|
203
|
+
manifest: /** @type {object} */ (result.marketplaceManifest),
|
|
204
|
+
pluginVersion: result.pluginVersion,
|
|
205
|
+
// The enablement check reads the same file this install just merged:
|
|
206
|
+
// a plugin Claude Code does not enable loads nothing.
|
|
207
|
+
...(result.settings ? { settingsPath: result.settings.path } : {}),
|
|
208
|
+
dryRun,
|
|
209
|
+
requireRegister,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
return describeClaudeArtifacts(result);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* `ctx update`: choose the harnesses, refresh each through its installer, report
|
|
217
|
+
* what changed. Every write happens inside `installHarness`'s call into the
|
|
218
|
+
* harness installer — this command adds selection and reporting only.
|
|
219
|
+
* @param {import("../lib/cli.mjs").ParsedArgs} parsed
|
|
220
|
+
* @param {string} programName
|
|
221
|
+
* @returns {Promise<number>} The exit code
|
|
222
|
+
*/
|
|
223
|
+
async function runUpdateCommand(parsed, programName) {
|
|
224
|
+
if (parsed.optionError) {
|
|
225
|
+
process.stderr.write(`${parsed.optionError}\n`);
|
|
226
|
+
return 1;
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
const cliVersion = await readPackageVersion();
|
|
230
|
+
const statuses = await collectHarnessStatus({ cli: cliVersion });
|
|
231
|
+
const all = parsed.all ?? false;
|
|
232
|
+
let harnesses = parsed.harnesses ?? [];
|
|
233
|
+
|
|
234
|
+
// Bare `ctx update` asks; a non-interactive shell gets the same table plus
|
|
235
|
+
// the flags that select without a prompt, and a non-zero exit so a script
|
|
236
|
+
// never mistakes "nothing chosen" for "nothing to do".
|
|
237
|
+
if (harnesses.length === 0 && !all) {
|
|
238
|
+
if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
|
|
239
|
+
process.stdout.write(`${formatHarnessTable(statuses)}\n\n${formatUpdateFlags(programName)}\n`);
|
|
240
|
+
return 1;
|
|
158
241
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
// A default install always materialises the marketplace manifest; the
|
|
164
|
-
// generate-only form (--output) is the only path where it is null, and
|
|
165
|
-
// this branch is not that path.
|
|
166
|
-
root: /** @type {string} */ (result.marketplaceRoot),
|
|
167
|
-
manifest: /** @type {object} */ (result.marketplaceManifest),
|
|
168
|
-
pluginVersion: result.pluginVersion,
|
|
169
|
-
// The enablement check reads the same file this install just merged:
|
|
170
|
-
// a plugin Claude Code does not enable loads nothing.
|
|
171
|
-
...(result.settings ? { settingsPath: result.settings.path } : {}),
|
|
172
|
-
dryRun: parsed.dryRun ?? false,
|
|
173
|
-
requireRegister: parsed.requireRegister ?? false,
|
|
174
|
-
});
|
|
242
|
+
harnesses = await promptHarnessSelection({ statuses });
|
|
243
|
+
if (harnesses.length === 0) {
|
|
244
|
+
process.stdout.write("No harness selected; nothing updated.\n");
|
|
245
|
+
return 0;
|
|
175
246
|
}
|
|
176
|
-
process.exit(0);
|
|
177
|
-
} catch (err) {
|
|
178
|
-
process.stderr.write(`Install failed: ${/** @type {Error} */ (err).message}\n`);
|
|
179
|
-
process.exit(1);
|
|
180
247
|
}
|
|
248
|
+
|
|
249
|
+
const plan = selectHarnesses({ statuses, harnesses, all, force: parsed.force ?? false });
|
|
250
|
+
if (plan.unknown.length > 0) {
|
|
251
|
+
const names = plan.unknown.map((name) => `"${name}"`).join(", ");
|
|
252
|
+
process.stderr.write(
|
|
253
|
+
`Unknown harness ${names}. Supported harnesses: ${SUPPORTED_HARNESSES.join(", ")}\nRun "${programName} --help" for usage.\n`,
|
|
254
|
+
);
|
|
255
|
+
return 1;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const entries = await runUpdate({
|
|
259
|
+
cli: cliVersion,
|
|
260
|
+
targets: plan.targets,
|
|
261
|
+
refresh: (harness) => installHarness(/** @type {"pi" | "claude"} */ (harness), {}),
|
|
262
|
+
});
|
|
263
|
+
process.stdout.write(`${formatUpdateReport({ cli: cliVersion, entries, skipped: plan.skipped })}\n`);
|
|
264
|
+
return 0;
|
|
265
|
+
} catch (err) {
|
|
266
|
+
process.stderr.write(`Update failed: ${/** @type {Error} */ (err).message}\n`);
|
|
267
|
+
return 1;
|
|
181
268
|
}
|
|
182
269
|
}
|
|
183
270
|
|
|
@@ -217,6 +304,9 @@ function printPiInstall(result) {
|
|
|
217
304
|
process.stdout.write(` ✓ ${agent.name}.agent.md → ${agent.filePath}\n`);
|
|
218
305
|
}
|
|
219
306
|
process.stdout.write(` ✓ Substituted ${result.skills.md} markdown file(s) across ${result.skills.skills} skill(s) → ${result.skills.dir}\n`);
|
|
307
|
+
if (result.docs.files.length > 0) {
|
|
308
|
+
process.stdout.write(` ✓ Copied ${result.docs.files.length} doc(s) → ${result.docs.dir}\n`);
|
|
309
|
+
}
|
|
220
310
|
process.stdout.write(` ✓ Copied ${result.extensions.files} extension file(s) → ${result.extensions.dir}\n`);
|
|
221
311
|
for (const name of result.packages.bundled) {
|
|
222
312
|
process.stdout.write(` ✓ Bundled pi package ${name} → ${join(result.packages.dir, name)}\n`);
|
|
@@ -22,6 +22,12 @@
|
|
|
22
22
|
// layout (scripts/lib/skill-links.mjs),
|
|
23
23
|
// so a grouped-tree handoff still
|
|
24
24
|
// resolves in the plugin.
|
|
25
|
+
// <output>/docs/... documents the skills read after
|
|
26
|
+
// install (docs/skills/skill-anatomy.md),
|
|
27
|
+
// copied verbatim once per install — a
|
|
28
|
+
// skill names them by {{PATH:key}}, so
|
|
29
|
+
// the token map decides both location
|
|
30
|
+
// and shipment
|
|
25
31
|
// <output>/hooks.json + hooks/<scripts> from hooks/claude/ (support files
|
|
26
32
|
// referenced via ${CLAUDE_PLUGIN_ROOT})
|
|
27
33
|
// <output>/.mcp.json + scripts/ hand-authored extras copied from
|
|
@@ -67,6 +73,7 @@ import { join, dirname, basename, resolve } from "node:path";
|
|
|
67
73
|
import { fileURLToPath } from "node:url";
|
|
68
74
|
import { homedir } from "node:os";
|
|
69
75
|
import { composeAgent, loadTokenMap, substituteTokens } from "../../scripts/lib/compose-agent.mjs";
|
|
76
|
+
import { installDocs } from "../../scripts/lib/docs.mjs";
|
|
70
77
|
import { isDirectory, isFile } from "../../scripts/lib/fs.mjs";
|
|
71
78
|
import { flattenSkillLinks } from "../../scripts/lib/skill-links.mjs";
|
|
72
79
|
|
|
@@ -633,6 +640,26 @@ async function isSymlink(filePath) {
|
|
|
633
640
|
}
|
|
634
641
|
}
|
|
635
642
|
|
|
643
|
+
/**
|
|
644
|
+
* Result of generating (or planning) the Claude Code plugin.
|
|
645
|
+
* @typedef {object} InstallClaudeResult
|
|
646
|
+
* @property {string} output
|
|
647
|
+
* @property {string | null} marketplaceRoot
|
|
648
|
+
* @property {string | null} manifestPath
|
|
649
|
+
* @property {object | null} marketplaceManifest
|
|
650
|
+
* @property {string[]} agents
|
|
651
|
+
* @property {{ names: string[], md: number, files: number, dir: string }} skills
|
|
652
|
+
* @property {{ files: string[], dir: string }} docs
|
|
653
|
+
* @property {boolean} hooks
|
|
654
|
+
* @property {string[]} hookFiles
|
|
655
|
+
* @property {string[]} handAuthored
|
|
656
|
+
* @property {{ path: string, action: string, removedSymlink: boolean } | null} settings
|
|
657
|
+
* @property {{ path: string, action: string, removedSymlink: boolean } | null} instructions
|
|
658
|
+
* @property {string} pluginVersion
|
|
659
|
+
* @property {string[]} warnings
|
|
660
|
+
* @property {boolean} dryRun
|
|
661
|
+
*/
|
|
662
|
+
|
|
636
663
|
/**
|
|
637
664
|
* Generate the Claude Code plugin subtree.
|
|
638
665
|
*
|
|
@@ -654,13 +681,7 @@ async function isSymlink(filePath) {
|
|
|
654
681
|
* explicitly materialises the section even
|
|
655
682
|
* with --output — the test/CI seam.
|
|
656
683
|
* @param {(msg: string) => void} [options.warn] Warning sink, also collected in `warnings`.
|
|
657
|
-
* @returns {Promise<
|
|
658
|
-
* marketplaceManifest: object|null, agents: string[],
|
|
659
|
-
* skills: {names: string[], md: number, files: number, dir: string},
|
|
660
|
-
* hooks: boolean, hookFiles: string[], handAuthored: string[],
|
|
661
|
-
* settings: {path: string, action: string, removedSymlink: boolean}|null,
|
|
662
|
-
* instructions: {path: string, action: string, removedSymlink: boolean}|null,
|
|
663
|
-
* pluginVersion: string, warnings: string[], dryRun: boolean }>}
|
|
684
|
+
* @returns {Promise<InstallClaudeResult>}
|
|
664
685
|
*/
|
|
665
686
|
export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = false, pluginRoot, settingsPath, instructionsPath, warn = DEFAULT_WARN } = {}) {
|
|
666
687
|
const tokenMap = await loadTokenMap(root);
|
|
@@ -690,7 +711,7 @@ export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = fals
|
|
|
690
711
|
// or a tmp dir, never claude-extras/, but clearing the output first would
|
|
691
712
|
// still destroy sources if they ever collided). They are re-copied fresh in
|
|
692
713
|
// the write phase. In dry-run nothing is cleaned or written.
|
|
693
|
-
const cleanable = ["agents", "skills", ".claude-plugin", "hooks", "scripts", "hooks.json", ".mcp.json"];
|
|
714
|
+
const cleanable = ["agents", "skills", "docs", ".claude-plugin", "hooks", "scripts", "hooks.json", ".mcp.json"];
|
|
694
715
|
const handAuthored = await buildHandAuthored(root);
|
|
695
716
|
|
|
696
717
|
// Regenerate the plugin children (removes stale output from earlier flows,
|
|
@@ -721,6 +742,7 @@ export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = fals
|
|
|
721
742
|
const agents = await buildAgents(root);
|
|
722
743
|
const skillOut = join(out, "skills");
|
|
723
744
|
const skills = await buildSkills(root, skillOut, { dryRun, pluginRoot: rootPlugin, tokenMap, warn: warnSink });
|
|
745
|
+
const docs = await installDocs({ root, output: out, dryRun, tokenMap });
|
|
724
746
|
const pluginJson = await buildPluginJson(root);
|
|
725
747
|
const hooksSrc = join(root, "hooks", "claude", "hooks.json");
|
|
726
748
|
const hooksJson = (await isFile(hooksSrc)) ? await readFile(hooksSrc, "utf-8") : null;
|
|
@@ -735,6 +757,7 @@ export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = fals
|
|
|
735
757
|
pluginVersion: JSON.parse(pluginJson).version,
|
|
736
758
|
agents: agents.files.map((f) => f.file.replace(/\.md$/, "")),
|
|
737
759
|
skills: { names: skills.names, md: skills.md, files: skills.files, dir: skillOut },
|
|
760
|
+
docs,
|
|
738
761
|
hooks: hooksJson !== null,
|
|
739
762
|
hookFiles: hookFiles.map((f) => f.rel),
|
|
740
763
|
handAuthored: handAuthored.map((f) => f.rel),
|
|
@@ -748,6 +771,7 @@ export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = fals
|
|
|
748
771
|
console.log(`DRY-RUN — would generate into ${out}, no files written:`);
|
|
749
772
|
console.log(` agents: ${summary.agents.join(", ")}`);
|
|
750
773
|
console.log(` skills: ${summary.skills.names.length} copied → ${skillOut}/<name>/ (token-substituted)`);
|
|
774
|
+
if (docs.files.length) console.log(` docs: ${docs.files.join(", ")} → ${docs.dir}/ (verbatim copies)`);
|
|
751
775
|
console.log(` .claude-plugin/plugin.json (version ${JSON.parse(pluginJson).version})`);
|
|
752
776
|
if (isDefaultInstall && manifestPath) {
|
|
753
777
|
console.log(` marketplace manifest → ${manifestPath} (plugin agent-cortex @ ./${basename(out)})`);
|
|
@@ -778,6 +802,7 @@ export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = fals
|
|
|
778
802
|
console.log(
|
|
779
803
|
`Copied ${summary.skills.names.length} skill(s) → ${skillOut}/<name>/ (token-substituted, no symlinks)`,
|
|
780
804
|
);
|
|
805
|
+
if (docs.files.length) console.log(`Copied ${docs.files.length} doc(s) → ${docs.dir}/ (verbatim)`);
|
|
781
806
|
|
|
782
807
|
await mkdir(join(out, ".claude-plugin"), { recursive: true });
|
|
783
808
|
await writeFile(join(out, ".claude-plugin", "plugin.json"), pluginJson);
|
package/bin/installers/pi.mjs
CHANGED
|
@@ -22,6 +22,11 @@
|
|
|
22
22
|
// <extension>/../../agents, i.e. this dir
|
|
23
23
|
// <output>/skills/<group>/<name>/ token-substituted skill copies — real
|
|
24
24
|
// files, never symlinks
|
|
25
|
+
// <output>/docs/... documents the skills read after install
|
|
26
|
+
// (docs/skills/skill-anatomy.md), copied
|
|
27
|
+
// verbatim once per install — a skill names
|
|
28
|
+
// them by {{PATH:key}}, so the token map
|
|
29
|
+
// decides both location and shipment
|
|
25
30
|
// <output>/extensions/ copy of the repo extensions/ tree (loaded
|
|
26
31
|
// by pi's package discovery)
|
|
27
32
|
// <output>/token-map.json the map agent-modes reads from the package
|
|
@@ -115,6 +120,7 @@ import { join, dirname, resolve } from "node:path";
|
|
|
115
120
|
import { fileURLToPath } from "node:url";
|
|
116
121
|
import { homedir } from "node:os";
|
|
117
122
|
import { composeAgent, loadTokenMap, substituteTokens, translateToolList } from "../../scripts/lib/compose-agent.mjs";
|
|
123
|
+
import { installDocs } from "../../scripts/lib/docs.mjs";
|
|
118
124
|
import { isDirectory, isFile } from "../../scripts/lib/fs.mjs";
|
|
119
125
|
|
|
120
126
|
const PI = "pi";
|
|
@@ -141,6 +147,7 @@ const PI = "pi";
|
|
|
141
147
|
* @property {string} output
|
|
142
148
|
* @property {{ name: string, filePath: string }[]} agents
|
|
143
149
|
* @property {{ skills: number, md: number, files: number, dir: string }} skills
|
|
150
|
+
* @property {{ files: string[], dir: string }} docs
|
|
144
151
|
* @property {{ files: number, dir: string }} extensions
|
|
145
152
|
* @property {{ dir: string, bundled: string[], staged: string[], resources: PackageResources }} packages
|
|
146
153
|
* @property {{ path: string, manifest: PiManifest }} manifest
|
|
@@ -181,7 +188,7 @@ const STAGED_NAME = "@jaybeeuu/agent-cortex-pi";
|
|
|
181
188
|
// before every write so re-staging cannot leave stale copies behind (a skill
|
|
182
189
|
// deleted in the repo, a package dropped from the manifest). Anything else in
|
|
183
190
|
// the output directory is left alone.
|
|
184
|
-
const OWNED_CHILDREN = ["package.json", "token-map.json", "agents", "skills", "extensions", "node_modules"];
|
|
191
|
+
const OWNED_CHILDREN = ["package.json", "token-map.json", "agents", "skills", "docs", "extensions", "node_modules"];
|
|
185
192
|
|
|
186
193
|
const GENERATED_MARKER =
|
|
187
194
|
"GENERATED by `agent-cortex install pi` — DO NOT EDIT; re-run the installer to update.";
|
|
@@ -242,6 +249,7 @@ const DEFAULT_LEGACY_SWEEP_RECORD = join(homedir(), ".agent-cortex", "legacy-swe
|
|
|
242
249
|
* @param {(msg: string) => void} [options.warn] Warning sink, also collected in `warnings`
|
|
243
250
|
* @returns {Promise<{ output: string, agents: {name: string, filePath: string}[],
|
|
244
251
|
* skills: {skills: number, md: number, files: number, dir: string},
|
|
252
|
+
* docs: {files: string[], dir: string},
|
|
245
253
|
* extensions: {files: number, dir: string},
|
|
246
254
|
* packages: {dir: string, bundled: string[], staged: string[], resources: {extensions: string[], skills: string[]}},
|
|
247
255
|
* manifest: {path: string, manifest: PiManifest},
|
|
@@ -285,6 +293,7 @@ export async function installPi(options = {}) {
|
|
|
285
293
|
|
|
286
294
|
const agents = await installAgents({ root, agentsDir: join(output, "agents"), dryRun, pluginRoot, tokenMap, warn });
|
|
287
295
|
const skills = await installSkills({ root, skillsDir: join(output, "skills"), dryRun, pluginRoot, tokenMap, warn });
|
|
296
|
+
const docs = await installDocs({ root, output, dryRun, tokenMap });
|
|
288
297
|
const extensions = await installExtensions({ root, output, dryRun });
|
|
289
298
|
const tokenMapFile = await installTokenMap({ output, tokenMap, dryRun });
|
|
290
299
|
const packages = await stagePiPackages({ dependencies, output, resolvePackage, dryRun, warn });
|
|
@@ -301,6 +310,7 @@ export async function installPi(options = {}) {
|
|
|
301
310
|
output,
|
|
302
311
|
agents,
|
|
303
312
|
skills,
|
|
313
|
+
docs,
|
|
304
314
|
extensions,
|
|
305
315
|
packages,
|
|
306
316
|
manifest,
|