@jaybeeuu/agent-cortex 2.1.0 → 2.3.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 (181) hide show
  1. package/CHANGELOG.md +411 -0
  2. package/README.md +33 -13
  3. package/agents/README.md +22 -10
  4. package/agents/plan/agent.md +2 -2
  5. package/agents/ralph/agent.md +126 -88
  6. package/agents/ralph/claude/frontmatter.json +3 -3
  7. package/agents/ralph/claude/wake.md +46 -0
  8. package/agents/ralph/pi/{polling.md → wake.md} +35 -20
  9. package/agents/ralph-plan/agent.md +23 -11
  10. package/bin/agent-cortex.mjs +66 -4
  11. package/bin/installers/claude.mjs +182 -68
  12. package/bin/installers/pi.mjs +200 -24
  13. package/extensions/agent-guardrails/README.md +1 -1
  14. package/extensions/agent-guardrails/guardrails.ts +1 -1
  15. package/extensions/agent-guardrails/index.ts +1 -1
  16. package/extensions/agent-modes/discover.test.ts +2 -2
  17. package/extensions/output-style/README.md +1 -1
  18. package/extensions/output-style/index.ts +2 -2
  19. package/extensions/output-style/output-style.ts +2 -2
  20. package/hooks/claude/scripts/notify.mjs +8 -1
  21. package/lib/cli.mjs +91 -2
  22. package/package.json +27 -3
  23. package/scripts/check-changeset.mjs +4 -0
  24. package/scripts/check-no-sync.mjs +5 -1
  25. package/scripts/lib/compose-agent.mjs +69 -9
  26. package/scripts/lib/fs.mjs +10 -2
  27. package/scripts/lib/migrate.mjs +736 -0
  28. package/scripts/lib/release-gates.mjs +21 -1
  29. package/scripts/lib/skill-frontmatter.mjs +23 -6
  30. package/scripts/lib/skill-links.mjs +52 -0
  31. package/scripts/lib/verify-worker-tree.mjs +25 -0
  32. package/scripts/resolve-publish-command.mjs +4 -0
  33. package/skills/engineering/design-an-interface/SKILL.md +3 -3
  34. package/skills/engineering/grill-with-docs/SKILL.md +3 -5
  35. package/skills/engineering/improve-codebase-architecture/SKILL.md +5 -4
  36. package/skills/engineering/tdd/SKILL.md +5 -7
  37. package/skills/engineering/technical-direction/SKILL.md +5 -5
  38. package/skills/planning/classify-bead/REFERENCE.md +3 -3
  39. package/skills/planning/classify-bead/SKILL.md +14 -9
  40. package/skills/planning/classify-bead/scripts/classify-bead.mjs +5 -4
  41. package/skills/planning/create-task/REFERENCE.md +52 -3
  42. package/skills/planning/create-task/SKILL.md +70 -26
  43. package/skills/planning/create-task/scripts/{create-chores.ts → create-chores.mjs} +48 -23
  44. package/skills/planning/flesh-out-idea/REFERENCE.md +116 -0
  45. package/skills/planning/flesh-out-idea/SKILL.md +146 -0
  46. package/skills/planning/prd-to-tasks/REFERENCE.md +54 -5
  47. package/skills/planning/prd-to-tasks/SKILL.md +22 -16
  48. package/skills/planning/record-idea/FORMAT.md +66 -17
  49. package/skills/planning/record-idea/SKILL.md +48 -24
  50. package/skills/planning/request-refactor-plan/SKILL.md +4 -4
  51. package/skills/planning/spec-to-tickets/SKILL.md +5 -5
  52. package/skills/planning/write-a-prd/SKILL.md +8 -9
  53. package/skills/planning/write-a-ticket/SKILL.md +4 -6
  54. package/skills/productivity/bd-tool/SKILL.md +15 -10
  55. package/skills/productivity/grill-me/SKILL.md +5 -0
  56. package/skills/productivity/init-beads/SKILL.md +3 -3
  57. package/skills/productivity/migration/REFERENCE.md +227 -0
  58. package/skills/productivity/migration/SKILL.md +188 -0
  59. package/skills/productivity/validate-agent-dir/scripts/{validate-agent-dir.ts → validate-agent-dir.mjs} +49 -28
  60. package/skills/productivity/write-a-skill/SKILL.md +5 -5
  61. package/skills/review/maintain-agent-docs/SKILL.md +5 -7
  62. package/skills/review/refactor-skill/SKILL.md +4 -3
  63. package/skills/review/review-security/SKILL.md +3 -5
  64. package/skills/style/style-code/SKILL.md +5 -10
  65. package/skills/style/style-comms/SKILL.md +5 -7
  66. package/skills/style/style-documentation/SKILL.md +5 -6
  67. package/skills/style/style-tests/SKILL.md +5 -7
  68. package/skills/workflow/git-workflow/SKILL.md +52 -26
  69. package/skills/workflow/hitl-collab/SKILL.md +4 -6
  70. package/skills/workflow/plan/SKILL.md +41 -30
  71. package/skills/workflow/ralph/REFERENCE.md +182 -51
  72. package/skills/workflow/ralph/SKILL.md +89 -50
  73. package/skills/workflow/run-pipeline-stage/SKILL.md +72 -71
  74. package/skills/workflow/run-pipeline-stage/playbooks/code.md +11 -0
  75. package/skills/workflow/run-pipeline-stage/playbooks/document.md +12 -0
  76. package/skills/workflow/run-pipeline-stage/playbooks/fix.md +12 -0
  77. package/skills/workflow/run-pipeline-stage/playbooks/review.md +11 -0
  78. package/skills/workflow/run-pipeline-stage/playbooks/verify.md +13 -0
  79. package/skills/workflow/run-pipeline-stage/prompts/stage-runner.md +73 -1
  80. package/token-map.README.md +9 -4
  81. package/token-map.json +13 -6
  82. package/.agents/skills/style-versioning/SKILL.md +0 -172
  83. package/.changeset/README.md +0 -8
  84. package/.changeset/config.json +0 -11
  85. package/.github/workflows/ci.yml +0 -302
  86. package/.junie/guidelines.md +0 -87
  87. package/.junie/mcp/mcp.json +0 -10
  88. package/AGENTS.md +0 -153
  89. package/agents/ralph/claude/polling.md +0 -31
  90. package/agents-native/ralph.md +0 -203
  91. package/docs/claude-hooks.md +0 -144
  92. package/docs/commit-signing.md +0 -231
  93. package/docs/ideas/auto-model-selection.md +0 -75
  94. package/docs/ideas/ban-content-assertion-tests.md +0 -75
  95. package/docs/ideas/bead-progress-notes-for-session-resumption.md +0 -75
  96. package/docs/ideas/classify-beads-via-deterministic-script-with-agent-fallback.md +0 -70
  97. package/docs/ideas/ctx-alias-version-and-update-commands.md +0 -65
  98. package/docs/ideas/ctx-feedback-command-for-capturing-ideas-from-any-machine.md +0 -67
  99. package/docs/ideas/dedicated-review-tests-skill.md +0 -71
  100. package/docs/ideas/deployment-beads-for-post-merge-pipelines.md +0 -36
  101. package/docs/ideas/design-and-delivery-as-separate-bead-trees/design-and-delivery-as-separate-bead-trees.md +0 -99
  102. package/docs/ideas/enable-signed-commit-enforcement-changesets-bot-commits-must-be-signed.md +0 -42
  103. package/docs/ideas/extension-manifests.md +0 -121
  104. package/docs/ideas/improve-pi-system-prompt.md +0 -43
  105. package/docs/ideas/map-every-cortex-tool-to-a-real-pi-equivalent.md +0 -95
  106. package/docs/ideas/model-selector.md +0 -40
  107. package/docs/ideas/pi-task-memory.md +0 -34
  108. package/docs/ideas/qa-extension.md +0 -43
  109. package/docs/ideas/record-idea-implementation-questions.md +0 -68
  110. package/docs/ideas/record-ideas-as-beads.md +0 -115
  111. package/docs/ideas/self-flushing-session.md +0 -72
  112. package/docs/ideas/state-in-beads-not-md.md +0 -69
  113. package/docs/ideas/subagent-visibility.md +0 -72
  114. package/docs/ideas/task-session-context.local.md +0 -95
  115. package/docs/ideas/task-session-context.md +0 -89
  116. package/docs/ideas/test-data-creation-helpers.md +0 -92
  117. package/docs/ideas/test-planning-stage-bead-in-feature-planning.md +0 -68
  118. package/docs/ideas/unified-ci-pipeline.md +0 -28
  119. package/docs/ideas/unify-the-ticket-and-task-decomposition-workflows.md +0 -71
  120. package/docs/ideas/worktree-housekeeping-for-closed-beads.md +0 -39
  121. package/docs/inspirations.md +0 -33
  122. package/docs/research/disable-model-invocation.md +0 -82
  123. package/docs/research/gas-city-research.md +0 -256
  124. package/docs/research/skill-design-research.md +0 -286
  125. package/docs/research/skill-improvements-analysis.md +0 -117
  126. package/docs/research/skill-patterns-research.md +0 -24
  127. package/docs/skills/skill-anatomy.md +0 -200
  128. package/docs/technical-direction/harness-config.md +0 -177
  129. package/docs/technical-direction/migration-execution-labels.md +0 -177
  130. package/docs/user-preferences.md +0 -17
  131. package/extensions/agent-guardrails/tsconfig.json +0 -12
  132. package/extensions/agent-modes/tsconfig.json +0 -12
  133. package/extensions/output-style/tsconfig.json +0 -12
  134. package/extensions/session-start/tsconfig.json +0 -12
  135. package/extensions/skill-stats/pnpm-lock.yaml +0 -329
  136. package/extensions/skill-stats/tsconfig.json +0 -12
  137. package/extensions/subagent/agents/planner.md +0 -37
  138. package/extensions/subagent/agents/reviewer.md +0 -35
  139. package/extensions/subagent/agents/scout.md +0 -50
  140. package/extensions/subagent/agents/worker.md +0 -24
  141. package/extensions/subagent/prompts/implement-and-review.md +0 -10
  142. package/extensions/subagent/prompts/implement.md +0 -10
  143. package/extensions/subagent/prompts/scout-and-plan.md +0 -9
  144. package/extensions/subagent/tsconfig.json +0 -12
  145. package/pi/keybindings.json +0 -3
  146. package/pi/settings.json +0 -34
  147. package/pnpm-workspace.yaml +0 -14
  148. package/skills/planning/create-task/scripts/package.json +0 -14
  149. package/skills/planning/create-task/scripts/pnpm-lock.yaml +0 -342
  150. package/skills/planning/create-task/scripts/tsconfig.json +0 -12
  151. package/skills/planning/record-idea/scripts/new-idea.sh +0 -89
  152. package/skills/productivity/validate-agent-dir/scripts/package.json +0 -15
  153. package/skills/productivity/validate-agent-dir/scripts/pnpm-lock.yaml +0 -329
  154. package/skills/productivity/validate-agent-dir/scripts/pnpm-workspace.yaml +0 -2
  155. package/skills/productivity/validate-agent-dir/scripts/tsconfig.json +0 -19
  156. package/skills/productivity/validate-agent-dir/scripts/validate-agent-dir.test.ts +0 -317
  157. package/skills/workflow/run-pipeline-stage/REFERENCE.md +0 -22
  158. package/skills/workflow/run-pipeline-stage/scripts/generate-progress.test.ts +0 -682
  159. package/skills/workflow/run-pipeline-stage/scripts/generate-progress.ts +0 -588
  160. package/skills/workflow/run-pipeline-stage/scripts/package.json +0 -15
  161. package/skills/workflow/run-pipeline-stage/scripts/pnpm-lock.yaml +0 -342
  162. package/skills/workflow/run-pipeline-stage/scripts/pnpm-workspace.yaml +0 -2
  163. package/skills/workflow/run-pipeline-stage/scripts/tsconfig.json +0 -12
  164. package/test/classify-bead.test.mjs +0 -277
  165. package/test/claude-installer.test.mjs +0 -308
  166. package/test/cli.test.mjs +0 -314
  167. package/test/create-chores.test.mjs +0 -85
  168. package/test/fake-bd.test.mjs +0 -72
  169. package/test/fs.test.mjs +0 -76
  170. package/test/helpers/fake-bd.mjs +0 -87
  171. package/test/helpers/fake-claude.mjs +0 -152
  172. package/test/helpers/fake-pi.mjs +0 -132
  173. package/test/installer.test.mjs +0 -806
  174. package/test/notify-hook.test.mjs +0 -63
  175. package/test/pending-gates.test.mjs +0 -147
  176. package/test/pi-installer.test.mjs +0 -785
  177. package/test/pi-register.test.mjs +0 -342
  178. package/test/pi-sweep.test.mjs +0 -619
  179. package/test/release-gates.test.mjs +0 -190
  180. package/test/skill-frontmatter.test.mjs +0 -137
  181. package/test/verify-worker-tree.test.mjs +0 -120
