@jaybeeuu/agent-cortex 1.39.0 → 1.41.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 +1 -1
- package/.github/workflows/ci.yml +47 -14
- package/AGENTS.md +35 -8
- package/CHANGELOG.md +60 -0
- package/README.md +174 -97
- package/agents/README.md +14 -11
- package/bin/agent-cortex.mjs +55 -10
- package/bin/installers/claude.mjs +674 -174
- package/bin/installers/copilot.mjs +13 -18
- package/bin/installers/pi.mjs +293 -51
- package/claude/settings.json +4 -0
- package/{claude → claude-extras}/scripts/statusline-command.sh +32 -4
- package/docs/claude-hooks.md +15 -3
- package/docs/ideas/ban-content-assertion-tests.md +73 -0
- package/docs/ideas/bead-progress-notes-for-session-resumption.md +75 -0
- package/docs/ideas/classify-beads-via-deterministic-script-with-agent-fallback.md +70 -0
- package/docs/ideas/ctx-alias-version-and-update-commands.md +65 -0
- package/docs/ideas/ctx-feedback-command-for-capturing-ideas-from-any-machine.md +67 -0
- package/docs/ideas/deployment-beads-for-post-merge-pipelines.md +36 -0
- package/docs/ideas/design-and-delivery-as-separate-bead-trees/design-and-delivery-as-separate-bead-trees.md +99 -0
- package/docs/ideas/enable-signed-commit-enforcement-changesets-bot-commits-must-be-signed.md +33 -0
- package/docs/ideas/extension-manifests.md +55 -14
- package/docs/ideas/improve-pi-system-prompt.md +5 -2
- package/docs/ideas/map-every-cortex-tool-to-a-real-pi-equivalent.md +95 -0
- package/docs/ideas/record-idea-implementation-questions.md +68 -0
- package/docs/ideas/self-flushing-session.md +72 -0
- package/docs/ideas/state-in-beads-not-md.md +69 -0
- package/docs/ideas/task-session-context.local.md +95 -0
- package/docs/ideas/test-data-creation-helpers.md +92 -0
- package/docs/ideas/test-planning-stage-bead-in-feature-planning.md +68 -0
- package/docs/ideas/worktree-housekeeping-for-closed-beads.md +39 -0
- package/docs/technical-direction/harness-config.md +161 -0
- package/docs/technical-direction/migration-execution-labels.md +60 -23
- package/extensions/agent-guardrails/README.md +45 -0
- package/extensions/agent-guardrails/guardrails.test.ts +42 -0
- package/extensions/agent-guardrails/guardrails.ts +66 -0
- package/extensions/agent-guardrails/index.ts +22 -0
- package/extensions/agent-guardrails/node_modules/.bin/pi +21 -0
- package/extensions/agent-guardrails/node_modules/.bin/tsc +21 -0
- package/extensions/agent-guardrails/node_modules/.bin/tsserver +21 -0
- package/extensions/agent-guardrails/node_modules/.bin/tsx +21 -0
- package/extensions/agent-guardrails/package.json +19 -0
- package/extensions/agent-guardrails/tsconfig.json +12 -0
- package/extensions/agent-modes/README.md +12 -5
- package/extensions/agent-modes/discover.test.ts +30 -7
- package/extensions/agent-modes/discover.ts +1 -1
- package/extensions/agent-modes/index.test.ts +208 -0
- package/extensions/agent-modes/index.ts +23 -6
- package/extensions/agent-modes/package.json +1 -1
- package/extensions/skill-stats/index.test.ts +3 -2
- package/lib/cli.mjs +30 -12
- package/lib/pi-packages.mjs +169 -0
- package/package.json +8 -4
- package/pi/settings.json +23 -12
- package/plugin.json +1 -1
- package/scripts/build-copilot-agents.mjs +1 -1
- package/scripts/check-no-sync.mjs +49 -0
- package/scripts/lib/compose-agent.mjs +32 -14
- package/scripts/lib/fs.mjs +31 -0
- package/skills/engineering/grill-with-docs/SKILL.md +1 -1
- package/skills/planning/classify-bead/REFERENCE.md +85 -0
- package/skills/planning/classify-bead/SKILL.md +32 -36
- package/skills/planning/classify-bead/scripts/classify-bead.mjs +292 -0
- package/skills/planning/create-task/SKILL.md +19 -10
- package/skills/planning/write-a-prd/SKILL.md +2 -2
- package/skills/style/style-code/REFERENCE.md +32 -2
- package/skills/style/style-code/SKILL.md +5 -21
- package/skills/style/style-tests/SKILL.md +69 -17
- package/skills/workflow/hitl-collab/SKILL.md +1 -1
- package/skills/workflow/run-pipeline-stage/SKILL.md +13 -1
- package/skills/workflow/run-pipeline-stage/prompts/stage-runner.md +12 -0
- package/test/classify-bead.test.mjs +277 -0
- package/test/claude-installer.test.mjs +199 -139
- package/test/cli.test.mjs +95 -41
- package/test/copilot-installer.test.mjs +92 -82
- package/test/fs.test.mjs +76 -0
- package/test/helpers/fake-claude.mjs +152 -0
- package/test/installer.test.mjs +542 -57
- package/test/pi-installer.test.mjs +457 -102
- package/test/pi-packages.test.mjs +295 -0
- package/token-map.README.md +23 -12
- package/token-map.json +8 -7
- package/.claude-plugin/marketplace.json +0 -16
- package/claude/.claude-plugin/plugin.json +0 -11
- package/claude/agents/plan.md +0 -58
- package/claude/agents/ralph-plan.md +0 -121
- package/claude/agents/ralph.md +0 -193
- package/claude/agents/strategy.md +0 -111
- package/claude/hooks/scripts/notify.mjs +0 -54
- package/claude/hooks.json +0 -38
- /package/{claude → claude-extras}/.mcp.json +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it } from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import {
|
|
4
|
-
import { join, dirname } from "node:path";
|
|
3
|
+
import { mkdtemp, mkdir, writeFile, rm, readFile, readdir, stat, lstat, symlink } from "node:fs/promises";
|
|
4
|
+
import { join, dirname, resolve } 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";
|
|
@@ -10,6 +10,16 @@ import { installPi } from "../bin/installers/pi.mjs";
|
|
|
10
10
|
// works against the canonical sources (agents/, skills/, token-map.json).
|
|
11
11
|
const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
12
12
|
|
|
13
|
+
/** True when a path exists (stat succeeds). */
|
|
14
|
+
async function pathExists(p) {
|
|
15
|
+
try {
|
|
16
|
+
await stat(p);
|
|
17
|
+
return true;
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
13
23
|
// ─── Fixtures ────────────────────────────────────────────────────────────────
|
|
14
24
|
|
|
15
25
|
const TOOL_MAP = {
|
|
@@ -37,29 +47,48 @@ function tokenMap(extra = {}) {
|
|
|
37
47
|
};
|
|
38
48
|
}
|
|
39
49
|
|
|
40
|
-
function makeFixture() {
|
|
41
|
-
const root =
|
|
50
|
+
async function makeFixture() {
|
|
51
|
+
const root = await mkdtemp(join(tmpdir(), "pi-installer-"));
|
|
42
52
|
const output = join(root, "out");
|
|
43
53
|
return {
|
|
44
54
|
root,
|
|
45
55
|
output,
|
|
46
|
-
cleanup: () =>
|
|
56
|
+
cleanup: async () => rm(root, { recursive: true, force: true }),
|
|
47
57
|
};
|
|
48
58
|
}
|
|
49
59
|
|
|
50
|
-
function writeFixture(fx, relPath, content) {
|
|
60
|
+
async function writeFixture(fx, relPath, content) {
|
|
51
61
|
const p = join(fx.root, relPath);
|
|
52
|
-
|
|
53
|
-
|
|
62
|
+
await mkdir(dirname(p), { recursive: true });
|
|
63
|
+
await writeFile(p, content);
|
|
64
|
+
}
|
|
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
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function defaultPiKeybindings() {
|
|
77
|
+
return { "tui.input.newLine": ["shift+enter"] };
|
|
54
78
|
}
|
|
55
79
|
|
|
56
|
-
/** Seed a minimal but realistic package tree (agents + token-map + skills). */
|
|
57
|
-
function seedPackage(fx, opts = {}) {
|
|
58
|
-
const { agents = {}, skills = {}, tokenMap: map, version } = opts;
|
|
59
|
-
writeFixture(fx, "token-map.json", JSON.stringify(map ?? tokenMap(version ? { version } : {})));
|
|
80
|
+
/** Seed a minimal but realistic package tree (agents + token-map + skills + pi templates). */
|
|
81
|
+
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));
|
|
60
89
|
for (const [name, def] of Object.entries(agents)) {
|
|
61
|
-
writeFixture(fx, `agents/${name}/agent.md`, def.body ?? `# ${name}\nBody of ${name}.`);
|
|
62
|
-
writeFixture(
|
|
90
|
+
await writeFixture(fx, `agents/${name}/agent.md`, def.body ?? `# ${name}\nBody of ${name}.`);
|
|
91
|
+
await writeFixture(
|
|
63
92
|
fx,
|
|
64
93
|
`agents/${name}/pi/frontmatter.json`,
|
|
65
94
|
JSON.stringify(
|
|
@@ -72,20 +101,20 @@ function seedPackage(fx, opts = {}) {
|
|
|
72
101
|
),
|
|
73
102
|
);
|
|
74
103
|
for (const [section, content] of Object.entries(def.sections ?? {})) {
|
|
75
|
-
writeFixture(fx, `agents/${name}/pi/${section}.md`, content);
|
|
104
|
+
await writeFixture(fx, `agents/${name}/pi/${section}.md`, content);
|
|
76
105
|
}
|
|
77
106
|
}
|
|
78
107
|
for (const [path, content] of Object.entries(skills)) {
|
|
79
|
-
writeFixture(fx, `skills/${path}`, content);
|
|
108
|
+
await writeFixture(fx, `skills/${path}`, content);
|
|
80
109
|
}
|
|
81
110
|
}
|
|
82
111
|
|
|
83
|
-
function listFiles(dir) {
|
|
84
|
-
if (!
|
|
112
|
+
async function listFiles(dir) {
|
|
113
|
+
if (!(await pathExists(dir))) return [];
|
|
85
114
|
const out = [];
|
|
86
|
-
for (const entry of
|
|
115
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
87
116
|
const p = join(dir, entry.name);
|
|
88
|
-
if (entry.isDirectory()) out.push(...listFiles(p).map((f) => join(entry.name, f)));
|
|
117
|
+
if (entry.isDirectory()) out.push(...(await listFiles(p)).map((f) => join(entry.name, f)));
|
|
89
118
|
else out.push(entry.name);
|
|
90
119
|
}
|
|
91
120
|
return out;
|
|
@@ -94,10 +123,10 @@ function listFiles(dir) {
|
|
|
94
123
|
// ─── Agent composition ───────────────────────────────────────────────────────
|
|
95
124
|
|
|
96
125
|
describe("installPi — agent composition", () => {
|
|
97
|
-
it("writes one .agent.md per composable agent dir with pi frontmatter and no leftover tokens", () => {
|
|
98
|
-
const fx = makeFixture();
|
|
126
|
+
it("writes one .agent.md per composable agent dir with pi frontmatter and no leftover tokens", async () => {
|
|
127
|
+
const fx = await makeFixture();
|
|
99
128
|
try {
|
|
100
|
-
seedPackage(fx, {
|
|
129
|
+
await seedPackage(fx, {
|
|
101
130
|
agents: {
|
|
102
131
|
alpha: {
|
|
103
132
|
body: "# alpha\nUse {{TOOL:view}} to read.\n{{SECTION:polling}}\nRead {{PATH:skills_dir}}.",
|
|
@@ -112,11 +141,11 @@ describe("installPi — agent composition", () => {
|
|
|
112
141
|
},
|
|
113
142
|
});
|
|
114
143
|
|
|
115
|
-
const result = installPi({ root: fx.root, output: fx.output });
|
|
144
|
+
const result = await installPi({ root: fx.root, output: fx.output });
|
|
116
145
|
|
|
117
146
|
const file = join(fx.output, "agents", "alpha.agent.md");
|
|
118
|
-
assert.ok(
|
|
119
|
-
const content =
|
|
147
|
+
assert.ok(await pathExists(file), "agent file written");
|
|
148
|
+
const content = await readFile(file, "utf-8");
|
|
120
149
|
assert.ok(content.startsWith("---\n"), "yaml frontmatter");
|
|
121
150
|
assert.match(content, /name: "agent-cortex:alpha"/);
|
|
122
151
|
assert.match(content, /description: "Reads things\."/);
|
|
@@ -128,55 +157,55 @@ describe("installPi — agent composition", () => {
|
|
|
128
157
|
assert.equal(result.agents.length, 1);
|
|
129
158
|
assert.deepEqual(result.agents[0], { name: "alpha", filePath: file });
|
|
130
159
|
} finally {
|
|
131
|
-
fx.cleanup();
|
|
160
|
+
await fx.cleanup();
|
|
132
161
|
}
|
|
133
162
|
});
|
|
134
163
|
|
|
135
|
-
it("drops null-mapped tool tokens in prose and reports a warning", () => {
|
|
136
|
-
const fx = makeFixture();
|
|
164
|
+
it("drops null-mapped tool tokens in prose and reports a warning", async () => {
|
|
165
|
+
const fx = await makeFixture();
|
|
137
166
|
try {
|
|
138
|
-
seedPackage(fx, {
|
|
167
|
+
await seedPackage(fx, {
|
|
139
168
|
agents: {
|
|
140
169
|
alpha: { body: "# alpha\nAsk the user with {{TOOL:ask_user}}, then use {{TOOL:bash}}." },
|
|
141
170
|
},
|
|
142
171
|
});
|
|
143
172
|
|
|
144
|
-
const result = installPi({ root: fx.root, output: fx.output });
|
|
145
|
-
const content =
|
|
173
|
+
const result = await installPi({ root: fx.root, output: fx.output });
|
|
174
|
+
const content = await readFile(join(fx.output, "agents", "alpha.agent.md"), "utf-8");
|
|
146
175
|
|
|
147
176
|
assert.ok(!content.includes("ask_user"), "null-mapped token dropped from prose");
|
|
148
177
|
assert.ok(content.includes("use bash"), "mapped token substituted");
|
|
149
178
|
assert.ok(!/{{/.test(content));
|
|
150
179
|
assert.ok(result.warnings.some((w) => w.includes("{{TOOL:ask_user}}")), "drop reported as warning");
|
|
151
180
|
} finally {
|
|
152
|
-
fx.cleanup();
|
|
181
|
+
await fx.cleanup();
|
|
153
182
|
}
|
|
154
183
|
});
|
|
155
184
|
|
|
156
|
-
it("throws on an unknown tool token", () => {
|
|
157
|
-
const fx = makeFixture();
|
|
185
|
+
it("throws on an unknown tool token", async () => {
|
|
186
|
+
const fx = await makeFixture();
|
|
158
187
|
try {
|
|
159
|
-
seedPackage(fx, { agents: { alpha: { body: "# alpha\nUse {{TOOL:nope}} now." } } });
|
|
160
|
-
assert.
|
|
188
|
+
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}}/);
|
|
161
190
|
} finally {
|
|
162
|
-
fx.cleanup();
|
|
191
|
+
await fx.cleanup();
|
|
163
192
|
}
|
|
164
193
|
});
|
|
165
194
|
|
|
166
|
-
it("throws when a section file referenced by {{SECTION:...}} is missing", () => {
|
|
167
|
-
const fx = makeFixture();
|
|
195
|
+
it("throws when a section file referenced by {{SECTION:...}} is missing", async () => {
|
|
196
|
+
const fx = await makeFixture();
|
|
168
197
|
try {
|
|
169
|
-
seedPackage(fx, { agents: { alpha: { body: "# alpha\n{{SECTION:missing}}" } } });
|
|
170
|
-
assert.
|
|
198
|
+
await seedPackage(fx, { agents: { alpha: { body: "# alpha\n{{SECTION:missing}}" } } });
|
|
199
|
+
await assert.rejects(installPi({ root: fx.root, output: fx.output }), /missing section "missing"/);
|
|
171
200
|
} finally {
|
|
172
|
-
fx.cleanup();
|
|
201
|
+
await fx.cleanup();
|
|
173
202
|
}
|
|
174
203
|
});
|
|
175
204
|
|
|
176
|
-
it("resolves named and relative {{PATH:...}} tokens against the plugin root", () => {
|
|
177
|
-
const fx = makeFixture();
|
|
205
|
+
it("resolves named and relative {{PATH:...}} tokens against the plugin root", async () => {
|
|
206
|
+
const fx = await makeFixture();
|
|
178
207
|
try {
|
|
179
|
-
seedPackage(fx, {
|
|
208
|
+
await seedPackage(fx, {
|
|
180
209
|
agents: {
|
|
181
210
|
alpha: {
|
|
182
211
|
body: "# alpha\nNamed: {{PATH:skills_dir}}\nRelative: {{PATH:skills/x/SKILL.md}}",
|
|
@@ -184,8 +213,8 @@ describe("installPi — agent composition", () => {
|
|
|
184
213
|
},
|
|
185
214
|
});
|
|
186
215
|
|
|
187
|
-
const result = installPi({ root: fx.root, output: fx.output });
|
|
188
|
-
const content =
|
|
216
|
+
const result = await installPi({ root: fx.root, output: fx.output });
|
|
217
|
+
const content = await readFile(join(fx.output, "agents", "alpha.agent.md"), "utf-8");
|
|
189
218
|
|
|
190
219
|
assert.ok(
|
|
191
220
|
content.includes("Named: ~/.pi/agent/npm/node_modules/@jaybeeuu/agent-cortex/skills"),
|
|
@@ -197,38 +226,137 @@ describe("installPi — agent composition", () => {
|
|
|
197
226
|
);
|
|
198
227
|
assert.equal(result.warnings.length, 0);
|
|
199
228
|
} finally {
|
|
200
|
-
fx.cleanup();
|
|
229
|
+
await fx.cleanup();
|
|
201
230
|
}
|
|
202
231
|
});
|
|
203
232
|
|
|
204
|
-
it("honours the --plugin-root override for path resolution", () => {
|
|
205
|
-
const fx = makeFixture();
|
|
233
|
+
it("honours the --plugin-root override for path resolution", async () => {
|
|
234
|
+
const fx = await makeFixture();
|
|
206
235
|
try {
|
|
207
|
-
seedPackage(fx, {
|
|
236
|
+
await seedPackage(fx, {
|
|
208
237
|
agents: {
|
|
209
238
|
alpha: { body: "# alpha\n{{PATH:skills/x/SKILL.md}}" },
|
|
210
239
|
},
|
|
211
240
|
});
|
|
212
241
|
|
|
213
242
|
const pluginRoot = join(fx.root, "checkout");
|
|
214
|
-
installPi({ root: fx.root, output: fx.output, pluginRoot });
|
|
215
|
-
const content =
|
|
243
|
+
await installPi({ root: fx.root, output: fx.output, pluginRoot });
|
|
244
|
+
const content = await readFile(join(fx.output, "agents", "alpha.agent.md"), "utf-8");
|
|
216
245
|
assert.ok(content.includes(join(pluginRoot, "skills", "x", "SKILL.md")), "plugin root override used");
|
|
217
246
|
} finally {
|
|
218
|
-
fx.cleanup();
|
|
247
|
+
await fx.cleanup();
|
|
219
248
|
}
|
|
220
249
|
});
|
|
221
250
|
|
|
222
|
-
it("rejects token-map.json versions newer than the implemented contract", () => {
|
|
223
|
-
const fx = makeFixture();
|
|
251
|
+
it("rejects token-map.json versions newer than the implemented contract", async () => {
|
|
252
|
+
const fx = await makeFixture();
|
|
224
253
|
try {
|
|
225
|
-
seedPackage(fx, {
|
|
254
|
+
await seedPackage(fx, {
|
|
226
255
|
version: 99,
|
|
227
256
|
agents: { alpha: { body: "# alpha" } },
|
|
228
257
|
});
|
|
229
|
-
assert.
|
|
258
|
+
await assert.rejects(installPi({ root: fx.root, output: fx.output }), /version 99/);
|
|
230
259
|
} finally {
|
|
231
|
-
fx.cleanup();
|
|
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({
|
|
326
|
+
root: fx.root,
|
|
327
|
+
output: fx.output,
|
|
328
|
+
provisionPackages: true,
|
|
329
|
+
dryRun: true,
|
|
330
|
+
runInstall,
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
assert.deepEqual(calls, []);
|
|
334
|
+
assert.deepEqual(result.packages.planned, ["npm:pi-questions"]);
|
|
335
|
+
} finally {
|
|
336
|
+
await fx.cleanup();
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("warns but still installs agents when a package install fails", async () => {
|
|
341
|
+
const fx = await makeFixture();
|
|
342
|
+
try {
|
|
343
|
+
await seedPackage(fx, {
|
|
344
|
+
agents: { alpha: { body: "# alpha" } },
|
|
345
|
+
packages: ["npm:pi-questions"],
|
|
346
|
+
});
|
|
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");
|
|
358
|
+
} finally {
|
|
359
|
+
await fx.cleanup();
|
|
232
360
|
}
|
|
233
361
|
});
|
|
234
362
|
});
|
|
@@ -236,10 +364,10 @@ describe("installPi — agent composition", () => {
|
|
|
236
364
|
// ─── Skills ──────────────────────────────────────────────────────────────────
|
|
237
365
|
|
|
238
366
|
describe("installPi — skills", () => {
|
|
239
|
-
it("copies skills into <output>/skills preserving the grouped layout with substituted tokens", () => {
|
|
240
|
-
const fx = makeFixture();
|
|
367
|
+
it("copies skills into <output>/skills preserving the grouped layout with substituted tokens", async () => {
|
|
368
|
+
const fx = await makeFixture();
|
|
241
369
|
try {
|
|
242
|
-
seedPackage(fx, {
|
|
370
|
+
await seedPackage(fx, {
|
|
243
371
|
agents: { alpha: { body: "# alpha" } },
|
|
244
372
|
skills: {
|
|
245
373
|
"engineering/echo/SKILL.md":
|
|
@@ -248,11 +376,11 @@ describe("installPi — skills", () => {
|
|
|
248
376
|
},
|
|
249
377
|
});
|
|
250
378
|
|
|
251
|
-
installPi({ root: fx.root, output: fx.output });
|
|
379
|
+
await installPi({ root: fx.root, output: fx.output });
|
|
252
380
|
|
|
253
381
|
const skillFile = join(fx.output, "skills", "engineering", "echo", "SKILL.md");
|
|
254
|
-
assert.ok(
|
|
255
|
-
const content =
|
|
382
|
+
assert.ok(await pathExists(skillFile), "skill copied");
|
|
383
|
+
const content = await readFile(skillFile, "utf-8");
|
|
256
384
|
assert.ok(content.includes("description: Echoes read."), "token substituted in skill frontmatter");
|
|
257
385
|
assert.ok(content.includes("Use read to read and for help."), "null-mapped {{TOOL:skill}} dropped");
|
|
258
386
|
assert.ok(
|
|
@@ -263,10 +391,10 @@ describe("installPi — skills", () => {
|
|
|
263
391
|
);
|
|
264
392
|
assert.ok(!/{{/.test(content), "no literal tokens remain in skill");
|
|
265
393
|
|
|
266
|
-
const script =
|
|
394
|
+
const script = await readFile(join(fx.output, "skills", "engineering", "echo", "scripts", "run.sh"), "utf-8");
|
|
267
395
|
assert.equal(script, "#!/bin/sh\necho hi\n", "non-md files copied verbatim");
|
|
268
396
|
} finally {
|
|
269
|
-
fx.cleanup();
|
|
397
|
+
await fx.cleanup();
|
|
270
398
|
}
|
|
271
399
|
});
|
|
272
400
|
});
|
|
@@ -274,37 +402,258 @@ describe("installPi — skills", () => {
|
|
|
274
402
|
// ─── Output / dry-run ────────────────────────────────────────────────────────
|
|
275
403
|
|
|
276
404
|
describe("installPi — output & dry-run", () => {
|
|
277
|
-
it("dry-run writes nothing but reports the planned output", () => {
|
|
278
|
-
const fx = makeFixture();
|
|
405
|
+
it("dry-run writes nothing but reports the planned output", async () => {
|
|
406
|
+
const fx = await makeFixture();
|
|
279
407
|
try {
|
|
280
|
-
seedPackage(fx, {
|
|
408
|
+
await seedPackage(fx, {
|
|
281
409
|
agents: { alpha: { body: "# alpha" } },
|
|
282
410
|
skills: { "engineering/echo/SKILL.md": "---\nname: echo\ndescription: d\n---\n\nBody." },
|
|
283
411
|
});
|
|
284
412
|
|
|
285
|
-
const result = installPi({ root: fx.root, output: fx.output, dryRun: true });
|
|
413
|
+
const result = await installPi({ root: fx.root, output: fx.output, dryRun: true });
|
|
286
414
|
|
|
287
415
|
assert.equal(result.dryRun, true);
|
|
288
|
-
assert.equal(listFiles(fx.output).length, 0, "no files written in dry-run");
|
|
416
|
+
assert.equal((await listFiles(fx.output)).length, 0, "no files written in dry-run");
|
|
289
417
|
assert.equal(result.agents.length, 1);
|
|
290
418
|
assert.equal(result.agents[0].name, "alpha");
|
|
291
419
|
assert.equal(result.skills.skills, 1);
|
|
292
420
|
// Planned paths are still reported so the user can see what would happen.
|
|
293
421
|
assert.ok(result.agents[0].filePath.startsWith(fx.output));
|
|
294
422
|
} finally {
|
|
295
|
-
fx.cleanup();
|
|
423
|
+
await fx.cleanup();
|
|
296
424
|
}
|
|
297
425
|
});
|
|
298
426
|
|
|
299
|
-
it("creates the output directory hierarchy when needed", () => {
|
|
300
|
-
const fx = makeFixture();
|
|
427
|
+
it("creates the output directory hierarchy when needed", async () => {
|
|
428
|
+
const fx = await makeFixture();
|
|
301
429
|
try {
|
|
302
|
-
seedPackage(fx, { agents: { alpha: { body: "# alpha" } } });
|
|
430
|
+
await seedPackage(fx, { agents: { alpha: { body: "# alpha" } } });
|
|
303
431
|
const deep = join(fx.output, "nested", "deep");
|
|
304
|
-
installPi({ root: fx.root, output: deep });
|
|
305
|
-
assert.ok(
|
|
432
|
+
await installPi({ root: fx.root, output: deep });
|
|
433
|
+
assert.ok(await pathExists(join(deep, "agents", "alpha.agent.md")));
|
|
434
|
+
} finally {
|
|
435
|
+
await fx.cleanup();
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// ─── Pi settings.json ────────────────────────────────────────────────────────
|
|
441
|
+
|
|
442
|
+
describe("installPi — pi settings.json", () => {
|
|
443
|
+
it("writes a real settings.json with the repo path package and template keys", async () => {
|
|
444
|
+
const fx = await makeFixture();
|
|
445
|
+
try {
|
|
446
|
+
await seedPackage(fx, { agents: { alpha: { body: "# alpha" } } });
|
|
447
|
+
|
|
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"));
|
|
453
|
+
|
|
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");
|
|
461
|
+
} finally {
|
|
462
|
+
await fx.cleanup();
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it("preserves personal values and unknown keys while filling template defaults", async () => {
|
|
467
|
+
const fx = await makeFixture();
|
|
468
|
+
try {
|
|
469
|
+
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
|
+
|
|
481
|
+
await installPi({ root: fx.root, output: fx.output });
|
|
482
|
+
|
|
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
|
+
);
|
|
493
|
+
} finally {
|
|
494
|
+
await fx.cleanup();
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it("replaces a legacy symlink with a real managed file", async () => {
|
|
499
|
+
const fx = await makeFixture();
|
|
500
|
+
try {
|
|
501
|
+
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"));
|
|
504
|
+
|
|
505
|
+
const result = await installPi({ root: fx.root, output: fx.output });
|
|
506
|
+
|
|
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");
|
|
512
|
+
} finally {
|
|
513
|
+
await fx.cleanup();
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it("re-running leaves settings.json unchanged", async () => {
|
|
518
|
+
const fx = await makeFixture();
|
|
519
|
+
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");
|
|
523
|
+
|
|
524
|
+
const result = await installPi({ root: fx.root, output: fx.output });
|
|
525
|
+
|
|
526
|
+
assert.equal(result.settings.action, "unchanged");
|
|
527
|
+
assert.equal(await readFile(join(fx.output, "settings.json"), "utf-8"), first);
|
|
528
|
+
} finally {
|
|
529
|
+
await fx.cleanup();
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it("dry-run plans the settings write without touching the filesystem", async () => {
|
|
534
|
+
const fx = await makeFixture();
|
|
535
|
+
try {
|
|
536
|
+
await seedPackage(fx, { agents: { alpha: { body: "# alpha" } } });
|
|
537
|
+
|
|
538
|
+
const result = await installPi({ root: fx.root, output: fx.output, dryRun: true });
|
|
539
|
+
|
|
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");
|
|
542
|
+
} finally {
|
|
543
|
+
await fx.cleanup();
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it("dry-run reports would-remove-symlink for a legacy symlink", async () => {
|
|
548
|
+
const fx = await makeFixture();
|
|
549
|
+
try {
|
|
550
|
+
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);
|
|
554
|
+
|
|
555
|
+
const result = await installPi({ root: fx.root, output: fx.output, dryRun: true });
|
|
556
|
+
|
|
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");
|
|
560
|
+
} finally {
|
|
561
|
+
await fx.cleanup();
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
// ─── Pi keybindings.json ─────────────────────────────────────────────────────
|
|
567
|
+
|
|
568
|
+
describe("installPi — pi keybindings.json", () => {
|
|
569
|
+
it("writes the template bindings as a real generated file", async () => {
|
|
570
|
+
const fx = await makeFixture();
|
|
571
|
+
try {
|
|
572
|
+
await seedPackage(fx, { agents: { alpha: { body: "# alpha" } } });
|
|
573
|
+
|
|
574
|
+
const result = await installPi({ root: fx.root, output: fx.output });
|
|
575
|
+
|
|
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");
|
|
582
|
+
} finally {
|
|
583
|
+
await fx.cleanup();
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it("leaves a user-modified keybindings file alone", async () => {
|
|
588
|
+
const fx = await makeFixture();
|
|
589
|
+
try {
|
|
590
|
+
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
|
+
);
|
|
596
|
+
|
|
597
|
+
const result = await installPi({ root: fx.root, output: fx.output });
|
|
598
|
+
|
|
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");
|
|
602
|
+
} finally {
|
|
603
|
+
await fx.cleanup();
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it("refreshes an unmodified install when the template changes", async () => {
|
|
608
|
+
const fx = await makeFixture();
|
|
609
|
+
try {
|
|
610
|
+
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
|
+
);
|
|
617
|
+
|
|
618
|
+
const result = await installPi({ root: fx.root, output: fx.output });
|
|
619
|
+
|
|
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");
|
|
623
|
+
} finally {
|
|
624
|
+
await fx.cleanup();
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
it("replaces a legacy symlink with a real file", async () => {
|
|
629
|
+
const fx = await makeFixture();
|
|
630
|
+
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"));
|
|
634
|
+
|
|
635
|
+
const result = await installPi({ root: fx.root, output: fx.output });
|
|
636
|
+
|
|
637
|
+
assert.equal(result.keybindings.action, "removed-symlink");
|
|
638
|
+
assert.equal((await lstat(join(fx.output, "keybindings.json"))).isSymbolicLink(), false);
|
|
639
|
+
} finally {
|
|
640
|
+
await fx.cleanup();
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
it("re-running leaves keybindings.json unchanged", async () => {
|
|
645
|
+
const fx = await makeFixture();
|
|
646
|
+
try {
|
|
647
|
+
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);
|
|
306
655
|
} finally {
|
|
307
|
-
fx.cleanup();
|
|
656
|
+
await fx.cleanup();
|
|
308
657
|
}
|
|
309
658
|
});
|
|
310
659
|
});
|
|
@@ -312,60 +661,66 @@ describe("installPi — output & dry-run", () => {
|
|
|
312
661
|
// ─── Integration against the real repo ───────────────────────────────────────
|
|
313
662
|
|
|
314
663
|
describe("installPi — real repo integration", () => {
|
|
315
|
-
it("installs every canonical agent with pi tool names and no leftover tokens", () => {
|
|
316
|
-
const fx = makeFixture();
|
|
664
|
+
it("installs every canonical agent with pi tool names and no leftover tokens", async () => {
|
|
665
|
+
const fx = await makeFixture();
|
|
317
666
|
try {
|
|
318
|
-
const result = installPi({ root: REPO_ROOT, output: fx.output });
|
|
667
|
+
const result = await installPi({ root: REPO_ROOT, output: fx.output });
|
|
319
668
|
|
|
320
669
|
const names = result.agents.map((a) => a.name).sort();
|
|
321
670
|
assert.deepEqual(names, ["plan", "ralph", "ralph-plan", "strategy"]);
|
|
322
671
|
|
|
323
672
|
// ralph pi frontmatter: ["bash", "view", "rg", "glob", "task", "read_agent", "wait_for_agents"]
|
|
324
|
-
const ralph =
|
|
673
|
+
const ralph = await readFile(join(fx.output, "agents", "ralph.agent.md"), "utf-8");
|
|
325
674
|
assert.match(ralph, /name: "agent-cortex:ralph"/);
|
|
326
675
|
assert.match(ralph, /tools: "bash read grep find task read_agent wait_for_agents"/);
|
|
327
676
|
assert.ok(ralph.includes("## Spawning subagents (PI)"), "{{SECTION:polling}} composed from pi/polling.md");
|
|
328
677
|
const bodyTokens = ralph.split("---\n").slice(2).join("---\n");
|
|
329
678
|
assert.ok(!/{{/.test(bodyTokens), "no literal tokens in ralph body");
|
|
330
679
|
|
|
331
|
-
// plan body carries the relative {{PATH:...}} resolved against the plugin root
|
|
332
|
-
|
|
333
|
-
|
|
680
|
+
// plan body carries the relative {{PATH:...}} resolved against the plugin root.
|
|
681
|
+
// Tool list preserves authoring order (composer translateToolList, unlike the
|
|
682
|
+
// runtime agent-modes translateTools which sorts): ask_user → ask_questions,
|
|
683
|
+
// skill → read (last position).
|
|
684
|
+
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"/);
|
|
334
686
|
assert.ok(
|
|
335
687
|
plan.includes("~/.pi/agent/npm/node_modules/@jaybeeuu/agent-cortex/skills/workflow/plan/SKILL.md"),
|
|
336
688
|
"plan PATH token resolved against token-map plugin root",
|
|
337
689
|
);
|
|
338
690
|
|
|
339
|
-
// ralph-plan prose {{TOOL:ask_user}}
|
|
340
|
-
const ralphPlan =
|
|
341
|
-
assert.ok(!ralphPlan.includes("ask_user"), "
|
|
691
|
+
// ralph-plan prose {{TOOL:ask_user}} now substitutes to ask_questions
|
|
692
|
+
const ralphPlan = await readFile(join(fx.output, "agents", "ralph-plan.agent.md"), "utf-8");
|
|
693
|
+
assert.ok(!ralphPlan.includes("ask_user"), "ask_user never appears in ralph-plan body");
|
|
694
|
+
assert.ok(ralphPlan.includes("ask_questions"), "{{TOOL:ask_user}} substituted to ask_questions");
|
|
342
695
|
assert.ok(
|
|
343
|
-
result.warnings.some((w) => w.includes("ask_user")),
|
|
344
|
-
"ralph-plan
|
|
696
|
+
!result.warnings.some((w) => w.includes("ask_user")),
|
|
697
|
+
"no null-tool warning for ralph-plan (ask_user now maps to ask_questions)",
|
|
345
698
|
);
|
|
346
699
|
|
|
347
|
-
// strategy: web_fetch → fetch_content; ask_user
|
|
348
|
-
|
|
349
|
-
|
|
700
|
+
// strategy: web_fetch → fetch_content; ask_user → ask_questions; skill → read
|
|
701
|
+
// (authoring order preserved: bash, view→read, rg→grep, glob→find, ask_user→
|
|
702
|
+
// ask_questions, web_fetch→fetch_content, skill→read, edit, create)
|
|
703
|
+
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"/);
|
|
350
705
|
} finally {
|
|
351
|
-
fx.cleanup();
|
|
706
|
+
await fx.cleanup();
|
|
352
707
|
}
|
|
353
708
|
});
|
|
354
709
|
|
|
355
|
-
it("substitutes tokens in every installed skill markdown", () => {
|
|
356
|
-
const fx = makeFixture();
|
|
710
|
+
it("substitutes tokens in every installed skill markdown", async () => {
|
|
711
|
+
const fx = await makeFixture();
|
|
357
712
|
try {
|
|
358
|
-
const result = installPi({ root: REPO_ROOT, output: fx.output });
|
|
713
|
+
const result = await installPi({ root: REPO_ROOT, output: fx.output });
|
|
359
714
|
assert.ok(result.skills.skills > 20, `expected the full skill tree, got ${result.skills.skills}`);
|
|
360
715
|
|
|
361
|
-
const files = listFiles(join(fx.output, "skills")).filter((f) => f.endsWith(".md"));
|
|
716
|
+
const files = (await listFiles(join(fx.output, "skills"))).filter((f) => f.endsWith(".md"));
|
|
362
717
|
assert.ok(files.length > 0);
|
|
363
718
|
for (const f of files) {
|
|
364
|
-
const content =
|
|
719
|
+
const content = await readFile(join(fx.output, "skills", f), "utf-8");
|
|
365
720
|
assert.ok(!/{{/.test(content), `no literal tokens in skill ${f}`);
|
|
366
721
|
}
|
|
367
722
|
} finally {
|
|
368
|
-
fx.cleanup();
|
|
723
|
+
await fx.cleanup();
|
|
369
724
|
}
|
|
370
725
|
});
|
|
371
726
|
});
|