@jaybeeuu/agent-cortex 1.37.1 → 1.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/.agents/skills/style-versioning/SKILL.md +9 -8
  2. package/.github/workflows/ci.yml +190 -8
  3. package/AGENTS.md +27 -6
  4. package/CHANGELOG.md +72 -0
  5. package/README.md +156 -39
  6. package/agents/README.md +49 -13
  7. package/agents/plan/agent.md +51 -0
  8. package/agents/plan/claude/frontmatter.json +6 -0
  9. package/agents/plan/copilot/frontmatter.json +6 -0
  10. package/agents/plan/pi/frontmatter.json +6 -0
  11. package/agents/plan.agent.md +3 -0
  12. package/agents/ralph/agent.md +262 -0
  13. package/agents/ralph/claude/frontmatter.json +6 -0
  14. package/agents/ralph/claude/polling.md +31 -0
  15. package/agents/ralph/copilot/frontmatter.json +6 -0
  16. package/agents/ralph/copilot/polling.md +62 -0
  17. package/agents/ralph/pi/frontmatter.json +6 -0
  18. package/agents/ralph/pi/polling.md +74 -0
  19. package/agents/ralph-plan/agent.md +114 -0
  20. package/agents/ralph-plan/claude/frontmatter.json +6 -0
  21. package/agents/ralph-plan/copilot/frontmatter.json +6 -0
  22. package/agents/ralph-plan/pi/frontmatter.json +6 -0
  23. package/agents/ralph-plan.agent.md +6 -3
  24. package/agents/ralph.agent.md +83 -57
  25. package/agents/strategy/agent.md +104 -0
  26. package/agents/strategy/claude/frontmatter.json +6 -0
  27. package/agents/strategy/copilot/frontmatter.json +6 -0
  28. package/agents/strategy/pi/frontmatter.json +6 -0
  29. package/agents/strategy.agent.md +3 -0
  30. package/bin/agent-cortex.mjs +76 -1
  31. package/bin/installers/claude.mjs +376 -0
  32. package/bin/installers/copilot.mjs +129 -0
  33. package/bin/installers/pi.mjs +197 -0
  34. package/claude/.claude-plugin/plugin.json +1 -1
  35. package/claude/agents/plan.md +3 -1
  36. package/claude/agents/ralph-plan.md +6 -4
  37. package/claude/agents/strategy.md +3 -1
  38. package/claude/hooks/scripts/notify.mjs +54 -0
  39. package/claude/hooks.json +15 -0
  40. package/docs/claude-hooks.md +115 -0
  41. package/docs/ideas/auto-model-selection.md +75 -0
  42. package/docs/ideas/extension-manifests.md +75 -0
  43. package/docs/ideas/improve-pi-system-prompt.md +34 -0
  44. package/docs/ideas/model-selector.md +40 -0
  45. package/docs/ideas/pi-task-memory.md +34 -0
  46. package/docs/ideas/qa-extension.md +43 -0
  47. package/docs/ideas/record-ideas-as-beads.md +115 -0
  48. package/docs/ideas/subagent-visibility.md +72 -0
  49. package/docs/ideas/task-session-context.md +89 -0
  50. package/docs/ideas/unified-ci-pipeline.md +28 -0
  51. package/docs/research/skill-design-research.md +4 -4
  52. package/docs/research/skill-improvements-analysis.md +1 -1
  53. package/docs/skills/skill-anatomy.md +47 -2
  54. package/docs/technical-direction/migration-execution-labels.md +132 -0
  55. package/extensions/agent-modes/README.md +54 -0
  56. package/extensions/agent-modes/discover.test.ts +241 -0
  57. package/extensions/agent-modes/discover.ts +458 -0
  58. package/extensions/agent-modes/index.ts +13 -224
  59. package/extensions/agent-modes/node_modules/.bin/pi +21 -0
  60. package/extensions/agent-modes/node_modules/.bin/tsc +21 -0
  61. package/extensions/agent-modes/node_modules/.bin/tsserver +21 -0
  62. package/extensions/agent-modes/node_modules/.bin/tsx +21 -0
  63. package/extensions/agent-modes/package.json +20 -0
  64. package/extensions/agent-modes/tsconfig.json +12 -0
  65. package/extensions/subagent/README.md +19 -0
  66. package/extensions/subagent/index.ts +131 -45
  67. package/extensions/subagent/node_modules/.bin/pi +21 -0
  68. package/extensions/subagent/node_modules/.bin/pi-ai +21 -0
  69. package/extensions/subagent/node_modules/.bin/tsc +21 -0
  70. package/extensions/subagent/node_modules/.bin/tsserver +21 -0
  71. package/extensions/subagent/node_modules/.bin/tsx +21 -0
  72. package/extensions/subagent/package.json +23 -0
  73. package/extensions/subagent/tsconfig.json +12 -0
  74. package/extensions/subagent/wait.test.ts +202 -0
  75. package/extensions/subagent/wait.ts +118 -0
  76. package/hooks/claude/hooks.json +38 -0
  77. package/hooks/claude/scripts/notify.mjs +54 -0
  78. package/lib/cli.mjs +68 -3
  79. package/package.json +6 -4
  80. package/plugin.json +1 -1
  81. package/pnpm-workspace.yaml +1 -1
  82. package/scripts/build-copilot-agents.mjs +30 -0
  83. package/scripts/lib/compose-agent.mjs +241 -0
  84. package/skills/engineering/design-an-interface/SKILL.md +78 -58
  85. package/skills/engineering/grill-with-docs/REFERENCE.md +78 -0
  86. package/skills/engineering/grill-with-docs/SKILL.md +66 -63
  87. package/skills/engineering/improve-codebase-architecture/SKILL.md +66 -48
  88. package/skills/engineering/technical-direction/FORMAT.md +38 -0
  89. package/skills/engineering/technical-direction/SKILL.md +70 -77
  90. package/skills/planning/classify-bead/SKILL.md +59 -35
  91. package/skills/planning/create-task/REFERENCE.md +40 -0
  92. package/skills/planning/create-task/SKILL.md +70 -66
  93. package/skills/planning/create-task/scripts/create-chores.ts +6 -1
  94. package/skills/planning/create-task/scripts/node_modules/.bin/tsc +21 -0
  95. package/skills/planning/create-task/scripts/node_modules/.bin/tsserver +21 -0
  96. package/skills/planning/create-task/scripts/node_modules/.bin/tsx +21 -0
  97. package/skills/planning/prd-to-tasks/REFERENCE.md +66 -0
  98. package/skills/planning/prd-to-tasks/SKILL.md +86 -170
  99. package/skills/planning/record-idea/FORMAT.md +44 -0
  100. package/skills/planning/record-idea/SKILL.md +70 -53
  101. package/skills/planning/request-refactor-plan/FORMAT.md +45 -0
  102. package/skills/planning/request-refactor-plan/SKILL.md +57 -46
  103. package/skills/planning/write-a-prd/FORMAT.md +54 -0
  104. package/skills/planning/write-a-prd/SKILL.md +49 -48
  105. package/skills/planning/write-a-ticket/SKILL.md +68 -53
  106. package/skills/productivity/handoff/SKILL.md +12 -0
  107. package/skills/productivity/init-beads/SKILL.md +48 -40
  108. package/skills/productivity/validate-agent-dir/scripts/node_modules/.bin/tsc +21 -0
  109. package/skills/productivity/validate-agent-dir/scripts/node_modules/.bin/tsserver +21 -0
  110. package/skills/productivity/validate-agent-dir/scripts/node_modules/.bin/tsx +21 -0
  111. package/skills/productivity/write-a-skill/SKILL.md +2 -1
  112. package/skills/review/maintain-agent-docs/SKILL.md +71 -51
  113. package/skills/workflow/hitl-collab/SKILL.md +86 -17
  114. package/skills/workflow/ralph/REFERENCE.md +24 -19
  115. package/skills/workflow/ralph/SKILL.md +88 -74
  116. package/skills/workflow/run-pipeline-stage/REFERENCE.md +22 -0
  117. package/skills/workflow/run-pipeline-stage/SKILL.md +91 -93
  118. package/skills/workflow/run-pipeline-stage/scripts/generate-progress.test.ts +6 -107
  119. package/skills/workflow/run-pipeline-stage/scripts/generate-progress.ts +4 -2
  120. package/skills/workflow/run-pipeline-stage/scripts/node_modules/.bin/tsc +21 -0
  121. package/skills/workflow/run-pipeline-stage/scripts/node_modules/.bin/tsserver +21 -0
  122. package/skills/workflow/run-pipeline-stage/scripts/node_modules/.bin/tsx +21 -0
  123. package/skills/workflow/using-agent-skills/SKILL.md +122 -0
  124. package/test/claude-installer.test.mjs +248 -0
  125. package/test/cli.test.mjs +115 -2
  126. package/test/copilot-installer.test.mjs +298 -0
  127. package/test/installer.test.mjs +179 -0
  128. package/test/notify-hook.test.mjs +63 -0
  129. package/test/pi-installer.test.mjs +371 -0
  130. package/token-map.README.md +130 -0
  131. package/token-map.json +106 -0
  132. package/.github/workflows/ci.test.ts +0 -217
  133. package/.github/workflows/release.yml +0 -58
  134. package/scripts/build-claude-agents.mjs +0 -179
  135. package/scripts/publish-release.sh +0 -32