@@ -11,14 +11,17 @@
11
11
  // agent.md + claude/ harness dirs,
12
12
  // {{TOOL:...}}/{{PATH:...}}
13
13
  // substituted against token-map.json's
14
- // claude column) plus agents-native
15
- // (ralph verbatim)
14
+ // claude column)
16
15
  // <output>/skills/<name>/ skill dirs COPIED flat (one dir per
17
16
  // skill, as Claude's <name>/SKILL.md
18
17
  // plugin scan expects) with .md files
19
18
  // token-substituted — no symlinks, so
20
19
  // literal {{TOOL:...}}/{{PATH:...}}
21
- // never reaches Claude
20
+ // never reaches Claude. Cross-skill
21
+ // links are rewritten for the flat
22
+ // layout (scripts/lib/skill-links.mjs),
23
+ // so a grouped-tree handoff still
24
+ // resolves in the plugin.
22
25
  // <output>/hooks.json + hooks/<scripts> from hooks/claude/ (support files
23
26
  // referenced via ${CLAUDE_PLUGIN_ROOT})
24
27
  // <output>/.mcp.json + scripts/ hand-authored extras copied from
@@ -60,10 +63,33 @@ import { fileURLToPath } from "node:url";
60
63
  import { homedir } from "node:os";
61
64
  import { composeAgent, loadTokenMap, substituteTokens } from "../../scripts/lib/compose-agent.mjs";
