@jaybeeuu/agent-cortex 1.42.0 → 1.43.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.
Files changed (62) hide show
  1. package/.agents/skills/style-versioning/SKILL.md +73 -2
  2. package/.github/workflows/ci.yml +47 -27
  3. package/AGENTS.md +23 -2
  4. package/CHANGELOG.md +166 -0
  5. package/README.md +125 -73
  6. package/agents/README.md +17 -4
  7. package/bin/agent-cortex.mjs +48 -30
  8. package/bin/installers/pi.mjs +885 -237
  9. package/docs/claude-hooks.md +5 -4
  10. package/docs/commit-signing.md +231 -0
  11. package/docs/ideas/enable-signed-commit-enforcement-changesets-bot-commits-must-be-signed.md +10 -1
  12. package/docs/ideas/extension-manifests.md +5 -0
  13. package/docs/research/disable-model-invocation.md +103 -0
  14. package/docs/research/skill-design-research.md +2 -2
  15. package/docs/skills/skill-anatomy.md +5 -1
  16. package/docs/technical-direction/harness-config.md +25 -1
  17. package/docs/technical-direction/migration-execution-labels.md +17 -9
  18. package/extensions/agent-modes/discover.test.ts +19 -0
  19. package/extensions/agent-modes/discover.ts +25 -3
  20. package/extensions/session-start/README.md +38 -0
  21. package/extensions/session-start/index.test.ts +122 -0
  22. package/extensions/session-start/index.ts +27 -0
  23. package/extensions/session-start/package.json +19 -0
  24. package/extensions/session-start/tsconfig.json +12 -0
  25. package/extensions/skill-stats/pnpm-lock.yaml +329 -0
  26. package/extensions/subagent/README.md +1 -1
  27. package/extensions/subagent/agents.test.ts +176 -0
  28. package/extensions/subagent/agents.ts +102 -11
  29. package/extensions/subagent/index.ts +2 -1
  30. package/extensions/subagent/package.json +1 -1
  31. package/lib/cli.mjs +35 -9
  32. package/package.json +6 -5
  33. package/pi/settings.json +1 -7
  34. package/plugin.json +1 -1
  35. package/pnpm-workspace.yaml +6 -0
  36. package/scripts/check-changeset.mjs +42 -0
  37. package/scripts/lib/release-gates.mjs +61 -0
  38. package/scripts/lib/verify-worker-tree.mjs +107 -0
  39. package/scripts/resolve-publish-command.mjs +17 -0
  40. package/skills/planning/create-task/scripts/pnpm-lock.yaml +342 -0
  41. package/skills/productivity/validate-agent-dir/scripts/pnpm-lock.yaml +329 -0
  42. package/skills/workflow/git-workflow/SKILL.md +35 -1
  43. package/skills/workflow/ralph/REFERENCE.md +17 -1
  44. package/skills/workflow/ralph/SKILL.md +4 -1
  45. package/skills/workflow/run-pipeline-stage/SKILL.md +11 -4
  46. package/skills/workflow/run-pipeline-stage/playbooks/code.md +2 -0
  47. package/skills/workflow/run-pipeline-stage/playbooks/document.md +3 -0
  48. package/skills/workflow/run-pipeline-stage/playbooks/fix.md +2 -0
  49. package/skills/workflow/run-pipeline-stage/prompts/stage-runner.md +11 -0
  50. package/skills/workflow/run-pipeline-stage/scripts/pnpm-lock.yaml +342 -0
  51. package/test/cli.test.mjs +26 -29
  52. package/test/helpers/fake-pi.mjs +132 -0
  53. package/test/pi-installer.test.mjs +345 -286
  54. package/test/pi-register.test.mjs +342 -0
  55. package/test/pi-sweep.test.mjs +619 -0
  56. package/test/release-gates.test.mjs +190 -0
  57. package/test/verify-worker-tree.test.mjs +120 -0
  58. package/token-map.README.md +7 -6
  59. package/token-map.json +6 -5
  60. package/extensions/session-start.ts +0 -44
  61. package/lib/pi-packages.mjs +0 -169
  62. package/test/pi-packages.test.mjs +0 -295