@@ -0,0 +1,371 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync, existsSync, readdirSync } from "node:fs";
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/, token-map.json).
11
+ const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
12
+
13
+ // ─── Fixtures ────────────────────────────────────────────────────────────────
14
+
15
+ const TOOL_MAP = {
16
+ bash: { pi: "bash" },
17
+ view: { pi: "read" },
18
+ create: { pi: "write" },
19
+ glob: { pi: "find" },
20
+ ask_user: { pi: null },
21
+ skill: { pi: null },
22
+ task: { pi: "task" },
23
+ };
24
+
25
+ function tokenMap(extra = {}) {
26
+ return {
27
+ name: "fixture token map",
28
+ version: 1,
29
+ harnesses: ["pi"],
30
+ tools: { ...TOOL_MAP },
31
+ paths: {
32
+ plugin_root: { pi: "~/.pi/agent/npm/node_modules/@jaybeeuu/agent-cortex" },
33
+ skills_dir: { pi: { base: "plugin_root", relative: "skills" } },
34
+ agents_dir: { pi: { base: "plugin_root", relative: "agents" } },
35
+ },
36
+ ...extra,
37
+ };
38
+ }
39
+
40
+ function makeFixture() {
41
+ const root = mkdtempSync(join(tmpdir(), "pi-installer-"));
42
+ const output = join(root, "out");
43
+ return {
44
+ root,
45
+ output,
46
+ cleanup: () => rmSync(root, { recursive: true, force: true }),
47
+ };
48
+ }
49
+
50
+ function writeFixture(fx, relPath, content) {
51
+ const p = join(fx.root, relPath);
52
+ mkdirSync(dirname(p), { recursive: true });
53
+ writeFileSync(p, content);
54
+ }
55
+
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 } : {})));
60
+ for (const [name, def] of Object.entries(agents)) {
61
+ writeFixture(fx, `agents/${name}/agent.md`, def.body ?? `# ${name}\nBody of ${name}.`);
62
+ writeFixture(
63
+ fx,
64
+ `agents/${name}/pi/frontmatter.json`,
65
+ JSON.stringify(
66
+ def.frontmatter ?? {
67
+ name: `agent-cortex:${name}`,
68
+ description: `Description of ${name}`,
69
+ tools: ["bash", "view"],
70
+ argumentHint: `Hint for ${name}`,
71
+ },
72
+ ),
73
+ );
74
+ for (const [section, content] of Object.entries(def.sections ?? {})) {
75
+ writeFixture(fx, `agents/${name}/pi/${section}.md`, content);
76
+ }
77
+ }
78
+ for (const [path, content] of Object.entries(skills)) {
79
+ writeFixture(fx, `skills/${path}`, content);
80
+ }
81
+ }
82
+
83
+ function listFiles(dir) {
84
+ if (!existsSync(dir)) return [];
85
+ const out = [];
86
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
87
+ const p = join(dir, entry.name);
88
+ if (entry.isDirectory()) out.push(...listFiles(p).map((f) => join(entry.name, f)));
89
+ else out.push(entry.name);
90
+ }
91
+ return out;
92
+ }
93
+
94
+ // ─── Agent composition ───────────────────────────────────────────────────────
95
+
96
+ 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();
99
+ try {
100
+ seedPackage(fx, {
101
+ agents: {
102
+ alpha: {
103
+ body: "# alpha\nUse {{TOOL:view}} to read.\n{{SECTION:polling}}\nRead {{PATH:skills_dir}}.",
104
+ sections: { polling: "Spawn with {{TOOL:task}}." },
105
+ frontmatter: {
106
+ name: "agent-cortex:alpha",
107
+ description: "Reads things.",
108
+ tools: ["bash", "view", "glob", "ask_user"],
109
+ argumentHint: "Read something",
110
+ },
111
+ },
112
+ },
113
+ });
114
+
115
+ const result = installPi({ root: fx.root, output: fx.output });
116
+
117
+ const file = join(fx.output, "agents", "alpha.agent.md");
118
+ assert.ok(existsSync(file), "agent file written");
119
+ const content = readFileSync(file, "utf-8");
120
+ assert.ok(content.startsWith("---\n"), "yaml frontmatter");
121
+ assert.match(content, /name: "agent-cortex:alpha"/);
122
+ assert.match(content, /description: "Reads things\."/);
123
+ assert.match(content, /tools: "bash read find"/, "tools translated to pi names, ask_user omitted");
124
+ assert.match(content, /argument-hint: "Read something"/);
125
+ assert.ok(content.includes("Use read to read."), "{{TOOL:view}} substituted");
126
+ assert.ok(content.includes("Spawn with task."), "{{SECTION:polling}} composed from pi/polling.md");
127
+ assert.ok(!/{{/.test(content), "no literal tokens remain");
128
+ assert.equal(result.agents.length, 1);
129
+ assert.deepEqual(result.agents[0], { name: "alpha", filePath: file });
130
+ } finally {
131
+ fx.cleanup();
132
+ }
133
+ });
134
+
135
+ it("drops null-mapped tool tokens in prose and reports a warning", () => {
136
+ const fx = makeFixture();
137
+ try {
138
+ seedPackage(fx, {
139
+ agents: {
140
+ alpha: { body: "# alpha\nAsk the user with {{TOOL:ask_user}}, then use {{TOOL:bash}}." },
141
+ },
142
+ });
143
+
144
+ const result = installPi({ root: fx.root, output: fx.output });
145
+ const content = readFileSync(join(fx.output, "agents", "alpha.agent.md"), "utf-8");
146
+
147
+ assert.ok(!content.includes("ask_user"), "null-mapped token dropped from prose");
148
+ assert.ok(content.includes("use bash"), "mapped token substituted");
149
+ assert.ok(!/{{/.test(content));
150
+ assert.ok(result.warnings.some((w) => w.includes("{{TOOL:ask_user}}")), "drop reported as warning");
151
+ } finally {
152
+ fx.cleanup();
153
+ }
154
+ });
155
+
156
+ it("throws on an unknown tool token", () => {
157
+ const fx = makeFixture();
158
+ try {
159
+ seedPackage(fx, { agents: { alpha: { body: "# alpha\nUse {{TOOL:nope}} now." } } });
160
+ assert.throws(() => installPi({ root: fx.root, output: fx.output }), /unknown tool token {{TOOL:nope}}/);
161
+ } finally {
162
+ fx.cleanup();
163
+ }
164
+ });
165
+
166
+ it("throws when a section file referenced by {{SECTION:...}} is missing", () => {
167
+ const fx = makeFixture();
168
+ try {
169
+ seedPackage(fx, { agents: { alpha: { body: "# alpha\n{{SECTION:missing}}" } } });
170
+ assert.throws(() => installPi({ root: fx.root, output: fx.output }), /missing section "missing"/);
171
+ } finally {
172
+ fx.cleanup();
173
+ }
174
+ });
175
+
176
+ it("resolves named and relative {{PATH:...}} tokens against the plugin root", () => {
177
+ const fx = makeFixture();
178
+ try {
179
+ seedPackage(fx, {
180
+ agents: {
181
+ alpha: {
182
+ body: "# alpha\nNamed: {{PATH:skills_dir}}\nRelative: {{PATH:skills/x/SKILL.md}}",
183
+ },
184
+ },
185
+ });
186
+
187
+ const result = installPi({ root: fx.root, output: fx.output });
188
+ const content = readFileSync(join(fx.output, "agents", "alpha.agent.md"), "utf-8");
189
+
190
+ assert.ok(
191
+ content.includes("Named: ~/.pi/agent/npm/node_modules/@jaybeeuu/agent-cortex/skills"),
192
+ "named path resolves through token-map paths table",
193
+ );
194
+ assert.ok(
195
+ content.includes("Relative: ~/.pi/agent/npm/node_modules/@jaybeeuu/agent-cortex/skills/x/SKILL.md"),
196
+ "relative path resolves against plugin root",
197
+ );
198
+ assert.equal(result.warnings.length, 0);
199
+ } finally {
200
+ fx.cleanup();
201
+ }
202
+ });
203
+
204
+ it("honours the --plugin-root override for path resolution", () => {
205
+ const fx = makeFixture();
206
+ try {
207
+ seedPackage(fx, {
208
+ agents: {
209
+ alpha: { body: "# alpha\n{{PATH:skills/x/SKILL.md}}" },
210
+ },
211
+ });
212
+
213
+ const pluginRoot = join(fx.root, "checkout");
214
+ installPi({ root: fx.root, output: fx.output, pluginRoot });
215
+ const content = readFileSync(join(fx.output, "agents", "alpha.agent.md"), "utf-8");
216
+ assert.ok(content.includes(join(pluginRoot, "skills", "x", "SKILL.md")), "plugin root override used");
217
+ } finally {
218
+ fx.cleanup();
219
+ }
220
+ });
221
+
222
+ it("rejects token-map.json versions newer than the implemented contract", () => {
223
+ const fx = makeFixture();
224
+ try {
225
+ seedPackage(fx, {
226
+ version: 99,
227
+ agents: { alpha: { body: "# alpha" } },
228
+ });
229
+ assert.throws(() => installPi({ root: fx.root, output: fx.output }), /version 99/);
230
+ } finally {
231
+ fx.cleanup();
232
+ }
233
+ });
234
+ });
235
+
236
+ // ─── Skills ──────────────────────────────────────────────────────────────────
237
+
238
+ describe("installPi — skills", () => {
239
+ it("copies skills into <output>/skills preserving the grouped layout with substituted tokens", () => {
240
+ const fx = makeFixture();
241
+ try {
242
+ seedPackage(fx, {
243
+ agents: { alpha: { body: "# alpha" } },
244
+ skills: {
245
+ "engineering/echo/SKILL.md":
246
+ "---\nname: echo\ndescription: Echoes {{TOOL:view}}.\n---\n\nUse {{TOOL:view}} to read and {{TOOL:skill}} for help.\nOpen {{PATH:skills/echo/SKILL.md}}.",
247
+ "engineering/echo/scripts/run.sh": "#!/bin/sh\necho hi\n",
248
+ },
249
+ });
250
+
251
+ installPi({ root: fx.root, output: fx.output });
252
+
253
+ const skillFile = join(fx.output, "skills", "engineering", "echo", "SKILL.md");
254
+ assert.ok(existsSync(skillFile), "skill copied");
255
+ const content = readFileSync(skillFile, "utf-8");
256
+ assert.ok(content.includes("description: Echoes read."), "token substituted in skill frontmatter");
257
+ assert.ok(content.includes("Use read to read and for help."), "null-mapped {{TOOL:skill}} dropped");
258
+ assert.ok(
259
+ content.includes(
260
+ "Open ~/.pi/agent/npm/node_modules/@jaybeeuu/agent-cortex/skills/echo/SKILL.md.",
261
+ ),
262
+ "relative path resolved against plugin root",
263
+ );
264
+ assert.ok(!/{{/.test(content), "no literal tokens remain in skill");
265
+
266
+ const script = readFileSync(join(fx.output, "skills", "engineering", "echo", "scripts", "run.sh"), "utf-8");
267
+ assert.equal(script, "#!/bin/sh\necho hi\n", "non-md files copied verbatim");
268
+ } finally {
269
+ fx.cleanup();
270
+ }
271
+ });
272
+ });
273
+
274
+ // ─── Output / dry-run ────────────────────────────────────────────────────────
275
+
276
+ describe("installPi — output & dry-run", () => {
277
+ it("dry-run writes nothing but reports the planned output", () => {
278
+ const fx = makeFixture();
279
+ try {
280
+ seedPackage(fx, {
281
+ agents: { alpha: { body: "# alpha" } },
282
+ skills: { "engineering/echo/SKILL.md": "---\nname: echo\ndescription: d\n---\n\nBody." },
283
+ });
284
+
285
+ const result = installPi({ root: fx.root, output: fx.output, dryRun: true });
286
+
287
+ assert.equal(result.dryRun, true);
288
+ assert.equal(listFiles(fx.output).length, 0, "no files written in dry-run");
289
+ assert.equal(result.agents.length, 1);
290
+ assert.equal(result.agents[0].name, "alpha");
291
+ assert.equal(result.skills.skills, 1);
292
+ // Planned paths are still reported so the user can see what would happen.
293
+ assert.ok(result.agents[0].filePath.startsWith(fx.output));
294
+ } finally {
295
+ fx.cleanup();
296
+ }
297
+ });
298
+
299
+ it("creates the output directory hierarchy when needed", () => {
300
+ const fx = makeFixture();
301
+ try {
302
+ seedPackage(fx, { agents: { alpha: { body: "# alpha" } } });
303
+ const deep = join(fx.output, "nested", "deep");
304
+ installPi({ root: fx.root, output: deep });
305
+ assert.ok(existsSync(join(deep, "agents", "alpha.agent.md")));
306
+ } finally {
307
+ fx.cleanup();
308
+ }
309
+ });
310
+ });
311
+
312
+ // ─── Integration against the real repo ───────────────────────────────────────
313
+
314
+ describe("installPi — real repo integration", () => {
315
+ it("installs every canonical agent with pi tool names and no leftover tokens", () => {
316
+ const fx = makeFixture();
317
+ try {
318
+ const result = installPi({ root: REPO_ROOT, output: fx.output });
319
+
320
+ const names = result.agents.map((a) => a.name).sort();
321
+ assert.deepEqual(names, ["plan", "ralph", "ralph-plan", "strategy"]);
322
+
323
+ // ralph pi frontmatter: ["bash", "view", "rg", "glob", "task", "read_agent", "wait_for_agents"]
324
+ const ralph = readFileSync(join(fx.output, "agents", "ralph.agent.md"), "utf-8");
325
+ assert.match(ralph, /name: "agent-cortex:ralph"/);
326
+ assert.match(ralph, /tools: "bash read grep find task read_agent wait_for_agents"/);
327
+ assert.ok(ralph.includes("## Spawning subagents (PI)"), "{{SECTION:polling}} composed from pi/polling.md");
328
+ const bodyTokens = ralph.split("---\n").slice(2).join("---\n");
329
+ assert.ok(!/{{/.test(bodyTokens), "no literal tokens in ralph body");
330
+
331
+ // plan body carries the relative {{PATH:...}} resolved against the plugin root
332
+ const plan = readFileSync(join(fx.output, "agents", "plan.agent.md"), "utf-8");
333
+ assert.match(plan, /tools: "bash read edit write grep find task read_agent"/);
334
+ assert.ok(
335
+ plan.includes("~/.pi/agent/npm/node_modules/@jaybeeuu/agent-cortex/skills/workflow/plan/SKILL.md"),
336
+ "plan PATH token resolved against token-map plugin root",
337
+ );
338
+
339
+ // ralph-plan prose {{TOOL:ask_user}} is dropped (null for pi)
340
+ const ralphPlan = readFileSync(join(fx.output, "agents", "ralph-plan.agent.md"), "utf-8");
341
+ assert.ok(!ralphPlan.includes("ask_user"), "null-mapped tool omitted from ralph-plan body");
342
+ assert.ok(
343
+ result.warnings.some((w) => w.includes("ask_user")),
344
+ "ralph-plan null tool dropped with a warning",
345
+ );
346
+
347
+ // strategy: web_fetch → fetch_content; ask_user/skill omitted
348
+ const strategy = readFileSync(join(fx.output, "agents", "strategy.agent.md"), "utf-8");
349
+ assert.match(strategy, /tools: "bash read grep find fetch_content edit write"/);
350
+ } finally {
351
+ fx.cleanup();
352
+ }
353
+ });
354
+
355
+ it("substitutes tokens in every installed skill markdown", () => {
356
+ const fx = makeFixture();
357
+ try {
358
+ const result = installPi({ root: REPO_ROOT, output: fx.output });
359
+ assert.ok(result.skills.skills > 20, `expected the full skill tree, got ${result.skills.skills}`);
360
+
361
+ const files = listFiles(join(fx.output, "skills")).filter((f) => f.endsWith(".md"));
362
+ assert.ok(files.length > 0);
363
+ for (const f of files) {
364
+ const content = readFileSync(join(fx.output, "skills", f), "utf-8");
365
+ assert.ok(!/{{/.test(content), `no literal tokens in skill ${f}`);
366
+ }
367
+ } finally {
368
+ fx.cleanup();
369
+ }
370
+ });
371
+ });
@@ -0,0 +1,130 @@
1
+ # token-map.json — multi-harness token substitution
2
+
3
+ `token-map.json` is the central, single-source mapping of **tool names**, **paths**, and
4
+ **agent naming conventions** across the three harnesses this repo ships to. It is the
5
+ foundation for the install-time generation system: each harness installer (`agent-cortex
6
+ install <harness>`) reads the map and substitutes `{{TOOL:...}}` / `{{PATH:...}}` tokens in
7
+ the files it generates.
8
+
9
+ **The substitution contract lives inside `token-map.json`** (the `contract` section) — that
10
+ file is the single reference installers must implement. This README explains the design
11
+ decisions behind the map.
12
+
13
+ ## Harnesses
14
+
15
+ | id | Platform | Agent format | Plugin root |
16
+ |---|---|---|---|
17
+ | `copilot` | GitHub Copilot CLI | `agents/<name>/` composable dirs → generated `agents/*.agent.md` | `~/.copilot/installed-plugins/_direct/agent-cortex` |
18
+ | `pi` | pi coding agent | `agents/<name>/` composable dirs (composed at runtime by agent-modes, or installed install-time by `agent-cortex install pi`) | `~/.pi/agent/npm/node_modules/@jaybeeuu/agent-cortex` |
19
+ | `claude` | Claude Code | `claude/agents/*.md` (generated) | `${CLAUDE_PLUGIN_ROOT}` (env var) |
20
+
21
+ The harness ids match the values accepted by `agent-cortex install <harness>` in
22
+ `lib/cli.mjs` (`SUPPORTED_HARNESSES`). The pi `plugin_root` row is the npm-layout
23
+ literal that `agent-cortex install pi` uses by default; local checkout and symlinked
24
+ installs pass `--plugin-root <dir>` to override it (see README "Pi harness agents &
25
+ skills").
26
+
27
+ ## Design decisions
28
+
29
+ ### 1. Token naming convention: `{{TOOL:name}}`, not `{{tool.name}}`
30
+
31
+ `agents/README.md` (ADR #63) already standardised `{{TOOL:name}}`, `{{PATH:name}}`, and
32
+ `{{SECTION:name}}` for the composable agent directory format. The map adopts that grammar
33
+ rather than inventing a dotted form:
34
+
35
+ - **The convention is established** — skill authors and the ADR already write tokens this
36
+ way; the map should not fork the language.
37
+ - **Upper-case type prefix disambiguates** — `{{TOOL:read}}` (a tool) can never collide with
38
+ `{{PATH:read}}` (a path) in the same document.
39
+ - **One namespace per type** — tool keys, path keys, and section names never share a lookup
40
+ table, so a tool called `skills_dir` would not shadow the path key.
41
+ - **Greppable and schema-friendly** — simple `{{TYPE:NAME}}` is trivially matched by a
42
+ regex; delimiters make unbalanced/typo'd tokens visible.
43
+
44
+ ### 2. What is mapped
45
+
46
+ - **`tools`** — the twelve canonical tool names as authored in `agents/<name>/agent.md`
47
+ (copilot names are the authoring names). Each row gives the equivalent tool name per
48
+ harness. Source of truth for the copilot → claude half is `token-map.json`'s own claude
49
+ column, consumed by `bin/installers/claude.mjs` (the install-time generator — PR #91, #92); the pi column is grounded
50
+ in pi's documented built-in tool surface (`read`, `bash`, `edit`, `write`, `grep`, `find`,
51
+ `ls`) plus `task` / `read_agent` / `wait_for_agents` subagent tools.
52
+ - **`paths`** — `plugin_root`, `agents_dir`, and `skills_dir`. These are the install-time
53
+ variables that differ per harness. `plugin_root` is the anchor; the other two resolve
54
+ relative to it (`{base, relative}` specs) so a harness only overrides the single root it
55
+ actually installs into.
56
+ - **`agents`** — the `agent-cortex:` name-prefix convention per harness (claude strips the
57
+ prefix during conversion) and the deferred-agent exception (claude's `ralph` is authored
58
+ natively, never generated). Agent names themselves do not get per-row mappings because
59
+ the prefix rule covers all of them; the deferred list captures the only structural
60
+ difference.
61
+ - **Deliberately not mapped:** hook patterns, MCP servers, and tool descriptions. Those
62
+ differ *structurally* per harness (different hook event names, different config files),
63
+ not by a single substitutable token; each installer already hardcodes its own shape. The
64
+ map records them in `notes` where a tool differs in behaviour (`read_agent`, `skill`,
65
+ `ask_user`).
66
+
67
+ ### 3. Null mappings
68
+
69
+ A null value means **"this harness has no equivalent"** — not "empty string" and not "leave
70
+ the token as-is". The contract defines the two contexts:
71
+
72
+ - **Prose** (e.g. `Use {{TOOL:read_agent}} to poll`): the token is dropped and the
73
+ installer warns. Dangling sentences after a drop are the author's responsibility — the
74
+ warning exists so the installer surfaces them.
75
+ - **Tool-list frontmatter** (e.g. `tools: [..., "read_agent"]`): the entry is omitted from
76
+ the array.
77
+
78
+ Unknown (unmapped) tokens are a **hard error**, not a null — a silently-passed-through
79
+ token is exactly the bug the map exists to prevent, and mirrors the build script's
80
+ `unknown tool "x"` throw.
81
+
82
+ The pi install-time installer (`bin/installers/pi.mjs`) follows the strict contract above.
83
+ One consumer deviates deliberately: the pi `agent-modes` extension reads the map at
84
+ **runtime** (not install time) to compose agent prompts from the composable agents
85
+ format. For it, an unmapped tool name passes through unchanged — it may be a
86
+ native PI tool — while a mapped-null tool (`ask_user`, `skill`) is omitted with a
87
+ warning. See `extensions/agent-modes/README.md`.
88
+
89
+ ### 4. Token substitution contract
90
+
91
+ The full contract is embedded in `token-map.json` under `contract`. In summary, installers:
92
+
93
+ 1. Bind a harness id (`copilot` | `claude` | `pi`).
94
+ 2. Substitute in order: `SECTION` (left for the composer), then `TOOL`, then `PATH`.
95
+ 3. For `TOOL`: look up the canonical key, replace with the harness name; null → drop/omit;
96
+ unknown → throw.
97
+ 4. For `PATH`: named key → resolve from `paths`; anything else → resolve relative to
98
+ `plugin_root`; `${ENV_VAR}` values pass through verbatim for runtime expansion.
99
+ 5. Reject a map `version` they do not implement.
100
+
101
+ ## Authoring guidance
102
+
103
+ - Write tokens with the **canonical (copilot) names**: `Use {{TOOL:task}} to spawn
104
+ subagents`, `Read {{PATH:skills/workflow/plan/SKILL.md}}`.
105
+ - Never embed a harness-specific name in the canonical sources (`agents/<name>/agent.md`,
106
+ `skills/**/SKILL.md`) — that defeats the map. The generated `claude/` subtree, the
107
+ generated `agents/*.agent.md` files, and any installed copies are the only places
108
+ substituted names appear.
109
+ - When you add a new tool to an agent's frontmatter, add a row to `tools` in
110
+ `token-map.json` in the same change — the contract treats unknown tools as errors, so a
111
+ missing row breaks the install pipeline loudly instead of silently.
112
+
113
+ ## Related
114
+
115
+ - `agents/README.md` — composable agent directory format (ADR #63), defines the token
116
+ grammar the map implements.
117
+ - `scripts/build-copilot-agents.mjs` / `bin/installers/copilot.mjs` — the copilot
118
+ harness token composer (scripts/lib/compose-agent.mjs) resolves `{{TOOL:...}}` /
119
+ `{{PATH:...}}` against the map and `{{SECTION:...}}` from the per-harness section
120
+ files; `bin/installers/copilot.mjs` is the shared generator both `pnpm build:copilot`
121
+ and `agent-cortex install copilot` run.
122
+ - `bin/installers/claude.mjs` — the single Claude generator, behind plain
123
+ `agent-cortex install claude` (which also registers the plugin with Claude Code) and
124
+ its generate-only `pnpm build:claude` alias (`install claude --output claude`; no
125
+ build-time counterpart — the committed `claude/` subtree is regenerated by it and
126
+ drift-checked).
127
+ - `bin/installers/pi.mjs` — the pi harness installer (`agent-cortex install pi`), an
128
+ install-time consumer of the map's contract.
129
+ - `lib/cli.mjs` — `SUPPORTED_HARNESSES`, the install command surface the harness ids come
130
+ from.
package/token-map.json ADDED
@@ -0,0 +1,106 @@
1
+ {
2
+ "name": "agent-cortex token map",
3
+ "description": "Central mapping of tool names, paths, and agent naming conventions across the copilot, claude, and pi harnesses. Single source of truth for install-time token substitution. See token-map.README.md for the design rationale.",
4
+ "version": 1,
5
+ "harnesses": ["copilot", "claude", "pi"],
6
+ "token_format": {
7
+ "convention": "{{TYPE:NAME}}",
8
+ "decision": "Adopt the {{TOOL:name}} / {{PATH:name}} / {{SECTION:name}} grammar already standardised by ADR #63 (agents/README.md) rather than a dotted {{tool.name}} form. The uppercase type prefix disambiguates token kinds at a glance, keeps tool/path/section names in one namespace collision-free, and matches the token grammar skills are already written against.",
9
+ "types": {
10
+ "TOOL": "{{TOOL:<canonical tool key>}} — replaced at install time with the bound harness's tool name (see tools).",
11
+ "PATH": "{{PATH:<path key or relative path>}} — replaced at install time with the harness-resolved path (see paths).",
12
+ "SECTION": "{{SECTION:<name>}} — replaced by the agent composer with <harness>/<name>.md section content. Not a global substitution; out of scope for token-map.json (see contract)."
13
+ }
14
+ },
15
+ "tools": {
16
+ "bash": { "copilot": "bash", "claude": "Bash", "pi": "bash" },
17
+ "view": { "copilot": "view", "claude": "Read", "pi": "read" },
18
+ "edit": { "copilot": "edit", "claude": "Edit", "pi": "edit" },
19
+ "create": { "copilot": "create", "claude": "Write", "pi": "write" },
20
+ "grep": { "copilot": "grep", "claude": "Grep", "pi": "grep" },
21
+ "rg": { "copilot": "rg", "claude": "Grep", "pi": "grep" },
22
+ "glob": { "copilot": "glob", "claude": "Glob", "pi": "find" },
23
+ "ask_user": { "copilot": "ask_user", "claude": "AskUserQuestion", "pi": null },
24
+ "web_fetch": { "copilot": "web_fetch", "claude": "WebFetch", "pi": "fetch_content" },
25
+ "skill": { "copilot": "skill", "claude": "Skill", "pi": null },
26
+ "task": { "copilot": "task", "claude": "Task", "pi": "task" },
27
+ "read_agent": { "copilot": "read_agent", "claude": null, "pi": "read_agent" },
28
+ "wait_for_agents": { "copilot": null, "claude": null, "pi": "wait_for_agents" }
29
+ },
30
+ "paths": {
31
+ "plugin_root": {
32
+ "copilot": "~/.copilot/installed-plugins/_direct/agent-cortex",
33
+ "claude": "${CLAUDE_PLUGIN_ROOT}",
34
+ "pi": "~/.pi/agent/npm/node_modules/@jaybeeuu/agent-cortex"
35
+ },
36
+ "agents_dir": {
37
+ "copilot": { "base": "plugin_root", "relative": "agents" },
38
+ "claude": { "base": "plugin_root", "relative": "agents" },
39
+ "pi": { "base": "plugin_root", "relative": "agents" }
40
+ },
41
+ "skills_dir": {
42
+ "copilot": { "base": "plugin_root", "relative": "skills" },
43
+ "claude": { "base": "plugin_root", "relative": "skills" },
44
+ "pi": { "base": "plugin_root", "relative": "skills" }
45
+ }
46
+ },
47
+ "agents": {
48
+ "prefix": {
49
+ "copilot": "agent-cortex:",
50
+ "claude": "",
51
+ "pi": "agent-cortex:"
52
+ },
53
+ "deferred": {
54
+ "ralph": ["claude"]
55
+ },
56
+ "sources": {
57
+ "copilot": "agents/<name>/ composable dirs (canonical) — composed to agents/*.agent.md by scripts/build-copilot-agents.mjs",
58
+ "pi": "agents/<name>/ composable dirs (canonical) — composed at runtime by the agent-modes extension",
59
+ "claude": "agents/<name>/ composable dirs (canonical) — composed by bin/installers/claude.mjs (install-time generator; pnpm build:claude aliases it), plus agents-native/*.md for deferred agents"
60
+ }
61
+ },
62
+ "notes": {
63
+ "tools": {
64
+ "rg": "Copilot alias for grep. Claude and pi collapse it onto their single search tool (Grep / grep) — same collapse rule as the build script.",
65
+ "glob": "Claude has a dedicated Glob tool. pi's built-in pattern-search tool is find (hypa_find when the pi-hypa extension is installed).",
66
+ "ask_user": "Copilot (ask_user) and Claude (AskUserQuestion) prompt the user mid-run. pi intentionally ships no permission-popup/ask tool (per pi docs 'it intentionally does not include permission popups'), so the mapping is null.",
67
+ "web_fetch": "pi's fetch_content comes from the pi-web-access package (declared in pi/settings.json).",
68
+ "skill": "Copilot (skill) and Claude (Skill) expose skills as a tool. pi discovers skills but the agent loads them via read and /skill:<name> commands — there is no dedicated tool, so the mapping is null.",
69
+ "read_agent": "Claude's Task returns results inline — there is no separate read step, so read_agent drops on claude (null). Copilot uses the task + read_agent poll loop; pi blocks on wait_for_agents and keeps read_agent as a fallback.",
70
+ "wait_for_agents": "PI-only blocking wait over background subagent runs (the task tool): returns the results of completed agents and STILL RUNNING for the rest. Copilot has no equivalent tool (its task + read_agent poll loop stays) and Claude's Task delivers results inline with no wait step — both null."
71
+ },
72
+ "paths": {
73
+ "pi": "plugin_root defaults to the npm-package install layout (~/.pi/agent/npm/node_modules/@jaybeeuu/agent-cortex). Local-checkout installs (pi/settings.json packages entry referencing a local src path) or symlinked installs differ — installers must compute plugin_root from the actual resolution and can override it."
74
+ },
75
+ "agents": {
76
+ "prefix": "Claude strips the agent-cortex: name prefix during conversion (NAME_PREFIX in bin/installers/claude.mjs): copilot/pi agents are agent-cortex:<slug>, claude agents are <slug>.",
77
+ "deferred": "ralph has no generated claude counterpart — it is authored natively as agents-native/ralph.md (its event-driven orchestration cannot be produced by section composition from the polling copilot source)."
78
+ }
79
+ },
80
+ "contract": {
81
+ "version": 1,
82
+ "summary": "Installers substitute {{TOOL:...}}, {{PATH:...}}, and {{SECTION:...}} tokens in generated harness files using this map. This contract is the single reference installers must implement; token-map.json is the data those rules operate on.",
83
+ "harness_ids": ["copilot", "claude", "pi"],
84
+ "binding": "The harness id is the same value the installer is invoked with (agent-cortex install <harness>). Each harness reads its own column from the tools, paths, and agents sections.",
85
+ "substitution_order": ["SECTION", "TOOL", "PATH"],
86
+ "rules": [
87
+ "TOOL: replace {{TOOL:<key>}} with tools.<key>.<harness>. Canonical keys are exact, case-sensitive strings (the copilot tool names, which are the authoring names in agents/<name>/agent.md).",
88
+ "TOOL null: a null value means the harness has no equivalent. In prose, remove the token (and emit a warning). In a tool-list context (frontmatter tools: [...]), omit the entry. Do not substitute an empty string.",
89
+ "TOOL unknown: a {{TOOL:<key>}} with no entry in tools is a hard error — the map must be extended. This mirrors the generators (scripts/build-copilot-agents.mjs, bin/installers/claude.mjs), which throw on unknown tools.",
90
+ "PATH named key: replace {{PATH:<key>}} with the resolved value of paths.<key>.<harness>.",
91
+ "PATH relative: replace {{PATH:<relative>}} with plugin_root + '/' + <relative> (e.g. {{PATH:skills/workflow/plan/SKILL.md}}).",
92
+ "PATH resolution: a value may be a literal string, an ${ENV_VAR} token, or a {base, relative} spec. Resolve {base, relative} by joining relative onto the resolved base. Leave ${ENV_VAR} values verbatim — the harness expands them at runtime (claude's ${CLAUDE_PLUGIN_ROOT}).",
93
+ "PATH unknown base: a {base, relative} spec whose base is not a key in paths is a hard error.",
94
+ "SECTION: {{SECTION:<name>}} is resolved by the agent composer from <agents_dir>/<harness>/<name>.md; token-map.json does not substitute it. Installers must leave SECTION tokens untouched for the composer.",
95
+ "Contract version: installers must reject token-map.json versions higher than the contract version they implement.",
96
+ "Case sensitivity: all token arguments and map keys are case-sensitive."
97
+ ],
98
+ "error_handling": {
99
+ "unknown_tool": "throw — the map must stay complete",
100
+ "unknown_path_base": "throw — the map must stay complete",
101
+ "null_tool_in_prose": "warn and drop the token",
102
+ "null_tool_in_tool_list": "omit the entry from the array",
103
+ "unsupported_version": "throw with the supported version range"
104
+ }
105
+ }
106
+ }