@jaybeeuu/agent-cortex 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/CHANGELOG.md +437 -0
  2. package/README.md +46 -15
  3. package/agents/README.md +22 -10
  4. package/agents/plan/agent.md +2 -2
  5. package/agents/ralph/agent.md +128 -91
  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 +262 -77
  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 +58 -0
  18. package/extensions/output-style/index.ts +23 -0
  19. package/extensions/output-style/output-style.md +43 -0
  20. package/extensions/output-style/output-style.test.ts +50 -0
  21. package/extensions/output-style/output-style.ts +32 -0
  22. package/extensions/output-style/package.json +19 -0
  23. package/hooks/claude/scripts/notify.mjs +8 -1
  24. package/lib/cli.mjs +91 -2
  25. package/package.json +29 -4
  26. package/scripts/check-changeset.mjs +4 -0
  27. package/scripts/check-no-sync.mjs +5 -1
  28. package/scripts/check-skill-frontmatter.mjs +21 -0
  29. package/scripts/lib/compose-agent.mjs +69 -9
  30. package/scripts/lib/fs.mjs +10 -2
  31. package/scripts/lib/migrate.mjs +736 -0
  32. package/scripts/lib/release-gates.mjs +21 -1
  33. package/scripts/lib/skill-frontmatter.mjs +152 -0
  34. package/scripts/lib/skill-links.mjs +52 -0
  35. package/scripts/lib/verify-worker-tree.mjs +25 -0
  36. package/scripts/resolve-publish-command.mjs +4 -0
  37. package/skills/engineering/design-an-interface/SKILL.md +3 -3
  38. package/skills/engineering/grill-with-docs/SKILL.md +3 -5
  39. package/skills/engineering/improve-codebase-architecture/SKILL.md +5 -4
  40. package/skills/engineering/tdd/SKILL.md +5 -7
  41. package/skills/engineering/technical-direction/SKILL.md +5 -5
  42. package/skills/planning/classify-bead/REFERENCE.md +3 -3
  43. package/skills/planning/classify-bead/SKILL.md +14 -9
  44. package/skills/planning/classify-bead/scripts/classify-bead.mjs +5 -4
  45. package/skills/planning/create-task/REFERENCE.md +52 -3
  46. package/skills/planning/create-task/SKILL.md +70 -26
  47. package/skills/planning/create-task/scripts/{create-chores.ts → create-chores.mjs} +48 -23
  48. package/skills/planning/flesh-out-idea/REFERENCE.md +116 -0
  49. package/skills/planning/flesh-out-idea/SKILL.md +146 -0
  50. package/skills/planning/prd-to-tasks/REFERENCE.md +54 -5
  51. package/skills/planning/prd-to-tasks/SKILL.md +22 -16
  52. package/skills/planning/record-idea/FORMAT.md +66 -17
  53. package/skills/planning/record-idea/SKILL.md +48 -24
  54. package/skills/planning/request-refactor-plan/SKILL.md +4 -4
  55. package/skills/planning/spec-to-tickets/SKILL.md +5 -5
  56. package/skills/planning/write-a-prd/SKILL.md +8 -9
  57. package/skills/planning/write-a-ticket/SKILL.md +4 -6
  58. package/skills/productivity/bd-tool/SKILL.md +15 -10
  59. package/skills/productivity/grill-me/SKILL.md +5 -0
  60. package/skills/productivity/init-beads/SKILL.md +3 -3
  61. package/skills/productivity/migration/REFERENCE.md +227 -0
  62. package/skills/productivity/migration/SKILL.md +188 -0
  63. package/skills/productivity/validate-agent-dir/scripts/{validate-agent-dir.ts → validate-agent-dir.mjs} +49 -28
  64. package/skills/productivity/write-a-skill/SKILL.md +5 -5
  65. package/skills/review/maintain-agent-docs/SKILL.md +5 -7
  66. package/skills/review/refactor-skill/SKILL.md +4 -3
  67. package/skills/review/review-security/SKILL.md +3 -5
  68. package/skills/style/style-code/SKILL.md +5 -10
  69. package/skills/style/style-comms/SKILL.md +5 -7
  70. package/skills/style/style-documentation/SKILL.md +5 -6
  71. package/skills/style/style-tests/SKILL.md +5 -7
  72. package/skills/workflow/git-workflow/SKILL.md +28 -4
  73. package/skills/workflow/hitl-collab/SKILL.md +4 -6
  74. package/skills/workflow/plan/SKILL.md +41 -30
  75. package/skills/workflow/ralph/REFERENCE.md +209 -55
  76. package/skills/workflow/ralph/SKILL.md +89 -50
  77. package/skills/workflow/ralph/scripts/pending-gates.mjs +134 -0
  78. package/skills/workflow/run-pipeline-stage/SKILL.md +72 -71
  79. package/skills/workflow/run-pipeline-stage/playbooks/code.md +11 -0
  80. package/skills/workflow/run-pipeline-stage/playbooks/document.md +12 -0
  81. package/skills/workflow/run-pipeline-stage/playbooks/fix.md +12 -0
  82. package/skills/workflow/run-pipeline-stage/playbooks/review.md +11 -0
  83. package/skills/workflow/run-pipeline-stage/playbooks/verify.md +13 -0
  84. package/skills/workflow/run-pipeline-stage/prompts/stage-runner.md +73 -1
  85. package/token-map.README.md +9 -4
  86. package/token-map.json +13 -6
  87. package/.agents/skills/style-versioning/SKILL.md +0 -172
  88. package/.changeset/README.md +0 -8
  89. package/.changeset/config.json +0 -11
  90. package/.github/workflows/ci.yml +0 -302
  91. package/.junie/guidelines.md +0 -87
  92. package/.junie/mcp/mcp.json +0 -10
  93. package/AGENTS.md +0 -151
  94. package/agents/ralph/claude/polling.md +0 -31
  95. package/agents-native/ralph.md +0 -193
  96. package/docs/claude-hooks.md +0 -128
  97. package/docs/commit-signing.md +0 -231
  98. package/docs/ideas/auto-model-selection.md +0 -75
  99. package/docs/ideas/ban-content-assertion-tests.md +0 -75
  100. package/docs/ideas/bead-progress-notes-for-session-resumption.md +0 -75
  101. package/docs/ideas/classify-beads-via-deterministic-script-with-agent-fallback.md +0 -70
  102. package/docs/ideas/ctx-alias-version-and-update-commands.md +0 -65
  103. package/docs/ideas/ctx-feedback-command-for-capturing-ideas-from-any-machine.md +0 -67
  104. package/docs/ideas/dedicated-review-tests-skill.md +0 -71
  105. package/docs/ideas/deployment-beads-for-post-merge-pipelines.md +0 -36
  106. package/docs/ideas/design-and-delivery-as-separate-bead-trees/design-and-delivery-as-separate-bead-trees.md +0 -99
  107. package/docs/ideas/enable-signed-commit-enforcement-changesets-bot-commits-must-be-signed.md +0 -42
  108. package/docs/ideas/extension-manifests.md +0 -121
  109. package/docs/ideas/improve-pi-system-prompt.md +0 -37
  110. package/docs/ideas/map-every-cortex-tool-to-a-real-pi-equivalent.md +0 -95
  111. package/docs/ideas/model-selector.md +0 -40
  112. package/docs/ideas/pi-task-memory.md +0 -34
  113. package/docs/ideas/qa-extension.md +0 -43
  114. package/docs/ideas/record-idea-implementation-questions.md +0 -68
  115. package/docs/ideas/record-ideas-as-beads.md +0 -115
  116. package/docs/ideas/self-flushing-session.md +0 -72
  117. package/docs/ideas/state-in-beads-not-md.md +0 -69
  118. package/docs/ideas/subagent-visibility.md +0 -72
  119. package/docs/ideas/task-session-context.local.md +0 -95
  120. package/docs/ideas/task-session-context.md +0 -89
  121. package/docs/ideas/test-data-creation-helpers.md +0 -92
  122. package/docs/ideas/test-planning-stage-bead-in-feature-planning.md +0 -68
  123. package/docs/ideas/unified-ci-pipeline.md +0 -28
  124. package/docs/ideas/unify-the-ticket-and-task-decomposition-workflows.md +0 -71
  125. package/docs/ideas/worktree-housekeeping-for-closed-beads.md +0 -39
  126. package/docs/inspirations.md +0 -33
  127. package/docs/research/disable-model-invocation.md +0 -82
  128. package/docs/research/gas-city-research.md +0 -256
  129. package/docs/research/skill-design-research.md +0 -286
  130. package/docs/research/skill-improvements-analysis.md +0 -117
  131. package/docs/research/skill-patterns-research.md +0 -24
  132. package/docs/skills/skill-anatomy.md +0 -197
  133. package/docs/technical-direction/harness-config.md +0 -177
  134. package/docs/technical-direction/migration-execution-labels.md +0 -177
  135. package/docs/user-preferences.md +0 -17
  136. package/extensions/agent-guardrails/tsconfig.json +0 -12
  137. package/extensions/agent-modes/tsconfig.json +0 -12
  138. package/extensions/session-start/tsconfig.json +0 -12
  139. package/extensions/skill-stats/pnpm-lock.yaml +0 -329
  140. package/extensions/skill-stats/tsconfig.json +0 -12
  141. package/extensions/subagent/agents/planner.md +0 -37
  142. package/extensions/subagent/agents/reviewer.md +0 -35
  143. package/extensions/subagent/agents/scout.md +0 -50
  144. package/extensions/subagent/agents/worker.md +0 -24
  145. package/extensions/subagent/prompts/implement-and-review.md +0 -10
  146. package/extensions/subagent/prompts/implement.md +0 -10
  147. package/extensions/subagent/prompts/scout-and-plan.md +0 -9
  148. package/extensions/subagent/tsconfig.json +0 -12
  149. package/pi/keybindings.json +0 -3
  150. package/pi/settings.json +0 -34
  151. package/pnpm-workspace.yaml +0 -14
  152. package/skills/planning/create-task/scripts/package.json +0 -14
  153. package/skills/planning/create-task/scripts/pnpm-lock.yaml +0 -342
  154. package/skills/planning/create-task/scripts/tsconfig.json +0 -12
  155. package/skills/planning/record-idea/scripts/new-idea.sh +0 -89
  156. package/skills/productivity/validate-agent-dir/scripts/package.json +0 -15
  157. package/skills/productivity/validate-agent-dir/scripts/pnpm-lock.yaml +0 -329
  158. package/skills/productivity/validate-agent-dir/scripts/pnpm-workspace.yaml +0 -2
  159. package/skills/productivity/validate-agent-dir/scripts/tsconfig.json +0 -19
  160. package/skills/productivity/validate-agent-dir/scripts/validate-agent-dir.test.ts +0 -317
  161. package/skills/workflow/run-pipeline-stage/REFERENCE.md +0 -22
  162. package/skills/workflow/run-pipeline-stage/scripts/generate-progress.test.ts +0 -682
  163. package/skills/workflow/run-pipeline-stage/scripts/generate-progress.ts +0 -588
  164. package/skills/workflow/run-pipeline-stage/scripts/package.json +0 -15
  165. package/skills/workflow/run-pipeline-stage/scripts/pnpm-lock.yaml +0 -342
  166. package/skills/workflow/run-pipeline-stage/scripts/pnpm-workspace.yaml +0 -2
  167. package/skills/workflow/run-pipeline-stage/scripts/tsconfig.json +0 -12
  168. package/test/classify-bead.test.mjs +0 -277
  169. package/test/claude-installer.test.mjs +0 -308
  170. package/test/cli.test.mjs +0 -314
  171. package/test/create-chores.test.mjs +0 -85
  172. package/test/fake-bd.test.mjs +0 -72
  173. package/test/fs.test.mjs +0 -76
  174. package/test/helpers/fake-bd.mjs +0 -87
  175. package/test/helpers/fake-claude.mjs +0 -152
  176. package/test/helpers/fake-pi.mjs +0 -132
  177. package/test/installer.test.mjs +0 -664
  178. package/test/notify-hook.test.mjs +0 -63
  179. package/test/pi-installer.test.mjs +0 -785
  180. package/test/pi-register.test.mjs +0 -342
  181. package/test/pi-sweep.test.mjs +0 -619
  182. package/test/release-gates.test.mjs +0 -190
  183. 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