package/README.md CHANGED
@@ -105,6 +105,13 @@ does its own checkout and `pnpm install` rather than sharing build artifacts fro
105
105
  the `setup` job — pnpm workspace symlinks don't survive artifact upload/download,
106
106
  so artifact sharing would break the workspace resolution that the build depends on.
107
107
 
108
+ The root `pnpm-workspace.yaml` carries a strict `allowBuilds` map: on pnpm 12 (what CI
109
+ pins) any dependency with install or build scripts that is not named there aborts
110
+ `pnpm install` with `ERR_PNPM_IGNORED_BUILDS`. pnpm 10 only warns, so a newly pulled-in
111
+ native dependency surfaces as a red `Setup` job before it surfaces locally. Adding one
112
+ therefore means deciding its build policy in that map — `false` unless it genuinely
113
+ needs to compile, with a comment saying why.
114
+
108
115
  The repo commits no generated `claude/` plugin output, so the `claude-plugin-check` job
109
116
  validates the Claude plugin **materialiser** instead of diffing a committed mirror:
110
117
  it runs `node bin/agent-cortex.mjs install claude --output <tmp dir>` and checks the
@@ -114,10 +121,14 @@ and no symlinks leak into the copied tree. The Copilot drift check
114
121
  (`pnpm build:copilot` + `git diff --exit-code -- 'agents/*.agent.md'`) still guards the
115
122
  committed flat agent files.
116
123
 
117
- A separate `changeset-check` job runs only on pull requests and fails any PR that
118
- touches a versioned path (`extensions/`, `skills/`, `agents/`, `package.json`, or
119
- `plugin.json`) without a changeset in `.changeset/`. Add one with `pnpm changeset` —
120
- the `style-versioning` skill documents the format.
124
+ A separate `changeset-check` job runs on every pull request and fails it unless the
125
+ PR adds a `.changeset/*.md` file. **Publishing is the default and `no-publish` is the
126
+ only opt-out**, so shipping nothing has to be a deliberate, reviewable act rather
127
+ than an accident. Two exemptions: the changesets bot's Version Packages PR (it
128
+ consumes changesets and adds none) and PRs labelled `no-publish`. The `no-publish`
129
+ label must exist in the repository's label settings — a one-off repo-settings step
130
+ that no PR can ship. The `style-versioning` skill documents the format of a
131
+ changeset.
121
132
 
122
133
  On pushes to `main`, a `release` job (gated on `lint`, `test`, and
123
134
  `claude-plugin-check`) runs changesets to open a `chore: version packages` PR when
@@ -125,90 +136,131 @@ changesets are pending, then publishes to npm once it lands. The version step ru
125
136
  `pnpm version-packages` — bumping `package.json`, syncing `plugin.json`, and
126
137
  regenerating the committed Copilot agent files (the Claude plugin is materialised at
127
138
  install time with the package version, so it has no committed output to regenerate)
