@jaybeeuu/agent-cortex 1.38.1 → 1.40.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 (88) hide show
  1. package/.agents/skills/style-versioning/SKILL.md +1 -1
  2. package/.github/workflows/ci.yml +47 -14
  3. package/AGENTS.md +19 -3
  4. package/CHANGELOG.md +42 -0
  5. package/README.md +118 -63
  6. package/agents/README.md +13 -10
  7. package/agents/ralph-plan/agent.md +1 -1
  8. package/agents/ralph-plan.agent.md +1 -1
  9. package/bin/agent-cortex.mjs +29 -8
  10. package/bin/installers/claude.mjs +569 -105
  11. package/bin/installers/copilot.mjs +22 -12
  12. package/bin/installers/pi.mjs +37 -34
  13. package/docs/claude-hooks.md +3 -3
  14. package/docs/ideas/extension-manifests.md +75 -0
  15. package/docs/ideas/record-ideas-as-beads.md +115 -0
  16. package/docs/ideas/subagent-visibility.md +72 -0
  17. package/docs/ideas/task-session-context.md +89 -0
  18. package/docs/research/skill-design-research.md +4 -4
  19. package/docs/research/skill-improvements-analysis.md +1 -1
  20. package/docs/skills/skill-anatomy.md +38 -2
  21. package/docs/technical-direction/migration-execution-labels.md +132 -0
  22. package/extensions/agent-modes/README.md +3 -2
  23. package/extensions/agent-modes/discover.test.ts +30 -7
  24. package/extensions/agent-modes/discover.ts +1 -1
  25. package/extensions/skill-stats/index.test.ts +3 -2
  26. package/lib/cli.mjs +24 -6
  27. package/package.json +4 -4
  28. package/plugin.json +1 -1
  29. package/pnpm-workspace.yaml +1 -1
  30. package/scripts/build-copilot-agents.mjs +1 -1
  31. package/scripts/check-no-sync.mjs +49 -0
  32. package/scripts/lib/compose-agent.mjs +32 -14
  33. package/skills/engineering/design-an-interface/SKILL.md +78 -58
  34. package/skills/engineering/grill-with-docs/REFERENCE.md +78 -0
  35. package/skills/engineering/grill-with-docs/SKILL.md +66 -63
  36. package/skills/engineering/improve-codebase-architecture/SKILL.md +66 -48
  37. package/skills/engineering/technical-direction/FORMAT.md +38 -0
  38. package/skills/engineering/technical-direction/SKILL.md +70 -77
  39. package/skills/planning/classify-bead/SKILL.md +59 -35
  40. package/skills/planning/create-task/REFERENCE.md +40 -0
  41. package/skills/planning/create-task/SKILL.md +70 -66
  42. package/skills/planning/create-task/scripts/create-chores.ts +6 -1
  43. package/skills/planning/create-task/scripts/node_modules/.bin/tsc +21 -0
  44. package/skills/planning/create-task/scripts/node_modules/.bin/tsserver +21 -0
  45. package/skills/planning/create-task/scripts/node_modules/.bin/tsx +21 -0
  46. package/skills/planning/prd-to-tasks/REFERENCE.md +66 -0
  47. package/skills/planning/prd-to-tasks/SKILL.md +86 -170
  48. package/skills/planning/record-idea/FORMAT.md +44 -0
  49. package/skills/planning/record-idea/SKILL.md +70 -53
  50. package/skills/planning/request-refactor-plan/FORMAT.md +45 -0
  51. package/skills/planning/request-refactor-plan/SKILL.md +57 -46
  52. package/skills/planning/write-a-prd/FORMAT.md +54 -0
  53. package/skills/planning/write-a-prd/SKILL.md +49 -48
  54. package/skills/planning/write-a-ticket/SKILL.md +68 -53
  55. package/skills/productivity/handoff/SKILL.md +12 -0
  56. package/skills/productivity/init-beads/SKILL.md +48 -40
  57. package/skills/productivity/validate-agent-dir/scripts/node_modules/.bin/tsc +21 -0
  58. package/skills/productivity/validate-agent-dir/scripts/node_modules/.bin/tsserver +21 -0
  59. package/skills/productivity/validate-agent-dir/scripts/node_modules/.bin/tsx +21 -0
  60. package/skills/productivity/write-a-skill/SKILL.md +2 -1
  61. package/skills/review/maintain-agent-docs/SKILL.md +71 -51
  62. package/skills/workflow/hitl-collab/SKILL.md +86 -17
  63. package/skills/workflow/ralph/REFERENCE.md +22 -17
  64. package/skills/workflow/ralph/SKILL.md +88 -74
  65. package/skills/workflow/run-pipeline-stage/REFERENCE.md +22 -0
  66. package/skills/workflow/run-pipeline-stage/SKILL.md +91 -93
  67. package/skills/workflow/run-pipeline-stage/scripts/generate-progress.test.ts +6 -107
  68. package/skills/workflow/run-pipeline-stage/scripts/node_modules/.bin/tsc +21 -0
  69. package/skills/workflow/run-pipeline-stage/scripts/node_modules/.bin/tsserver +21 -0
  70. package/skills/workflow/run-pipeline-stage/scripts/node_modules/.bin/tsx +21 -0
  71. package/test/claude-installer.test.mjs +308 -0
  72. package/test/cli.test.mjs +40 -30
  73. package/test/copilot-installer.test.mjs +92 -82
  74. package/test/helpers/fake-claude.mjs +152 -0
  75. package/test/installer.test.mjs +309 -57
  76. package/test/pi-installer.test.mjs +115 -99
  77. package/token-map.README.md +21 -10
  78. package/token-map.json +7 -6
  79. package/.claude-plugin/marketplace.json +0 -16
  80. package/claude/.claude-plugin/plugin.json +0 -11
  81. package/claude/agents/plan.md +0 -58
  82. package/claude/agents/ralph-plan.md +0 -121
  83. package/claude/agents/ralph.md +0 -193
  84. package/claude/agents/strategy.md +0 -111
  85. package/claude/hooks/scripts/notify.mjs +0 -54
  86. package/claude/hooks.json +0 -38
  87. /package/{claude → claude-extras}/.mcp.json +0 -0
  88. /package/{claude → claude-extras}/scripts/statusline-command.sh +0 -0
