@jaybeeuu/agent-cortex 1.41.1 → 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.
- package/.agents/skills/style-versioning/SKILL.md +73 -2
- package/.github/workflows/ci.yml +52 -32
- package/AGENTS.md +23 -2
- package/CHANGELOG.md +189 -0
- package/README.md +125 -73
- package/agents/README.md +17 -4
- package/agents/ralph-plan/agent.md +7 -3
- package/agents/ralph-plan.agent.md +7 -3
- package/bin/agent-cortex.mjs +48 -30
- package/bin/installers/pi.mjs +885 -237
- package/claude-extras/scripts/statusline-command.sh +0 -0
- package/docs/claude-hooks.md +5 -4
- package/docs/commit-signing.md +231 -0
- package/docs/ideas/ban-content-assertion-tests.md +3 -1
- package/docs/ideas/dedicated-review-tests-skill.md +71 -0
- package/docs/ideas/enable-signed-commit-enforcement-changesets-bot-commits-must-be-signed.md +10 -1
- package/docs/ideas/extension-manifests.md +5 -0
- package/docs/ideas/unify-the-ticket-and-task-decomposition-workflows.md +71 -0
- package/docs/research/disable-model-invocation.md +103 -0
- package/docs/research/skill-design-research.md +2 -2
- package/docs/skills/skill-anatomy.md +5 -1
- package/docs/technical-direction/harness-config.md +25 -1
- package/docs/technical-direction/migration-execution-labels.md +17 -9
- package/extensions/agent-modes/discover.test.ts +19 -0
- package/extensions/agent-modes/discover.ts +25 -3
- package/extensions/session-start/README.md +38 -0
- package/extensions/session-start/index.test.ts +122 -0
- package/extensions/session-start/index.ts +27 -0
- package/extensions/session-start/package.json +19 -0
- package/extensions/session-start/tsconfig.json +12 -0
- package/extensions/subagent/README.md +1 -1
- package/extensions/subagent/agents.test.ts +176 -0
- package/extensions/subagent/agents.ts +102 -11
- package/extensions/subagent/index.ts +2 -1
- package/extensions/subagent/package.json +1 -1
- package/lib/cli.mjs +35 -9
- package/package.json +10 -6
- package/pi/settings.json +1 -7
- package/plugin.json +1 -1
- package/pnpm-workspace.yaml +6 -0
- package/scripts/check-changeset.mjs +42 -0
- package/scripts/install-instructions.sh +0 -0
- package/scripts/lib/release-gates.mjs +61 -0
- package/scripts/lib/verify-worker-tree.mjs +107 -0
- package/scripts/resolve-publish-command.mjs +17 -0
- package/scripts/sync-plugin-version.sh +0 -0
- package/skills/engineering/grill-with-docs/SKILL.md +3 -2
- package/skills/engineering/improve-codebase-architecture/REFERENCE.md +5 -0
- package/skills/engineering/improve-codebase-architecture/SKILL.md +2 -1
- package/skills/planning/create-task/SKILL.md +8 -3
- package/skills/planning/create-task/scripts/create-chores.ts +4 -1
- package/skills/planning/prd-to-tasks/REFERENCE.md +2 -2
- package/skills/planning/prd-to-tasks/SKILL.md +9 -4
- package/skills/planning/request-refactor-plan/FORMAT.md +5 -0
- package/skills/planning/request-refactor-plan/SKILL.md +3 -2
- package/skills/planning/spec-to-tickets/REFERENCE.md +82 -0
- package/skills/planning/spec-to-tickets/SKILL.md +143 -0
- package/skills/planning/write-a-prd/FORMAT.md +6 -0
- package/skills/planning/write-a-prd/SKILL.md +9 -6
- package/skills/planning/write-a-ticket/EXAMPLES.md +133 -0
- package/skills/planning/write-a-ticket/SKILL.md +77 -59
- package/skills/productivity/bd-tool/SKILL.md +27 -2
- package/skills/productivity/grill-me/SKILL.md +11 -0
- package/skills/review/review-security/scripts/install-gitleaks.sh +0 -0
- package/skills/workflow/git-workflow/SKILL.md +35 -1
- package/skills/workflow/plan/SKILL.md +20 -8
- package/skills/workflow/ralph/REFERENCE.md +20 -2
- package/skills/workflow/ralph/SKILL.md +4 -1
- package/skills/workflow/run-pipeline-stage/SKILL.md +17 -5
- package/skills/workflow/run-pipeline-stage/playbooks/code.md +2 -0
- package/skills/workflow/run-pipeline-stage/playbooks/document.md +3 -0
- package/skills/workflow/run-pipeline-stage/playbooks/fix.md +2 -0
- package/skills/workflow/run-pipeline-stage/prompts/stage-runner.md +11 -0
- package/skills/workflow/using-agent-skills/SKILL.md +1 -0
- package/test/cli.test.mjs +26 -29
- package/test/create-chores.test.mjs +12 -0
- package/test/fake-bd.test.mjs +72 -0
- package/test/helpers/fake-bd.mjs +26 -6
- package/test/helpers/fake-pi.mjs +132 -0
- package/test/pi-installer.test.mjs +345 -286
- package/test/pi-register.test.mjs +342 -0
- package/test/pi-sweep.test.mjs +619 -0
- package/test/release-gates.test.mjs +190 -0
- package/test/verify-worker-tree.test.mjs +120 -0
- package/token-map.README.md +7 -6
- package/token-map.json +6 -5
- package/extensions/agent-guardrails/node_modules/.bin/pi +0 -21
- package/extensions/agent-guardrails/node_modules/.bin/tsc +0 -21
- package/extensions/agent-guardrails/node_modules/.bin/tsserver +0 -21
- package/extensions/agent-guardrails/node_modules/.bin/tsx +0 -21
- package/extensions/agent-modes/node_modules/.bin/pi +0 -21
- package/extensions/agent-modes/node_modules/.bin/tsc +0 -21
- package/extensions/agent-modes/node_modules/.bin/tsserver +0 -21
- package/extensions/agent-modes/node_modules/.bin/tsx +0 -21
- package/extensions/session-start.ts +0 -44
- package/extensions/skill-stats/node_modules/.bin/pi +0 -21
- package/extensions/skill-stats/node_modules/.bin/tsc +0 -21
- package/extensions/skill-stats/node_modules/.bin/tsserver +0 -21
- package/extensions/skill-stats/node_modules/.bin/tsx +0 -21
- package/extensions/subagent/node_modules/.bin/pi +0 -21
- package/extensions/subagent/node_modules/.bin/pi-ai +0 -21
- package/extensions/subagent/node_modules/.bin/tsc +0 -21
- package/extensions/subagent/node_modules/.bin/tsserver +0 -21
- package/extensions/subagent/node_modules/.bin/tsx +0 -21
- package/lib/pi-packages.mjs +0 -169
- package/skills/planning/create-task/scripts/node_modules/.bin/tsc +0 -21
- package/skills/planning/create-task/scripts/node_modules/.bin/tsserver +0 -21
- package/skills/planning/create-task/scripts/node_modules/.bin/tsx +0 -21
- package/skills/productivity/validate-agent-dir/scripts/node_modules/.bin/tsc +0 -21
- package/skills/productivity/validate-agent-dir/scripts/node_modules/.bin/tsserver +0 -21
- package/skills/productivity/validate-agent-dir/scripts/node_modules/.bin/tsx +0 -21
- package/skills/workflow/run-pipeline-stage/scripts/node_modules/.bin/tsc +0 -21
- package/skills/workflow/run-pipeline-stage/scripts/node_modules/.bin/tsserver +0 -21
- package/skills/workflow/run-pipeline-stage/scripts/node_modules/.bin/tsx +0 -21
- 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
|
|
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: "~/.
|
|
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
|
-
/**
|
|
67
|
-
function
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
/**
|
|
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 = {},
|
|
83
|
-
await writeFixture(fx, "token-map.json", JSON.stringify(map ?? tokenMap(
|
|
84
|
-
|
|
85
|
-
|
|
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(
|
|
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(
|
|
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("
|
|
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({
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
runInstall,
|
|
304
|
+
pluginRoot,
|
|
305
|
+
resolvePackage: fakeResolver({}).resolvePackage,
|
|
331
306
|
});
|
|
332
|
-
|
|
333
|
-
assert.
|
|
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("
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
// ───
|
|
368
|
+
// ─── Extensions ──────────────────────────────────────────────────────────────
|
|
403
369
|
|
|
404
|
-
describe("installPi —
|
|
405
|
-
it("
|
|
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
|
-
|
|
410
|
-
|
|
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,
|
|
378
|
+
const result = await installPi({ root: fx.root, output: fx.output, resolvePackage: fakeResolver({}).resolvePackage });
|
|
414
379
|
|
|
415
|
-
assert.equal(result.
|
|
416
|
-
assert.
|
|
417
|
-
assert.
|
|
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("
|
|
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
|
-
|
|
432
|
-
await
|
|
433
|
-
|
|
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
|
-
// ───
|
|
406
|
+
// ─── Staged package manifest ─────────────────────────────────────────────────
|
|
441
407
|
|
|
442
|
-
describe("installPi —
|
|
443
|
-
it("writes a
|
|
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
|
|
455
|
-
assert.deepEqual(
|
|
456
|
-
assert.equal(
|
|
457
|
-
assert.
|
|
458
|
-
assert.
|
|
459
|
-
assert.
|
|
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("
|
|
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
|
|
484
|
-
assert.
|
|
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("
|
|
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
|
|
503
|
-
|
|
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
|
-
|
|
456
|
+
// ─── Bundled third-party packages ────────────────────────────────────────────
|
|
506
457
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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("
|
|
500
|
+
it("copies the transitive closure, resolving each dependency from its dependent", async () => {
|
|
518
501
|
const fx = await makeFixture();
|
|
519
502
|
try {
|
|
520
|
-
|
|
521
|
-
await
|
|
522
|
-
|
|
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.
|
|
527
|
-
assert.
|
|
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("
|
|
537
|
+
it("leaves ordinary dependencies out of node_modules and the manifest", async () => {
|
|
534
538
|
const fx = await makeFixture();
|
|
535
539
|
try {
|
|
536
|
-
|
|
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({
|
|
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.
|
|
541
|
-
assert.equal(await pathExists(join(fx.output, "
|
|
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("
|
|
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
|
|
552
|
-
const
|
|
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
|
-
|
|
567
|
+
await writeFixture(fx, "package.json", JSON.stringify(defaultPackageJson("9.9.9", { "pi-questions": "^1.0.0" })));
|
|
556
568
|
|
|
557
|
-
assert.
|
|
558
|
-
|
|
559
|
-
|
|
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
|
-
|
|
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
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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
|
-
|
|
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
|
|
592
|
-
|
|
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
|
-
|
|
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.
|
|
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("
|
|
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
|
|
612
|
-
await
|
|
613
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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, {
|
|
632
|
-
|
|
633
|
-
|
|
649
|
+
await seedPackage(fx, {
|
|
650
|
+
agents: { alpha: { body: "# alpha" } },
|
|
651
|
+
});
|
|
634
652
|
|
|
635
|
-
const result = await installPi({
|
|
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.
|
|
638
|
-
assert.equal((await
|
|
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("
|
|
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
|
-
|
|
649
|
-
|
|
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("~/.
|
|
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
|
+
});
|