@@ -79,6 +105,14 @@ const DEFAULT_SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
79
105
  // The committed user-settings template.
80
106
  const SETTINGS_TEMPLATE = "claude/settings.json";
81
107
 
108
+ // Claude user memory — the global prompt. The installer owns only the marked
109
+ // section; everything outside the markers is the user's own text.
110
+ const DEFAULT_INSTRUCTIONS_PATH = join(homedir(), ".claude", "CLAUDE.md");
111
+ const INSTRUCTIONS_BEGIN = "<!-- BEGIN agent-cortex:output-style -->";
112
+ const INSTRUCTIONS_END = "<!-- END agent-cortex:output-style -->";
113
+ // Canonical prose, shared with the pi extension (extensions/output-style).
114
+ const INSTRUCTIONS_SOURCE = join("extensions", "output-style", "output-style.md");
115
+
82
116
  // The only keys the installer owns outright: everything else in the live file
83
117
  // is personal config the merge must preserve.
84
118
  const SETTINGS_OWNED_KEYS = ["enabledPlugins", "extraKnownMarketplaces"];
@@ -90,27 +124,27 @@ const CONTRACT_VERSION = 1;
90
124
 
91
125
  const CLAUDE = "claude";
92
126
 
93
- const DEFAULT_WARN = (msg) => console.warn(`[claude-installer] ${msg}`);
94
-
95
- // Agents NOT transformed from the composable sources. `ralph` is authored natively for Claude
96
- // instead (agents-native/ralph.md) — its event-driven orchestration can't be produced by
97
- // section composition from the poll-loop sources.
98
- const DEFER = new Set(["ralph"]);
127
+ const DEFAULT_WARN = (/** @type {string} */ msg) => console.warn(`[claude-installer] ${msg}`);
99
128
 
