@jaybeeuu/agent-cortex 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +437 -0
- package/README.md +46 -15
- package/agents/README.md +22 -10
- package/agents/plan/agent.md +2 -2
- package/agents/ralph/agent.md +128 -91
- package/agents/ralph/claude/frontmatter.json +3 -3
- package/agents/ralph/claude/wake.md +46 -0
- package/agents/ralph/pi/{polling.md → wake.md} +35 -20
- package/agents/ralph-plan/agent.md +23 -11
- package/bin/agent-cortex.mjs +66 -4
- package/bin/installers/claude.mjs +262 -77
- package/bin/installers/pi.mjs +200 -24
- package/extensions/agent-guardrails/README.md +1 -1
- package/extensions/agent-guardrails/guardrails.ts +1 -1
- package/extensions/agent-guardrails/index.ts +1 -1
- package/extensions/agent-modes/discover.test.ts +2 -2
- package/extensions/output-style/README.md +58 -0
- package/extensions/output-style/index.ts +23 -0
- package/extensions/output-style/output-style.md +43 -0
- package/extensions/output-style/output-style.test.ts +50 -0
- package/extensions/output-style/output-style.ts +32 -0
- package/extensions/output-style/package.json +19 -0
- package/hooks/claude/scripts/notify.mjs +8 -1
- package/lib/cli.mjs +91 -2
- package/package.json +29 -4
- package/scripts/check-changeset.mjs +4 -0
- package/scripts/check-no-sync.mjs +5 -1
- package/scripts/check-skill-frontmatter.mjs +21 -0
- package/scripts/lib/compose-agent.mjs +69 -9
- package/scripts/lib/fs.mjs +10 -2
- package/scripts/lib/migrate.mjs +736 -0
- package/scripts/lib/release-gates.mjs +21 -1
- package/scripts/lib/skill-frontmatter.mjs +152 -0
- package/scripts/lib/skill-links.mjs +52 -0
- package/scripts/lib/verify-worker-tree.mjs +25 -0
- package/scripts/resolve-publish-command.mjs +4 -0
- package/skills/engineering/design-an-interface/SKILL.md +3 -3
- package/skills/engineering/grill-with-docs/SKILL.md +3 -5
- package/skills/engineering/improve-codebase-architecture/SKILL.md +5 -4
- package/skills/engineering/tdd/SKILL.md +5 -7
- package/skills/engineering/technical-direction/SKILL.md +5 -5
- package/skills/planning/classify-bead/REFERENCE.md +3 -3
- package/skills/planning/classify-bead/SKILL.md +14 -9
- package/skills/planning/classify-bead/scripts/classify-bead.mjs +5 -4
- package/skills/planning/create-task/REFERENCE.md +52 -3
- package/skills/planning/create-task/SKILL.md +70 -26
- package/skills/planning/create-task/scripts/{create-chores.ts → create-chores.mjs} +48 -23
- package/skills/planning/flesh-out-idea/REFERENCE.md +116 -0
- package/skills/planning/flesh-out-idea/SKILL.md +146 -0
- package/skills/planning/prd-to-tasks/REFERENCE.md +54 -5
- package/skills/planning/prd-to-tasks/SKILL.md +22 -16
- package/skills/planning/record-idea/FORMAT.md +66 -17
- package/skills/planning/record-idea/SKILL.md +48 -24
- package/skills/planning/request-refactor-plan/SKILL.md +4 -4
- package/skills/planning/spec-to-tickets/SKILL.md +5 -5
- package/skills/planning/write-a-prd/SKILL.md +8 -9
- package/skills/planning/write-a-ticket/SKILL.md +4 -6
- package/skills/productivity/bd-tool/SKILL.md +15 -10
- package/skills/productivity/grill-me/SKILL.md +5 -0
- package/skills/productivity/init-beads/SKILL.md +3 -3
- package/skills/productivity/migration/REFERENCE.md +227 -0
- package/skills/productivity/migration/SKILL.md +188 -0
- package/skills/productivity/validate-agent-dir/scripts/{validate-agent-dir.ts → validate-agent-dir.mjs} +49 -28
- package/skills/productivity/write-a-skill/SKILL.md +5 -5
- package/skills/review/maintain-agent-docs/SKILL.md +5 -7
- package/skills/review/refactor-skill/SKILL.md +4 -3
- package/skills/review/review-security/SKILL.md +3 -5
- package/skills/style/style-code/SKILL.md +5 -10
- package/skills/style/style-comms/SKILL.md +5 -7
- package/skills/style/style-documentation/SKILL.md +5 -6
- package/skills/style/style-tests/SKILL.md +5 -7
- package/skills/workflow/git-workflow/SKILL.md +28 -4
- package/skills/workflow/hitl-collab/SKILL.md +4 -6
- package/skills/workflow/plan/SKILL.md +41 -30
- package/skills/workflow/ralph/REFERENCE.md +209 -55
- package/skills/workflow/ralph/SKILL.md +89 -50
- package/skills/workflow/ralph/scripts/pending-gates.mjs +134 -0
- package/skills/workflow/run-pipeline-stage/SKILL.md +72 -71
- package/skills/workflow/run-pipeline-stage/playbooks/code.md +11 -0
- package/skills/workflow/run-pipeline-stage/playbooks/document.md +12 -0
- package/skills/workflow/run-pipeline-stage/playbooks/fix.md +12 -0
- package/skills/workflow/run-pipeline-stage/playbooks/review.md +11 -0
- package/skills/workflow/run-pipeline-stage/playbooks/verify.md +13 -0
- package/skills/workflow/run-pipeline-stage/prompts/stage-runner.md +73 -1
- package/token-map.README.md +9 -4
- package/token-map.json +13 -6
- package/.agents/skills/style-versioning/SKILL.md +0 -172
- package/.changeset/README.md +0 -8
- package/.changeset/config.json +0 -11
- package/.github/workflows/ci.yml +0 -302
- package/.junie/guidelines.md +0 -87
- package/.junie/mcp/mcp.json +0 -10
- package/AGENTS.md +0 -151
- package/agents/ralph/claude/polling.md +0 -31
- package/agents-native/ralph.md +0 -193
- package/docs/claude-hooks.md +0 -128
- package/docs/commit-signing.md +0 -231
- package/docs/ideas/auto-model-selection.md +0 -75
- package/docs/ideas/ban-content-assertion-tests.md +0 -75
- package/docs/ideas/bead-progress-notes-for-session-resumption.md +0 -75
- package/docs/ideas/classify-beads-via-deterministic-script-with-agent-fallback.md +0 -70
- package/docs/ideas/ctx-alias-version-and-update-commands.md +0 -65
- package/docs/ideas/ctx-feedback-command-for-capturing-ideas-from-any-machine.md +0 -67
- package/docs/ideas/dedicated-review-tests-skill.md +0 -71
- package/docs/ideas/deployment-beads-for-post-merge-pipelines.md +0 -36
- package/docs/ideas/design-and-delivery-as-separate-bead-trees/design-and-delivery-as-separate-bead-trees.md +0 -99
- package/docs/ideas/enable-signed-commit-enforcement-changesets-bot-commits-must-be-signed.md +0 -42
- package/docs/ideas/extension-manifests.md +0 -121
- package/docs/ideas/improve-pi-system-prompt.md +0 -37
- package/docs/ideas/map-every-cortex-tool-to-a-real-pi-equivalent.md +0 -95
- package/docs/ideas/model-selector.md +0 -40
- package/docs/ideas/pi-task-memory.md +0 -34
- package/docs/ideas/qa-extension.md +0 -43
- package/docs/ideas/record-idea-implementation-questions.md +0 -68
- package/docs/ideas/record-ideas-as-beads.md +0 -115
- package/docs/ideas/self-flushing-session.md +0 -72
- package/docs/ideas/state-in-beads-not-md.md +0 -69
- package/docs/ideas/subagent-visibility.md +0 -72
- package/docs/ideas/task-session-context.local.md +0 -95
- package/docs/ideas/task-session-context.md +0 -89
- package/docs/ideas/test-data-creation-helpers.md +0 -92
- package/docs/ideas/test-planning-stage-bead-in-feature-planning.md +0 -68
- package/docs/ideas/unified-ci-pipeline.md +0 -28
- package/docs/ideas/unify-the-ticket-and-task-decomposition-workflows.md +0 -71
- package/docs/ideas/worktree-housekeeping-for-closed-beads.md +0 -39
- package/docs/inspirations.md +0 -33
- package/docs/research/disable-model-invocation.md +0 -82
- package/docs/research/gas-city-research.md +0 -256
- package/docs/research/skill-design-research.md +0 -286
- package/docs/research/skill-improvements-analysis.md +0 -117
- package/docs/research/skill-patterns-research.md +0 -24
- package/docs/skills/skill-anatomy.md +0 -197
- package/docs/technical-direction/harness-config.md +0 -177
- package/docs/technical-direction/migration-execution-labels.md +0 -177
- package/docs/user-preferences.md +0 -17
- package/extensions/agent-guardrails/tsconfig.json +0 -12
- package/extensions/agent-modes/tsconfig.json +0 -12
- package/extensions/session-start/tsconfig.json +0 -12
- package/extensions/skill-stats/pnpm-lock.yaml +0 -329
- package/extensions/skill-stats/tsconfig.json +0 -12
- package/extensions/subagent/agents/planner.md +0 -37
- package/extensions/subagent/agents/reviewer.md +0 -35
- package/extensions/subagent/agents/scout.md +0 -50
- package/extensions/subagent/agents/worker.md +0 -24
- package/extensions/subagent/prompts/implement-and-review.md +0 -10
- package/extensions/subagent/prompts/implement.md +0 -10
- package/extensions/subagent/prompts/scout-and-plan.md +0 -9
- package/extensions/subagent/tsconfig.json +0 -12
- package/pi/keybindings.json +0 -3
- package/pi/settings.json +0 -34
- package/pnpm-workspace.yaml +0 -14
- package/skills/planning/create-task/scripts/package.json +0 -14
- package/skills/planning/create-task/scripts/pnpm-lock.yaml +0 -342
- package/skills/planning/create-task/scripts/tsconfig.json +0 -12
- package/skills/planning/record-idea/scripts/new-idea.sh +0 -89
- package/skills/productivity/validate-agent-dir/scripts/package.json +0 -15
- package/skills/productivity/validate-agent-dir/scripts/pnpm-lock.yaml +0 -329
- package/skills/productivity/validate-agent-dir/scripts/pnpm-workspace.yaml +0 -2
- package/skills/productivity/validate-agent-dir/scripts/tsconfig.json +0 -19
- package/skills/productivity/validate-agent-dir/scripts/validate-agent-dir.test.ts +0 -317
- package/skills/workflow/run-pipeline-stage/REFERENCE.md +0 -22
- package/skills/workflow/run-pipeline-stage/scripts/generate-progress.test.ts +0 -682
- package/skills/workflow/run-pipeline-stage/scripts/generate-progress.ts +0 -588
- package/skills/workflow/run-pipeline-stage/scripts/package.json +0 -15
- package/skills/workflow/run-pipeline-stage/scripts/pnpm-lock.yaml +0 -342
- package/skills/workflow/run-pipeline-stage/scripts/pnpm-workspace.yaml +0 -2
- package/skills/workflow/run-pipeline-stage/scripts/tsconfig.json +0 -12
- package/test/classify-bead.test.mjs +0 -277
- package/test/claude-installer.test.mjs +0 -308
- package/test/cli.test.mjs +0 -314
- package/test/create-chores.test.mjs +0 -85
- package/test/fake-bd.test.mjs +0 -72
- package/test/fs.test.mjs +0 -76
- package/test/helpers/fake-bd.mjs +0 -87
- package/test/helpers/fake-claude.mjs +0 -152
- package/test/helpers/fake-pi.mjs +0 -132
- package/test/installer.test.mjs +0 -664
- package/test/notify-hook.test.mjs +0 -63
- package/test/pi-installer.test.mjs +0 -785
- package/test/pi-register.test.mjs +0 -342
- package/test/pi-sweep.test.mjs +0 -619
- package/test/release-gates.test.mjs +0 -190
- package/test/verify-worker-tree.test.mjs +0 -120
|
@@ -1,785 +0,0 @@
|
|
|
1
|
-
import { describe, it } from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { mkdtemp, mkdir, writeFile, rm, readFile, readdir, stat, lstat, symlink } from "node:fs/promises";
|
|
4
|
-
import { join, dirname } from "node:path";
|
|
5
|
-
import { tmpdir } from "node:os";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { installPi } from "../bin/installers/pi.mjs";
|
|
8
|
-
|
|
9
|
-
// Real repo root — used by the integration smoke test to prove the installer
|
|
10
|
-
// works against the canonical sources (agents/, skills/, extensions/, token-map.json).
|
|
11
|
-
const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
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
|
-
|
|
23
|
-
// ─── Fixtures ────────────────────────────────────────────────────────────────
|
|
24
|
-
|
|
25
|
-
const TOOL_MAP = {
|
|
26
|
-
bash: { pi: "bash" },
|
|
27
|
-
view: { pi: "read" },
|
|
28
|
-
create: { pi: "write" },
|
|
29
|
-
glob: { pi: "find" },
|
|
30
|
-
ask_user: { pi: null },
|
|
31
|
-
skill: { pi: null },
|
|
32
|
-
task: { pi: "task" },
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
function tokenMap(extra = {}) {
|
|
36
|
-
return {
|
|
37
|
-
name: "fixture token map",
|
|
38
|
-
version: 1,
|
|
39
|
-
harnesses: ["pi"],
|
|
40
|
-
tools: { ...TOOL_MAP },
|
|
41
|
-
paths: {
|
|
42
|
-
plugin_root: { pi: "~/.agent-cortex/pi" },
|
|
43
|
-
skills_dir: { pi: { base: "plugin_root", relative: "skills" } },
|
|
44
|
-
agents_dir: { pi: { base: "plugin_root", relative: "agents" } },
|
|
45
|
-
},
|
|
46
|
-
...extra,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function makeFixture() {
|
|
51
|
-
const root = await mkdtemp(join(tmpdir(), "pi-installer-"));
|
|
52
|
-
const output = join(root, "out");
|
|
53
|
-
return {
|
|
54
|
-
root,
|
|
55
|
-
output,
|
|
56
|
-
cleanup: async () => rm(root, { recursive: true, force: true }),
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function writeFixture(fx, relPath, content) {
|
|
61
|
-
const p = join(fx.root, relPath);
|
|
62
|
-
await mkdir(dirname(p), { recursive: true });
|
|
63
|
-
await writeFile(p, content);
|
|
64
|
-
}
|
|
65
|
-
|
|
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 };
|
|
69
|
-
}
|
|
70
|
-
|
|
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
|
-
};
|
|
101
|
-
}
|
|
102
|
-
|
|
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
|
-
*/
|
|
107
|
-
async function seedPackage(fx, opts = {}) {
|
|
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");
|
|
113
|
-
for (const [name, def] of Object.entries(agents)) {
|
|
114
|
-
await writeFixture(fx, `agents/${name}/agent.md`, def.body ?? `# ${name}\nBody of ${name}.`);
|
|
115
|
-
await writeFixture(
|
|
116
|
-
fx,
|
|
117
|
-
`agents/${name}/pi/frontmatter.json`,
|
|
118
|
-
JSON.stringify(
|
|
119
|
-
def.frontmatter ?? {
|
|
120
|
-
name: `agent-cortex:${name}`,
|
|
121
|
-
description: `Description of ${name}`,
|
|
122
|
-
tools: ["bash", "view"],
|
|
123
|
-
argumentHint: `Hint for ${name}`,
|
|
124
|
-
},
|
|
125
|
-
),
|
|
126
|
-
);
|
|
127
|
-
for (const [section, content] of Object.entries(def.sections ?? {})) {
|
|
128
|
-
await writeFixture(fx, `agents/${name}/pi/${section}.md`, content);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
async function listFiles(dir) {
|
|
134
|
-
if (!(await pathExists(dir))) return [];
|
|
135
|
-
const out = [];
|
|
136
|
-
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
137
|
-
const p = join(dir, entry.name);
|
|
138
|
-
if (entry.isDirectory()) out.push(...(await listFiles(p)).map((f) => join(entry.name, f)));
|
|
139
|
-
else out.push(entry.name);
|
|
140
|
-
}
|
|
141
|
-
return out;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// ─── Agent composition ───────────────────────────────────────────────────────
|
|
145
|
-
|
|
146
|
-
describe("installPi — agent composition", () => {
|
|
147
|
-
it("writes one .agent.md per composable agent dir with pi frontmatter and no leftover tokens", async () => {
|
|
148
|
-
const fx = await makeFixture();
|
|
149
|
-
try {
|
|
150
|
-
await seedPackage(fx, {
|
|
151
|
-
agents: {
|
|
152
|
-
alpha: {
|
|
153
|
-
body: "# alpha\nUse {{TOOL:view}} to read.\n{{SECTION:polling}}\nRead {{PATH:skills_dir}}.",
|
|
154
|
-
sections: { polling: "Spawn with {{TOOL:task}}." },
|
|
155
|
-
frontmatter: {
|
|
156
|
-
name: "agent-cortex:alpha",
|
|
157
|
-
description: "Reads things.",
|
|
158
|
-
tools: ["bash", "view", "glob", "ask_user"],
|
|
159
|
-
argumentHint: "Read something",
|
|
160
|
-
},
|
|
161
|
-
},
|
|
162
|
-
},
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
const result = await installPi({ root: fx.root, output: fx.output, resolvePackage: fakeResolver({}).resolvePackage });
|
|
166
|
-
|
|
167
|
-
const file = join(fx.output, "agents", "alpha.agent.md");
|
|
168
|
-
assert.ok(await pathExists(file), "agent file written");
|
|
169
|
-
const content = await readFile(file, "utf-8");
|
|
170
|
-
assert.ok(content.startsWith("---\n"), "yaml frontmatter");
|
|
171
|
-
assert.match(content, /name: "agent-cortex:alpha"/);
|
|
172
|
-
assert.match(content, /description: "Reads things\."/);
|
|
173
|
-
assert.match(content, /tools: "bash, read, find"/, "tools translated to pi names, ask_user omitted");
|
|
174
|
-
assert.match(content, /argument-hint: "Read something"/);
|
|
175
|
-
assert.ok(content.includes("Use read to read."), "{{TOOL:view}} substituted");
|
|
176
|
-
assert.ok(content.includes("Spawn with task."), "{{SECTION:polling}} composed from pi/polling.md");
|
|
177
|
-
assert.ok(!/{{/.test(content), "no literal tokens remain");
|
|
178
|
-
assert.equal(result.agents.length, 1);
|
|
179
|
-
assert.deepEqual(result.agents[0], { name: "alpha", filePath: file });
|
|
180
|
-
} finally {
|
|
181
|
-
await fx.cleanup();
|
|
182
|
-
}
|
|
183
|
-
});
|
|
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
|
-
|
|
219
|
-
it("drops null-mapped tool tokens in prose and reports a warning", async () => {
|
|
220
|
-
const fx = await makeFixture();
|
|
221
|
-
try {
|
|
222
|
-
await seedPackage(fx, {
|
|
223
|
-
agents: {
|
|
224
|
-
alpha: { body: "# alpha\nAsk the user with {{TOOL:ask_user}}, then use {{TOOL:bash}}." },
|
|
225
|
-
},
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
const result = await installPi({ root: fx.root, output: fx.output, resolvePackage: fakeResolver({}).resolvePackage });
|
|
229
|
-
const content = await readFile(join(fx.output, "agents", "alpha.agent.md"), "utf-8");
|
|
230
|
-
|
|
231
|
-
assert.ok(!content.includes("ask_user"), "null-mapped token dropped from prose");
|
|
232
|
-
assert.ok(content.includes("use bash"), "mapped token substituted");
|
|
233
|
-
assert.ok(!/{{/.test(content));
|
|
234
|
-
assert.ok(result.warnings.some((w) => w.includes("{{TOOL:ask_user}}")), "drop reported as warning");
|
|
235
|
-
} finally {
|
|
236
|
-
await fx.cleanup();
|
|
237
|
-
}
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it("throws on an unknown tool token", async () => {
|
|
241
|
-
const fx = await makeFixture();
|
|
242
|
-
try {
|
|
243
|
-
await seedPackage(fx, { agents: { alpha: { body: "# alpha\nUse {{TOOL:nope}} now." } } });
|
|
244
|
-
await assert.rejects(
|
|
245
|
-
installPi({ root: fx.root, output: fx.output, resolvePackage: fakeResolver({}).resolvePackage }),
|
|
246
|
-
/unknown tool token {{TOOL:nope}}/,
|
|
247
|
-
);
|
|
248
|
-
} finally {
|
|
249
|
-
await fx.cleanup();
|
|
250
|
-
}
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it("throws when a section file referenced by {{SECTION:...}} is missing", async () => {
|
|
254
|
-
const fx = await makeFixture();
|
|
255
|
-
try {
|
|
256
|
-
await seedPackage(fx, { agents: { alpha: { body: "# alpha\n{{SECTION:missing}}" } } });
|
|
257
|
-
await assert.rejects(
|
|
258
|
-
installPi({ root: fx.root, output: fx.output, resolvePackage: fakeResolver({}).resolvePackage }),
|
|
259
|
-
/missing section "missing"/,
|
|
260
|
-
);
|
|
261
|
-
} finally {
|
|
262
|
-
await fx.cleanup();
|
|
263
|
-
}
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
it("resolves named and relative {{PATH:...}} tokens against the plugin root", async () => {
|
|
267
|
-
const fx = await makeFixture();
|
|
268
|
-
try {
|
|
269
|
-
await seedPackage(fx, {
|
|
270
|
-
agents: {
|
|
271
|
-
alpha: {
|
|
272
|
-
body: "# alpha\nNamed: {{PATH:skills_dir}}\nRelative: {{PATH:skills/x/SKILL.md}}",
|
|
273
|
-
},
|
|
274
|
-
},
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
const result = await installPi({ root: fx.root, output: fx.output, resolvePackage: fakeResolver({}).resolvePackage });
|
|
278
|
-
const content = await readFile(join(fx.output, "agents", "alpha.agent.md"), "utf-8");
|
|
279
|
-
|
|
280
|
-
assert.ok(content.includes("Named: ~/.agent-cortex/pi/skills"), "named path resolves through token-map paths table");
|
|
281
|
-
assert.ok(
|
|
282
|
-
content.includes("Relative: ~/.agent-cortex/pi/skills/x/SKILL.md"),
|
|
283
|
-
"relative path resolves against plugin root",
|
|
284
|
-
);
|
|
285
|
-
assert.equal(result.warnings.length, 0);
|
|
286
|
-
} finally {
|
|
287
|
-
await fx.cleanup();
|
|
288
|
-
}
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
it("honours the --plugin-root override for path resolution", async () => {
|
|
292
|
-
const fx = await makeFixture();
|
|
293
|
-
try {
|
|
294
|
-
await seedPackage(fx, {
|
|
295
|
-
agents: {
|
|
296
|
-
alpha: { body: "# alpha\n{{PATH:skills/x/SKILL.md}}" },
|
|
297
|
-
},
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
const pluginRoot = join(fx.root, "checkout");
|
|
301
|
-
await installPi({
|
|
302
|
-
root: fx.root,
|
|
303
|
-
output: fx.output,
|
|
304
|
-
pluginRoot,
|
|
305
|
-
resolvePackage: fakeResolver({}).resolvePackage,
|
|
306
|
-
});
|
|
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");
|
|
309
|
-
} finally {
|
|
310
|
-
await fx.cleanup();
|
|
311
|
-
}
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
it("rejects token-map.json versions newer than the implemented contract", async () => {
|
|
315
|
-
const fx = await makeFixture();
|
|
316
|
-
try {
|
|
317
|
-
await seedPackage(fx, {
|
|
318
|
-
tokenMapVersion: 99,
|
|
319
|
-
agents: { alpha: { body: "# alpha" } },
|
|
320
|
-
});
|
|
321
|
-
await assert.rejects(
|
|
322
|
-
installPi({ root: fx.root, output: fx.output, resolvePackage: fakeResolver({}).resolvePackage }),
|
|
323
|
-
/version 99/,
|
|
324
|
-
);
|
|
325
|
-
} finally {
|
|
326
|
-
await fx.cleanup();
|
|
327
|
-
}
|
|
328
|
-
});
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
// ─── Skills ──────────────────────────────────────────────────────────────────
|
|
332
|
-
|
|
333
|
-
describe("installPi — skills", () => {
|
|
334
|
-
it("copies skills into <output>/skills preserving the grouped layout with substituted tokens", async () => {
|
|
335
|
-
const fx = await makeFixture();
|
|
336
|
-
try {
|
|
337
|
-
await seedPackage(fx, {
|
|
338
|
-
agents: { alpha: { body: "# alpha" } },
|
|
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");
|
|
346
|
-
|
|
347
|
-
await installPi({ root: fx.root, output: fx.output, resolvePackage: fakeResolver({}).resolvePackage });
|
|
348
|
-
|
|
349
|
-
const skillFile = join(fx.output, "skills", "engineering", "echo", "SKILL.md");
|
|
350
|
-
assert.ok(await pathExists(skillFile), "skill copied");
|
|
351
|
-
const content = await readFile(skillFile, "utf-8");
|
|
352
|
-
assert.ok(content.includes("description: Echoes read."), "token substituted in skill frontmatter");
|
|
353
|
-
assert.ok(content.includes("Use read to read and for help."), "null-mapped {{TOOL:skill}} dropped");
|
|
354
|
-
assert.ok(
|
|
355
|
-
content.includes("Open ~/.agent-cortex/pi/skills/echo/SKILL.md."),
|
|
356
|
-
"relative path resolved against plugin root",
|
|
357
|
-
);
|
|
358
|
-
assert.ok(!/{{/.test(content), "no literal tokens remain in skill");
|
|
359
|
-
|
|
360
|
-
const script = await readFile(join(fx.output, "skills", "engineering", "echo", "scripts", "run.sh"), "utf-8");
|
|
361
|
-
assert.equal(script, "#!/bin/sh\necho hi\n", "non-md files copied verbatim");
|
|
362
|
-
} finally {
|
|
363
|
-
await fx.cleanup();
|
|
364
|
-
}
|
|
365
|
-
});
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
// ─── Extensions ──────────────────────────────────────────────────────────────
|
|
369
|
-
|
|
370
|
-
describe("installPi — extensions", () => {
|
|
371
|
-
it("copies the extensions tree into the staged package", async () => {
|
|
372
|
-
const fx = await makeFixture();
|
|
373
|
-
try {
|
|
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");
|
|
377
|
-
|
|
378
|
-
const result = await installPi({ root: fx.root, output: fx.output, resolvePackage: fakeResolver({}).resolvePackage });
|
|
379
|
-
|
|
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")));
|
|
383
|
-
} finally {
|
|
384
|
-
await fx.cleanup();
|
|
385
|
-
}
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
it("never copies node_modules or symlinks out of the extensions tree", async () => {
|
|
389
|
-
const fx = await makeFixture();
|
|
390
|
-
try {
|
|
391
|
-
await seedPackage(fx, { agents: { alpha: { body: "# alpha" } } });
|
|
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);
|
|
400
|
-
} finally {
|
|
401
|
-
await fx.cleanup();
|
|
402
|
-
}
|
|
403
|
-
});
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
// ─── Staged package manifest ─────────────────────────────────────────────────
|
|
407
|
-
|
|
408
|
-
describe("installPi — staged package manifest", () => {
|
|
409
|
-
it("writes a pi package manifest for the staged resources", async () => {
|
|
410
|
-
const fx = await makeFixture();
|
|
411
|
-
try {
|
|
412
|
-
await seedPackage(fx, { agents: { alpha: { body: "# alpha" } } });
|
|
413
|
-
|
|
414
|
-
const result = await installPi({ root: fx.root, output: fx.output, resolvePackage: fakeResolver({}).resolvePackage });
|
|
415
|
-
|
|
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"));
|
|
422
|
-
} finally {
|
|
423
|
-
await fx.cleanup();
|
|
424
|
-
}
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
it("ships token-map.json so the staged agent-modes extension keeps its source of truth", async () => {
|
|
428
|
-
const fx = await makeFixture();
|
|
429
|
-
try {
|
|
430
|
-
await seedPackage(fx, { agents: { alpha: { body: "# alpha" } } });
|
|
431
|
-
|
|
432
|
-
await installPi({ root: fx.root, output: fx.output, resolvePackage: fakeResolver({}).resolvePackage });
|
|
433
|
-
|
|
434
|
-
const staged = JSON.parse(await readFile(join(fx.output, "token-map.json"), "utf-8"));
|
|
435
|
-
assert.deepEqual(staged, tokenMap(), "token map copied verbatim");
|
|
436
|
-
} finally {
|
|
437
|
-
await fx.cleanup();
|
|
438
|
-
}
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
it("refuses to stage into the package root, where it would clear the sources", async () => {
|
|
442
|
-
const fx = await makeFixture();
|
|
443
|
-
try {
|
|
444
|
-
await seedPackage(fx, { agents: { alpha: { body: "# alpha" } } });
|
|
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
|
-
});
|
|
455
|
-
|
|
456
|
-
// ─── Bundled third-party packages ────────────────────────────────────────────
|
|
457
|
-
|
|
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"]);
|
|
495
|
-
} finally {
|
|
496
|
-
await fx.cleanup();
|
|
497
|
-
}
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
it("copies the transitive closure, resolving each dependency from its dependent", async () => {
|
|
501
|
-
const fx = await makeFixture();
|
|
502
|
-
try {
|
|
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 });
|
|
519
|
-
|
|
520
|
-
const result = await installPi({ root: fx.root, output: fx.output, resolvePackage: resolver.resolvePackage });
|
|
521
|
-
|
|
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"]);
|
|
532
|
-
} finally {
|
|
533
|
-
await fx.cleanup();
|
|
534
|
-
}
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
it("leaves ordinary dependencies out of node_modules and the manifest", async () => {
|
|
538
|
-
const fx = await makeFixture();
|
|
539
|
-
try {
|
|
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
|
-
});
|
|
546
|
-
|
|
547
|
-
const result = await installPi({
|
|
548
|
-
root: fx.root,
|
|
549
|
-
output: fx.output,
|
|
550
|
-
resolvePackage: fakeResolver({ "plain-util": util }).resolvePackage,
|
|
551
|
-
});
|
|
552
|
-
|
|
553
|
-
assert.deepEqual(result.packages.staged, []);
|
|
554
|
-
assert.equal(await pathExists(join(fx.output, "node_modules")), false);
|
|
555
|
-
} finally {
|
|
556
|
-
await fx.cleanup();
|
|
557
|
-
}
|
|
558
|
-
});
|
|
559
|
-
|
|
560
|
-
it("fails the install when a declared dependency cannot be resolved, leaving the previous staging intact", async () => {
|
|
561
|
-
const fx = await makeFixture();
|
|
562
|
-
try {
|
|
563
|
-
await seedPackage(fx, { agents: { alpha: { body: "# alpha" } } });
|
|
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");
|
|
566
|
-
|
|
567
|
-
await writeFixture(fx, "package.json", JSON.stringify(defaultPackageJson("9.9.9", { "pi-questions": "^1.0.0" })));
|
|
568
|
-
|
|
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);
|
|
574
|
-
} finally {
|
|
575
|
-
await fx.cleanup();
|
|
576
|
-
}
|
|
577
|
-
});
|
|
578
|
-
|
|
579
|
-
it("fails when a bundled package's declared resource is missing from its published files", async () => {
|
|
580
|
-
const fx = await makeFixture();
|
|
581
|
-
try {
|
|
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
|
-
});
|
|
591
|
-
|
|
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
|
-
);
|
|
600
|
-
} finally {
|
|
601
|
-
await fx.cleanup();
|
|
602
|
-
}
|
|
603
|
-
});
|
|
604
|
-
});
|
|
605
|
-
|
|
606
|
-
// ─── Idempotency ─────────────────────────────────────────────────────────────
|
|
607
|
-
|
|
608
|
-
describe("installPi — re-staging", () => {
|
|
609
|
-
it("removes artifacts of a previous run instead of accumulating them", async () => {
|
|
610
|
-
const fx = await makeFixture();
|
|
611
|
-
try {
|
|
612
|
-
await seedPackage(fx, { agents: { alpha: { body: "# alpha" } } });
|
|
613
|
-
await installPi({ root: fx.root, output: fx.output, resolvePackage: fakeResolver({}).resolvePackage });
|
|
614
|
-
const first = (await listFiles(fx.output)).sort();
|
|
615
|
-
|
|
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 });
|
|
618
|
-
|
|
619
|
-
assert.deepEqual((await listFiles(fx.output)).sort(), first, "same tree, no stale copies");
|
|
620
|
-
} finally {
|
|
621
|
-
await fx.cleanup();
|
|
622
|
-
}
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
it("drops a skill that no longer exists in the sources", async () => {
|
|
626
|
-
const fx = await makeFixture();
|
|
627
|
-
try {
|
|
628
|
-
await seedPackage(fx, { agents: { alpha: { body: "# alpha" } } });
|
|
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")));
|
|
632
|
-
|
|
633
|
-
await rm(join(fx.root, "skills", "engineering", "retired"), { recursive: true });
|
|
634
|
-
await installPi({ root: fx.root, output: fx.output, resolvePackage: fakeResolver({}).resolvePackage });
|
|
635
|
-
|
|
636
|
-
assert.equal(await pathExists(join(fx.output, "skills", "engineering", "retired")), false);
|
|
637
|
-
} finally {
|
|
638
|
-
await fx.cleanup();
|
|
639
|
-
}
|
|
640
|
-
});
|
|
641
|
-
});
|
|
642
|
-
|
|
643
|
-
// ─── Output / dry-run ────────────────────────────────────────────────────────
|
|
644
|
-
|
|
645
|
-
describe("installPi — output & dry-run", () => {
|
|
646
|
-
it("dry-run writes nothing but reports the planned output", async () => {
|
|
647
|
-
const fx = await makeFixture();
|
|
648
|
-
try {
|
|
649
|
-
await seedPackage(fx, {
|
|
650
|
-
agents: { alpha: { body: "# alpha" } },
|
|
651
|
-
});
|
|
652
|
-
|
|
653
|
-
const result = await installPi({
|
|
654
|
-
root: fx.root,
|
|
655
|
-
output: fx.output,
|
|
656
|
-
dryRun: true,
|
|
657
|
-
resolvePackage: fakeResolver({}).resolvePackage,
|
|
658
|
-
});
|
|
659
|
-
|
|
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));
|
|
667
|
-
} finally {
|
|
668
|
-
await fx.cleanup();
|
|
669
|
-
}
|
|
670
|
-
});
|
|
671
|
-
|
|
672
|
-
it("creates the output directory hierarchy when needed", async () => {
|
|
673
|
-
const fx = await makeFixture();
|
|
674
|
-
try {
|
|
675
|
-
await seedPackage(fx, { agents: { alpha: { body: "# alpha" } } });
|
|
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")));
|
|
679
|
-
} finally {
|
|
680
|
-
await fx.cleanup();
|
|
681
|
-
}
|
|
682
|
-
});
|
|
683
|
-
});
|
|
684
|
-
|
|
685
|
-
// ─── Integration against the real repo ───────────────────────────────────────
|
|
686
|
-
|
|
687
|
-
describe("installPi — real repo integration", () => {
|
|
688
|
-
it("installs every canonical agent with pi tool names and no leftover tokens", async () => {
|
|
689
|
-
const fx = await makeFixture();
|
|
690
|
-
try {
|
|
691
|
-
const result = await installPi({ root: REPO_ROOT, output: fx.output });
|
|
692
|
-
|
|
693
|
-
const names = result.agents.map((a) => a.name).sort();
|
|
694
|
-
assert.deepEqual(names, ["plan", "ralph", "ralph-plan", "strategy"]);
|
|
695
|
-
|
|
696
|
-
// ralph pi frontmatter: ["bash", "view", "rg", "glob", "task", "read_agent", "wait_for_agents"]
|
|
697
|
-
const ralph = await readFile(join(fx.output, "agents", "ralph.agent.md"), "utf-8");
|
|
698
|
-
assert.match(ralph, /name: "agent-cortex:ralph"/);
|
|
699
|
-
assert.match(ralph, /tools: "bash, read, grep, find, task, read_agent, wait_for_agents"/);
|
|
700
|
-
assert.ok(ralph.includes("## Spawning subagents (PI)"), "{{SECTION:polling}} composed from pi/polling.md");
|
|
701
|
-
const bodyTokens = ralph.split("---\n").slice(2).join("---\n");
|
|
702
|
-
assert.ok(!/{{/.test(bodyTokens), "no literal tokens in ralph body");
|
|
703
|
-
|
|
704
|
-
// plan body carries the relative {{PATH:...}} resolved against the plugin root.
|
|
705
|
-
// Tool list preserves authoring order (composer translateToolList, unlike the
|
|
706
|
-
// runtime agent-modes translateTools which sorts): ask_user → ask_questions,
|
|
707
|
-
// skill → read (last position).
|
|
708
|
-
const plan = await readFile(join(fx.output, "agents", "plan.agent.md"), "utf-8");
|
|
709
|
-
assert.match(plan, /tools: "bash, read, edit, write, grep, find, ask_questions, task, read_agent, read"/);
|
|
710
|
-
assert.ok(
|
|
711
|
-
plan.includes("~/.agent-cortex/pi/skills/workflow/plan/SKILL.md"),
|
|
712
|
-
"plan PATH token resolved against token-map plugin root",
|
|
713
|
-
);
|
|
714
|
-
|
|
715
|
-
// ralph-plan prose {{TOOL:ask_user}} now substitutes to ask_questions
|
|
716
|
-
const ralphPlan = await readFile(join(fx.output, "agents", "ralph-plan.agent.md"), "utf-8");
|
|
717
|
-
assert.ok(!ralphPlan.includes("ask_user"), "ask_user never appears in ralph-plan body");
|
|
718
|
-
assert.ok(ralphPlan.includes("ask_questions"), "{{TOOL:ask_user}} substituted to ask_questions");
|
|
719
|
-
assert.ok(
|
|
720
|
-
!result.warnings.some((w) => w.includes("ask_user")),
|
|
721
|
-
"no null-tool warning for ralph-plan (ask_user now maps to ask_questions)",
|
|
722
|
-
);
|
|
723
|
-
|
|
724
|
-
// strategy: web_fetch → fetch_content; ask_user → ask_questions; skill → read
|
|
725
|
-
// (authoring order preserved: bash, view→read, rg→grep, glob→find, ask_user→
|
|
726
|
-
// ask_questions, web_fetch→fetch_content, skill→read, edit, create)
|
|
727
|
-
const strategy = await readFile(join(fx.output, "agents", "strategy.agent.md"), "utf-8");
|
|
728
|
-
assert.match(strategy, /tools: "bash, read, grep, find, ask_questions, fetch_content, read, edit, write"/);
|
|
729
|
-
} finally {
|
|
730
|
-
await fx.cleanup();
|
|
731
|
-
}
|
|
732
|
-
});
|
|
733
|
-
|
|
734
|
-
it("substitutes tokens in every installed skill markdown", async () => {
|
|
735
|
-
const fx = await makeFixture();
|
|
736
|
-
try {
|
|
737
|
-
const result = await installPi({ root: REPO_ROOT, output: fx.output });
|
|
738
|
-
assert.ok(result.skills.skills > 20, `expected the full skill tree, got ${result.skills.skills}`);
|
|
739
|
-
|
|
740
|
-
const files = (await listFiles(join(fx.output, "skills"))).filter((f) => f.endsWith(".md"));
|
|
741
|
-
assert.ok(files.length > 0);
|
|
742
|
-
for (const f of files) {
|
|
743
|
-
const content = await readFile(join(fx.output, "skills", f), "utf-8");
|
|
744
|
-
assert.ok(!/{{/.test(content), `no literal tokens in skill ${f}`);
|
|
745
|
-
}
|
|
746
|
-
} finally {
|
|
747
|
-
await fx.cleanup();
|
|
748
|
-
}
|
|
749
|
-
});
|
|
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
|
-
});
|