@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,376 @@
1
+ // Install-time generator + registrar for the Claude Code plugin. Producing
2
+ // claude/ from the canonical sources is the ONLY code path — there is no
3
+ // separate build-time generator: the package script `pnpm build:claude` runs
4
+ // the same command as the CLI with `--output` (`node bin/agent-cortex.mjs
5
+ // install claude --output claude`), so the committed subtree and installer
6
+ // output can never diverge:
7
+ //
8
+ // node bin/agent-cortex.mjs install claude regenerates claude/ in place
9
+ // AND registers the plugin with Claude Code via
10
+ // the `claude plugin` CLI (marketplace add →
11
+ // install → marketplace update → plugin update)
12
+ // node bin/agent-cortex.mjs install claude --output <dir>
13
+ // generates only — no runtime registration
14
+ // node bin/agent-cortex.mjs install claude --dry-run
15
+ // plans generation + registration without writing
16
+ //
17
+ // claude/ stays committed (not gitignored): registration points Claude Code's
18
+ // marketplace at this checkout (`.claude-plugin/marketplace.json` exposes
19
+ // ./claude), so the plugin content is the working tree and a fresh clone
20
+ // installs as-is with no generation step. CI regenerates claude/ via
21
+ // `--output` and runs `git diff --exit-code claude/...` — byte-for-byte
22
+ // validation that the committed output equals installer output, catching stale
23
+ // output and hand-edits to generated files alike.
24
+ //
25
+ // Generated from:
26
+ // claude/agents/<slug>.md agents/<name>/agent.md +
27
+ // agents/<name>/claude/frontmatter.json +
28
+ // agents/<name>/claude/<section>.md ({{SECTION:...}}),
29
+ // with {{TOOL:...}}/{{PATH:...}} substituted against
30
+ // the claude column of token-map.json. Agents in the
31
+ // DEFER set ship instead from agents-native/<name>.md.
32
+ // claude/skills/<name> symlinks to the grouped skills/<group>/<name> dirs
33
+ // (single source — Claude scans skills one level deep)
34
+ // claude/.claude-plugin/plugin.json name/version/description from package.json
35
+ // (version tracks the package — never stale)
36
+ // claude/hooks.json copied from hooks/claude/hooks.json when present
37
+ // claude/hooks/<scripts> copied from hooks/claude/ (support files bundled
38
+ // alongside hooks.json, referenced via ${CLAUDE_PLUGIN_ROOT})
39
+ //
40
+ // The claude/ subtree stays committed and CI drift-checks it (build:claude +
41
+ // git diff --exit-code), so a fresh clone remains installable as-is. Hand-authored
42
+ // support files under claude/ (claude/.mcp.json, claude/scripts/) are left untouched.
43
+ //
44
+ // Zero dependencies so it runs on the CI Node and local Node alike.
45
+
46
+ import { readFileSync, writeFileSync, mkdirSync, rmSync, symlinkSync, statSync, existsSync, readdirSync } from "node:fs";
47
+ import { spawnSync } from "node:child_process";
48
+ import { join, dirname, relative, resolve } from "node:path";
49
+ import { fileURLToPath } from "node:url";
50
+ import { composeAgent } from "../../scripts/lib/compose-agent.mjs";
51
+
52
+ const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
53
+ const DEFAULT_ROOT = join(MODULE_DIR, "..", "..");
54
+
55
+ // Agents NOT transformed from the composable sources. `ralph` is authored natively for Claude
56
+ // instead (agents-native/ralph.md) — its event-driven orchestration can't be produced by
57
+ // section composition from the poll-loop sources.
58
+ const DEFER = new Set(["ralph"]);
59
+
60
+ // Ralph-coupled Copilot skills that don't apply to the lean Claude Ralph (and carry dead
61
+ // ~/.copilot orchestration paths). Not symlinked into the Claude plugin.
62
+ const SKILL_EXCLUDE = new Set(["ralph", "run-pipeline-stage"]);
63
+
64
+ const NAME_PREFIX = "agent-cortex:";
65
+
66
+ function isFile(p) {
67
+ try {
68
+ return statSync(p).isFile();
69
+ } catch {
70
+ return false;
71
+ }
72
+ }
73
+
74
+ function byName(a, b) {
75
+ return a.localeCompare(b);
76
+ }
77
+
78
+ /** Compose agents from agents/<name>/; DEFERed agents come from agents-native/ instead. */
79
+ function buildAgents(root) {
80
+ const files = [];
81
+ const skipped = [];
82
+ const entries = readdirSync(join(root, "agents"), { withFileTypes: true }).sort((a, b) => byName(a.name, b.name));
83
+
84
+ for (const entry of entries) {
85
+ if (!entry.isDirectory()) continue;
86
+ const dir = join(root, "agents", entry.name);
87
+ if (!isFile(join(dir, "agent.md")) || !isFile(join(dir, "claude", "frontmatter.json"))) continue;
88
+
89
+ if (DEFER.has(entry.name)) {
90
+ skipped.push(entry.name);
91
+ continue;
92
+ }
93
+
94
+ const fm = composeAgent(root, entry.name, "claude");
95
+ const slug = fm.name.startsWith(NAME_PREFIX) ? fm.name.slice(NAME_PREFIX.length) : fm.name;
96
+ const header =
97
+ `---\n` +
98
+ `# GENERATED from agents/${entry.name}/ by bin/installers/claude.mjs — DO NOT EDIT.\n` +
99
+ `name: ${slug}\n` +
100
+ `description: ${JSON.stringify(fm.description)}\n` +
101
+ `tools: ${fm.tools.join(", ")}\n` +
102
+ `---\n`;
103
+ files.push({ file: `${slug}.md`, content: `${header}\n${fm.body}\n` });
104
+ }
105
+ return { files, skipped };
106
+ }
107
+
108
+ /** Copy hand-authored Claude-native agents into claude/agents/ verbatim. */
109
+ function buildNatives(root, nativeSrc) {
110
+ const files = [];
111
+ if (!existsSync(nativeSrc)) return files;
112
+ for (const file of readdirSync(nativeSrc).filter((f) => f.endsWith(".md")).sort(byName)) {
113
+ files.push({ file, content: readFileSync(join(nativeSrc, file), "utf-8") });
114
+ }
115
+ return files;
116
+ }
117
+
118
+ /** Flatten grouped skills (skills/<group>/<name>/) into claude/skills/ as relative symlinks. */
119
+ function buildSkills(root) {
120
+ const links = [];
121
+ const seen = new Map();
122
+ for (const group of readdirSync(join(root, "skills")).sort(byName)) {
123
+ const groupDir = join(root, "skills", group);
124
+ if (!statSync(groupDir).isDirectory()) continue;
125
+ for (const name of readdirSync(groupDir).sort(byName)) {
126
+ const skillDir = join(groupDir, name);
127
+ if (!isFile(join(skillDir, "SKILL.md"))) continue;
128
+ if (SKILL_EXCLUDE.has(name)) continue;
129
+ if (seen.has(name)) {
130
+ throw new Error(`duplicate skill name "${name}" (${seen.get(name)} and ${group}) — names must be unique when flattened`);
131
+ }
132
+ seen.set(name, group);
133
+ links.push({ name, target: skillDir });
134
+ }
135
+ }
136
+ return links;
137
+ }
138
+
139
+ /** plugin.json manifest; version tracks the package so it can never go stale. */
140
+ function buildPluginJson(root) {
141
+ const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf-8"));
142
+ return (
143
+ JSON.stringify(
144
+ {
145
+ name: "agent-cortex",
146
+ version: pkg.version,
147
+ description: "Personal Claude Code plugin with custom agents and skills",
148
+ author: { name: "jaybeeuu" },
149
+ license: pkg.license ?? "MIT",
150
+ repository: pkg.repository,
151
+ hooks: "./hooks.json",
152
+ },
153
+ null,
154
+ 2,
155
+ ) + "\n"
156
+ );
157
+ }
158
+
159
+ /**
160
+ * Hook support files (hooks/claude/ except hooks.json) bundled into
161
+ * claude/hooks/ so hook commands can reference them via ${CLAUDE_PLUGIN_ROOT}.
162
+ * hooks.json itself is special-cased to the plugin root (claude/hooks.json).
163
+ */
164
+ function buildHookFiles(root) {
165
+ const files = [];
166
+ const srcDir = join(root, "hooks", "claude");
167
+ if (!existsSync(srcDir)) return files;
168
+ const walk = (dir, rel) => {
169
+ for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) => byName(a.name, b.name))) {
170
+ const full = join(dir, entry.name);
171
+ if (entry.isDirectory()) walk(full, join(rel, entry.name));
172
+ else if (entry.name !== "hooks.json")
173
+ files.push({ rel: join(rel, entry.name), content: readFileSync(full, "utf-8") });
174
+ }
175
+ };
176
+ walk(srcDir, "");
177
+ return files;
178
+ }
179
+
180
+ /**
181
+ * Generate the Claude Code plugin subtree.
182
+ *
183
+ * @param {object} options
184
+ * @param {string} [options.root] Package root (contains agents/, skills/, token-map.json, …).
185
+ * Defaults to the repo containing this module.
186
+ * @param {string} [options.output] Target directory (default: <root>/claude).
187
+ * @param {boolean} [options.dryRun] Plan and report without writing anything.
188
+ * @returns {{ output: string, agents: string[], natives: string[], skills: string[], hooks: boolean, dryRun: boolean }}
189
+ */
190
+ export function installClaude({ root = DEFAULT_ROOT, output, dryRun = false } = {}) {
191
+ const out = output ?? join(root, "claude");
192
+
193
+ const agents = buildAgents(root);
194
+ const natives = buildNatives(root, join(root, "agents-native"));
195
+ const skills = buildSkills(root);
196
+ const pluginJson = buildPluginJson(root);
197
+ const hooksSrc = join(root, "hooks", "claude", "hooks.json");
198
+ const hooksJson = isFile(hooksSrc) ? readFileSync(hooksSrc, "utf-8") : null;
199
+ const hookFiles = buildHookFiles(root);
200
+
201
+ const summary = {
202
+ output: out,
203
+ agents: agents.files.map((f) => f.file.replace(/\.md$/, "")),
204
+ natives: natives.map((f) => f.file.replace(/\.md$/, "")),
205
+ skills: skills.map((l) => l.name),
206
+ hooks: hooksJson !== null,
207
+ hookFiles: hookFiles.map((f) => f.rel),
208
+ dryRun,
209
+ };
210
+
211
+ if (agents.skipped.length) console.log(`Skipped composable source(s): ${agents.skipped.join(", ")}`);
212
+
213
+ if (dryRun) {
214
+ console.log(`DRY-RUN — would generate into ${out}, no files written:`);
215
+ console.log(` agents: ${summary.agents.join(", ")}`);
216
+ if (summary.natives.length) console.log(` native agents: ${summary.natives.join(", ")}`);
217
+ console.log(` skills: ${summary.skills.length} symlink(s)`);
218
+ console.log(` .claude-plugin/plugin.json (version ${JSON.parse(pluginJson).version})`);
219
+ if (summary.hooks) console.log(" hooks.json (from hooks/claude/hooks.json)");
220
+ if (summary.hookFiles.length)
221
+ console.log(` hook scripts: ${summary.hookFiles.join(", ")} (bundled into claude/hooks/)`);
222
+ return summary;
223
+ }
224
+
225
+ // Generated subsets only — hand-authored support files (.mcp.json, scripts/)
226
+ // under output are left untouched.
227
+ const agentOut = join(out, "agents");
228
+ rmSync(agentOut, { recursive: true, force: true });
229
+ mkdirSync(agentOut, { recursive: true });
230
+ for (const { file, content } of [...agents.files, ...natives]) {
231
+ writeFileSync(join(agentOut, file), content);
232
+ }
233
+ console.log(`Generated ${summary.agents.length} Claude agent(s): ${summary.agents.join(", ")}`);
234
+ if (summary.natives.length) console.log(`Copied ${summary.natives.length} native Claude agent(s): ${summary.natives.join(", ")}`);
235
+
236
+ const skillOut = join(out, "skills");
237
+ rmSync(skillOut, { recursive: true, force: true });
238
+ mkdirSync(skillOut, { recursive: true });
239
+ for (const { name, target } of skills) {
240
+ symlinkSync(relative(skillOut, target), join(skillOut, name));
241
+ }
242
+ console.log(`Linked ${summary.skills.length} skill(s) into ${skillOut}`);
243
+
244
+ mkdirSync(join(out, ".claude-plugin"), { recursive: true });
245
+ writeFileSync(join(out, ".claude-plugin", "plugin.json"), pluginJson);
246
+ console.log(`Wrote ${join(out, ".claude-plugin", "plugin.json")}`);
247
+
248
+ if (summary.hooks) {
249
+ writeFileSync(join(out, "hooks.json"), hooksJson);
250
+ console.log(`Wrote ${join(out, "hooks.json")} (from ${hooksSrc})`);
251
+
252
+ if (hookFiles.length > 0) {
253
+ const hookOut = join(out, "hooks");
254
+ rmSync(hookOut, { recursive: true, force: true });
255
+ mkdirSync(hookOut, { recursive: true });
256
+ for (const { rel, content } of hookFiles) {
257
+ const dest = join(hookOut, rel);
258
+ mkdirSync(dirname(dest), { recursive: true });
259
+ writeFileSync(dest, content);
260
+ }
261
+ console.log(`Bundled ${hookFiles.length} hook support file(s) into ${hookOut}`);
262
+ }
263
+ } else {
264
+ console.log(`Skipped hooks.json — ${hooksSrc} not found (existing file left untouched)`);
265
+ }
266
+
267
+ return summary;
268
+ }
269
+
270
+ // ─── Claude Code registration ────────────────────────────────────────────────
271
+
272
+ // The `claude` CLI on PATH. Overridable in tests via the claudeBin option.
273
+ const DEFAULT_CLAUDE_BIN = "claude";
274
+
275
+ /**
276
+ * Read the marketplace manifest that exposes this checkout's claude/ subtree
277
+ * (`.claude-plugin/marketplace.json`); returns the marketplace and plugin
278
+ * names that `claude plugin` commands address. Registration is only possible
279
+ * from a checkout carrying the manifest.
280
+ */
281
+ function readMarketplaceManifest(root) {
282
+ const manifestPath = join(root, ".claude-plugin", "marketplace.json");
283
+ let manifest;
284
+ try {
285
+ manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
286
+ } catch (err) {
287
+ throw new Error(
288
+ `no Claude marketplace manifest at ${manifestPath} (${err.message}) — ` +
289
+ `"agent-cortex install claude" registers the plugin by pointing Claude Code at this checkout`,
290
+ );
291
+ }
292
+ const marketplace = manifest.name;
293
+ const plugin = manifest.plugins?.[0]?.name;
294
+ if (typeof marketplace !== "string" || typeof plugin !== "string") {
295
+ throw new Error(
296
+ `unparsable marketplace manifest at ${manifestPath}: expected { name, plugins: [{ name }] }`,
297
+ );
298
+ }
299
+ return { marketplace, plugin };
300
+ }
301
+
302
+ /** Run one claude plugin command, inheriting its output; throws on failure. */
303
+ function runClaudeCommand(claudeBin, args) {
304
+ const label = `${claudeBin} ${args.join(" ")}`;
305
+ const result = spawnSync(claudeBin, args, { stdio: "inherit" });
306
+ if (result.error) {
307
+ throw new Error(`failed to run "${label}": ${result.error.message}`);
308
+ }
309
+ if (result.status !== 0) {
310
+ throw new Error(`"${label}" exited ${result.status}`);
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Register the freshly generated claude/ subtree with Claude Code by driving
316
+ * the `claude plugin` CLI against this checkout as the marketplace source:
317
+ *
318
+ * 1. `claude plugin marketplace add <root>` (idempotent: already on disk)
319
+ * 2. `claude plugin install <plugin>@<market> -y` (idempotent: already installed)
320
+ * 3. `claude plugin marketplace update <market>` (re-validate the directory source)
321
+ * 4. `claude plugin update <plugin> -y` (refresh the cache on version bumps)
322
+ *
323
+ * Requires a Claude Code binary whose `plugin` subcommand exists (v2+); older
324
+ * CLIs treat the args as a chat prompt, so support is probed first. In
325
+ * --dry-run mode nothing is spawned — the commands are reported as a plan.
326
+ *
327
+ * @param {object} options
328
+ * @param {string} [options.root] Checkout root carrying .claude-plugin/marketplace.json
329
+ * (defaults to the repo containing this module)
330
+ * @param {string} [options.claudeBin] The `claude` executable (default: `claude` from PATH)
331
+ * @param {boolean} [options.dryRun] Plan and report without running or writing
332
+ * @returns {{ marketplace: string, plugin: string, commands: string[][], dryRun: boolean }}
333
+ */
334
+ export function registerClaude({ root = DEFAULT_ROOT, claudeBin = DEFAULT_CLAUDE_BIN, dryRun = false } = {}) {
335
+ const { marketplace, plugin } = readMarketplaceManifest(root);
336
+ const pluginId = `${plugin}@${marketplace}`;
337
+ const commands = [
338
+ ["plugin", "marketplace", "add", resolve(root)],
339
+ ["plugin", "install", pluginId, "-y"],
340
+ ["plugin", "marketplace", "update", marketplace],
341
+ ["plugin", "update", plugin, "-y"],
342
+ ];
343
+
344
+ const summary = { marketplace, plugin, commands, dryRun };
345
+ console.log(`Registering plugin "${pluginId}" with Claude Code (via ${claudeBin})…`);
346
+
347
+ if (dryRun) {
348
+ for (const args of commands) console.log(` would run: ${claudeBin} ${args.join(" ")}`);
349
+ console.log("(dry-run — no Claude Code state written)");
350
+ return summary;
351
+ }
352
+
353
+ // Older Claude Code builds treat `claude plugin …` as a chat prompt; reject
354
+ // them up-front instead of letting the registration appear to succeed.
355
+ const probe = spawnSync(claudeBin, ["plugin", "--help"], { stdio: "ignore" });
356
+ if (probe.error) {
357
+ throw new Error(
358
+ `cannot run "${claudeBin}" (${probe.error.message}) — is Claude Code installed and on PATH?`,
359
+ );
360
+ }
361
+ if (probe.status !== 0) {
362
+ throw new Error(
363
+ `"${claudeBin}" lacks the "plugin" subcommand — its Claude Code build predates plugin support; ` +
364
+ `update to a build with the plugin CLI (v2+), e.g. "pnpm add -g @anthropic-ai/claude-code@latest", and re-run`,
365
+ );
366
+ }
367
+
368
+ for (const args of commands) {
369
+ console.log(` ${claudeBin} ${args.join(" ")}`);
370
+ runClaudeCommand(claudeBin, args);
371
+ }
372
+ console.log(
373
+ `Installed — verify with "claude plugin list" / "claude plugin details ${plugin}" (restart Claude Code to apply).`,
374
+ );
375
+ return summary;
376
+ }
@@ -0,0 +1,129 @@
1
+ // Install-time generator for the Copilot CLI flat agent files (agents/*.agent.md).
2
+ // This is the single code path for producing those files from the canonical
3
+ // composable sources — both entry points below call installCopilot(), so
4
+ // install-time and build-time output can never diverge:
5
+ //
6
+ // - bin/agent-cortex.mjs `agent-cortex install copilot` (install-time;
7
+ // `--output <dir>` overrides the target,
8
+ // `--dry-run` plans without writing)
9
+ // - scripts/build-copilot-agents.mjs `pnpm build:copilot` (dev/CI: regenerates
10
+ // the committed agents/*.agent.md files in place)
11
+ //
12
+ // Output lands in <root>/agents/ (the default `output`) because that is the
13
+ // directory the Copilot plugin actually loads: plugin.json declares
14
+ // "agents": "agents/", which scans for *.agent.md files. The generated flat
15
+ // files therefore share the agents/ directory with the canonical composable
16
+ // dirs they are composed from (agents/<name>/) — only flat *.agent.md files are
17
+ // written, nothing is deleted, so a generated file can never clobber a
18
+ // composable source. `--output` exists for the same preview/test contract as the
19
+ // claude and pi installers: point it at a scratch dir and inspect the output.
20
+ //
21
+ // agents/<name>.agent.md composed from agents/<name>/agent.md +
22
+ // agents/<name>/copilot/frontmatter.json +
23
+ // agents/<name>/copilot/<section>.md ({{SECTION:...}}),
24
+ // with {{TOOL:...}}/{{PATH:...}} substituted against
25
+ // the copilot column of token-map.json.
26
+ //
27
+ // The flat files stay committed and CI drift-checks them (build:copilot +
28
+ // git diff --exit-code -- 'agents/*.agent.md'), so a fresh clone (or a direct
29
+ // path plugin install) ships the plugin read-ready.
30
+ //
31
+ // Zero dependencies so it runs on the CI Node and local Node alike.
32
+
33
+ import { writeFileSync, mkdirSync, readdirSync, statSync, existsSync } from "node:fs";
34
+ import { join, dirname } from "node:path";
35
+ import { fileURLToPath } from "node:url";
36
+ import { composeAgent } from "../../scripts/lib/compose-agent.mjs";
37
+
38
+ const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
39
+ const DEFAULT_ROOT = join(MODULE_DIR, "..", "..");
40
+
41
+ function isDirectory(p) {
42
+ try {
43
+ return statSync(p).isDirectory();
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+
49
+ function byName(a, b) {
50
+ return a.localeCompare(b);
51
+ }
52
+
53
+ /**
54
+ * Serialise the flat-file YAML frontmatter. Field order and names mirror the
55
+ * pre-migration flat sources (description, name, tools, argument-hint) so
56
+ * Copilot CLI sees the same agent metadata. The GENERATED comment references
57
+ * scripts/build-copilot-agents.mjs — the committed entry point — so the
58
+ * generated bytes are identical whichever of the two entry points ran.
59
+ */
60
+ function frontmatterYaml(name, fm) {
61
+ const lines = [
62
+ "---",
63
+ `# GENERATED from agents/${name}/ by scripts/build-copilot-agents.mjs — DO NOT EDIT.`,
64
+ `description: ${JSON.stringify(fm.description)}`,
65
+ `name: ${JSON.stringify(fm.name)}`,
66
+ `tools: [${fm.tools.map((t) => JSON.stringify(t)).join(", ")}]`,
67
+ ];
68
+ if (fm.argumentHint) lines.push(`argument-hint: ${JSON.stringify(fm.argumentHint)}`);
69
+ lines.push("---");
70
+ return lines.join("\n");
71
+ }
72
+
73
+ /** Compose agents from agents/<name>/; every composable dir must ship a copilot harness. */
74
+ function buildAgents(root) {
75
+ const files = [];
76
+ const entries = readdirSync(join(root, "agents"), { withFileTypes: true }).sort((a, b) => byName(a.name, b.name));
77
+
78
+ for (const entry of entries) {
79
+ if (!entry.isDirectory()) continue; // skip generated *.agent.md flat files and README
80
+ const dir = join(root, "agents", entry.name);
81
+ if (!existsSync(join(dir, "agent.md"))) continue; // not a composable agent dir
82
+ if (!isDirectory(join(dir, "copilot")) || !existsSync(join(dir, "copilot", "frontmatter.json"))) {
83
+ throw new Error(`agent "${entry.name}": composable directory without copilot/frontmatter.json — define the copilot harness or exclude the agent`);
84
+ }
85
+
86
+ const fm = composeAgent(root, entry.name, "copilot");
87
+ files.push({
88
+ name: entry.name,
89
+ file: `${entry.name}.agent.md`,
90
+ content: `${frontmatterYaml(entry.name, fm)}\n\n${fm.body}\n`,
91
+ });
92
+ }
93
+ return files;
94
+ }
95
+
96
+ /**
97
+ * Generate the Copilot CLI flat agent files.
98
+ *
99
+ * @param {object} options
100
+ * @param {string} [options.root] Package root (contains agents/, token-map.json, …).
101
+ * Defaults to the repo containing this module.
102
+ * @param {string} [options.output] Directory to write the flat *.agent.md files into
103
+ * (default: <root>/agents — the dir plugin.json's
104
+ * "agents": "agents/" scans for the plugin).
105
+ * @param {boolean} [options.dryRun] Plan and report without writing anything.
106
+ * @returns {{ output: string, agents: string[], dryRun: boolean }}
107
+ */
108
+ export function installCopilot({ root = DEFAULT_ROOT, output, dryRun = false } = {}) {
109
+ const out = output ?? join(root, "agents");
110
+ const files = buildAgents(root);
111
+ const summary = {
112
+ output: out,
113
+ agents: files.map((f) => f.name),
114
+ dryRun,
115
+ };
116
+
117
+ if (dryRun) {
118
+ console.log(`DRY-RUN — would generate into ${out}, no files written:`);
119
+ console.log(` agents: ${summary.agents.join(", ")}`);
120
+ return summary;
121
+ }
122
+
123
+ mkdirSync(out, { recursive: true });
124
+ for (const { file, content } of files) {
125
+ writeFileSync(join(out, file), content);
126
+ }
127
+ console.log(`Generated ${files.length} Copilot agent file(s): ${summary.agents.join(", ")}`);
128
+ return summary;
129
+ }