100
- // Ralph-coupled pipeline skills that don't apply to the lean Claude Ralph (its completion
101
- // handling is event-driven, not the pi orchestrator's). Not copied into the Claude plugin.
102
- const SKILL_EXCLUDE = new Set(["ralph", "run-pipeline-stage"]);
103
129
 
104
130
  const NAME_PREFIX = "agent-cortex:";
105
131
 
132
+ /**
133
+ * @param {string} a
134
+ * @param {string} b
135
+ * @returns {number}
136
+ */
106
137
  function byName(a, b) {
107
138
  return a.localeCompare(b);
108
139
  }
109
140
 
110
- /** 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
+ */
111
146
  async function buildAgents(root) {
112
147
  const files = [];
113
- const skipped = [];
114
148
  const entries = (await readdir(join(root, "agents"), { withFileTypes: true })).sort((a, b) =>
115
149
  byName(a.name, b.name),
116
150
  );
@@ -120,11 +154,6 @@ async function buildAgents(root) {
120
154
  const dir = join(root, "agents", entry.name);
121
155
  if (!(await isFile(join(dir, "agent.md"))) || !(await isFile(join(dir, "claude", "frontmatter.json")))) continue;
122
156
 
123
- if (DEFER.has(entry.name)) {
124
- skipped.push(entry.name);
125
- continue;
126
- }
127
-
128
157
  const fm = await composeAgent(root, entry.name, CLAUDE);
129
158
  const slug = fm.name.startsWith(NAME_PREFIX) ? fm.name.slice(NAME_PREFIX.length) : fm.name;
130
159
  const header =
@@ -136,18 +165,7 @@ async function buildAgents(root) {
136
165
  `---\n`;
137
166
  files.push({ file: `${slug}.md`, content: `${header}\n${fm.body}\n` });
138
167
  }
139
- return { files, skipped };
140
- }
141
-
142
- /** Copy hand-authored Claude-native agents into agents/ verbatim. */
143
- async function buildNatives(root, nativeSrc) {
144
- const files = [];
145
- if (!(await isDirectory(nativeSrc))) return files;
146
- const names = (await readdir(nativeSrc)).filter((f) => f.endsWith(".md")).sort(byName);
147
- for (const file of names) {
148
- files.push({ file, content: await readFile(join(nativeSrc, file), "utf-8") });
149
- }
150
- return files;
168
+ return { files };
151
169
  }
152
170
 
153
171
  /**
@@ -156,36 +174,63 @@ async function buildNatives(root, nativeSrc) {
156
174
  * under skills/), token-substituting .md files against token-map.json's claude
157
175
  * column (the same materialisation as bin/installers/pi.mjs: no symlinks, so
158
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 }>}
159
183
  */
160
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 = [];
161
188
  const seen = new Map();
162
- const transform = (content) =>
163
- substituteTokens(content, CLAUDE, tokenMap, { dropNullTools: true, pluginRoot, resolveRelativePaths: true, warn });
164
-
165
- const names = [];
166
- let md = 0;
167
- let files = 0;
168
189
  for (const group of (await readdir(join(root, "skills"))).sort(byName)) {
169
190
  const groupDir = join(root, "skills", group);
170
191
  if (!(await isDirectory(groupDir))) continue;
171
192
  for (const name of (await readdir(groupDir)).sort(byName)) {
172
193
  const skillDir = join(groupDir, name);
173
194
  if (!(await isFile(join(skillDir, "SKILL.md")))) continue;
174
- if (SKILL_EXCLUDE.has(name)) continue;
175
195
  if (seen.has(name)) {
176
196
  throw new Error(`duplicate skill name "${name}" (${seen.get(name)} and ${group}) — names must be unique when flattened`);
177
197
  }
178
198
  seen.set(name, group);
179
- names.push(name);
180
- const stats = await copyTree(skillDir, join(skillOut, name), transform, dryRun);
181
- md += stats.md;
182
- files += stats.files;
199
+ sources.push({ name, dir: skillDir });
183
200
  }
184
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
+ }
185
223
  return { names, md, files, dir: skillOut };
186
224
  }
