@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
@@ -1,13 +1,13 @@
1
1
  import { describe, it } from "node:test";
2
2
  import assert from "node:assert/strict";
3
3
  import { mkdtemp, mkdir, writeFile, rm, readFile, readdir, stat, lstat, symlink } from "node:fs/promises";
4
- import { join, dirname, resolve } from "node:path";
4
+ import { join, dirname } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { installPi } from "../bin/installers/pi.mjs";
8
8
 
9
9
  // Real repo root — used by the integration smoke test to prove the installer
10
- // works against the canonical sources (agents/, skills/, token-map.json).
10
+ // works against the canonical sources (agents/, skills/, extensions/, token-map.json).
11
11
  const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
12
12
 
13
13
  /** True when a path exists (stat succeeds). */
@@ -39,7 +39,7 @@ function tokenMap(extra = {}) {
39
39
  harnesses: ["pi"],
40
40
  tools: { ...TOOL_MAP },
41
41
  paths: {
42
- plugin_root: { pi: "~/.pi/agent/npm/node_modules/@jaybeeuu/agent-cortex" },
42
+ plugin_root: { pi: "~/.agent-cortex/pi" },
43
43
  skills_dir: { pi: { base: "plugin_root", relative: "skills" } },
44
44
  agents_dir: { pi: { base: "plugin_root", relative: "agents" } },
45
45
  },
@@ -63,29 +63,53 @@ async function writeFixture(fx, relPath, content) {
63
63
  await writeFile(p, content);
64
64
  }
65
65
 
66
- /** Template pi settings the fixture package ships (defaults the installer merges). */
67
- function defaultPiSettings() {
68
- return {
69
- packages: [{ source: "../../src/agent-cortex", skills: [] }, "npm:pi-web-access@0.10.7"],
70
- defaultModel: "template/model",
71
- theme: "dark",
72
- tinyModel: { model: "template/tiny", maxNameLength: 40 },
73
- };
66
+ /** Write the fixture package's own package.json (version + declared runtime dependencies). */
67
+ function defaultPackageJson(version = "9.9.9", dependencies = {}) {
68
+ return { name: "fixture-package", version, dependencies };
74
69
  }
75
70
 
76
- function defaultPiKeybindings() {
77
- return { "tui.input.newLine": ["shift+enter"] };
71
+ /**
72
+ * Create a fake installed dependency — its own directory plus the package.json
73
+ * the installer reads to decide whether it ships pi resources.
74
+ */
75
+ async function createInstalledPackage(baseDir, { name, version = "1.0.0", dependencies = {}, pi, files = {} }) {
76
+ const dir = join(baseDir, name);
77
+ await mkdir(dir, { recursive: true });
78
+ await writeFile(
79
+ join(dir, "package.json"),
80
+ JSON.stringify({ name, version, dependencies, ...(pi === undefined ? {} : { pi }) }, null, 2),
81
+ );
82
+ for (const [rel, content] of Object.entries(files)) {
83
+ await mkdir(dirname(join(dir, rel)), { recursive: true });
84
+ await writeFile(join(dir, rel), content);
85
+ }
86
+ return dir;
87
+ }
88
+
89
+ /** Resolve the fake installed packages by name, recording who asked for them. */
90
+ function fakeResolver(packages) {
91
+ const calls = [];
92
+ return {
93
+ calls,
94
+ resolvePackage: async (name, fromDir) => {
95
+ calls.push({ name, fromDir });
96
+ const dir = packages[name];
97
+ if (dir === undefined) throw new Error(`cannot resolve "${name}"`);
98
+ return dir;
99
+ },
100
+ };
78
101
  }
79
102
 
80
- /** Seed a minimal but realistic package tree (agents + token-map + skills + pi templates). */
103
+ /**
104
+ * Seed a minimal but realistic package tree: token-map, package.json, agents,
105
+ * skills, and extensions — the sources the staged package is built from.
106
+ */
81
107
  async function seedPackage(fx, opts = {}) {
82
- const { agents = {}, skills = {}, tokenMap: map, version, packages } = opts;
83
- await writeFixture(fx, "token-map.json", JSON.stringify(map ?? tokenMap(version ? { version } : {})));
84
- if (packages) {
85
- await writeFixture(fx, "package.json", JSON.stringify({ name: "fixture", pi: { packages } }));
86
- }
87
- await writeFixture(fx, "pi/settings.json", JSON.stringify(opts.piSettings ?? defaultPiSettings(), null, 2));
88
- await writeFixture(fx, "pi/keybindings.json", JSON.stringify(opts.piKeybindings ?? defaultPiKeybindings(), null, 2));
108
+ const { agents = {}, tokenMap: map, tokenMapVersion, packageVersion = "9.9.9", dependencies } = opts;
109
+ await writeFixture(fx, "token-map.json", JSON.stringify(map ?? tokenMap(tokenMapVersion ? { version: tokenMapVersion } : {})));
110
+ await writeFixture(fx, "package.json", JSON.stringify(defaultPackageJson(packageVersion, dependencies), null, 2));
111
+ await writeFixture(fx, "extensions/echo/index.ts", "export default () => {};\n");
112
+ await writeFixture(fx, "skills/engineering/echo/SKILL.md", "---\nname: echo\ndescription: d\n---\n\nBody.\n");
89
113
  for (const [name, def] of Object.entries(agents)) {
90
114
  await writeFixture(fx, `agents/${name}/agent.md`, def.body ?? `# ${name}\nBody of ${name}.`);
91
115
  await writeFixture(
@@ -104,9 +128,6 @@ async function seedPackage(fx, opts = {}) {
104
128
  await writeFixture(fx, `agents/${name}/pi/${section}.md`, content);
105
129
  }
106
130
  }
107
- for (const [path, content] of Object.entries(skills)) {
108
- await writeFixture(fx, `skills/${path}`, content);
109
- }
110
131
  }
111
132
 
112
133
  async function listFiles(dir) {
@@ -141,7 +162,7 @@ describe("installPi — agent composition", () => {
141
162
  },
142
163
  });
143
164
 
144
- const result = await installPi({ root: fx.root, output: fx.output });
165
+ const result = await installPi({ root: fx.root, output: fx.output, resolvePackage: fakeResolver({}).resolvePackage });
145
166
 
146
167
  const file = join(fx.output, "agents", "alpha.agent.md");
147
168
  assert.ok(await pathExists(file), "agent file written");
@@ -149,7 +170,7 @@ describe("installPi — agent composition", () => {
149
170
  assert.ok(content.startsWith("---\n"), "yaml frontmatter");
150
171
  assert.match(content, /name: "agent-cortex:alpha"/);
151
172
  assert.match(content, /description: "Reads things\."/);
152
- assert.match(content, /tools: "bash read find"/, "tools translated to pi names, ask_user omitted");
173
+ assert.match(content, /tools: "bash, read, find"/, "tools translated to pi names, ask_user omitted");
153
174
  assert.match(content, /argument-hint: "Read something"/);
154
175
  assert.ok(content.includes("Use read to read."), "{{TOOL:view}} substituted");
155
176
  assert.ok(content.includes("Spawn with task."), "{{SECTION:polling}} composed from pi/polling.md");
@@ -161,6 +182,40 @@ describe("installPi — agent composition", () => {
161
182
  }
162
183
  });
163
184
 
185
+ it("renders the tool list with the separator its consumers split on (round trip)", async () => {
186
+ const fx = await makeFixture();
187
+ try {
188
+ await seedPackage(fx, {
189
+ agents: {
190
+ alpha: {
191
+ frontmatter: {
192
+ name: "agent-cortex:alpha",
193
+ description: "Delegates.",
194
+ tools: ["bash", "view", "glob", "task", "read_agent"],
195
+ },
196
+ },
197
+ },
198
+ });
199
+
200
+ await installPi({ root: fx.root, output: fx.output, resolvePackage: fakeResolver({}).resolvePackage });
201
+ const content = await readFile(join(fx.output, "agents", "alpha.agent.md"), "utf-8");
202
+
203
+ // pi's parseFrontmatter hands the tools value back as one scalar string.
204
+ const scalar = JSON.parse(content.match(/^tools: (.*)$/m)[1]);
205
+ assert.equal(typeof scalar, "string");
206
+
207
+ // extensions/subagent/agents.ts turns that scalar into `pi --tools`, and pi's
208
+ // flag parser splits on commas — so the rendered form must survive a comma
209
+ // split with the tool names intact, or the subagent starts with no tools.
210
+ assert.deepEqual(
211
+ scalar.split(",").map((t) => t.trim()),
212
+ ["bash", "read", "find", "task", "read_agent"],
213
+ );
214
+ } finally {
215
+ await fx.cleanup();
216
+ }
217
+ });
218
+
164
219
  it("drops null-mapped tool tokens in prose and reports a warning", async () => {
165
220
  const fx = await makeFixture();
166
221
  try {
@@ -170,7 +225,7 @@ describe("installPi — agent composition", () => {
170
225
  },
171
226
  });
172
227
 
173
- const result = await installPi({ root: fx.root, output: fx.output });
228
+ const result = await installPi({ root: fx.root, output: fx.output, resolvePackage: fakeResolver({}).resolvePackage });
174
229
  const content = await readFile(join(fx.output, "agents", "alpha.agent.md"), "utf-8");
175
230
 
176
231
  assert.ok(!content.includes("ask_user"), "null-mapped token dropped from prose");
@@ -186,7 +241,10 @@ describe("installPi — agent composition", () => {
186
241
  const fx = await makeFixture();
187
242
  try {
188
243
  await seedPackage(fx, { agents: { alpha: { body: "# alpha\nUse {{TOOL:nope}} now." } } });
189
- await assert.rejects(installPi({ root: fx.root, output: fx.output }), /unknown tool token {{TOOL:nope}}/);
244
+ await assert.rejects(
245
+ installPi({ root: fx.root, output: fx.output, resolvePackage: fakeResolver({}).resolvePackage }),
246
+ /unknown tool token {{TOOL:nope}}/,
247
+ );
190
248
  } finally {
191
249
  await fx.cleanup();
192
250
  }
@@ -196,7 +254,10 @@ describe("installPi — agent composition", () => {
196
254
  const fx = await makeFixture();
197
255
  try {
198
256
  await seedPackage(fx, { agents: { alpha: { body: "# alpha\n{{SECTION:missing}}" } } });
199
- await assert.rejects(installPi({ root: fx.root, output: fx.output }), /missing section "missing"/);
257
+ await assert.rejects(
258
+ installPi({ root: fx.root, output: fx.output, resolvePackage: fakeResolver({}).resolvePackage }),
259
+ /missing section "missing"/,
260
+ );
200
261
  } finally {
201
262
  await fx.cleanup();
202
263
  }
@@ -213,15 +274,12 @@ describe("installPi — agent composition", () => {
213
274
  },
214
275
  });
215
276
 
216
- const result = await installPi({ root: fx.root, output: fx.output });
277
+ const result = await installPi({ root: fx.root, output: fx.output, resolvePackage: fakeResolver({}).resolvePackage });
217
278
  const content = await readFile(join(fx.output, "agents", "alpha.agent.md"), "utf-8");
218
279
 
280
+ assert.ok(content.includes("Named: ~/.agent-cortex/pi/skills"), "named path resolves through token-map paths table");
219
281
  assert.ok(
220
- content.includes("Named: ~/.pi/agent/npm/node_modules/@jaybeeuu/agent-cortex/skills"),
221
- "named path resolves through token-map paths table",
222
- );
223
- assert.ok(
224
- content.includes("Relative: ~/.pi/agent/npm/node_modules/@jaybeeuu/agent-cortex/skills/x/SKILL.md"),
282
+ content.includes("Relative: ~/.agent-cortex/pi/skills/x/SKILL.md"),
225
283
  "relative path resolves against plugin root",
226
284
  );
227
285
  assert.equal(result.warnings.length, 0);
@@ -240,121 +298,30 @@ describe("installPi — agent composition", () => {
240
298
  });
241
299
 
242
300
  const pluginRoot = join(fx.root, "checkout");
243
- await installPi({ root: fx.root, output: fx.output, pluginRoot });
244
- const content = await readFile(join(fx.output, "agents", "alpha.agent.md"), "utf-8");
245
- assert.ok(content.includes(join(pluginRoot, "skills", "x", "SKILL.md")), "plugin root override used");
246
- } finally {
247
- await fx.cleanup();
248
- }
249
- });
250
-
251
- it("rejects token-map.json versions newer than the implemented contract", async () => {
252
- const fx = await makeFixture();
253
- try {
254
- await seedPackage(fx, {
255
- version: 99,
256
- agents: { alpha: { body: "# alpha" } },
257
- });
258
- await assert.rejects(installPi({ root: fx.root, output: fx.output }), /version 99/);
259
- } finally {
260
- await fx.cleanup();
261
- }
262
- });
263
- });
264
-
265
- // ─── Third-party package provisioning ────────────────────────────────────────
266
-
267
- describe("installPi — package provisioning", () => {
268
- function recordingRunner() {
269
- const calls = [];
270
- return {
271
- calls,
272
- runInstall: async (source) => {
273
- calls.push(source);
274
- return { ok: true, error: null };
275
- },
276
- };
277
- }
278
-
279
- it("does not touch the pi package store unless provisioning is requested", async () => {
280
- const fx = await makeFixture();
281
- try {
282
- await seedPackage(fx, {
283
- agents: { alpha: { body: "# alpha" } },
284
- packages: ["npm:pi-questions"],
285
- });
286
- const { calls, runInstall } = recordingRunner();
287
-
288
- const result = await installPi({ root: fx.root, output: fx.output, runInstall });
289
-
290
- assert.deepEqual(calls, []);
291
- assert.equal(result.packages, null);
292
- } finally {
293
- await fx.cleanup();
294
- }
295
- });
296
-
297
- it("installs the packages declared in the package manifest when enabled", async () => {
298
- const fx = await makeFixture();
299
- try {
300
- await seedPackage(fx, {
301
- agents: { alpha: { body: "# alpha" } },
302
- packages: ["npm:pi-questions", "npm:pi-web-access@0.10.7"],
303
- });
304
- const { calls, runInstall } = recordingRunner();
305
-
306
- const result = await installPi({ root: fx.root, output: fx.output, provisionPackages: true, runInstall });
307
-
308
- assert.deepEqual(calls, ["npm:pi-questions", "npm:pi-web-access@0.10.7"]);
309
- assert.deepEqual(result.packages.installed, ["npm:pi-questions", "npm:pi-web-access@0.10.7"]);
310
- assert.deepEqual(result.packages.failed, []);
311
- } finally {
312
- await fx.cleanup();
313
- }
314
- });
315
-
316
- it("dry-run reports the package plan without installing", async () => {
317
- const fx = await makeFixture();
318
- try {
319
- await seedPackage(fx, {
320
- agents: { alpha: { body: "# alpha" } },
321
- packages: ["npm:pi-questions"],
322
- });
323
- const { calls, runInstall } = recordingRunner();
324
-
325
- const result = await installPi({
301
+ await installPi({
326
302
  root: fx.root,
327
303
  output: fx.output,
328
- provisionPackages: true,
329
- dryRun: true,
330
- runInstall,
304
+ pluginRoot,
305
+ resolvePackage: fakeResolver({}).resolvePackage,
331
306
  });
332
-
333
- assert.deepEqual(calls, []);
334
- assert.deepEqual(result.packages.planned, ["npm:pi-questions"]);
307
+ const content = await readFile(join(fx.output, "agents", "alpha.agent.md"), "utf-8");
308
+ assert.ok(content.includes(join(pluginRoot, "skills", "x", "SKILL.md")), "plugin root override used");
335
309
  } finally {
336
310
  await fx.cleanup();
337
311
  }
338
312
  });
339
313
 
340
- it("warns but still installs agents when a package install fails", async () => {
314
+ it("rejects token-map.json versions newer than the implemented contract", async () => {
341
315
  const fx = await makeFixture();
342
316
  try {
343
317
  await seedPackage(fx, {
318
+ tokenMapVersion: 99,
344
319
  agents: { alpha: { body: "# alpha" } },
345
- packages: ["npm:pi-questions"],
346
320
  });
347
-
348
- const result = await installPi({
349
- root: fx.root,
350
- output: fx.output,
351
- provisionPackages: true,
352
- runInstall: async () => ({ ok: false, error: "offline" }),
353
- });
354
-
355
- assert.ok(await pathExists(join(fx.output, "agents", "alpha.agent.md")), "agents still installed");
356
- assert.deepEqual(result.packages.failed, [{ source: "npm:pi-questions", error: "offline" }]);
357
- assert.ok(result.warnings.some((w) => w.includes("npm:pi-questions")), "failure surfaced as a warning");
321
+ await assert.rejects(
322
+ installPi({ root: fx.root, output: fx.output, resolvePackage: fakeResolver({}).resolvePackage }),
323
+ /version 99/,
324
+ );
358
325
  } finally {
359
326
  await fx.cleanup();
360
327
  }
@@ -369,14 +336,15 @@ describe("installPi — skills", () => {
369
336
  try {
370
337
  await seedPackage(fx, {
371
338
  agents: { alpha: { body: "# alpha" } },
372
- skills: {
373
- "engineering/echo/SKILL.md":
374
- "---\nname: echo\ndescription: Echoes {{TOOL:view}}.\n---\n\nUse {{TOOL:view}} to read and {{TOOL:skill}} for help.\nOpen {{PATH:skills/echo/SKILL.md}}.",
375
- "engineering/echo/scripts/run.sh": "#!/bin/sh\necho hi\n",
376
- },
377
339
  });
340
+ await writeFixture(
341
+ fx,
342
+ "skills/engineering/echo/SKILL.md",
343
+ "---\nname: echo\ndescription: Echoes {{TOOL:view}}.\n---\n\nUse {{TOOL:view}} to read and {{TOOL:skill}} for help.\nOpen {{PATH:skills/echo/SKILL.md}}.",
344
+ );
345
+ await writeFixture(fx, "skills/engineering/echo/scripts/run.sh", "#!/bin/sh\necho hi\n");
378
346
 
379
- await installPi({ root: fx.root, output: fx.output });
347
+ await installPi({ root: fx.root, output: fx.output, resolvePackage: fakeResolver({}).resolvePackage });
380
348
 
381
349
  const skillFile = join(fx.output, "skills", "engineering", "echo", "SKILL.md");
382
350
  assert.ok(await pathExists(skillFile), "skill copied");
@@ -384,9 +352,7 @@ describe("installPi — skills", () => {
384
352
  assert.ok(content.includes("description: Echoes read."), "token substituted in skill frontmatter");
385
353
  assert.ok(content.includes("Use read to read and for help."), "null-mapped {{TOOL:skill}} dropped");
386
354
  assert.ok(
387
- content.includes(
388
- "Open ~/.pi/agent/npm/node_modules/@jaybeeuu/agent-cortex/skills/echo/SKILL.md.",
389
- ),
355
+ content.includes("Open ~/.agent-cortex/pi/skills/echo/SKILL.md."),
390
356
  "relative path resolved against plugin root",
391
357
  );
392
358
  assert.ok(!/{{/.test(content), "no literal tokens remain in skill");
@@ -399,259 +365,317 @@ describe("installPi — skills", () => {
399
365
  });
400
366
  });
401
367
 
402
- // ─── Output / dry-run ────────────────────────────────────────────────────────
368
+ // ─── Extensions ──────────────────────────────────────────────────────────────
403
369
 
404
- describe("installPi — output & dry-run", () => {
405
- it("dry-run writes nothing but reports the planned output", async () => {
370
+ describe("installPi — extensions", () => {
371
+ it("copies the extensions tree into the staged package", async () => {
406
372
  const fx = await makeFixture();
407
373
  try {
408
- await seedPackage(fx, {
409
- agents: { alpha: { body: "# alpha" } },
410
- skills: { "engineering/echo/SKILL.md": "---\nname: echo\ndescription: d\n---\n\nBody." },
411
- });
374
+ await seedPackage(fx, { agents: { alpha: { body: "# alpha" } } });
375
+ await writeFixture(fx, "extensions/modes/index.ts", "export default () => {};\n");
376
+ await writeFixture(fx, "extensions/auto-name.ts", "export default () => {};\n");
412
377
 
413
- const result = await installPi({ root: fx.root, output: fx.output, dryRun: true });
378
+ const result = await installPi({ root: fx.root, output: fx.output, resolvePackage: fakeResolver({}).resolvePackage });
414
379
 
415
- assert.equal(result.dryRun, true);
416
- assert.equal((await listFiles(fx.output)).length, 0, "no files written in dry-run");
417
- assert.equal(result.agents.length, 1);
418
- assert.equal(result.agents[0].name, "alpha");
419
- assert.equal(result.skills.skills, 1);
420
- // Planned paths are still reported so the user can see what would happen.
421
- assert.ok(result.agents[0].filePath.startsWith(fx.output));
380
+ assert.equal(result.extensions.files, 3, "every extension file copied");
381
+ assert.ok(await pathExists(join(fx.output, "extensions", "modes", "index.ts")));
382
+ assert.ok(await pathExists(join(fx.output, "extensions", "auto-name.ts")));
422
383
  } finally {
423
384
  await fx.cleanup();
424
385
  }
425
386
  });
426
387
 
427
- it("creates the output directory hierarchy when needed", async () => {
388
+ it("never copies node_modules or symlinks out of the extensions tree", async () => {
428
389
  const fx = await makeFixture();
429
390
  try {
430
391
  await seedPackage(fx, { agents: { alpha: { body: "# alpha" } } });
431
- const deep = join(fx.output, "nested", "deep");
432
- await installPi({ root: fx.root, output: deep });
433
- assert.ok(await pathExists(join(deep, "agents", "alpha.agent.md")));
392
+ await writeFixture(fx, "extensions/linked/index.ts", "export default () => {};\n");
393
+ await writeFixture(fx, "extensions/linked/node_modules/dep/package.json", "{}\n");
394
+ await symlink(join(fx.root, "extensions", "linked", "index.ts"), join(fx.root, "extensions", "alias.ts"));
395
+
396
+ await installPi({ root: fx.root, output: fx.output, resolvePackage: fakeResolver({}).resolvePackage });
397
+
398
+ assert.equal(await pathExists(join(fx.output, "extensions", "linked", "node_modules")), false);
399
+ assert.equal(await pathExists(join(fx.output, "extensions", "alias.ts")), false);
434
400
  } finally {
435
401
  await fx.cleanup();
436
402
  }
437
403
  });
438
404
  });
439
405
 
440
- // ─── Pi settings.json ────────────────────────────────────────────────────────
406
+ // ─── Staged package manifest ─────────────────────────────────────────────────
441
407
 
442
- describe("installPi — pi settings.json", () => {
443
- it("writes a real settings.json with the repo path package and template keys", async () => {
408
+ describe("installPi — staged package manifest", () => {
409
+ it("writes a pi package manifest for the staged resources", async () => {
444
410
  const fx = await makeFixture();
445
411
  try {
446
412
  await seedPackage(fx, { agents: { alpha: { body: "# alpha" } } });
447
413
 
448
- const result = await installPi({ root: fx.root, output: fx.output });
449
-
450
- const settingsPath = join(fx.output, "settings.json");
451
- assert.equal((await lstat(settingsPath)).isSymbolicLink(), false, "settings.json is a real file");
452
- const settings = JSON.parse(await readFile(settingsPath, "utf-8"));
414
+ const result = await installPi({ root: fx.root, output: fx.output, resolvePackage: fakeResolver({}).resolvePackage });
453
415
 
454
- const repoEntry = settings.packages.find((p) => !(typeof p === "string" ? p : p.source).startsWith("npm:"));
455
- assert.deepEqual(repoEntry.skills, [], "repo path package keeps the package filter shape");
456
- assert.equal(resolve(dirname(settingsPath), repoEntry.source), fx.root, "repo path package resolves to the package root");
457
- assert.ok(settings.packages.includes("npm:pi-web-access@0.10.7"), "template npm deps preserved");
458
- assert.equal(settings.defaultModel, "template/model", "template key present");
459
- assert.match(settings["//"], /GENERATED from pi\/settings\.json/);
460
- assert.equal(result.settings.action, "written");
416
+ const manifest = JSON.parse(await readFile(join(fx.output, "package.json"), "utf-8"));
417
+ assert.deepEqual(manifest.keywords, ["pi-package"]);
418
+ assert.equal(manifest.version, "9.9.9", "version tracks the package being staged");
419
+ assert.deepEqual(manifest.pi, { extensions: ["./extensions"], skills: ["./skills"] });
420
+ assert.match(manifest["//"], /GENERATED by `agent-cortex install pi`/);
421
+ assert.equal(result.manifest.path, join(fx.output, "package.json"));
461
422
  } finally {
462
423
  await fx.cleanup();
463
424
  }
464
425
  });
465
426
 
466
- it("preserves personal values and unknown keys while filling template defaults", async () => {
427
+ it("ships token-map.json so the staged agent-modes extension keeps its source of truth", async () => {
467
428
  const fx = await makeFixture();
468
429
  try {
469
430
  await seedPackage(fx, { agents: { alpha: { body: "# alpha" } } });
470
- await writeFixture(
471
- fx,
472
- "out/settings.json",
473
- JSON.stringify({
474
- defaultModel: "personal/model",
475
- theme: "light",
476
- personalOnly: { nested: true },
477
- tinyModel: { model: "personal/tiny" },
478
- }),
479
- );
480
431
 
481
- await installPi({ root: fx.root, output: fx.output });
432
+ await installPi({ root: fx.root, output: fx.output, resolvePackage: fakeResolver({}).resolvePackage });
482
433
 
483
- const settings = JSON.parse(await readFile(join(fx.output, "settings.json"), "utf-8"));
484
- assert.equal(settings.defaultModel, "personal/model", "personal value wins over the template default");
485
- assert.equal(settings.theme, "light", "personal value wins over the template default");
486
- assert.deepEqual(settings.personalOnly, { nested: true }, "unknown key preserved");
487
- assert.equal(settings.tinyModel.model, "personal/tiny", "nested personal leaf retained");
488
- assert.equal(settings.tinyModel.maxNameLength, 40, "nested template default filled in");
489
- assert.ok(
490
- settings.packages.some((p) => !(typeof p === "string" ? p : p.source).startsWith("npm:")),
491
- "packages stays CLI-managed",
492
- );
434
+ const staged = JSON.parse(await readFile(join(fx.output, "token-map.json"), "utf-8"));
435
+ assert.deepEqual(staged, tokenMap(), "token map copied verbatim");
493
436
  } finally {
494
437
  await fx.cleanup();
495
438
  }
496
439
  });
497
440
 
498
- it("replaces a legacy symlink with a real managed file", async () => {
441
+ it("refuses to stage into the package root, where it would clear the sources", async () => {
499
442
  const fx = await makeFixture();
500
443
  try {
501
444
  await seedPackage(fx, { agents: { alpha: { body: "# alpha" } } });
502
- await mkdir(fx.output, { recursive: true });
503
- await symlink(join(fx.root, "pi", "settings.json"), join(fx.output, "settings.json"));
445
+ await assert.rejects(
446
+ installPi({ root: fx.root, output: fx.root, resolvePackage: fakeResolver({}).resolvePackage }),
447
+ /refusing to stage/,
448
+ );
449
+ assert.ok(await pathExists(join(fx.root, "agents", "alpha", "agent.md")), "sources untouched");
450
+ } finally {
451
+ await fx.cleanup();
452
+ }
453
+ });
454
+ });
504
455
 
505
- const result = await installPi({ root: fx.root, output: fx.output });
456
+ // ─── Bundled third-party packages ────────────────────────────────────────────
506
457
 
507
- assert.equal(result.settings.action, "removed-symlink");
508
- const settingsPath = join(fx.output, "settings.json");
509
- assert.equal((await lstat(settingsPath)).isSymbolicLink(), false, "symlink replaced by a real file");
510
- const settings = JSON.parse(await readFile(settingsPath, "utf-8"));
511
- assert.ok(settings.packages.length > 0, "managed packages written");
458
+ describe("installPi — bundled pi packages", () => {
459
+ it("copies declared pi packages into node_modules and loads their resources from the manifest", async () => {
460
+ const fx = await makeFixture();
461
+ try {
462
+ const installed = join(fx.root, "installed");
463
+ const questions = await createInstalledPackage(installed, {
464
+ name: "pi-questions",
465
+ pi: { extensions: ["./extensions/ask-questions.ts"] },
466
+ files: { "extensions/ask-questions.ts": "export default () => {};\n" },
467
+ });
468
+ const web = await createInstalledPackage(installed, {
469
+ name: "pi-web-access",
470
+ pi: { extensions: ["./index.ts"], skills: ["./skills"] },
471
+ files: { "index.ts": "export default () => {};\n", "skills/web/SKILL.md": "---\nname: web\n---\n" },
472
+ });
473
+ await seedPackage(fx, {
474
+ agents: { alpha: { body: "# alpha" } },
475
+ dependencies: { "pi-questions": "^1.0.0", "pi-web-access": "1.0.0" },
476
+ });
477
+
478
+ const result = await installPi({
479
+ root: fx.root,
480
+ output: fx.output,
481
+ resolvePackage: fakeResolver({ "pi-questions": questions, "pi-web-access": web }).resolvePackage,
482
+ });
483
+
484
+ assert.deepEqual(result.packages.bundled, ["pi-questions", "pi-web-access"]);
485
+ assert.ok(await pathExists(join(fx.output, "node_modules", "pi-questions", "extensions", "ask-questions.ts")));
486
+ assert.ok(await pathExists(join(fx.output, "node_modules", "pi-web-access", "skills", "web", "SKILL.md")));
487
+
488
+ const manifest = JSON.parse(await readFile(join(fx.output, "package.json"), "utf-8"));
489
+ assert.deepEqual(manifest.pi.extensions, [
490
+ "./extensions",
491
+ "node_modules/pi-questions/extensions/ask-questions.ts",
492
+ "node_modules/pi-web-access/index.ts",
493
+ ]);
494
+ assert.deepEqual(manifest.pi.skills, ["./skills", "node_modules/pi-web-access/skills"]);
512
495
  } finally {
513
496
  await fx.cleanup();
514
497
  }
515
498
  });
516
499
 
517
- it("re-running leaves settings.json unchanged", async () => {
500
+ it("copies the transitive closure, resolving each dependency from its dependent", async () => {
518
501
  const fx = await makeFixture();
519
502
  try {
520
- await seedPackage(fx, { agents: { alpha: { body: "# alpha" } } });
521
- await installPi({ root: fx.root, output: fx.output });
522
- const first = await readFile(join(fx.output, "settings.json"), "utf-8");
503
+ const installed = join(fx.root, "installed");
504
+ const web = await createInstalledPackage(installed, {
505
+ name: "pi-web-access",
506
+ dependencies: { "p-limit": "^6.0.0" },
507
+ pi: { extensions: ["./index.ts"] },
508
+ files: { "index.ts": "export default () => {};\n" },
509
+ });
510
+ const limit = await createInstalledPackage(installed, {
511
+ name: "p-limit",
512
+ files: { "index.js": "export const limit = 1;\n" },
513
+ });
514
+ await seedPackage(fx, {
515
+ agents: { alpha: { body: "# alpha" } },
516
+ dependencies: { "pi-web-access": "1.0.0" },
517
+ });
518
+ const resolver = fakeResolver({ "pi-web-access": web, "p-limit": limit });
523
519
 
524
- const result = await installPi({ root: fx.root, output: fx.output });
520
+ const result = await installPi({ root: fx.root, output: fx.output, resolvePackage: resolver.resolvePackage });
525
521
 
526
- assert.equal(result.settings.action, "unchanged");
527
- assert.equal(await readFile(join(fx.output, "settings.json"), "utf-8"), first);
522
+ assert.deepEqual(result.packages.bundled, ["pi-web-access"], "only declared packages contribute resources");
523
+ assert.deepEqual(result.packages.staged, ["pi-web-access", "p-limit"]);
524
+ assert.ok(await pathExists(join(fx.output, "node_modules", "p-limit", "index.js")));
525
+ assert.deepEqual(
526
+ resolver.calls.find((call) => call.name === "p-limit").fromDir,
527
+ web,
528
+ "transitive dependency resolved from its dependent, not a hoisted root",
529
+ );
530
+ const manifest = JSON.parse(await readFile(join(fx.output, "package.json"), "utf-8"));
531
+ assert.deepEqual(manifest.pi.extensions, ["./extensions", "node_modules/pi-web-access/index.ts"]);
528
532
  } finally {
529
533
  await fx.cleanup();
530
534
  }
531
535
  });
532
536
 
533
- it("dry-run plans the settings write without touching the filesystem", async () => {
537
+ it("leaves ordinary dependencies out of node_modules and the manifest", async () => {
534
538
  const fx = await makeFixture();
535
539
  try {
536
- await seedPackage(fx, { agents: { alpha: { body: "# alpha" } } });
540
+ const installed = join(fx.root, "installed");
541
+ const util = await createInstalledPackage(installed, { name: "plain-util", files: { "index.js": "export {};\n" } });
542
+ await seedPackage(fx, {
543
+ agents: { alpha: { body: "# alpha" } },
544
+ dependencies: { "plain-util": "^1.0.0" },
545
+ });
537
546
 
538
- const result = await installPi({ root: fx.root, output: fx.output, dryRun: true });
547
+ const result = await installPi({
548
+ root: fx.root,
549
+ output: fx.output,
550
+ resolvePackage: fakeResolver({ "plain-util": util }).resolvePackage,
551
+ });
539
552
 
540
- assert.equal(result.settings.action, "would-write");
541
- assert.equal(await pathExists(join(fx.output, "settings.json")), false, "no file written in dry-run");
553
+ assert.deepEqual(result.packages.staged, []);
554
+ assert.equal(await pathExists(join(fx.output, "node_modules")), false);
542
555
  } finally {
543
556
  await fx.cleanup();
544
557
  }
545
558
  });
546
559
 
547
- it("dry-run reports would-remove-symlink for a legacy symlink", async () => {
560
+ it("fails the install when a declared dependency cannot be resolved, leaving the previous staging intact", async () => {
548
561
  const fx = await makeFixture();
549
562
  try {
550
563
  await seedPackage(fx, { agents: { alpha: { body: "# alpha" } } });
551
- await mkdir(fx.output, { recursive: true });
552
- const link = join(fx.output, "settings.json");
553
- await symlink(join(fx.root, "pi", "settings.json"), link);
564
+ await installPi({ root: fx.root, output: fx.output, resolvePackage: fakeResolver({}).resolvePackage });
565
+ const staged = await readFile(join(fx.output, "agents", "alpha.agent.md"), "utf-8");
554
566
 
555
- const result = await installPi({ root: fx.root, output: fx.output, dryRun: true });
567
+ await writeFixture(fx, "package.json", JSON.stringify(defaultPackageJson("9.9.9", { "pi-questions": "^1.0.0" })));
556
568
 
557
- assert.equal(result.settings.action, "would-remove-symlink");
558
- assert.equal(result.settings.removedSymlink, true);
559
- assert.equal((await lstat(link)).isSymbolicLink(), true, "symlink untouched in dry-run");
569
+ await assert.rejects(
570
+ installPi({ root: fx.root, output: fx.output, resolvePackage: fakeResolver({}).resolvePackage }),
571
+ /cannot resolve "pi-questions"/,
572
+ );
573
+ assert.equal(await readFile(join(fx.output, "agents", "alpha.agent.md"), "utf-8"), staged);
560
574
  } finally {
561
575
  await fx.cleanup();
562
576
  }
563
577
  });
564
- });
565
-
566
- // ─── Pi keybindings.json ─────────────────────────────────────────────────────
567
578
 
568
- describe("installPi — pi keybindings.json", () => {
569
- it("writes the template bindings as a real generated file", async () => {
579
+ it("fails when a bundled package's declared resource is missing from its published files", async () => {
570
580
  const fx = await makeFixture();
571
581
  try {
572
- await seedPackage(fx, { agents: { alpha: { body: "# alpha" } } });
573
-
574
- const result = await installPi({ root: fx.root, output: fx.output });
582
+ const installed = join(fx.root, "installed");
583
+ const questions = await createInstalledPackage(installed, {
584
+ name: "pi-questions",
585
+ pi: { extensions: ["./extensions/ask-questions.ts"] },
586
+ });
587
+ await seedPackage(fx, {
588
+ agents: { alpha: { body: "# alpha" } },
589
+ dependencies: { "pi-questions": "^1.0.0" },
590
+ });
575
591
 
576
- const keybindingsPath = join(fx.output, "keybindings.json");
577
- assert.equal((await lstat(keybindingsPath)).isSymbolicLink(), false, "keybindings.json is a real file");
578
- const keybindings = JSON.parse(await readFile(keybindingsPath, "utf-8"));
579
- assert.deepEqual(keybindings["tui.input.newLine"], ["shift+enter"], "template bindings copied");
580
- assert.match(keybindings["//"], /GENERATED from pi\/keybindings\.json/);
581
- assert.equal(result.keybindings.action, "written");
592
+ await assert.rejects(
593
+ installPi({
594
+ root: fx.root,
595
+ output: fx.output,
596
+ resolvePackage: fakeResolver({ "pi-questions": questions }).resolvePackage,
597
+ }),
598
+ /references missing resources: node_modules\/pi-questions\/extensions\/ask-questions\.ts/,
599
+ );
582
600
  } finally {
583
601
  await fx.cleanup();
584
602
  }
585
603
  });
604
+ });
605
+
606
+ // ─── Idempotency ─────────────────────────────────────────────────────────────
586
607
 
587
- it("leaves a user-modified keybindings file alone", async () => {
608
+ describe("installPi — re-staging", () => {
609
+ it("removes artifacts of a previous run instead of accumulating them", async () => {
588
610
  const fx = await makeFixture();
589
611
  try {
590
612
  await seedPackage(fx, { agents: { alpha: { body: "# alpha" } } });
591
- await writeFixture(
592
- fx,
593
- "out/keybindings.json",
594
- JSON.stringify({ "tui.input.newLine": ["ctrl+j"] }, null, 2),
595
- );
613
+ await installPi({ root: fx.root, output: fx.output, resolvePackage: fakeResolver({}).resolvePackage });
614
+ const first = (await listFiles(fx.output)).sort();
596
615
 
597
- const result = await installPi({ root: fx.root, output: fx.output });
616
+ await writeFixture(fx, "skills/engineering/echo/SKILL.md", "---\nname: echo\ndescription: d\n---\n\nUpdated.\n");
617
+ await installPi({ root: fx.root, output: fx.output, resolvePackage: fakeResolver({}).resolvePackage });
598
618
 
599
- assert.equal(result.keybindings.action, "skipped");
600
- const keybindings = JSON.parse(await readFile(join(fx.output, "keybindings.json"), "utf-8"));
601
- assert.deepEqual(keybindings["tui.input.newLine"], ["ctrl+j"], "user edit preserved");
619
+ assert.deepEqual((await listFiles(fx.output)).sort(), first, "same tree, no stale copies");
602
620
  } finally {
603
621
  await fx.cleanup();
604
622
  }
605
623
  });
606
624
 
607
- it("refreshes an unmodified install when the template changes", async () => {
625
+ it("drops a skill that no longer exists in the sources", async () => {
608
626
  const fx = await makeFixture();
609
627
  try {
610
628
  await seedPackage(fx, { agents: { alpha: { body: "# alpha" } } });
611
- await installPi({ root: fx.root, output: fx.output });
612
- await writeFixture(
613
- fx,
614
- "pi/keybindings.json",
615
- JSON.stringify({ "tui.input.newLine": ["ctrl+j"] }, null, 2),
616
- );
629
+ await writeFixture(fx, "skills/engineering/retired/SKILL.md", "---\nname: retired\ndescription: d\n---\n\nBody.\n");
630
+ await installPi({ root: fx.root, output: fx.output, resolvePackage: fakeResolver({}).resolvePackage });
631
+ assert.ok(await pathExists(join(fx.output, "skills", "engineering", "retired", "SKILL.md")));
617
632
 
618
- const result = await installPi({ root: fx.root, output: fx.output });
633
+ await rm(join(fx.root, "skills", "engineering", "retired"), { recursive: true });
634
+ await installPi({ root: fx.root, output: fx.output, resolvePackage: fakeResolver({}).resolvePackage });
619
635
 
620
- assert.equal(result.keybindings.action, "written");
621
- const keybindings = JSON.parse(await readFile(join(fx.output, "keybindings.json"), "utf-8"));
622
- assert.deepEqual(keybindings["tui.input.newLine"], ["ctrl+j"], "updated template applied");
636
+ assert.equal(await pathExists(join(fx.output, "skills", "engineering", "retired")), false);
623
637
  } finally {
624
638
  await fx.cleanup();
625
639
  }
626
640
  });
641
+ });
642
+
643
+ // ─── Output / dry-run ────────────────────────────────────────────────────────
627
644
 
628
- it("replaces a legacy symlink with a real file", async () => {
645
+ describe("installPi — output & dry-run", () => {
646
+ it("dry-run writes nothing but reports the planned output", async () => {
629
647
  const fx = await makeFixture();
630
648
  try {
631
- await seedPackage(fx, { agents: { alpha: { body: "# alpha" } } });
632
- await mkdir(fx.output, { recursive: true });
633
- await symlink(join(fx.root, "pi", "keybindings.json"), join(fx.output, "keybindings.json"));
649
+ await seedPackage(fx, {
650
+ agents: { alpha: { body: "# alpha" } },
651
+ });
634
652
 
635
- const result = await installPi({ root: fx.root, output: fx.output });
653
+ const result = await installPi({
654
+ root: fx.root,
655
+ output: fx.output,
656
+ dryRun: true,
657
+ resolvePackage: fakeResolver({}).resolvePackage,
658
+ });
636
659
 
637
- assert.equal(result.keybindings.action, "removed-symlink");
638
- assert.equal((await lstat(join(fx.output, "keybindings.json"))).isSymbolicLink(), false);
660
+ assert.equal(result.dryRun, true);
661
+ assert.equal((await listFiles(fx.output)).length, 0, "no files written in dry-run");
662
+ assert.equal(result.agents.length, 1);
663
+ assert.equal(result.agents[0].name, "alpha");
664
+ assert.equal(result.skills.skills, 1);
665
+ assert.equal(result.manifest.path, join(fx.output, "package.json"));
666
+ assert.ok(result.agents[0].filePath.startsWith(fx.output));
639
667
  } finally {
640
668
  await fx.cleanup();
641
669
  }
642
670
  });
643
671
 
644
- it("re-running leaves keybindings.json unchanged", async () => {
672
+ it("creates the output directory hierarchy when needed", async () => {
645
673
  const fx = await makeFixture();
646
674
  try {
647
675
  await seedPackage(fx, { agents: { alpha: { body: "# alpha" } } });
648
- await installPi({ root: fx.root, output: fx.output });
649
- const first = await readFile(join(fx.output, "keybindings.json"), "utf-8");
650
-
651
- const result = await installPi({ root: fx.root, output: fx.output });
652
-
653
- assert.equal(result.keybindings.action, "unchanged");
654
- assert.equal(await readFile(join(fx.output, "keybindings.json"), "utf-8"), first);
676
+ const deep = join(fx.output, "nested", "deep");
677
+ await installPi({ root: fx.root, output: deep, resolvePackage: fakeResolver({}).resolvePackage });
678
+ assert.ok(await pathExists(join(deep, "agents", "alpha.agent.md")));
655
679
  } finally {
656
680
  await fx.cleanup();
657
681
  }
@@ -672,7 +696,7 @@ describe("installPi — real repo integration", () => {
672
696
  // ralph pi frontmatter: ["bash", "view", "rg", "glob", "task", "read_agent", "wait_for_agents"]
673
697
  const ralph = await readFile(join(fx.output, "agents", "ralph.agent.md"), "utf-8");
674
698
  assert.match(ralph, /name: "agent-cortex:ralph"/);
675
- assert.match(ralph, /tools: "bash read grep find task read_agent wait_for_agents"/);
699
+ assert.match(ralph, /tools: "bash, read, grep, find, task, read_agent, wait_for_agents"/);
676
700
  assert.ok(ralph.includes("## Spawning subagents (PI)"), "{{SECTION:polling}} composed from pi/polling.md");
677
701
  const bodyTokens = ralph.split("---\n").slice(2).join("---\n");
678
702
  assert.ok(!/{{/.test(bodyTokens), "no literal tokens in ralph body");
@@ -682,9 +706,9 @@ describe("installPi — real repo integration", () => {
682
706
  // runtime agent-modes translateTools which sorts): ask_user → ask_questions,
683
707
  // skill → read (last position).
684
708
  const plan = await readFile(join(fx.output, "agents", "plan.agent.md"), "utf-8");
685
- assert.match(plan, /tools: "bash read edit write grep find ask_questions task read_agent read"/);
709
+ assert.match(plan, /tools: "bash, read, edit, write, grep, find, ask_questions, task, read_agent, read"/);
686
710
  assert.ok(
687
- plan.includes("~/.pi/agent/npm/node_modules/@jaybeeuu/agent-cortex/skills/workflow/plan/SKILL.md"),
711
+ plan.includes("~/.agent-cortex/pi/skills/workflow/plan/SKILL.md"),
688
712
  "plan PATH token resolved against token-map plugin root",
689
713
  );
690
714
 
@@ -701,7 +725,7 @@ describe("installPi — real repo integration", () => {
701
725
  // (authoring order preserved: bash, view→read, rg→grep, glob→find, ask_user→
702
726
  // ask_questions, web_fetch→fetch_content, skill→read, edit, create)
703
727
  const strategy = await readFile(join(fx.output, "agents", "strategy.agent.md"), "utf-8");
704
- assert.match(strategy, /tools: "bash read grep find ask_questions fetch_content read edit write"/);
728
+ assert.match(strategy, /tools: "bash, read, grep, find, ask_questions, fetch_content, read, edit, write"/);
705
729
  } finally {
706
730
  await fx.cleanup();
707
731
  }
@@ -723,4 +747,39 @@ describe("installPi — real repo integration", () => {
723
747
  await fx.cleanup();
724
748
  }
725
749
  });
726
- });
750
+
751
+ it("stages a self-contained package: real files only, bundled pi tools, nothing outside the output", async () => {
752
+ const fx = await makeFixture();
753
+ try {
754
+ const result = await installPi({ root: REPO_ROOT, output: fx.output });
755
+
756
+ const manifest = JSON.parse(await readFile(join(fx.output, "package.json"), "utf-8"));
757
+ assert.deepEqual(manifest.pi.extensions, [
758
+ "./extensions",
759
+ "node_modules/pi-questions/extensions/ask-questions.ts",
760
+ "node_modules/pi-web-access/index.ts",
761
+ ]);
762
+ assert.deepEqual(manifest.pi.skills, ["./skills", "node_modules/pi-web-access/skills"]);
763
+ assert.deepEqual(result.packages.bundled, ["pi-questions", "pi-web-access"]);
764
+
765
+ for (const rel of [...manifest.pi.extensions, ...manifest.pi.skills]) {
766
+ assert.ok(await pathExists(join(fx.output, rel)), `manifest resource ${rel} exists`);
767
+ }
768
+ for (const name of ["pi-questions", "pi-web-access"]) {
769
+ assert.equal((await lstat(join(fx.output, "node_modules", name))).isSymbolicLink(), false, `${name} is a real directory`);
770
+ }
771
+
772
+ const tree = await listFiles(fx.output);
773
+ assert.ok(tree.some((f) => f.startsWith(join("extensions", "agent-modes"))), "extensions staged");
774
+ assert.ok(tree.some((f) => f === "token-map.json"), "token map staged");
775
+
776
+ const links = [];
777
+ for (const rel of tree) {
778
+ if ((await lstat(join(fx.output, rel))).isSymbolicLink()) links.push(rel);
779
+ }
780
+ assert.deepEqual(links, [], "staged tree contains no symlinks");
781
+ } finally {
782
+ await fx.cleanup();
783
+ }
784
+ });
785
+ });