128
- so the drift gates stay green; the
129
- publish step runs `pnpm publish-package` (pack + provenance publish). Publish
130
- authenticates via npm Trusted Publishing (OIDC) — no npm token or GitHub
131
- secret is needed, only the one-time npm-side setup on npmjs.com (package →
139
+ so the drift gates stay green; the publish step runs `pnpm publish-package` (pack +
140
+ provenance publish), or
141
+ `pnpm publish-noop` when the merged PR carried `no-publish`. Labels are invisible on
142
+ a push event, so the job resolves the PR merged for the pushed commit through the
143
+ GitHub API and reads its labels — no extra secret or scope, since the job already
144
+ holds `GITHUB_TOKEN` and `pull-requests: write`. A direct push with no merged PR
145
+ resolves to no labels and therefore publishes, so attempting to publish an
146
+ already-published version still fails loudly; there is deliberately no idempotency
147
+ guard. Publishing authenticates via npm Trusted Publishing (OIDC) — no npm token or
148
+ GitHub secret is needed, only the one-time npm-side setup on npmjs.com (package →
132
149
  Access → Trusted Publishing for the `jaybeeuu/agent-cortex` repo). Releases
133
150
  are sourced entirely from `main`.
134
151
 
135
- ## Installation
136
-
137
- ### Pi config files (`settings.json` + `keybindings.json`)
138
-
139
- `agent-cortex install pi` materialises `~/.pi/agent/settings.json` and
140
- `~/.pi/agent/keybindings.json` as real CLI-managed files — the legacy symlinks
141
- into this repo's `pi/` directory are gone. `pi/settings.json` and
142
- `pi/keybindings.json` are the committed **templates**; the live files exist only
143
- on the machine, so re-run the installer after pulling changes.
144
-
145
- `settings.json` is merged. The template supplies defaults for keys the live file
146
- lacks, every value already present in the live file wins, and keys the template
147
- does not know about are preserved — so personal config (and anything pi itself
148
- wrote) survives re-install. The one key the CLI owns outright is `packages`: it
149
- is rebuilt from the template with the repo path entry resolved against the live
150
- settings file.
151
-
152
- Two consequences follow from that precedence:
153
-
154
- - **Template changes to non-`packages` keys never reach an existing install.**
155
- Because the live value always wins, editing a default in `pi/settings.json`
156
- only affects a fresh install (or a manually deleted live file) — edit the live
157
- file to adopt a new default.
158
- - **`pi install npm:<pkg>` entries do not survive re-install.** `packages` is
159
- rebuilt from the template, so any `npm:` package pi added to the live file is
160
- dropped on the next `agent-cortex install pi`. Declare long-lived packages in
161
- the repo template instead.
162
-
163
- An unparseable live `settings.json` is warned about and overwritten.
164
- `keybindings.json` follows a checksum rule: written from the template, refreshed
165
- when the template changes, and left untouched once you edit it by hand (delete
166
- it to re-adopt the template).
167
-
168
- ### Pi package dependencies
169
-
170
- These packages are declared in the package manifest (`package.json` → `pi.packages`) and
171
- provisioned by `agent-cortex install pi`, so a clean `~/.pi` gets the tools they provide
172
- without a manual `pi install`. They are also declared in the template `pi/settings.json`,
173
- so the materialised `settings.json` registers them with pi.
174
-
175
- | Package | Version | Purpose |
176
- |---|---|---|
177
- | [`pi-web-access`](https://www.npmjs.com/package/pi-web-access) | 0.10.7 | Web search, URL fetching, GitHub repo access, PDF/YouTube/video analysis (`fetch_content`) |
178
- | [`pi-questions`](https://www.npmjs.com/package/pi-questions) | latest | Structured interactive questions (`ask_questions`) |
179
-
180
- Pass `--no-provision` to skip the package install step (e.g. offline machines); a
181
- missing `pi` CLI or failed install warns and leaves the rest of the install intact.
152
+ Merge the version PR whenever its plan and diff look right, but approve its CI
153
+ first: `changesets/action` pushes that branch with `GITHUB_TOKEN`, so GitHub parks
154
+ the CI run the push creates behind **“Approve workflows to run”** — a run with no
155
+ jobs, which reports nothing, so the merge box shows `BLOCKED` with an empty check
156
+ list even though no gate has run or failed. Approving it from the merge box (or
157
+ `gh api -X POST repos/{owner}/{repo}/actions/runs/<run-id>/approve`) starts the five
158
+ required checks. Do it after the last merge to `main`, not once at the start of a
159
+ release: every subsequent merge force-updates the version branch, and a new head
160
+ brings a new parked run. This stays manual by design — letting the bot's push
161
+ trigger CI on its own means a personal access token or GitHub App token, a
162
+ long-lived credential in repository secrets to save one click per release.
182
163
 
183
- Desktop notifications are handled by the local `extensions/notify/` extension
184
- (replaces the former `pi-notify` dependency). It sends an OSC desktop
185
- notification on multi-turn tasks, labelled with the tmux session:window.pane
186
- if available, or the project directory name otherwise.
164
+ ## Installation
187
165
 
188
- ### Pi harness agents & skills (`agent-cortex install pi`)
166
+ ### Pi package staging (`agent-cortex install pi`)
189
167
 
190
- The agents are already available to pi through the package (`pi.skills` + the
191
- `agent-modes` extension compose them at runtime), but the raw package files carry
192
- literal `{{TOOL:...}}` / `{{PATH:...}}` tokens. Run the pi installer to materialise
193
- composed agents and token-substituted skills into pi's user scope:
168
+ `agent-cortex install pi` materialises agent-cortex as a self-contained pi
169
+ package in `~/.agent-cortex/pi` — the pi analogue of `~/.agent-cortex/claude` —
170
+ and registers that directory with pi through pi's own `pi install`. Nothing is
171
+ generated into pi's own directories (`~/.pi/agent/…`): pi owns its settings
172
+ file, so the installer only reads it to decide the plan and lets the `pi` CLI
173
+ write it. Once the staged package is registered, the artifacts an older
174
+ installer rendered there are swept (see below).
194
175
 
195
176
  ```sh
196
- agent-cortex install pi
197
- # → ~/.pi/agent/agents/<name>.agent.md (ralph, plan, ralph-plan, strategy)
198
- # → ~/.pi/agent/skills/ (token-substituted skill tree)
199
- # → ~/.pi/agent/settings.json + keybindings.json (merged from the repo templates)
177
+ agent-cortex install pi # stage ~/.agent-cortex/pi + register it with pi
200
178
  ```
201
179
 
202
- Flags:
180
+ Registration is idempotent by state: the staged path is registered once (a
181
+ re-run is a true no-op — a local package source is read from disk on every pi
182
+ start, so nothing needs refreshing), and a source pointing at a raw agent-cortex
183
+ checkout is removed first, so exactly one agent-cortex source stays configured.
184
+ The installer decides that plan by reading pi's settings at
185
+ `~/.pi/agent/settings.json`, so a pi agent dir relocated with
186
+ `PI_CODING_AGENT_DIR` is not inspected — a superseded raw-repo source configured
187
+ there has to be removed by hand with `pi remove <path>`.
188
+
189
+ Under the old model the same installer rendered agents and skills into
190
+ `~/.pi/agent/{agents,skills}`. Those copies are a second source of the same
191
+ resource — the skill-collision diagnostics the staged model exists to remove —
192
+ so once registration succeeds the installer removes the ones it can attribute to
193
+ agent-cortex: `agents/<name>.agent.md` and `skills/<group>/<name>` for agents and
194
+ skills this package ships or once shipped, and a `.agent-cortex-install.json`
195
+ path record left there listing those copies. It is a one-time migration:
196
+ attribution is by name, which cannot tell a rendered copy from a file you wrote
197
+ under the same name, so once it has finished it records that in
198
+ `~/.agent-cortex/legacy-sweep.json` — keyed by the pi user directory, never
199
+ inside it — and never removes anything there again. A completed migration
200
+ leaves `~/.pi/agent` with no agent-cortex file in it. Only a migration that
201
+ removed every copy it attributed, from directories it could read, is recorded:
202
+ a copy that could not be deleted (permissions, `EBUSY`) or a directory that
203
+ could not be listed is warned about and retried on the next install, and a
204
+ record that cannot be written is a warning, not a failed install. Anything
205
+ whose name this package does not ship is left alone, and
206
+ `~/.pi/agent/extensions` is never touched at all — no
207
+ installer has written there, so its contents are yours. `--output <dir>` and
208
+ `--dry-run` sweep nothing.
209
+ The sweep follows the settings file the installer reads (`~/.pi/agent`): a pi
210
+ agent dir relocated with `PI_CODING_AGENT_DIR` keeps its copies.
211
+
212
+ The staged package contains:
213
+
214
+ | Path | Contents |
215
+ | --- | --- |
216
+ | `package.json` | pi manifest — `keywords: ["pi-package"]`, `pi.extensions` + `pi.skills`; the version tracks the package |
217
+ | `agents/` | composed flat agents (ralph, plan, ralph-plan, strategy) with `{{TOOL:...}}` / `{{PATH:...}}` substituted — the shipped `agent-modes` extension resolves them from the package root |
218
+ | `skills/` | token-substituted skill copies (real files, never symlinks) |
219
+ | `extensions/` | copy of the repo `extensions/` tree |
220
+ | `token-map.json` | the token map `agent-modes` reads from the package root |
221
+ | `node_modules/` | the required third-party pi packages and their transitive closure |
222
+
223
+ The third-party packages supply the tools the agents need — `pi-questions`
224
+ (`ask_questions`) and `pi-web-access` (`fetch_content`). They are ordinary
225
+ runtime dependencies of this package and travel inside the staged tree, so pi
226
+ loads them from a local directory without any npm resolution of its own. An
227
+ unresolvable dependency fails the install rather than staging a package that
228
+ cannot provide those tools. Re-staging clears the children the installer owns, so
229
+ a re-run never accumulates stale copies.
203
230
 
204
231
  | Flag | Meaning |
205
232
  | --- | --- |
206
- | `--dry-run` | Show what would be installed without writing anything |
207
- | `--output <dir>` | Install into `<dir>/agents`, `<dir>/skills` and the config files (default `~/.pi/agent`) |
233
+ | `--dry-run` | Show what would be staged and registered without writing or spawning anything |
234
+ | `--output <dir>` | Stage into `<dir>` only — no registration (tests/CI) |
208
235
  | `--plugin-root <dir>` | Override the plugin root used for `{{PATH:...}}` tokens (default: token-map.json's pi value — use it for checkout or symlinked installs) |
209
- | `--no-provision` | Skip provisioning the third-party pi packages declared in `package.json` `pi.packages` (default: install them via the `pi` CLI) |
236
+ | `--require-register` | Fail the install when the `pi` CLI cannot drive registration; by default a missing CLI warns and prints the commands to run |
237
+
238
+ If the `pi` CLI is not on `PATH`, the staged package is correct but unregistered:
239
+ the installer warns and prints the exact commands (`pi install
240
+ ~/.agent-cortex/pi`, plus a `pi remove` for any superseded raw-repo source).
241
+
242
+ A superseded raw-repo source that `pi remove` cannot drop fails the install
243
+ (non-zero, naming the source still configured): pi would load both sources, so
244
+ reporting success would leave the two-source state registration exists to
245
+ prevent. Nothing is installed on that path — remove the source by hand and re-run.
246
+
247
+ Re-run `agent-cortex install pi` after every `git pull` to refresh the staged
248
+ package; registration is already in place, so that re-run changes nothing in
249
+ pi's settings. Verify with `pi list` and `pi config` (restart pi to apply).
250
+
251
+ ### Desktop notifications
252
+
253
+ Desktop notifications are handled by the local `extensions/notify/` extension
254
+ (replaces the former `pi-notify` dependency). It sends an OSC desktop
255
+ notification on multi-turn tasks, labelled with the tmux session:window.pane
256
+ if available, or the project directory name otherwise.
257
+
258
+ ### Pi config files
210
259
 
211
- Re-run whenever you pull changes (`git pull` + reinstall, or after `pnpm build:copilot`).
260
+ `pi/settings.json` and `pi/keybindings.json` are committed **reference
261
+ templates** for your own pi configuration. They are not installer output: pi's
262
+ `settings.json` and `keybindings.json` are pi's own user config, so
263
+ `agent-cortex install pi` leaves them alone.
212
264
 
213
265
  ### Copilot plugin (separate)
214
266
 
package/agents/README.md CHANGED
@@ -153,6 +153,16 @@ composes agent prompts on the fly from `agent.md` + `pi/frontmatter.json`, subst
153
153
  tokens against `token-map.json`'s pi column with no install step. See
154
154
  `extensions/agent-modes/README.md`.
155
155
 
156
+ ### Rendered tool lists are comma-separated
157
+
158
+ pi's `parseFrontmatter` returns the `tools` scalar as one string, and its consumers split
159
+ that string on commas — the subagent extension forwards it straight to `pi --tools`, whose
160
+ parser is comma-separated too. Generated pi agents therefore render
161
+ `tools: "bash, read, task"`; a space-separated list collapses to a single bogus name and
162
+ leaves the agent with an allowlist matching no tool. Both parsers
163
+ (`extensions/subagent/agents.ts`, `extensions/agent-modes/discover.ts`) accept whitespace
164
+ as well, so either form round-trips safely.
165
+
156
166
  ## Composition Rules
157
167
 
158
168
  ### For Harnesses with Sections
@@ -216,8 +226,11 @@ flat `agents/*.agent.md` files are built from them:
216
226
  from `agents-native/ralph.md`, copied verbatim).
217
227
  - The pi harness delivers two ways: the `agent-modes` extension composes `agent.md` +
218
228
  `pi/frontmatter.json` at runtime against `token-map.json`, and `agent-cortex install pi`
219
- materialises composed agents + token-substituted skills into `~/.pi/agent` at install
220
- time (see README "Pi harness agents & skills").
229
+ renders flat pi agents + token-substituted skills into a self-contained package at
230
+ `~/.agent-cortex/pi`, which pi loads once registered (see README "Pi package staging
231
+ (`agent-cortex install pi`)"). `agent-modes` and the `subagent` extension both read agents
232
+ beside themselves,
233
+ so the staged package needs no `~/.pi/agent` writes.
221
234
 
222
235
  Never edit the generated flat materialised files by hand — edit the composable directory.
223
236
  Generation is install-time: `claude` ships
@@ -229,8 +242,8 @@ in `claude-extras/`), and CI validates the materialiser with structural checks o
229
242
  install; `copilot` ships via
230
243
  `bin/installers/copilot.mjs`, the shared code path behind both `agent-cortex install
231
244
  copilot` and `scripts/build-copilot-agents.mjs`; `pi` ships via
232
- `bin/installers/pi.mjs`, which materialises composed agents + token-substituted skills
233
- into `~/.pi/agent`.
245
+ `bin/installers/pi.mjs`, which stages a self-contained pi package (manifest, rendered
246
+ agents, token-substituted skills, extensions) at `~/.agent-cortex/pi`.
234
247
 
235
248
  ## Validation
236
249
 
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { parseArgs, buildHelpText, validateHarness } from "../lib/cli.mjs";
4
- import { installPi } from "./installers/pi.mjs";
4
+ import { join, dirname } from "node:path";
5
+ import { installPi, registerPi, sweepLegacyPiUserDirs } from "./installers/pi.mjs";
5
6
 
6
7
  const argv = process.argv.slice(2);
7
8
  const parsed = parseArgs(argv);
@@ -29,18 +30,41 @@ if (parsed.command === "install") {
29
30
  }
30
31
 
31
32
  if (parsed.harness === "pi") {
33
+ // A plain `agent-cortex install pi` stages the package into the home
34
+ // install root (~/.agent-cortex/pi) and registers that directory with pi
35
+ // through pi's own `pi install` (idempotent by state: a re-run is a no-op
36
+ // update); `--output <dir>` is the stage-only form (tests/CI) and prints
37
+ // the registration command instead.
32
38
  try {
33
39
  const result = await installPi({
34
40
  dryRun: parsed.dryRun ?? false,
35
- // Third-party pi packages are only provisioned for a real install:
36
- // --output is the generate-only form, --no-provision opts an offline
37
- // machine out explicitly.
38
- provisionPackages: parsed.output === undefined && !parsed.noProvision,
39
41
  warn: () => {}, // warnings surface once in the printed summary
40
42
  ...(parsed.output ? { output: parsed.output } : {}),
41
43
  ...(parsed.pluginRoot ? { pluginRoot: parsed.pluginRoot } : {}),
42
44
  });
43
45
  printPiInstall(result);
46
+ if (parsed.output === undefined) {
47
+ const registration = await registerPi({
48
+ output: result.output,
49
+ dryRun: parsed.dryRun ?? false,
50
+ requireRegister: parsed.requireRegister ?? false,
51
+ warn: (msg) => process.stdout.write(` ⚠ ${msg}\n`),
52
+ });
53
+ // The old installer's rendered copies are only stale once the staged
54
+ // package is what pi loads — and nothing is deleted on a dry run. The
55
+ // flag is passed through as well, so the sweep's own guarantee does not
56
+ // depend on this guard staying in place.
57
+ if (registration.registered && !(parsed.dryRun ?? false)) {
58
+ printPiSweep(
59
+ await sweepLegacyPiUserDirs({
60
+ piUserDir: dirname(registration.settingsPath),
61
+ dryRun: parsed.dryRun ?? false,
62
+ }),
63
+ );
64
+ }
65
+ } else {
66
+ process.stdout.write(` → register it with pi: pi install ${result.output}\n`);
67
+ }
44
68
  } catch (err) {
45
69
  process.stderr.write(`Install failed: ${err.message}\n`);
46
70
  process.exit(1);
@@ -106,20 +130,32 @@ if (parsed.command === "install") {
106
130
  process.exit(0);
107
131
  }
108
132
 
133
+ function printPiSweep({ removed }) {
134
+ if (removed.length === 0) return;
135
+ process.stdout.write(
136
+ ` ✓ Removed ${removed.length} artifact(s) the pre-staged-model installer rendered into pi's user directory (one-time migration — the staged package is the source from now on)\n`,
137
+ );
138
+ for (const path of removed) {
139
+ process.stdout.write(` - ${path}\n`);
140
+ }
141
+ }
142
+
109
143
  function printPiInstall(result) {
110
144
  process.stdout.write(`Installing agent-cortex for "pi" harness…\n`);
145
+ process.stdout.write(` ✓ Staged pi package → ${result.output}\n`);
111
146
  for (const agent of result.agents) {
112
147
  process.stdout.write(` ✓ ${agent.name}.agent.md → ${agent.filePath}\n`);
113
148
  }
114
149
  process.stdout.write(` ✓ Substituted ${result.skills.md} markdown file(s) across ${result.skills.skills} skill(s) → ${result.skills.dir}\n`);
115
- printManagedConfig("settings.json", result.settings);
116
- printManagedConfig("keybindings.json", result.keybindings);
117
- // A dry run installs nothing, so report the plan — otherwise the documented
118
- // "show what would be installed" contract prints no packages at all.
119
- const packages = result.packages ?? { planned: [], installed: [] };
120
- for (const source of result.dryRun ? packages.planned : packages.installed) {
121
- process.stdout.write(` ✓ ${result.dryRun ? "Would install" : "Installed"} pi package ${source}\n`);
150
+ process.stdout.write(` ✓ Copied ${result.extensions.files} extension file(s) → ${result.extensions.dir}\n`);
151
+ for (const name of result.packages.bundled) {
152
+ process.stdout.write(` ✓ Bundled pi package ${name} → ${join(result.packages.dir, name)}\n`);
153
+ }
154
+ const transitive = result.packages.staged.length - result.packages.bundled.length;
155
+ if (transitive > 0) {
156
+ process.stdout.write(` ✓ Copied ${transitive} transitive dependency tree(s) → ${result.packages.dir}\n`);
122
157
  }
158
+ process.stdout.write(` ✓ ${result.dryRun ? "Would write" : "Wrote"} ${result.manifest.path} (version ${result.manifest.manifest.version})\n`);
123
159
  for (const warning of result.warnings) {
124
160
  process.stdout.write(` ⚠ ${warning}\n`);
125
161
  }
@@ -127,21 +163,3 @@ function printPiInstall(result) {
127
163
  process.stdout.write("(dry-run — nothing written)\n");
128
164
  }
129
165
  }
130
-
131
- /** Report one managed pi config file (settings.json / keybindings.json). */
132
- function printManagedConfig(label, info) {
133
- if (!info) return;
134
- if (info.action === "would-remove-symlink") {
135
- process.stdout.write(` → would remove symlink ${info.path} and write ${label}\n`);
136
- } else if (info.action === "would-write") {
137
- process.stdout.write(` → would write ${label} → ${info.path}\n`);
138
- } else if (info.action === "unchanged") {
139
- process.stdout.write(` · ${label} → ${info.path} (unchanged)\n`);
140
- } else if (info.action === "skipped") {
141
- process.stdout.write(` ⚠ ${label} modified after install — left untouched → ${info.path}\n`);
142
- } else if (info.action === "removed-symlink") {
143
- process.stdout.write(` ✓ ${label} → ${info.path} (removed legacy symlink)\n`);
144
- } else {
145
- process.stdout.write(` ✓ ${label} → ${info.path}\n`);
146
- }
147
- }