62
65
  import { isDirectory, isFile } from "../../scripts/lib/fs.mjs";
66
+ import { flattenSkillLinks } from "../../scripts/lib/skill-links.mjs";
63
67
 
64
68
  const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
65
69
  const PACKAGE_ROOT = join(MODULE_DIR, "..", "..");
66
70
 
71
+ /** @typedef {import("../../scripts/lib/compose-agent.mjs").TokenMap} TokenMap */
72
+ /** @typedef {(msg: string) => void} Warn */
73
+
74
+ /**
75
+ * Result of writing (or planning to write) a managed file.
76
+ * @typedef {object} ManagedFileResult
77
+ * @property {string} path
78
+ * @property {"would-remove-symlink" | "unchanged" | "would-write" | "removed-symlink" | "written" | "skipped-unmanaged"} action
79
+ * @property {boolean} removedSymlink
80
+ */
81
+
82
+ /**
83
+ * Registration plan/outcome returned by registerClaude.
84
+ * @typedef {object} RegisterClaudeResult
85
+ * @property {string} marketplace
86
+ * @property {string} plugin
87
+ * @property {string[][]} commands
88
+ * @property {boolean} dryRun
89
+ * @property {boolean} registered
90
+ * @property {string[]} skipped
91
+ */
92
+
67
93
  // Home-scoped plugin install root. The parent dir (~/.agent-cortex) is the
68
94
  // marketplace root: it carries .claude-plugin/marketplace.json exposing
69
95
  // ./claude, so `claude plugin marketplace add ~/.agent-cortex` points Claude
@@ -98,27 +124,27 @@ const CONTRACT_VERSION = 1;
98
124
 
99
125
  const CLAUDE = "claude";
100
126
 
101
- const DEFAULT_WARN = (msg) => console.warn(`[claude-installer] ${msg}`);
127
+ const DEFAULT_WARN = (/** @type {string} */ msg) => console.warn(`[claude-installer] ${msg}`);
102
128
 