@@ -1,6 +1,6 @@
1
1
  import { describe, it } from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync, existsSync, readdirSync } from "node:fs";
3
+ import { mkdtemp, mkdir, writeFile, rm, readFile, readdir, stat } from "node:fs/promises";
4
4
  import { join, dirname } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
6
  import { fileURLToPath } from "node:url";
@@ -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,29 @@ function tokenMap(extra = {}) {
37
47
  };
38
48
  }
39
49
 
40
- function makeFixture() {
41
- const root = mkdtempSync(join(tmpdir(), "pi-installer-"));
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: () => rmSync(root, { recursive: true, force: true }),
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
- mkdirSync(dirname(p), { recursive: true });
53
- writeFileSync(p, content);
62
+ await mkdir(dirname(p), { recursive: true });
63
+ await writeFile(p, content);
54
64
  }
55
65
 
56
66
  /** Seed a minimal but realistic package tree (agents + token-map + skills). */
57
- function seedPackage(fx, opts = {}) {
67
+ async function seedPackage(fx, opts = {}) {
58
68
  const { agents = {}, skills = {}, tokenMap: map, version } = opts;
59
- writeFixture(fx, "token-map.json", JSON.stringify(map ?? tokenMap(version ? { version } : {})));
69
+ await writeFixture(fx, "token-map.json", JSON.stringify(map ?? tokenMap(version ? { version } : {})));
60
70
  for (const [name, def] of Object.entries(agents)) {
61
- writeFixture(fx, `agents/${name}/agent.md`, def.body ?? `# ${name}\nBody of ${name}.`);
62
- writeFixture(
71
+ await writeFixture(fx, `agents/${name}/agent.md`, def.body ?? `# ${name}\nBody of ${name}.`);
72
+ await writeFixture(
63
73
  fx,
64
74
  `agents/${name}/pi/frontmatter.json`,
65
75
  JSON.stringify(
@@ -72,20 +82,20 @@ function seedPackage(fx, opts = {}) {
72
82
  ),
73
83
  );
74
84
  for (const [section, content] of Object.entries(def.sections ?? {})) {
75
- writeFixture(fx, `agents/${name}/pi/${section}.md`, content);
85
+ await writeFixture(fx, `agents/${name}/pi/${section}.md`, content);
76
86
  }
77
87
  }
78
88
  for (const [path, content] of Object.entries(skills)) {
79
- writeFixture(fx, `skills/${path}`, content);
89
+ await writeFixture(fx, `skills/${path}`, content);
80
90
  }
81
91
  }
82
92
 
83
- function listFiles(dir) {
84
- if (!existsSync(dir)) return [];
93
+ async function listFiles(dir) {
94
+ if (!(await pathExists(dir))) return [];
85
95
  const out = [];
86
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
96
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
87
97
  const p = join(dir, entry.name);
88
- if (entry.isDirectory()) out.push(...listFiles(p).map((f) => join(entry.name, f)));
98
+ if (entry.isDirectory()) out.push(...(await listFiles(p)).map((f) => join(entry.name, f)));
89
99
  else out.push(entry.name);
90
100
  }
91
101
  return out;
@@ -94,10 +104,10 @@ function listFiles(dir) {
94
104
  // ─── Agent composition ───────────────────────────────────────────────────────
95
105
 
96
106
  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();
107
+ it("writes one .agent.md per composable agent dir with pi frontmatter and no leftover tokens", async () => {
108
+ const fx = await makeFixture();
99
109
  try {
100
- seedPackage(fx, {
110
+ await seedPackage(fx, {
101
111
  agents: {
102
112
  alpha: {
103
113
  body: "# alpha\nUse {{TOOL:view}} to read.\n{{SECTION:polling}}\nRead {{PATH:skills_dir}}.",
@@ -112,11 +122,11 @@ describe("installPi — agent composition", () => {
112
122
  },
113
123
  });
114
124
 
115
- const result = installPi({ root: fx.root, output: fx.output });
125
+ const result = await installPi({ root: fx.root, output: fx.output });
116
126
 
117
127
  const file = join(fx.output, "agents", "alpha.agent.md");
118
- assert.ok(existsSync(file), "agent file written");
119
- const content = readFileSync(file, "utf-8");
128
+ assert.ok(await pathExists(file), "agent file written");
129
+ const content = await readFile(file, "utf-8");
120
130
  assert.ok(content.startsWith("---\n"), "yaml frontmatter");
121
131
  assert.match(content, /name: "agent-cortex:alpha"/);
122
132
  assert.match(content, /description: "Reads things\."/);
@@ -128,55 +138,55 @@ describe("installPi — agent composition", () => {
128
138
  assert.equal(result.agents.length, 1);
129
139
  assert.deepEqual(result.agents[0], { name: "alpha", filePath: file });
130
140
  } finally {
131
- fx.cleanup();
141
+ await fx.cleanup();
132
142
  }
133
143
  });
134
144
 
135
- it("drops null-mapped tool tokens in prose and reports a warning", () => {
136
- const fx = makeFixture();
145
+ it("drops null-mapped tool tokens in prose and reports a warning", async () => {
146
+ const fx = await makeFixture();
137
147
  try {
138
- seedPackage(fx, {
148
+ await seedPackage(fx, {
139
149
  agents: {
140
150
  alpha: { body: "# alpha\nAsk the user with {{TOOL:ask_user}}, then use {{TOOL:bash}}." },
141
151
  },
142
152
  });
143
153
 
144
- const result = installPi({ root: fx.root, output: fx.output });
145
- const content = readFileSync(join(fx.output, "agents", "alpha.agent.md"), "utf-8");
154
+ const result = await installPi({ root: fx.root, output: fx.output });
155
+ const content = await readFile(join(fx.output, "agents", "alpha.agent.md"), "utf-8");
146
156
 
147
157
  assert.ok(!content.includes("ask_user"), "null-mapped token dropped from prose");
148
158
  assert.ok(content.includes("use bash"), "mapped token substituted");
149
159
  assert.ok(!/{{/.test(content));
150
160
  assert.ok(result.warnings.some((w) => w.includes("{{TOOL:ask_user}}")), "drop reported as warning");
151
161
  } finally {
152
- fx.cleanup();
162
+ await fx.cleanup();
153
163
  }
154
164
  });
155
165
 
156
- it("throws on an unknown tool token", () => {
157
- const fx = makeFixture();
166
+ it("throws on an unknown tool token", async () => {
167
+ const fx = await makeFixture();
158
168
  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}}/);
169
+ await seedPackage(fx, { agents: { alpha: { body: "# alpha\nUse {{TOOL:nope}} now." } } });
170
+ await assert.rejects(installPi({ root: fx.root, output: fx.output }), /unknown tool token {{TOOL:nope}}/);
161
171
  } finally {
162
- fx.cleanup();
172
+ await fx.cleanup();
163
173
  }
164
174
  });
165
175
 
166
- it("throws when a section file referenced by {{SECTION:...}} is missing", () => {
167
- const fx = makeFixture();
176
+ it("throws when a section file referenced by {{SECTION:...}} is missing", async () => {
177
+ const fx = await makeFixture();
168
178
  try {
169
- seedPackage(fx, { agents: { alpha: { body: "# alpha\n{{SECTION:missing}}" } } });
170
- assert.throws(() => installPi({ root: fx.root, output: fx.output }), /missing section "missing"/);
179
+ await seedPackage(fx, { agents: { alpha: { body: "# alpha\n{{SECTION:missing}}" } } });
180
+ await assert.rejects(installPi({ root: fx.root, output: fx.output }), /missing section "missing"/);
171
181
  } finally {
172
- fx.cleanup();
182
+ await fx.cleanup();
173
183
  }
174
184
  });
175
185
 
176
- it("resolves named and relative {{PATH:...}} tokens against the plugin root", () => {
177
- const fx = makeFixture();
186
+ it("resolves named and relative {{PATH:...}} tokens against the plugin root", async () => {
187
+ const fx = await makeFixture();
178
188
  try {
179
- seedPackage(fx, {
189
+ await seedPackage(fx, {
180
190
  agents: {
181
191
  alpha: {
182
192
  body: "# alpha\nNamed: {{PATH:skills_dir}}\nRelative: {{PATH:skills/x/SKILL.md}}",
@@ -184,8 +194,8 @@ describe("installPi — agent composition", () => {
184
194
  },
185
195
  });
186
196
 
187
- const result = installPi({ root: fx.root, output: fx.output });
188
- const content = readFileSync(join(fx.output, "agents", "alpha.agent.md"), "utf-8");
197
+ const result = await installPi({ root: fx.root, output: fx.output });
198
+ const content = await readFile(join(fx.output, "agents", "alpha.agent.md"), "utf-8");
189
199
 
190
200
  assert.ok(
191
201
  content.includes("Named: ~/.pi/agent/npm/node_modules/@jaybeeuu/agent-cortex/skills"),
@@ -197,38 +207,38 @@ describe("installPi — agent composition", () => {
197
207
  );
198
208
  assert.equal(result.warnings.length, 0);
199
209
  } finally {
200
- fx.cleanup();
210
+ await fx.cleanup();
201
211
  }
202
212
  });
203
213
 
204
- it("honours the --plugin-root override for path resolution", () => {
205
- const fx = makeFixture();
214
+ it("honours the --plugin-root override for path resolution", async () => {
215
+ const fx = await makeFixture();
206
216
  try {
207
- seedPackage(fx, {
217
+ await seedPackage(fx, {
208
218
  agents: {
209
219
  alpha: { body: "# alpha\n{{PATH:skills/x/SKILL.md}}" },
210
220
  },
211
221
  });
212
222
 
213
223
  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");
224
+ await installPi({ root: fx.root, output: fx.output, pluginRoot });
225
+ const content = await readFile(join(fx.output, "agents", "alpha.agent.md"), "utf-8");
216
226
  assert.ok(content.includes(join(pluginRoot, "skills", "x", "SKILL.md")), "plugin root override used");
217
227
  } finally {
218
- fx.cleanup();
228
+ await fx.cleanup();
219
229
  }
220
230
  });
221
231
 
222
- it("rejects token-map.json versions newer than the implemented contract", () => {
223
- const fx = makeFixture();
232
+ it("rejects token-map.json versions newer than the implemented contract", async () => {
233
+ const fx = await makeFixture();
224
234
  try {
225
- seedPackage(fx, {
235
+ await seedPackage(fx, {
226
236
  version: 99,
227
237
  agents: { alpha: { body: "# alpha" } },
228
238
  });
229
- assert.throws(() => installPi({ root: fx.root, output: fx.output }), /version 99/);
239
+ await assert.rejects(installPi({ root: fx.root, output: fx.output }), /version 99/);
230
240
  } finally {
231
- fx.cleanup();
241
+ await fx.cleanup();
232
242
  }
233
243
  });
234
244
  });
@@ -236,10 +246,10 @@ describe("installPi — agent composition", () => {
236
246
  // ─── Skills ──────────────────────────────────────────────────────────────────
237
247
 
238
248
  describe("installPi — skills", () => {
239
- it("copies skills into <output>/skills preserving the grouped layout with substituted tokens", () => {
240
- const fx = makeFixture();
249
+ it("copies skills into <output>/skills preserving the grouped layout with substituted tokens", async () => {
250
+ const fx = await makeFixture();
241
251
  try {
242
- seedPackage(fx, {
252
+ await seedPackage(fx, {
243
253
  agents: { alpha: { body: "# alpha" } },
244
254
  skills: {
245
255
  "engineering/echo/SKILL.md":
@@ -248,11 +258,11 @@ describe("installPi — skills", () => {
248
258
  },
249
259
  });
250
260
 
251
- installPi({ root: fx.root, output: fx.output });
261
+ await installPi({ root: fx.root, output: fx.output });
252
262
 
253
263
  const skillFile = join(fx.output, "skills", "engineering", "echo", "SKILL.md");
254
- assert.ok(existsSync(skillFile), "skill copied");
255
- const content = readFileSync(skillFile, "utf-8");
264
+ assert.ok(await pathExists(skillFile), "skill copied");
265
+ const content = await readFile(skillFile, "utf-8");
256
266
  assert.ok(content.includes("description: Echoes read."), "token substituted in skill frontmatter");
257
267
  assert.ok(content.includes("Use read to read and for help."), "null-mapped {{TOOL:skill}} dropped");
258
268
  assert.ok(
@@ -263,10 +273,10 @@ describe("installPi — skills", () => {
263
273
  );
264
274
  assert.ok(!/{{/.test(content), "no literal tokens remain in skill");
265
275
 
266
- const script = readFileSync(join(fx.output, "skills", "engineering", "echo", "scripts", "run.sh"), "utf-8");
276
+ const script = await readFile(join(fx.output, "skills", "engineering", "echo", "scripts", "run.sh"), "utf-8");
267
277
  assert.equal(script, "#!/bin/sh\necho hi\n", "non-md files copied verbatim");
268
278
  } finally {
269
- fx.cleanup();
279
+ await fx.cleanup();
270
280
  }
271
281
  });
272
282
  });
@@ -274,37 +284,37 @@ describe("installPi — skills", () => {
274
284
  // ─── Output / dry-run ────────────────────────────────────────────────────────
275
285
 
276
286
  describe("installPi — output & dry-run", () => {
277
- it("dry-run writes nothing but reports the planned output", () => {
278
- const fx = makeFixture();
287
+ it("dry-run writes nothing but reports the planned output", async () => {
288
+ const fx = await makeFixture();
279
289
  try {
280
- seedPackage(fx, {
290
+ await seedPackage(fx, {
281
291
  agents: { alpha: { body: "# alpha" } },
282
292
  skills: { "engineering/echo/SKILL.md": "---\nname: echo\ndescription: d\n---\n\nBody." },
283
293
  });
284
294
 
285
- const result = installPi({ root: fx.root, output: fx.output, dryRun: true });
295
+ const result = await installPi({ root: fx.root, output: fx.output, dryRun: true });
286
296
 
287
297
  assert.equal(result.dryRun, true);
288
- assert.equal(listFiles(fx.output).length, 0, "no files written in dry-run");
298
+ assert.equal((await listFiles(fx.output)).length, 0, "no files written in dry-run");
289
299
  assert.equal(result.agents.length, 1);
290
300
  assert.equal(result.agents[0].name, "alpha");
291
301
  assert.equal(result.skills.skills, 1);
292
302
  // Planned paths are still reported so the user can see what would happen.
293
303
  assert.ok(result.agents[0].filePath.startsWith(fx.output));
294
304
  } finally {
295
- fx.cleanup();
305
+ await fx.cleanup();
296
306
  }
297
307
  });
298
308
 
299
- it("creates the output directory hierarchy when needed", () => {
300
- const fx = makeFixture();
309
+ it("creates the output directory hierarchy when needed", async () => {
310
+ const fx = await makeFixture();
301
311
  try {
302
- seedPackage(fx, { agents: { alpha: { body: "# alpha" } } });
312
+ await seedPackage(fx, { agents: { alpha: { body: "# alpha" } } });
303
313
  const deep = join(fx.output, "nested", "deep");
304
- installPi({ root: fx.root, output: deep });
305
- assert.ok(existsSync(join(deep, "agents", "alpha.agent.md")));
314
+ await installPi({ root: fx.root, output: deep });
315
+ assert.ok(await pathExists(join(deep, "agents", "alpha.agent.md")));
306
316
  } finally {
307
- fx.cleanup();
317
+ await fx.cleanup();
308
318
  }
309
319
  });
310
320
  });
@@ -312,60 +322,66 @@ describe("installPi — output & dry-run", () => {
312
322
  // ─── Integration against the real repo ───────────────────────────────────────
313
323
 
314
324
  describe("installPi — real repo integration", () => {
315
- it("installs every canonical agent with pi tool names and no leftover tokens", () => {
316
- const fx = makeFixture();
325
+ it("installs every canonical agent with pi tool names and no leftover tokens", async () => {
326
+ const fx = await makeFixture();
317
327
  try {
318
- const result = installPi({ root: REPO_ROOT, output: fx.output });
328
+ const result = await installPi({ root: REPO_ROOT, output: fx.output });
319
329
 
320
330
  const names = result.agents.map((a) => a.name).sort();
321
331
  assert.deepEqual(names, ["plan", "ralph", "ralph-plan", "strategy"]);
322
332
 
323
333
  // 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");
334
+ const ralph = await readFile(join(fx.output, "agents", "ralph.agent.md"), "utf-8");
325
335
  assert.match(ralph, /name: "agent-cortex:ralph"/);
326
336
  assert.match(ralph, /tools: "bash read grep find task read_agent wait_for_agents"/);
327
337
  assert.ok(ralph.includes("## Spawning subagents (PI)"), "{{SECTION:polling}} composed from pi/polling.md");
328
338
  const bodyTokens = ralph.split("---\n").slice(2).join("---\n");
329
339
  assert.ok(!/{{/.test(bodyTokens), "no literal tokens in ralph body");
330
340
 
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"/);
341
+ // plan body carries the relative {{PATH:...}} resolved against the plugin root.
342
+ // Tool list preserves authoring order (composer translateToolList, unlike the
343
+ // runtime agent-modes translateTools which sorts): ask_user → ask_questions,
344
+ // skill → read (last position).
345
+ const plan = await readFile(join(fx.output, "agents", "plan.agent.md"), "utf-8");
346
+ assert.match(plan, /tools: "bash read edit write grep find ask_questions task read_agent read"/);
334
347
  assert.ok(
335
348
  plan.includes("~/.pi/agent/npm/node_modules/@jaybeeuu/agent-cortex/skills/workflow/plan/SKILL.md"),
336
349
  "plan PATH token resolved against token-map plugin root",
337
350
  );
338
351
 
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");
352
+ // ralph-plan prose {{TOOL:ask_user}} now substitutes to ask_questions
353
+ const ralphPlan = await readFile(join(fx.output, "agents", "ralph-plan.agent.md"), "utf-8");
354
+ assert.ok(!ralphPlan.includes("ask_user"), "ask_user never appears in ralph-plan body");
355
+ assert.ok(ralphPlan.includes("ask_questions"), "{{TOOL:ask_user}} substituted to ask_questions");
342
356
  assert.ok(
343
- result.warnings.some((w) => w.includes("ask_user")),
344
- "ralph-plan null tool dropped with a warning",
357
+ !result.warnings.some((w) => w.includes("ask_user")),
358
+ "no null-tool warning for ralph-plan (ask_user now maps to ask_questions)",
345
359
  );
346
360
 
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"/);
361
+ // strategy: web_fetch → fetch_content; ask_user → ask_questions; skill → read
362
+ // (authoring order preserved: bash, view→read, rg→grep, glob→find, ask_user→
363
+ // ask_questions, web_fetch→fetch_content, skill→read, edit, create)
364
+ const strategy = await readFile(join(fx.output, "agents", "strategy.agent.md"), "utf-8");
365
+ assert.match(strategy, /tools: "bash read grep find ask_questions fetch_content read edit write"/);
350
366
  } finally {
351
- fx.cleanup();
367
+ await fx.cleanup();
352
368
  }
353
369
  });
354
370
 
355
- it("substitutes tokens in every installed skill markdown", () => {
356
- const fx = makeFixture();
371
+ it("substitutes tokens in every installed skill markdown", async () => {
372
+ const fx = await makeFixture();
357
373
  try {
358
- const result = installPi({ root: REPO_ROOT, output: fx.output });
374
+ const result = await installPi({ root: REPO_ROOT, output: fx.output });
359
375
  assert.ok(result.skills.skills > 20, `expected the full skill tree, got ${result.skills.skills}`);
360
376
 
361
- const files = listFiles(join(fx.output, "skills")).filter((f) => f.endsWith(".md"));
377
+ const files = (await listFiles(join(fx.output, "skills"))).filter((f) => f.endsWith(".md"));
362
378
  assert.ok(files.length > 0);
363
379
  for (const f of files) {
364
- const content = readFileSync(join(fx.output, "skills", f), "utf-8");
380
+ const content = await readFile(join(fx.output, "skills", f), "utf-8");
365
381
  assert.ok(!/{{/.test(content), `no literal tokens in skill ${f}`);
366
382
  }
367
383
  } finally {
368
- fx.cleanup();
384
+ await fx.cleanup();
369
385
  }
370
386
  });
371
387
  });
@@ -16,7 +16,7 @@ decisions behind the map.
16
16
  |---|---|---|---|
17
17
  | `copilot` | GitHub Copilot CLI | `agents/<name>/` composable dirs → generated `agents/*.agent.md` | `~/.copilot/installed-plugins/_direct/agent-cortex` |
18
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) |
19
+ | `claude` | Claude Code | `~/.agent-cortex/claude/agents/*.md` (materialised home install) | `${CLAUDE_PLUGIN_ROOT}` (env var) |
20
20
 
21
21
  The harness ids match the values accepted by `agent-cortex install <harness>` in
22
22
  `lib/cli.mjs` (`SUPPORTED_HARNESSES`). The pi `plugin_root` row is the npm-layout
@@ -48,11 +48,19 @@ rather than inventing a dotted form:
48
48
  harness. Source of truth for the copilot → claude half is `token-map.json`'s own claude
49
49
  column, consumed by `bin/installers/claude.mjs` (the install-time generator — PR #91, #92); the pi column is grounded
50
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.
51
+ `ls`) plus `ask_questions` — added by the pi-questions package (mapped from
52
+ `ask_user`) — and `task` / `read_agent` / `wait_for_agents` subagent tools; `skill` maps
53
+ to `read` (skills load by reading the listed SKILL.md path).
52
54
  - **`paths`** — `plugin_root`, `agents_dir`, and `skills_dir`. These are the install-time
53
55
  variables that differ per harness. `plugin_root` is the anchor; the other two resolve
54
56
  relative to it (`{base, relative}` specs) so a harness only overrides the single root it
55
57
  actually installs into.
58
+ Claude's `plugin_root` stays the `${CLAUDE_PLUGIN_ROOT}` env-var token, never a literal
59
+ `~/.agent-cortex/claude`: Claude Code **copies** marketplace-installed plugins — including
60
+ local directory sources like `~/.agent-cortex/claude` — into its own cache, so only the
61
+ runtime env var (expanded per context by Claude Code) points at wherever the plugin
62
+ actually runs from. A literal home path in substituted files would break under
63
+ cache-copy, so the installer passes the env form through verbatim.
56
64
  - **`agents`** — the `agent-cortex:` name-prefix convention per harness (claude strips the
57
65
  prefix during conversion) and the deferred-agent exception (claude's `ralph` is authored
58
66
  natively, never generated). Agent names themselves do not get per-row mappings because
@@ -83,8 +91,10 @@ The pi install-time installer (`bin/installers/pi.mjs`) follows the strict contr
83
91
  One consumer deviates deliberately: the pi `agent-modes` extension reads the map at
84
92
  **runtime** (not install time) to compose agent prompts from the composable agents
85
93
  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`.
94
+ native PI tool — while a mapped-null tool is omitted with a warning. The pi column
95
+ currently maps every canonical tool (`ask_user` → `ask_questions`, `skill` → `read`),
96
+ so the warn-and-omit path applies only to future null entries. See
97
+ `extensions/agent-modes/README.md`.
88
98
 
89
99
  ### 4. Token substitution contract
90
100
 
@@ -103,9 +113,9 @@ The full contract is embedded in `token-map.json` under `contract`. In summary,
103
113
  - Write tokens with the **canonical (copilot) names**: `Use {{TOOL:task}} to spawn
104
114
  subagents`, `Read {{PATH:skills/workflow/plan/SKILL.md}}`.
105
115
  - 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.
116
+ `skills/**/SKILL.md`) — that defeats the map. The materialised Claude plugin
117
+ (`~/.agent-cortex/claude`), the generated `agents/*.agent.md` files, and any installed
118
+ copies are the only places substituted names appear.
109
119
  - When you add a new tool to an agent's frontmatter, add a row to `tools` in
110
120
  `token-map.json` in the same change — the contract treats unknown tools as errors, so a
111
121
  missing row breaks the install pipeline loudly instead of silently.
@@ -119,9 +129,10 @@ The full contract is embedded in `token-map.json` under `contract`. In summary,
119
129
  `{{PATH:...}}` against the map and `{{SECTION:...}}` from the per-harness section
120
130
  files; `bin/installers/copilot.mjs` is the shared generator both `pnpm build:copilot`
121
131
  and `agent-cortex install copilot` run.
122
- - `bin/installers/claude.mjs` — the single Claude generator, behind both
123
- `agent-cortex install claude` and the `pnpm build:claude` alias (no build-time
124
- counterpart; the committed `claude/` subtree is regenerated by it and drift-checked).
132
+ - `bin/installers/claude.mjs` — the single Claude generator: plain
133
+ `agent-cortex install claude` materialises the plugin into `~/.agent-cortex/claude`
134
+ with token-substituted skills and registers it with Claude Code; `--output <dir>` is the
135
+ generate-only form (tests/CI structural checks). The repo commits no `claude/` output.
125
136
  - `bin/installers/pi.mjs` — the pi harness installer (`agent-cortex install pi`), an
126
137
  install-time consumer of the map's contract.
127
138
  - `lib/cli.mjs` — `SUPPORTED_HARNESSES`, the install command surface the harness ids come
package/token-map.json CHANGED
@@ -20,9 +20,9 @@
20
20
  "grep": { "copilot": "grep", "claude": "Grep", "pi": "grep" },
21
21
  "rg": { "copilot": "rg", "claude": "Grep", "pi": "grep" },
22
22
  "glob": { "copilot": "glob", "claude": "Glob", "pi": "find" },
23
- "ask_user": { "copilot": "ask_user", "claude": "AskUserQuestion", "pi": null },
23
+ "ask_user": { "copilot": "ask_user", "claude": "AskUserQuestion", "pi": "ask_questions" },
24
24
  "web_fetch": { "copilot": "web_fetch", "claude": "WebFetch", "pi": "fetch_content" },
25
- "skill": { "copilot": "skill", "claude": "Skill", "pi": null },
25
+ "skill": { "copilot": "skill", "claude": "Skill", "pi": "read" },
26
26
  "task": { "copilot": "task", "claude": "Task", "pi": "task" },
27
27
  "read_agent": { "copilot": "read_agent", "claude": null, "pi": "read_agent" },
28
28
  "wait_for_agents": { "copilot": null, "claude": null, "pi": "wait_for_agents" }
@@ -56,21 +56,22 @@
56
56
  "sources": {
57
57
  "copilot": "agents/<name>/ composable dirs (canonical) — composed to agents/*.agent.md by scripts/build-copilot-agents.mjs",
58
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"
59
+ "claude": "agents/<name>/ composable dirs (canonical) — composed by bin/installers/claude.mjs (install-time generator, the only generator — the repo commits no claude/ output), plus agents-native/*.md for deferred agents"
60
60
  }
61
61
  },
62
62
  "notes": {
63
63
  "tools": {
64
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
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.",
66
+ "ask_user": "Copilot (ask_user) and Claude (AskUserQuestion) prompt the user mid-run. pi's equivalent is ask_questions, provided by the pi-questions package (pi install npm:pi-questions; declared in pi/settings.json) — the genuine interactive-questioning tool, restoring grilling in plan/ralph-plan/strategy modes. Like fetch_content (pi-web-access), it is only present when its package is installed, so the mapping depends on that package (see the extension-manifests idea). pi still intentionally ships no permission popups; ask_questions is the interactive mid-run question tool, not a popup.",
67
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.",
68
+ "skill": "Copilot (skill) and Claude (Skill) expose skills as a tool. pi has no dedicated skill tool — skills are surfaced in the always-present 'Available skills' context block and loaded by reading the listed SKILL.md path ({{PATH:<skill>/SKILL.md}}). The pi mapping is read, the tool used to load a skill's file; a dedicated skill tool is a follow-on design candidate if prompting proves insufficient.",
69
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
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
71
  },
72
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."
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
+ "claude": "plugin_root stays ${CLAUDE_PLUGIN_ROOT} (env var) for the materialised plugin — never a literal ~/.agent-cortex/claude. Claude Code copies marketplace-installed plugins (including local directory sources like ~/.agent-cortex/claude) into its own cache and expands the env var per context, so a literal home path in substituted files would break under cache-copy; the installer passes the env form through verbatim."
74
75
  },
75
76
  "agents": {
76
77
  "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>.",
@@ -1,16 +0,0 @@
1
- {
2
- "name": "jaybeeuu",
3
- "owner": {
4
- "name": "jaybeeuu"
5
- },
6
- "metadata": {
7
- "description": "jaybeeuu's personal Claude Code plugins."
8
- },
9
- "plugins": [
10
- {
11
- "name": "agent-cortex",
12
- "source": "./claude",
13
- "description": "Personal Claude Code plugin with custom agents and skills"
14
- }
15
- ]
16
- }
@@ -1,11 +0,0 @@
1
- {
2
- "name": "agent-cortex",
3
- "version": "1.38.1",
4
- "description": "Personal Claude Code plugin with custom agents and skills",
5
- "author": {
6
- "name": "jaybeeuu"
7
- },
8
- "license": "MIT",
9
- "repository": "https://github.com/jaybeeuu/agent-cortex",
10
- "hooks": "./hooks.json"
11
- }
@@ -1,58 +0,0 @@
1
- ---
2
- # GENERATED from agents/plan/ by bin/installers/claude.mjs — DO NOT EDIT.
3
- name: plan
4
- description: "Guide a feature from first discussion through PRD and task breakdown to classified beads ready for ralph execution. Full end-to-end planning workflow using the plan skill. Use when starting a new feature, scoping work to hand off to ralph, or saying 'plan this'."
5
- tools: Bash, Read, Edit, Write, Grep, Glob, AskUserQuestion, Task, Skill
6
- ---
7
-
8
- # plan — end-to-end feature planning agent
9
-
10
- You are a planning agent. Your job is to guide a feature from first discussion through PRD and task breakdown to classified beads ready for ralph execution.
11
-
12
- You follow the **plan** skill workflow. The skill is defined in `skills/workflow/plan/SKILL.md`. Read it now to load the full instructions.
13
-
14
- ## Workflow summary
15
-
16
- ### Phase 1 — Understand and specify
17
- 1. Ask the user to describe the problem, goal, or feature.
18
- 2. Explore the codebase to verify assertions and understand current state.
19
- 3. Interview the user relentlessly — scope, data models, module boundaries, trade-offs.
20
- 4. Sketch major modules.
21
- 5. Draft a PRD using the `write-a-prd` template.
22
- 6. Present the PRD for user approval.
23
- 7. Create an epic bead from the approved PRD.
24
-
25
- ### Phase 2 — Break into tasks
26
- 8. Review PRD stories and identify vertical-slice phases.
27
- 9. Define each phase (title, stories, what to build, acceptance criteria).
28
- 10. Create epics for multi-phase work.
29
- 11. Present the breakdown for user approval.
30
-
31
- ### Phase 3 — Create and classify beads
32
- 13. Create task beads via `create-task` for each task.
33
- 14. Tag parent epics with aggregate classification.
34
- 15. Present the full bead tree.
35
-
36
- ### Phase 4 — Prepare handoff to ralph
37
- 16. Determine the handoff bead (epic or meta-epic).
38
- 17. Tag it `workflow:ralph`.
39
- 18. Push bead state with `bd dolt push`.
40
- 19. Present the handoff summary.
41
-
42
- ## Permitted writes
43
-
44
- | Location | Purpose |
45
- |---|---|
46
- | `.agent-cortex/working-docs/` | Research notes, decisions, exploration findings — gitignored |
47
- | Bead fields (`description`, `design`) | Plans and specifications |
48
- | `docs/prd/<topic>.md` | PRD documents |
49
-
50
- Do **not** modify source code or tracked documentation outside these locations.
51
-
52
- ## Constraints
53
-
54
- - Never modify source code.
55
- - Always get user approval before moving between phases.
56
- - Each phase has a gate — do not skip it.
57
- - Work in `.agent-cortex/working-docs/` for scratchpad notes.
58
- - Use `bd` commands for all bead operations.