187
225
 
188
- /** 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
+ */
189
234
  async function copyTree(src, dest, transform, dryRun) {
190
235
  let md = 0;
191
236
  let files = 0;
@@ -208,7 +253,11 @@ async function copyTree(src, dest, transform, dryRun) {
208
253
  return { md, files };
209
254
  }
210
255
 
211
- /** 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
+ */
212
261
  async function buildPluginJson(root) {
213
262
  const pkg = JSON.parse(await readFile(join(root, "package.json"), "utf-8"));
214
263
  return (
@@ -234,6 +283,10 @@ async function buildPluginJson(root) {
234
283
  * install root ~/.agent-cortex doubles as the marketplace root), so `claude
235
284
  * plugin marketplace add ~/.agent-cortex` resolves ./claude relative to the
236
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 }[] }>}
237
290
  */
238
291
  async function buildMarketplaceManifest(root, outputName) {
239
292
  const pkg = JSON.parse(await readFile(join(root, "package.json"), "utf-8"));
@@ -255,11 +308,19 @@ async function buildMarketplaceManifest(root, outputName) {
255
308
  * Hook support files (hooks/claude/ except hooks.json) bundled into
256
309
  * hooks/ so hook commands can reference them via ${CLAUDE_PLUGIN_ROOT}.
257
310
  * hooks.json itself is special-cased to the plugin root (hooks.json).
311
+ * @param {string} root
312
+ * @returns {Promise<{ rel: string, content: string }[]>}
258
313
  */
259
314
  async function buildHookFiles(root) {
315
+ /** @type {{ rel: string, content: string }[]} */
260
316
  const files = [];
261
317
  const srcDir = join(root, "hooks", "claude");
262
318
  if (!(await isDirectory(srcDir))) return files;
319
+ /**
320
+ * @param {string} dir
321
+ * @param {string} rel
322
+ * @returns {Promise<void>}
323
+ */
263
324
  const walk = async (dir, rel) => {
264
325
  const entries = await readdir(dir, { withFileTypes: true });
265
326
  entries.sort((a, b) => byName(a.name, b.name));
@@ -281,8 +342,11 @@ async function buildHookFiles(root) {
281
342
  * at read time (not path-referenced) so the write-phase cleanup of the
282
343
  * output dir can never destroy the sources (outputs normally go to
283
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 }[]>}
284
347
  */
285
348
  async function buildHandAuthored(root) {
349
+ /** @type {{ rel: string, content: string | Buffer, mode: number }[]} */
286
350
  const items = [];
287
351
  const srcExtras = join(root, "claude-extras");
288
352
  if (await isFile(join(srcExtras, ".mcp.json"))) {
@@ -312,6 +376,8 @@ async function buildHandAuthored(root) {
312
376
  * - `enabledPlugins` and `extraKnownMarketplaces` are CLI-owned: they are
313
377
  * rewritten from the template outright. `permissions`, `hooks`, `env`,
314
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>}
315
381
  */
316
382
  async function installSettings({ root, target, dryRun, warn }) {
317
383
  const template = await readTemplateJson(join(root, SETTINGS_TEMPLATE), warn);
@@ -324,15 +390,19 @@ async function installSettings({ root, target, dryRun, warn }) {
324
390
  }
325
391
 
326
392
  const content = `${JSON.stringify(merged, null, 2)}\n`;
327
- return writeManagedJson({ target, content, dryRun });
393
+ return writeManagedFile({ target, content, dryRun });
328
394
  }
329
395
 
330
396
  /**
331
397
  * Merge the committed template over the live file. Template keys fill in
332
398
  * defaults, live values win (deeply), and keys the template does not know about
333
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>}
334
403
  */
335
404
  function mergeSettings(template, existing) {
405
+ /** @type {Record<string, any>} */
336
406
  const merged = {};
337
407
  for (const key of Object.keys(template)) {
338
408
  const templateValue = template[key];
@@ -350,12 +420,14 @@ function mergeSettings(template, existing) {
350
420
  }
351
421
 
352
422
  /**
353
- * Write the managed settings file. A legacy symlink is replaced with a real
354
- * file by writing a sibling temp file and renaming it over the target — rename
355
- * replaces the link itself, so the committed template is never written through.
423
+ * Write a managed file. A legacy symlink is replaced with a real file by writing
424
+ * a sibling temp file and renaming it over the target — rename replaces the link
425
+ * itself, so a committed template is never written through.
356
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>}
357
429
  */
358
- async function writeManagedJson({ target, content, dryRun }) {
430
+ async function writeManagedFile({ target, content, dryRun }) {
359
431
  const symlink = await isSymlink(target);
360
432
  const current = await readFileMaybe(target);
361
433
 
@@ -382,6 +454,62 @@ async function writeManagedJson({ target, content, dryRun }) {
382
454
  return { path: target, action: symlink ? "removed-symlink" : "written", removedSymlink: symlink };
383
455
  }
384
456
 
457
+ // ─── Claude user memory (CLAUDE.md) ──────────
458
+
459
+ /**
460
+ * Materialise the output-style block into the managed section of ~/.claude/CLAUDE.md.
461
+ *
462
+ * The section is delimited by HTML comments, so a re-install replaces only the
463
+ * text between them and every other line of the user's memory file survives. A
464
+ * file with exactly one marker (hand-edited or truncated) is left untouched with
465
+ * a warning, never guessed at.
466
+ * @param {{ root: string, target: string, dryRun: boolean, warn: Warn }} options
467
+ * @returns {Promise<ManagedFileResult | null>}
468
+ */
469
+ async function installInstructions({ root, target, dryRun, warn }) {
470
+ const block = await readFileMaybe(join(root, INSTRUCTIONS_SOURCE));
471
+ if (block === null) {
472
+ warn(`no ${INSTRUCTIONS_SOURCE} source in the package — skipping CLAUDE.md`);
473
+ return null;
474
+ }
475
+ const content = mergeManagedSection(await readFileMaybe(target), block);
476
+ if (content === null) {
477
+ warn(`${target} has an unterminated agent-cortex section — left unchanged; fix the markers by hand`);
478
+ return { path: target, action: "skipped-unmanaged", removedSymlink: false };
479
+ }
480
+ return writeManagedFile({ target, content, dryRun });
481
+ }
482
+
483
+ /**
484
+ * Return `existing` with the managed section replaced by `block`, or null when
485
+ * the file carries unmatched markers. A file without markers gets the section
486
+ * appended, so personal memory text is never disturbed.
487
+ * @param {string | null} existing
488
+ * @param {string} block
489
+ * @returns {string | null}
490
+ */
491
+ function mergeManagedSection(existing, block) {
492
+ const managed = `${INSTRUCTIONS_BEGIN}\n${block.trimEnd()}\n${INSTRUCTIONS_END}`;
493
+ if (existing === null) return `${managed}\n`;
494
+
495
+ const start = existing.indexOf(INSTRUCTIONS_BEGIN);
496
+ const end = existing.indexOf(INSTRUCTIONS_END);
497
+ if (start === -1 && end === -1) {
498
+ const head = existing.replace(/\s*$/, "");
499
+ return head === "" ? `${managed}\n` : `${head}\n\n${managed}\n`;
500
+ }
501
+ if (start === -1 || end === -1 || end < start) return null;
502
+
503
+ const before = existing.slice(0, start).replace(/\s*$/, "");
504
+ const after = existing.slice(end + INSTRUCTIONS_END.length).replace(/^\s*/, "").replace(/\s*$/, "");
505
+ return `${[before, managed, after].filter((part) => part !== "").join("\n\n")}\n`;
506
+ }
507
+
508
+ /**
509
+ * @param {string} filePath
510
+ * @param {Warn} warn
511
+ * @returns {Promise<Record<string, any> | null>}
512
+ */
385
513
  async function readTemplateJson(filePath, warn) {
386
514
  const raw = await readFileMaybe(filePath);
387
515
  if (raw === null) {
@@ -393,6 +521,11 @@ async function readTemplateJson(filePath, warn) {
393
521
  return parsed;
394
522
  }
395
523
 
524
+ /**
525
+ * @param {string} filePath
526
+ * @param {Warn} warn
527
+ * @returns {Promise<Record<string, any>>}
528
+ */
396
529
  async function readJsonObject(filePath, warn) {
397
530
  const raw = await readFileMaybe(filePath);
398
531
  if (raw === null) return {};
@@ -404,6 +537,10 @@ async function readJsonObject(filePath, warn) {
404
537
  return parsed;
405
538
  }
406
539
 
540
+ /**
541
+ * @param {string} raw
542
+ * @returns {Record<string, any> | null}
543
+ */
407
544
  function tryParseJsonObject(raw) {
408
545
  try {
409
546
  const parsed = JSON.parse(raw);
@@ -413,19 +550,31 @@ function tryParseJsonObject(raw) {
413
550
  }
414
551
  }
415
552
 
553
+ /**
554
+ * @param {unknown} value
555
+ * @returns {value is Record<string, any>}
556
+ */
416
557
  function isPlainObject(value) {
417
558
  return typeof value === "object" && value !== null && !Array.isArray(value);
418
559
  }
419
560
 
561
+ /**
562
+ * @param {string} filePath
563
+ * @returns {Promise<string | null>}
564
+ */
420
565
  async function readFileMaybe(filePath) {
421
566
  try {
422
567
  return await readFile(filePath, "utf-8");
423
568
  } catch (err) {
424
- if (err.code === "ENOENT") return null;
569
+ if (/** @type {NodeJS.ErrnoException} */ (err).code === "ENOENT") return null;
425
570
  throw err;
426
571
  }
427
572
  }
428
573
 
574
+ /**
575
+ * @param {string} filePath
576
+ * @returns {Promise<boolean>}
577
+ */
429
578
  async function isSymlink(filePath) {
430
579
  try {
431
580
  return (await lstat(filePath)).isSymbolicLink();
@@ -450,15 +599,20 @@ async function isSymlink(filePath) {
450
599
  * (default ~/.claude/settings.json). Passing it
451
600
  * explicitly materialises settings even with
452
601
  * --output — the test/CI seam.
602
+ * @param {string} [options.instructionsPath] Claude user-memory target
603
+ * (default ~/.claude/CLAUDE.md). Passing it
604
+ * explicitly materialises the section even
605
+ * with --output — the test/CI seam.
453
606
  * @param {(msg: string) => void} [options.warn] Warning sink, also collected in `warnings`.
454
- * @returns {{ output: string, marketplaceRoot: string|null, manifestPath: string|null,
455
- * marketplaceManifest: object|null, agents: string[], natives: string[],
607
+ * @returns {Promise<{ output: string, marketplaceRoot: string|null, manifestPath: string|null,
608
+ * marketplaceManifest: object|null, agents: string[],
456
609
  * skills: {names: string[], md: number, files: number, dir: string},
457
610
  * hooks: boolean, hookFiles: string[], handAuthored: string[],
458
611
  * settings: {path: string, action: string, removedSymlink: boolean}|null,
459
- * warnings: string[], dryRun: boolean }}
612
+ * instructions: {path: string, action: string, removedSymlink: boolean}|null,
613
+ * pluginVersion: string, warnings: string[], dryRun: boolean }>}
460
614
  */
461
- export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = false, pluginRoot, settingsPath, warn = DEFAULT_WARN } = {}) {
615
+ export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = false, pluginRoot, settingsPath, instructionsPath, warn = DEFAULT_WARN } = {}) {
462
616
  const tokenMap = await loadTokenMap(root);
463
617
  if (typeof tokenMap.version === "number" && tokenMap.version > CONTRACT_VERSION) {
464
618
  throw new Error(
@@ -466,8 +620,8 @@ export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = fals
466
620
  );
467
621
  }
468
622
 
469
- const warnings = [];
470
- const warnSink = (msg) => {
623
+ const warnings = /** @type {string[]} */ ([]);
624
+ const warnSink = (/** @type {string} */ msg) => {
471
625
  warnings.push(msg);
472
626
  warn(msg);
473
627
  };
@@ -477,9 +631,9 @@ export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = fals
477
631
  // --output is generate-only (tests/CI), matching its no-registration semantics.
478
632
  const isDefaultInstall = output === undefined;
479
633
  const marketplaceRoot = isDefaultInstall ? dirname(out) : null;
480
- const manifestPath = isDefaultInstall ? join(marketplaceRoot, ".claude-plugin", "marketplace.json") : null;
634
+ const manifestPath = isDefaultInstall && marketplaceRoot !== null ? join(marketplaceRoot, ".claude-plugin", "marketplace.json") : null;
481
635
 
482
- const rootPlugin = pluginRoot ?? tokenMap.paths.plugin_root?.[CLAUDE];
636
+ const rootPlugin = pluginRoot ?? /** @type {string | undefined} */ (tokenMap.paths.plugin_root?.[CLAUDE]);
483
637
 
484
638
  // Read hand-authored extras from the canonical claude-extras/ dir BEFORE
485
639
  // the cleanup below (defensive: the output is normally ~/.agent-cortex/claude
@@ -498,17 +652,23 @@ export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = fals
498
652
  await mkdir(out, { recursive: true });
499
653
  }
500
654
 
501
- // The default install owns ~/.claude/settings.json; --output is the
502
- // generate-only form and leaves the user's Claude config alone. An explicit
503
- // settingsPath requests the write regardless (the test/CI seam).
655
+ // The default install owns ~/.claude/settings.json and the managed section of
656
+ // ~/.claude/CLAUDE.md; --output is the generate-only form and leaves the
657
+ // user's Claude config alone. Explicit paths request the write regardless
658
+ // (the test/CI seam).
504
659
  const settingsTarget = settingsPath ?? DEFAULT_SETTINGS_PATH;
505
660
  const settings =
506
661
  isDefaultInstall || settingsPath !== undefined
507
662
  ? await installSettings({ root, target: settingsTarget, dryRun, warn: warnSink })
508
663
  : null;
509
664
 
665
+ const instructionsTarget = instructionsPath ?? DEFAULT_INSTRUCTIONS_PATH;
666
+ const instructions =
667
+ isDefaultInstall || instructionsPath !== undefined
668
+ ? await installInstructions({ root, target: instructionsTarget, dryRun, warn: warnSink })
669
+ : null;
670
+
510
671
  const agents = await buildAgents(root);
511
- const natives = await buildNatives(root, join(root, "agents-native"));
512
672
  const skillOut = join(out, "skills");
513
673
  const skills = await buildSkills(root, skillOut, { dryRun, pluginRoot: rootPlugin, tokenMap, warn: warnSink });
514
674
  const pluginJson = await buildPluginJson(root);
@@ -524,22 +684,19 @@ export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = fals
524
684
  marketplaceManifest,
525
685
  pluginVersion: JSON.parse(pluginJson).version,
526
686
  agents: agents.files.map((f) => f.file.replace(/\.md$/, "")),
527
- natives: natives.map((f) => f.file.replace(/\.md$/, "")),
528
687
  skills: { names: skills.names, md: skills.md, files: skills.files, dir: skillOut },
529
688
  hooks: hooksJson !== null,
530
689
  hookFiles: hookFiles.map((f) => f.rel),
531
690
  handAuthored: handAuthored.map((f) => f.rel),
532
691
  settings,
692
+ instructions,
533
693
  warnings,
534
694
  dryRun,
535
695
  };
536
696
 
537
- if (agents.skipped.length) console.log(`Skipped composable source(s): ${agents.skipped.join(", ")}`);
538
-
539
697
  if (dryRun) {
540
698
  console.log(`DRY-RUN — would generate into ${out}, no files written:`);
541
699
  console.log(` agents: ${summary.agents.join(", ")}`);
542
- if (summary.natives.length) console.log(` native agents: ${summary.natives.join(", ")}`);
543
700
  console.log(` skills: ${summary.skills.names.length} copied → ${skillOut}/<name>/ (token-substituted)`);
544
701
  console.log(` .claude-plugin/plugin.json (version ${JSON.parse(pluginJson).version})`);
545
702
  if (isDefaultInstall && manifestPath) {
@@ -551,18 +708,21 @@ export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = fals
551
708
  if (summary.handAuthored.length)
552
709
  console.log(` hand-authored files: ${summary.handAuthored.join(", ")} (copied from repo claude-extras/)`);
553
710
  if (summary.settings) console.log(` settings → ${summary.settings.path} (${summary.settings.action})`);
711
+ if (summary.instructions)
712
+ console.log(` CLAUDE.md → ${summary.instructions.path} (${summary.instructions.action})`);
554
713
  return summary;
555
714
  }
556
715
 
557
716
  // Write phase — the cleanup before buildSkills already cleared stale children.
558
717
  if (settings) console.log(`Materialised Claude settings → ${settings.path} (${settings.action})`);
718
+ if (instructions && instructions.action !== "skipped-unmanaged")
719
+ console.log(`Materialised CLAUDE.md section → ${instructions.path} (${instructions.action})`);
559
720
  const agentOut = join(out, "agents");
560
721
  await mkdir(agentOut, { recursive: true });
561
- for (const { file, content } of [...agents.files, ...natives]) {
722
+ for (const { file, content } of agents.files) {
562
723
  await writeFile(join(agentOut, file), content);
563
724
  }
564
725
  console.log(`Generated ${summary.agents.length} Claude agent(s): ${summary.agents.join(", ")}`);
565
- if (summary.natives.length) console.log(`Copied ${summary.natives.length} native Claude agent(s): ${summary.natives.join(", ")}`);
566
726
 
567
727
  await mkdir(skillOut, { recursive: true });
568
728
  console.log(
@@ -580,7 +740,7 @@ export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = fals
580
740
  console.log(`Wrote ${manifestPath} (plugin agent-cortex @ ./${basename(out)})`);
581
741
  }
582
742
 
583
- if (summary.hooks) {
743
+ if (hooksJson !== null) {
584
744
  await writeFile(join(out, "hooks.json"), hooksJson);
585
745
  console.log(`Wrote ${join(out, "hooks.json")} (from ${hooksSrc})`);
586
746
 
@@ -615,8 +775,13 @@ export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = fals
615
775
  // The `claude` CLI on PATH. Overridable in tests via the claudeBin option.
616
776
  const DEFAULT_CLAUDE_BIN = "claude";
617
777
 
618
- /** Validate a marketplace manifest object; returns the marketplace and plugin
619
- * 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
+ */
620
785
  function parseMarketplaceManifest(manifest, label) {
621
786
  const marketplace = manifest.name;
622
787
  const plugin = manifest.plugins?.[0]?.name;
@@ -631,6 +796,8 @@ function parseMarketplaceManifest(manifest, label) {
631
796
  /**
632
797
  * Read the marketplace manifest that exposes the plugin at install root
633
798
  * (<root>/.claude-plugin/marketplace.json).
799
+ * @param {string} root
800
+ * @returns {Promise<{ manifest: any, path: string }>}
634
801
  */
635
802
  async function readMarketplaceManifest(root) {
636
803
  const manifestPath = join(root, ".claude-plugin", "marketplace.json");
@@ -639,7 +806,7 @@ async function readMarketplaceManifest(root) {
639
806
  manifest = JSON.parse(await readFile(manifestPath, "utf-8"));
640
807
  } catch (err) {
641
808
  throw new Error(
642
- `no Claude marketplace manifest at ${manifestPath} (${err.message}) — ` +
809
+ `no Claude marketplace manifest at ${manifestPath} (${/** @type {Error} */ (err).message}) — ` +
643
810
  `"agent-cortex install claude" registers the plugin by pointing Claude Code at the install root`,
644
811
  );
645
812
  }
@@ -651,6 +818,9 @@ async function readMarketplaceManifest(root) {
651
818
  * where <source> is the marketplace manifest's plugin source) — the signature a
652
819
  * directory-source marketplace exposes to `claude plugin`. Read when the version
653
820
  * is not passed explicitly; undefined when it cannot be determined.
821
+ * @param {string} root
822
+ * @param {any} manifest
823
+ * @returns {Promise<string | undefined>}
654
824
  */
655
825
  async function readMaterialisedPluginVersion(root, manifest) {
656
826
  const source = manifest.plugins?.[0]?.source;
@@ -663,7 +833,10 @@ async function readMaterialisedPluginVersion(root, manifest) {
663
833
  }
664
834
  }
665
835
 
666
- /** 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>} */
667
840
  function runClaudeCommand(claudeBin, args) {
668
841
  const label = `${claudeBin} ${args.join(" ")}`;
669
842
  return new Promise((resolve, reject) => {
@@ -682,6 +855,8 @@ function runClaudeCommand(claudeBin, args) {
682
855
  * `claude plugin --help` with output suppressed. Resolves `{ ok: true }` when
683
856
  * it exits 0, otherwise `{ ok: false, message }` naming the failure (missing
684
857
  * binary vs missing subcommand) so registration can give targeted guidance.
858
+ * @param {string} claudeBin
859
+ * @returns {Promise<{ ok: true } | { ok: false, message: string }>}
685
860
  */
686
861
  function probePluginSupport(claudeBin) {
687
862
  return new Promise((resolve) => {
@@ -705,7 +880,12 @@ function probePluginSupport(claudeBin) {
705
880
  });
706
881
  }
707
882
 
708
- /** 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
+ */
709
889
  function queryClaudeCommand(claudeBin, args) {
710
890
  const label = `${claudeBin} ${args.join(" ")}`;
711
891
  return new Promise((resolve, reject) => {
@@ -725,6 +905,9 @@ function queryClaudeCommand(claudeBin, args) {
725
905
  * Whether `claude plugin marketplace list` already registers `marketplace`.
726
906
  * Unparseable output counts as not-registered: the fallback `marketplace add`
727
907
  * is itself idempotent (the CLI reports "already on disk", exit 0).
908
+ * @param {string} claudeBin
909
+ * @param {string} marketplace
910
+ * @returns {Promise<boolean>}
728
911
  */
729
912
  async function marketplaceRegistered(claudeBin, marketplace) {
730
913
  const out = await queryClaudeCommand(claudeBin, ["plugin", "marketplace", "list", "--json"]);
@@ -741,6 +924,9 @@ async function marketplaceRegistered(claudeBin, marketplace) {
741
924
  * Installed version of `pluginId` per `claude plugin list --json`; null when
742
925
  * not installed or the output is unreadable (the caller falls back to the
743
926
  * idempotent `plugin install`).
927
+ * @param {string} claudeBin
928
+ * @param {string} pluginId
929
+ * @returns {Promise<string | null>}
744
930
  */
745
931
  async function installedPluginVersion(claudeBin, pluginId) {
746
932
  const out = await queryClaudeCommand(claudeBin, ["plugin", "list", "--json"]);
@@ -790,8 +976,7 @@ async function installedPluginVersion(claudeBin, pluginId) {
790
976
  * @param {string} [options.pluginVersion] Materialised plugin version (default: read from plugin.json)
791
977
  * @param {boolean} [options.requireRegister] Exit non-zero when registration cannot run
792
978
  * @param {(msg: string) => void} [options.warn] Warning sink (default: console.warn)
793
- * @returns {{ marketplace: string, plugin: string, commands: string[][],
794
- * dryRun: boolean, registered: boolean, skipped: string[] }}
979
+ * @returns {Promise<RegisterClaudeResult>}
795
980
  */
796
981
  export async function registerClaude({
797
982
  root = PACKAGE_ROOT,
@@ -809,7 +994,7 @@ export async function registerClaude({
809
994
  const version = pluginVersion ?? (await readMaterialisedPluginVersion(root, manifestObj));
810
995
  const home = resolve(root);
811
996
 
812
- const summary = { marketplace, plugin, commands: [], dryRun, registered: true, skipped: [] };
997
+ const summary = /** @type {RegisterClaudeResult} */ ({ marketplace, plugin, commands: [], dryRun, registered: true, skipped: [] });
813
998
  console.log(`Registering plugin "${pluginId}" with Claude Code (via ${claudeBin})…`);
814
999
 
815
1000
  const manualCommands = [