103
- // Agents NOT transformed from the composable sources. `ralph` is authored natively for Claude
104
- // instead (agents-native/ralph.md) — its event-driven orchestration can't be produced by
105
- // section composition from the poll-loop sources.
106
- const DEFER = new Set(["ralph"]);
107
-
108
- // Ralph-coupled pipeline skills that don't apply to the lean Claude Ralph (its completion
109
- // handling is event-driven, not the pi orchestrator's). Not copied into the Claude plugin.
110
- const SKILL_EXCLUDE = new Set(["ralph", "run-pipeline-stage"]);
111
129
 
112
130
  const NAME_PREFIX = "agent-cortex:";
113
131
 
132
+ /**
133
+ * @param {string} a
134
+ * @param {string} b
135
+ * @returns {number}
136
+ */
114
137
  function byName(a, b) {
115
138
  return a.localeCompare(b);
116
139
  }
117
140
 
118
- /** Compose agents from agents/<name>/; DEFERed agents come from agents-native/ instead. */
141
+ /**
142
+ * Compose every agent from agents/<name>/ — all harnesses share the composable sources.
143
+ * @param {string} root
144
+ * @returns {Promise<{ files: { file: string, content: string }[] }>}
145
+ */
119
146
  async function buildAgents(root) {
120
147
  const files = [];
121
- const skipped = [];
122
148
  const entries = (await readdir(join(root, "agents"), { withFileTypes: true })).sort((a, b) =>
123
149
  byName(a.name, b.name),
124
150
  );
@@ -128,11 +154,6 @@ async function buildAgents(root) {
128
154
  const dir = join(root, "agents", entry.name);
129
155
  if (!(await isFile(join(dir, "agent.md"))) || !(await isFile(join(dir, "claude", "frontmatter.json")))) continue;
130
156
 
131
- if (DEFER.has(entry.name)) {
132
- skipped.push(entry.name);
133
- continue;
134
- }
135
-
136
157
  const fm = await composeAgent(root, entry.name, CLAUDE);
137
158
  const slug = fm.name.startsWith(NAME_PREFIX) ? fm.name.slice(NAME_PREFIX.length) : fm.name;
138
159
  const header =
@@ -144,18 +165,7 @@ async function buildAgents(root) {
144
165
  `---\n`;
145
166
  files.push({ file: `${slug}.md`, content: `${header}\n${fm.body}\n` });
146
167
  }
147
- return { files, skipped };
148
- }
149
-
150
- /** Copy hand-authored Claude-native agents into agents/ verbatim. */
151
- async function buildNatives(root, nativeSrc) {
152
- const files = [];
153
- if (!(await isDirectory(nativeSrc))) return files;
154
- const names = (await readdir(nativeSrc)).filter((f) => f.endsWith(".md")).sort(byName);
155
- for (const file of names) {
156
- files.push({ file, content: await readFile(join(nativeSrc, file), "utf-8") });
157
- }
158
- return files;
168
+ return { files };
159
169
  }
160
170
 
161
171
  /**
@@ -164,36 +174,63 @@ async function buildNatives(root, nativeSrc) {
164
174
  * under skills/), token-substituting .md files against token-map.json's claude
165
175
  * column (the same materialisation as bin/installers/pi.mjs: no symlinks, so
166
176
  * literal {{TOOL:...}}/{{PATH:...}} never survives; non-.md files verbatim).
177
+ * Flattening drops the group segment an authored cross-skill link relies on, so
178
+ * each .md file's skill links are rewritten to the flat sibling path.
179
+ * @param {string} root
180
+ * @param {string} skillOut
181
+ * @param {{ dryRun: boolean, pluginRoot: string | undefined, tokenMap: TokenMap, warn: Warn }} options
182
+ * @returns {Promise<{ names: string[], md: number, files: number, dir: string }>}
167
183
  */
168
184
  async function buildSkills(root, skillOut, { dryRun, pluginRoot, tokenMap, warn }) {
185
+ // Names first: flattening needs the full set to recognise a skill segment in a
186
+ // link, and every name is unique once flattened.
187
+ const sources = [];
169
188
  const seen = new Map();
170
- const transform = (content) =>
171
- substituteTokens(content, CLAUDE, tokenMap, { dropNullTools: true, pluginRoot, resolveRelativePaths: true, warn });
172
-
173
- const names = [];
174
- let md = 0;
175
- let files = 0;
176
189
  for (const group of (await readdir(join(root, "skills"))).sort(byName)) {
177
190
  const groupDir = join(root, "skills", group);
178
191
  if (!(await isDirectory(groupDir))) continue;
179
192
  for (const name of (await readdir(groupDir)).sort(byName)) {
180
193
  const skillDir = join(groupDir, name);
181
194
  if (!(await isFile(join(skillDir, "SKILL.md")))) continue;
182
- if (SKILL_EXCLUDE.has(name)) continue;
183
195
  if (seen.has(name)) {
184
196
  throw new Error(`duplicate skill name "${name}" (${seen.get(name)} and ${group}) — names must be unique when flattened`);
185
197
  }
186
198
  seen.set(name, group);
187
- names.push(name);
188
- const stats = await copyTree(skillDir, join(skillOut, name), transform, dryRun);
189
- md += stats.md;
190
- files += stats.files;
199
+ sources.push({ name, dir: skillDir });
191
200
  }
192
201
  }
202
+
203
+ const names = sources.map((source) => source.name);
204
+ const nameSet = new Set(names);
205
+ let md = 0;
206
+ let files = 0;
207
+ for (const source of sources) {
208
+ /** @param {string} content @returns {string} */
209
+ const transform = (content) =>
210
+ flattenSkillLinks(
211
+ substituteTokens(content, CLAUDE, tokenMap, {
212
+ dropNullTools: true,
213
+ pluginRoot,
214
+ resolveRelativePaths: true,
215
+ warn,
216
+ }),
217
+ nameSet,
218
+ );
219
+ const stats = await copyTree(source.dir, join(skillOut, source.name), transform, dryRun);
220
+ md += stats.md;
221
+ files += stats.files;
222
+ }
193
223
  return { names, md, files, dir: skillOut };
194
224
  }
195
225
 
196
- /** Recursively copy a tree; transform .md file contents, copy everything else verbatim. */
226
+ /**
227
+ * Recursively copy a tree; transform .md file contents, copy everything else verbatim.
228
+ * @param {string} src
229
+ * @param {string} dest
230
+ * @param {(content: string) => string} transform
231
+ * @param {boolean} dryRun
232
+ * @returns {Promise<{ md: number, files: number }>}
233
+ */
197
234
  async function copyTree(src, dest, transform, dryRun) {
198
235
  let md = 0;
199
236
  let files = 0;
@@ -216,7 +253,11 @@ async function copyTree(src, dest, transform, dryRun) {
216
253
  return { md, files };
217
254
  }
218
255
 
219
- /** plugin.json manifest; version tracks the package so it can never go stale. */
256
+ /**
257
+ * plugin.json manifest; version tracks the package so it can never go stale.
258
+ * @param {string} root
259
+ * @returns {Promise<string>}
260
+ */
220
261
  async function buildPluginJson(root) {
221
262
  const pkg = JSON.parse(await readFile(join(root, "package.json"), "utf-8"));
222
263
  return (
@@ -242,6 +283,10 @@ async function buildPluginJson(root) {
242
283
  * install root ~/.agent-cortex doubles as the marketplace root), so `claude
243
284
  * plugin marketplace add ~/.agent-cortex` resolves ./claude relative to the
244
285
  * manifest's directory.
286
+ * @param {string} root
287
+ * @param {string} outputName
288
+ * @returns {Promise<{ name: string, owner: { name: string }, metadata: { description: string },
289
+ * plugins: { name: string, source: string, description: string }[] }>}
245
290
  */
246
291
  async function buildMarketplaceManifest(root, outputName) {
247
292
  const pkg = JSON.parse(await readFile(join(root, "package.json"), "utf-8"));
@@ -263,11 +308,19 @@ async function buildMarketplaceManifest(root, outputName) {
263
308
  * Hook support files (hooks/claude/ except hooks.json) bundled into
264
309
  * hooks/ so hook commands can reference them via ${CLAUDE_PLUGIN_ROOT}.
265
310
  * hooks.json itself is special-cased to the plugin root (hooks.json).
311
+ * @param {string} root
312
+ * @returns {Promise<{ rel: string, content: string }[]>}
266
313
  */
267
314
  async function buildHookFiles(root) {
315
+ /** @type {{ rel: string, content: string }[]} */
268
316
  const files = [];
269
317
  const srcDir = join(root, "hooks", "claude");
270
318
  if (!(await isDirectory(srcDir))) return files;
319
+ /**
320
+ * @param {string} dir
321
+ * @param {string} rel
322
+ * @returns {Promise<void>}
323
+ */
271
324
  const walk = async (dir, rel) => {
272
325
  const entries = await readdir(dir, { withFileTypes: true });
273
326
  entries.sort((a, b) => byName(a.name, b.name));
@@ -289,8 +342,11 @@ async function buildHookFiles(root) {
289
342
  * at read time (not path-referenced) so the write-phase cleanup of the
290
343
  * output dir can never destroy the sources (outputs normally go to
291
344
  * ~/.agent-cortex/claude or a tmp dir, never into claude-extras/).
345
+ * @param {string} root
346
+ * @returns {Promise<{ rel: string, content: string | Buffer, mode: number }[]>}
292
347
  */
293
348
  async function buildHandAuthored(root) {
349
+ /** @type {{ rel: string, content: string | Buffer, mode: number }[]} */
294
350
  const items = [];
295
351
  const srcExtras = join(root, "claude-extras");
296
352
  if (await isFile(join(srcExtras, ".mcp.json"))) {
@@ -320,6 +376,8 @@ async function buildHandAuthored(root) {
320
376
  * - `enabledPlugins` and `extraKnownMarketplaces` are CLI-owned: they are
321
377
  * rewritten from the template outright. `permissions`, `hooks`, `env`,
322
378
  * `statusLine` and every other key are never touched.
379
+ * @param {{ root: string, target: string, dryRun: boolean, warn: Warn }} options
380
+ * @returns {Promise<ManagedFileResult | null>}
323
381
  */
324
382
  async function installSettings({ root, target, dryRun, warn }) {
325
383
  const template = await readTemplateJson(join(root, SETTINGS_TEMPLATE), warn);
@@ -339,8 +397,12 @@ async function installSettings({ root, target, dryRun, warn }) {
339
397
  * Merge the committed template over the live file. Template keys fill in
340
398
  * defaults, live values win (deeply), and keys the template does not know about
341
399
  * are preserved — re-installing never discards personal or Claude-written config.
400
+ * @param {Record<string, any>} template
401
+ * @param {Record<string, any>} existing
402
+ * @returns {Record<string, any>}
342
403
  */
343
404
  function mergeSettings(template, existing) {
405
+ /** @type {Record<string, any>} */
344
406
  const merged = {};
345
407
  for (const key of Object.keys(template)) {
346
408
  const templateValue = template[key];
@@ -362,6 +424,8 @@ function mergeSettings(template, existing) {
362
424
  * a sibling temp file and renaming it over the target — rename replaces the link
363
425
  * itself, so a committed template is never written through.
364
426
  * Returns the action taken so the CLI can report the plan (dry-run included).
427
+ * @param {{ target: string, content: string, dryRun: boolean }} options
428
+ * @returns {Promise<ManagedFileResult>}
365
429
  */
366
430
  async function writeManagedFile({ target, content, dryRun }) {
367
431
  const symlink = await isSymlink(target);
@@ -399,6 +463,8 @@ async function writeManagedFile({ target, content, dryRun }) {
399
463
  * text between them and every other line of the user's memory file survives. A
400
464
  * file with exactly one marker (hand-edited or truncated) is left untouched with
401
465
  * a warning, never guessed at.
466
+ * @param {{ root: string, target: string, dryRun: boolean, warn: Warn }} options
467
+ * @returns {Promise<ManagedFileResult | null>}
402
468
  */
403
469
  async function installInstructions({ root, target, dryRun, warn }) {
404
470
  const block = await readFileMaybe(join(root, INSTRUCTIONS_SOURCE));
@@ -418,6 +484,9 @@ async function installInstructions({ root, target, dryRun, warn }) {
418
484
  * Return `existing` with the managed section replaced by `block`, or null when
419
485
  * the file carries unmatched markers. A file without markers gets the section
420
486
  * appended, so personal memory text is never disturbed.
487
+ * @param {string | null} existing
488
+ * @param {string} block
489
+ * @returns {string | null}
421
490
  */
422
491
  function mergeManagedSection(existing, block) {
423
492
  const managed = `${INSTRUCTIONS_BEGIN}\n${block.trimEnd()}\n${INSTRUCTIONS_END}`;
@@ -436,6 +505,11 @@ function mergeManagedSection(existing, block) {
436
505
  return `${[before, managed, after].filter((part) => part !== "").join("\n\n")}\n`;
437
506
  }
438
507
 
508
+ /**
509
+ * @param {string} filePath
510
+ * @param {Warn} warn
511
+ * @returns {Promise<Record<string, any> | null>}
512
+ */
439
513
  async function readTemplateJson(filePath, warn) {
440
514
  const raw = await readFileMaybe(filePath);
441
515
  if (raw === null) {
@@ -447,6 +521,11 @@ async function readTemplateJson(filePath, warn) {
447
521
  return parsed;
448
522
  }
449
523
 
524
+ /**
525
+ * @param {string} filePath
526
+ * @param {Warn} warn
527
+ * @returns {Promise<Record<string, any>>}
528
+ */
450
529
  async function readJsonObject(filePath, warn) {
451
530
  const raw = await readFileMaybe(filePath);
452
531
  if (raw === null) return {};
@@ -458,6 +537,10 @@ async function readJsonObject(filePath, warn) {
458
537
  return parsed;
459
538
  }
460
539
 
540
+ /**
541
+ * @param {string} raw
542
+ * @returns {Record<string, any> | null}
543
+ */
461
544
  function tryParseJsonObject(raw) {
462
545
  try {
463
546
  const parsed = JSON.parse(raw);
@@ -467,19 +550,31 @@ function tryParseJsonObject(raw) {
467
550
  }
468
551
  }
469
552
 
553
+ /**
554
+ * @param {unknown} value
555
+ * @returns {value is Record<string, any>}
556
+ */
470
557
  function isPlainObject(value) {
471
558
  return typeof value === "object" && value !== null && !Array.isArray(value);
472
559
  }
473
560
 
561
+ /**
562
+ * @param {string} filePath
563
+ * @returns {Promise<string | null>}
564
+ */
474
565
  async function readFileMaybe(filePath) {
475
566
  try {
476
567
  return await readFile(filePath, "utf-8");
477
568
  } catch (err) {
478
- if (err.code === "ENOENT") return null;
569
+ if (/** @type {NodeJS.ErrnoException} */ (err).code === "ENOENT") return null;
479
570
  throw err;
480
571
  }
481
572
  }
482
573
 
574
+ /**
575
+ * @param {string} filePath
576
+ * @returns {Promise<boolean>}
577
+ */
483
578
  async function isSymlink(filePath) {
484
579
  try {
485
580
  return (await lstat(filePath)).isSymbolicLink();
@@ -509,13 +604,13 @@ async function isSymlink(filePath) {
509
604
  * explicitly materialises the section even
510
605
  * with --output — the test/CI seam.
511
606
  * @param {(msg: string) => void} [options.warn] Warning sink, also collected in `warnings`.
512
- * @returns {{ output: string, marketplaceRoot: string|null, manifestPath: string|null,
513
- * marketplaceManifest: object|null, agents: string[], natives: string[],
607
+ * @returns {Promise<{ output: string, marketplaceRoot: string|null, manifestPath: string|null,
608
+ * marketplaceManifest: object|null, agents: string[],
514
609
  * skills: {names: string[], md: number, files: number, dir: string},
515
610
  * hooks: boolean, hookFiles: string[], handAuthored: string[],
516
611
  * settings: {path: string, action: string, removedSymlink: boolean}|null,
517
612
  * instructions: {path: string, action: string, removedSymlink: boolean}|null,
518
- * warnings: string[], dryRun: boolean }}
613
+ * pluginVersion: string, warnings: string[], dryRun: boolean }>}
519
614
  */
520
615
  export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = false, pluginRoot, settingsPath, instructionsPath, warn = DEFAULT_WARN } = {}) {
521
616
  const tokenMap = await loadTokenMap(root);
@@ -525,8 +620,8 @@ export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = fals
525
620
  );
526
621
  }
527
622
 
528
- const warnings = [];
529
- const warnSink = (msg) => {
623
+ const warnings = /** @type {string[]} */ ([]);
624
+ const warnSink = (/** @type {string} */ msg) => {
530
625
  warnings.push(msg);
531
626
  warn(msg);
532
627
  };
@@ -536,9 +631,9 @@ export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = fals
536
631
  // --output is generate-only (tests/CI), matching its no-registration semantics.
537
632
  const isDefaultInstall = output === undefined;
538
633
  const marketplaceRoot = isDefaultInstall ? dirname(out) : null;
539
- const manifestPath = isDefaultInstall ? join(marketplaceRoot, ".claude-plugin", "marketplace.json") : null;
634
+ const manifestPath = isDefaultInstall && marketplaceRoot !== null ? join(marketplaceRoot, ".claude-plugin", "marketplace.json") : null;
540
635
 
541
- const rootPlugin = pluginRoot ?? tokenMap.paths.plugin_root?.[CLAUDE];
636
+ const rootPlugin = pluginRoot ?? /** @type {string | undefined} */ (tokenMap.paths.plugin_root?.[CLAUDE]);
542
637
 
543
638
  // Read hand-authored extras from the canonical claude-extras/ dir BEFORE
544
639
  // the cleanup below (defensive: the output is normally ~/.agent-cortex/claude
@@ -574,7 +669,6 @@ export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = fals
574
669
  : null;
575
670
 
576
671
  const agents = await buildAgents(root);
577
- const natives = await buildNatives(root, join(root, "agents-native"));
578
672
  const skillOut = join(out, "skills");
579
673
  const skills = await buildSkills(root, skillOut, { dryRun, pluginRoot: rootPlugin, tokenMap, warn: warnSink });
580
674
  const pluginJson = await buildPluginJson(root);
@@ -590,7 +684,6 @@ export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = fals
590
684
  marketplaceManifest,
591
685
  pluginVersion: JSON.parse(pluginJson).version,
592
686
  agents: agents.files.map((f) => f.file.replace(/\.md$/, "")),
593
- natives: natives.map((f) => f.file.replace(/\.md$/, "")),
594
687
  skills: { names: skills.names, md: skills.md, files: skills.files, dir: skillOut },
595
688
  hooks: hooksJson !== null,
596
689
  hookFiles: hookFiles.map((f) => f.rel),
@@ -601,12 +694,9 @@ export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = fals
601
694
  dryRun,
602
695
  };
603
696
 
604
- if (agents.skipped.length) console.log(`Skipped composable source(s): ${agents.skipped.join(", ")}`);
605
-
606
697
  if (dryRun) {
607
698
  console.log(`DRY-RUN — would generate into ${out}, no files written:`);
608
699
  console.log(` agents: ${summary.agents.join(", ")}`);
609
- if (summary.natives.length) console.log(` native agents: ${summary.natives.join(", ")}`);
610
700
  console.log(` skills: ${summary.skills.names.length} copied → ${skillOut}/<name>/ (token-substituted)`);
611
701
  console.log(` .claude-plugin/plugin.json (version ${JSON.parse(pluginJson).version})`);
612
702
  if (isDefaultInstall && manifestPath) {
@@ -629,11 +719,10 @@ export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = fals
629
719
  console.log(`Materialised CLAUDE.md section → ${instructions.path} (${instructions.action})`);
630
720
  const agentOut = join(out, "agents");
631
721
  await mkdir(agentOut, { recursive: true });
632
- for (const { file, content } of [...agents.files, ...natives]) {
722
+ for (const { file, content } of agents.files) {
633
723
  await writeFile(join(agentOut, file), content);
634
724
  }
635
725
  console.log(`Generated ${summary.agents.length} Claude agent(s): ${summary.agents.join(", ")}`);
636
- if (summary.natives.length) console.log(`Copied ${summary.natives.length} native Claude agent(s): ${summary.natives.join(", ")}`);
637
726
 
638
727
  await mkdir(skillOut, { recursive: true });
639
728
  console.log(
@@ -651,7 +740,7 @@ export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = fals
651
740
  console.log(`Wrote ${manifestPath} (plugin agent-cortex @ ./${basename(out)})`);
652
741
  }
653
742
 
654
- if (summary.hooks) {
743
+ if (hooksJson !== null) {
655
744
  await writeFile(join(out, "hooks.json"), hooksJson);
656
745
  console.log(`Wrote ${join(out, "hooks.json")} (from ${hooksSrc})`);
657
746
 
@@ -686,8 +775,13 @@ export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = fals
686
775
  // The `claude` CLI on PATH. Overridable in tests via the claudeBin option.
687
776
  const DEFAULT_CLAUDE_BIN = "claude";
688
777
 
689
- /** Validate a marketplace manifest object; returns the marketplace and plugin
690
- * names that `claude plugin` commands address. */
778
+ /**
779
+ * Validate a marketplace manifest object; returns the marketplace and plugin
780
+ * names that `claude plugin` commands address.
781
+ * @param {any} manifest
782
+ * @param {string} label
783
+ * @returns {{ marketplace: string, plugin: string }}
784
+ */
691
785
  function parseMarketplaceManifest(manifest, label) {
692
786
  const marketplace = manifest.name;
693
787
  const plugin = manifest.plugins?.[0]?.name;
@@ -702,6 +796,8 @@ function parseMarketplaceManifest(manifest, label) {
702
796
  /**
703
797
  * Read the marketplace manifest that exposes the plugin at install root
704
798
  * (<root>/.claude-plugin/marketplace.json).
799
+ * @param {string} root
800
+ * @returns {Promise<{ manifest: any, path: string }>}
705
801
  */
706
802
  async function readMarketplaceManifest(root) {
707
803
  const manifestPath = join(root, ".claude-plugin", "marketplace.json");
@@ -710,7 +806,7 @@ async function readMarketplaceManifest(root) {
710
806
  manifest = JSON.parse(await readFile(manifestPath, "utf-8"));
711
807
  } catch (err) {
712
808
  throw new Error(
713
- `no Claude marketplace manifest at ${manifestPath} (${err.message}) — ` +
809
+ `no Claude marketplace manifest at ${manifestPath} (${/** @type {Error} */ (err).message}) — ` +
714
810
  `"agent-cortex install claude" registers the plugin by pointing Claude Code at the install root`,
715
811
  );
716
812
  }
@@ -722,6 +818,9 @@ async function readMarketplaceManifest(root) {
722
818
  * where <source> is the marketplace manifest's plugin source) — the signature a
723
819
  * directory-source marketplace exposes to `claude plugin`. Read when the version
724
820
  * is not passed explicitly; undefined when it cannot be determined.
821
+ * @param {string} root
822
+ * @param {any} manifest
823
+ * @returns {Promise<string | undefined>}
725
824
  */
726
825
  async function readMaterialisedPluginVersion(root, manifest) {
727
826
  const source = manifest.plugins?.[0]?.source;
@@ -734,7 +833,10 @@ async function readMaterialisedPluginVersion(root, manifest) {
734
833
  }
735
834
  }
736
835
 
737
- /** Run one claude plugin command, inheriting its output; rejects on failure. */
836
+ /** Run one claude plugin command, inheriting its output; rejects on failure.
837
+ * @param {string} claudeBin
838
+ * @param {string[]} args
839
+ * @returns {Promise<void>} */
738
840
  function runClaudeCommand(claudeBin, args) {
739
841
  const label = `${claudeBin} ${args.join(" ")}`;
740
842
  return new Promise((resolve, reject) => {
@@ -753,6 +855,8 @@ function runClaudeCommand(claudeBin, args) {
753
855
  * `claude plugin --help` with output suppressed. Resolves `{ ok: true }` when
754
856
  * it exits 0, otherwise `{ ok: false, message }` naming the failure (missing
755
857
  * binary vs missing subcommand) so registration can give targeted guidance.
858
+ * @param {string} claudeBin
859
+ * @returns {Promise<{ ok: true } | { ok: false, message: string }>}
756
860
  */
757
861
  function probePluginSupport(claudeBin) {
758
862
  return new Promise((resolve) => {
@@ -776,7 +880,12 @@ function probePluginSupport(claudeBin) {
776
880
  });
777
881
  }
778
882
 
779
- /** Run a claude plugin command capturing stdout; null when the command fails. */
883
+ /**
884
+ * Run a claude plugin command capturing stdout; null when the command fails.
885
+ * @param {string} claudeBin
886
+ * @param {string[]} args
887
+ * @returns {Promise<string | null>}
888
+ */
780
889
  function queryClaudeCommand(claudeBin, args) {
781
890
  const label = `${claudeBin} ${args.join(" ")}`;
782
891
  return new Promise((resolve, reject) => {
@@ -796,6 +905,9 @@ function queryClaudeCommand(claudeBin, args) {
796
905
  * Whether `claude plugin marketplace list` already registers `marketplace`.
797
906
  * Unparseable output counts as not-registered: the fallback `marketplace add`
798
907
  * is itself idempotent (the CLI reports "already on disk", exit 0).
908
+ * @param {string} claudeBin
909
+ * @param {string} marketplace
910
+ * @returns {Promise<boolean>}
799
911
  */
800
912
  async function marketplaceRegistered(claudeBin, marketplace) {
801
913
  const out = await queryClaudeCommand(claudeBin, ["plugin", "marketplace", "list", "--json"]);
@@ -812,6 +924,9 @@ async function marketplaceRegistered(claudeBin, marketplace) {
812
924
  * Installed version of `pluginId` per `claude plugin list --json`; null when
813
925
  * not installed or the output is unreadable (the caller falls back to the
814
926
  * idempotent `plugin install`).
927
+ * @param {string} claudeBin
928
+ * @param {string} pluginId
929
+ * @returns {Promise<string | null>}
815
930
  */
816
931
  async function installedPluginVersion(claudeBin, pluginId) {
817
932
  const out = await queryClaudeCommand(claudeBin, ["plugin", "list", "--json"]);
@@ -861,8 +976,7 @@ async function installedPluginVersion(claudeBin, pluginId) {
861
976
  * @param {string} [options.pluginVersion] Materialised plugin version (default: read from plugin.json)
862
977
  * @param {boolean} [options.requireRegister] Exit non-zero when registration cannot run
863
978
  * @param {(msg: string) => void} [options.warn] Warning sink (default: console.warn)
864
- * @returns {{ marketplace: string, plugin: string, commands: string[][],
865
- * dryRun: boolean, registered: boolean, skipped: string[] }}
979
+ * @returns {Promise<RegisterClaudeResult>}
866
980
  */
867
981
  export async function registerClaude({
868
982
  root = PACKAGE_ROOT,
@@ -880,7 +994,7 @@ export async function registerClaude({
880
994
  const version = pluginVersion ?? (await readMaterialisedPluginVersion(root, manifestObj));
881
995
  const home = resolve(root);
882
996
 
883
- const summary = { marketplace, plugin, commands: [], dryRun, registered: true, skipped: [] };
997
+ const summary = /** @type {RegisterClaudeResult} */ ({ marketplace, plugin, commands: [], dryRun, registered: true, skipped: [] });
884
998
  console.log(`Registering plugin "${pluginId}" with Claude Code (via ${claudeBin})…`);
885
999
 
886
1000
  const manualCommands = [