@jaybeeuu/agent-cortex 2.6.0 → 2.8.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 (64) hide show
  1. package/CHANGELOG.md +269 -0
  2. package/README.md +102 -8
  3. package/agents/README.md +2 -2
  4. package/agents/ralph/agent.md +19 -16
  5. package/agents/ralph/claude/wake.md +4 -4
  6. package/agents/ralph/pi/wake.md +15 -20
  7. package/bin/agent-cortex.mjs +176 -67
  8. package/bin/installers/claude.mjs +33 -8
  9. package/bin/installers/pi.mjs +11 -1
  10. package/docs/skills/skill-anatomy.md +212 -0
  11. package/extensions/post-merge-watch/README.md +133 -0
  12. package/extensions/post-merge-watch/index.test.ts +247 -0
  13. package/extensions/post-merge-watch/index.ts +123 -0
  14. package/extensions/post-merge-watch/package.json +16 -0
  15. package/extensions/post-merge-watch/watch.test.ts +469 -0
  16. package/extensions/post-merge-watch/watch.ts +431 -0
  17. package/extensions/session-clear/README.md +74 -0
  18. package/extensions/session-clear/index.test.ts +524 -0
  19. package/extensions/session-clear/index.ts +159 -0
  20. package/extensions/session-clear/package.json +16 -0
  21. package/lib/cli.mjs +88 -1
  22. package/lib/harness-update.mjs +254 -0
  23. package/lib/harness-versions.mjs +36 -2
  24. package/lib/self-update.mjs +553 -0
  25. package/package.json +2 -1
  26. package/scripts/lib/docs.mjs +67 -0
  27. package/skills/engineering/implement-task/SKILL.md +124 -0
  28. package/skills/engineering/tdd/SKILL.md +2 -0
  29. package/skills/planning/create-task/REFERENCE.md +20 -3
  30. package/skills/planning/create-task/SKILL.md +1 -1
  31. package/skills/planning/create-task/pipeline.json +16 -0
  32. package/skills/planning/create-task/scripts/create-chores.mjs +51 -24
  33. package/skills/planning/create-task/templates/code.md +8 -3
  34. package/skills/planning/create-task/templates/document.md +8 -3
  35. package/skills/planning/create-task/templates/fix.md +8 -3
  36. package/skills/planning/create-task/templates/housekeeping.md +69 -0
  37. package/skills/planning/create-task/templates/post-merge.md +67 -0
  38. package/skills/planning/create-task/templates/review.md +8 -3
  39. package/skills/planning/create-task/templates/verify.md +8 -3
  40. package/skills/productivity/bd-tool/SKILL.md +55 -0
  41. package/skills/productivity/write-a-skill/SKILL.md +2 -2
  42. package/skills/review/refactor-skill/SKILL.md +2 -2
  43. package/skills/review/review-defects/EXAMPLES.md +122 -0
  44. package/skills/review/review-defects/SKILL.md +91 -0
  45. package/skills/style/style-tests/EXAMPLES.md +30 -0
  46. package/skills/style/style-tests/SKILL.md +10 -6
  47. package/skills/workflow/plan/SKILL.md +14 -3
  48. package/skills/workflow/ralph/REFERENCE.md +137 -30
  49. package/skills/workflow/ralph/SKILL.md +26 -16
  50. package/skills/workflow/ralph/scripts/post-merge-check.mjs +651 -0
  51. package/skills/workflow/run-pipeline-stage/SKILL.md +41 -23
  52. package/skills/workflow/run-pipeline-stage/playbooks/code.md +14 -8
  53. package/skills/workflow/run-pipeline-stage/playbooks/document.md +2 -2
  54. package/skills/workflow/run-pipeline-stage/playbooks/fix.md +2 -2
  55. package/skills/workflow/run-pipeline-stage/playbooks/housekeeping.md +58 -0
  56. package/skills/workflow/run-pipeline-stage/playbooks/post-merge.md +54 -0
  57. package/skills/workflow/run-pipeline-stage/playbooks/review.md +34 -3
  58. package/skills/workflow/run-pipeline-stage/playbooks/verify.md +2 -2
  59. package/skills/workflow/run-pipeline-stage/prompts/stage-runner.md +66 -10
  60. package/skills/workflow/run-pipeline-stage/scripts/recover-brief.mjs +805 -0
  61. package/skills/workflow/run-pipeline-stage/scripts/trail-entry.mjs +702 -0
  62. package/skills/workflow/using-agent-skills/SKILL.md +15 -7
  63. package/token-map.README.md +41 -4
  64. package/token-map.json +16 -5
@@ -46,30 +46,25 @@ Initialize `.agent-cortex/ralph/state.json` as `{ "timerShellId": null, "infligh
46
46
  - **inflight[].agentId** — the ID `task` returned for that worker; it is the argument `wait_for_agents` takes, the identifier for a `read_agent` fallback, and the key the completion is correlated to its bead.
47
47
  - **timerShellId** — stays `null` on PI forever: the blocking wait replaces a poll timer, so nothing is ever recorded there.
48
48
 
49
- ### Log flush
50
-
51
- Flush new log lines as part of handling each wait result — that is when the loop wakes, and a worker's final lines arrive with the completion. Run this procedure each time `wait_for_agents` returns (or whenever a new agent is dispatched):
52
-
53
- 1. **Query in-flight beads**: read `inflight` from `state.json`. Each entry has a `parentId` and `logLine`.
54
- 2. For each in-flight entry, read new lines from its log file:
55
- ```bash
56
- tail -n +<logLine> .agent-cortex/ralph/ralph-<parentId>.log 2>/dev/null
57
- ```
58
- 3. For each entry that has new lines:
59
- - **Post a chat summary** of new key events (stage transitions and notable events — not every line verbatim). Format:
60
- ```
61
- 📋 [parent-id] <title>
62
- coding → verifying (or whatever transition)
63
- Tests: 12 passed, 0 failed
64
- ```
65
- - Update `logLine` in `state.json` for that entry.
66
- 4. If no entry had new lines, post nothing — do not spam the chat with empty polls.
49
+ ### Reading the trail
50
+
51
+ There is no log file and nothing to flush. A worker appends its own checkpoints to its bead with
52
+ `trail-entry.mjs`, so progress is read from the bead itself when a wait returns:
53
+
54
+ ```bash
55
+ bd show <chore-id> # entries: Outcome, Evidence, What is left
56
+ bd provenance log <chore-id> # refs: git-sha, branch, pr, work-id
57
+ ```
58
+
59
+ Read each completed worker's bead once, post a chat summary of what is new — the stage transition,
60
+ the test and lint results, the close reason — and post nothing when nothing new arrived. Nothing in
61
+ `state.json` needs a bookmark, because the trail is not a stream to follow.
67
62
 
68
63
  ### Wait loop lifecycle
69
64
 
70
65
  - **Dispatch-and-wait** — during initialization, dispatch up to 5 AFK chore subagents with `task` (or an expansion worker for a chore-less AFK bead) and hold their agent IDs (recorded in `state.json`'s `inflight` list; expansion entries carry `kind: "expand"` and `choreId: null`).
71
66
  - **Wait** — call `wait_for_agents` with the agent IDs of every in-flight agent, stage worker or expansion worker (`timeout` defaults to 120s). The tool blocks until at least one finishes. For each completed agent:
72
- 1. Flush its log tail if it has new lines (see _Log flush_ above) to surface progress to chat.
67
+ 1. Read its bead trail (see _Reading the trail_ above) to surface progress to chat.
73
68
  2. Apply the shared completion handling described under _Completion arrival_ above.
74
69
  3. Drop the completed agent ID from the wait set.
75
70
  - **Repeat** — dispatch replacements for completed work (up to 5 in flight), add their new IDs to the wait set, and call `wait_for_agents` again with the remaining + new IDs. A completed expansion leaves a slot to fill with its parent's newly ready `stage:code` chore.
@@ -86,4 +81,4 @@ Nothing to kill: stopping means simply **stop calling `wait_for_agents`**. There
86
81
  - **Always** keep a `wait_for_agents` call outstanding while agents are in-flight — stage workers and expansion workers alike. Never let running agent work stall without a wait active.
87
82
  - **Never** re-dispatch an agent that is merely STILL RUNNING — wait again on the same ID.
88
83
  - **Never** start `sleep` / background timer loops for completion detection on PI — `wait_for_agents` replaces them, and there is nothing to kill on HITL Pause.
89
- - **Never** post empty poll updates to chat — only surface new log content.
84
+ - **Never** post empty poll updates to chat — only surface what a bead actually reports.
@@ -1,14 +1,26 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { parseArgs, buildHelpText, resolveProgramName, validateHarness } from "../lib/cli.mjs";
3
+ import { parseArgs, buildHelpText, resolveProgramName, validateHarness, SUPPORTED_HARNESSES } from "../lib/cli.mjs";
4
4
  import {
5
5
  collectVersionReport,
6
+ collectHarnessStatus,
6
7
  formatVersionReport,
7
8
  serialiseVersionReport,
8
9
  } from "../lib/harness-versions.mjs";
10
+ import {
11
+ describeClaudeArtifacts,
12
+ describePiArtifacts,
13
+ formatHarnessTable,
14
+ formatUpdateFlags,
15
+ formatUpdateReport,
16
+ promptHarnessSelection,
17
+ runUpdate,
18
+ selectHarnesses,
19
+ } from "../lib/harness-update.mjs";
9
20
  import { join, dirname } from "node:path";
10
21
  import { fileURLToPath } from "node:url";
11
- import { readFile } from "node:fs/promises";
22
+ import { readFile, realpath } from "node:fs/promises";
23
+ import { runSelfUpdate } from "../lib/self-update.mjs";
12
24
  import { installPi, registerPi, sweepLegacyPiUserDirs } from "./installers/pi.mjs";
13
25
 
14
26
  /** @typedef {import("./installers/pi.mjs").InstallPiResult} InstallPiResult */
@@ -90,50 +102,76 @@ if (parsed.command === "install") {
90
102
  process.exit(1);
91
103
  }
92
104
 
93
- if (parsed.harness === "pi") {
105
+ try {
106
+ await installHarness(/** @type {"pi" | "claude"} */ (parsed.harness), {
107
+ dryRun: parsed.dryRun ?? false,
108
+ ...(parsed.output ? { output: parsed.output } : {}),
109
+ ...(parsed.pluginRoot ? { pluginRoot: parsed.pluginRoot } : {}),
110
+ requireRegister: parsed.requireRegister ?? false,
111
+ });
112
+ } catch (err) {
113
+ process.stderr.write(`Install failed: ${/** @type {Error} */ (err).message}\n`);
114
+ process.exit(1);
115
+ }
116
+ process.exit(0);
117
+ }
118
+
119
+ if (parsed.command === "update") {
120
+ process.exitCode = await runUpdateCommand(parsed, programName);
121
+ }
122
+
123
+ /**
124
+ * Run one harness's installer and, for a plain install, its registration.
125
+ * `install <harness>` and `update` both come through here, so update-vs-install
126
+ * stays the installer's decision and neither command grows a second install path.
127
+ * @param {"pi" | "claude"} harness
128
+ * @param {object} [options]
129
+ * @param {boolean} [options.dryRun]
130
+ * @param {string} [options.output]
131
+ * @param {string} [options.pluginRoot]
132
+ * @param {boolean} [options.requireRegister]
133
+ * @returns {Promise<string[]>} What the installer wrote, for the update report
134
+ */
135
+ async function installHarness(harness, { dryRun = false, output, pluginRoot, requireRegister = false } = {}) {
136
+ if (harness === "pi") {
94
137
  // A plain `agent-cortex install pi` stages the package into the home
95
138
  // install root (~/.agent-cortex/pi) and registers that directory with pi
96
139
  // through pi's own `pi install` (idempotent by state: a re-run is a no-op
97
140
  // update); `--output <dir>` is the stage-only form (tests/CI) and prints
98
141
  // the registration command instead.
99
- try {
100
- const result = await installPi({
101
- dryRun: parsed.dryRun ?? false,
102
- warn: () => {}, // warnings surface once in the printed summary
103
- ...(parsed.output ? { output: parsed.output } : {}),
104
- ...(parsed.pluginRoot ? { pluginRoot: parsed.pluginRoot } : {}),
142
+ const result = await installPi({
143
+ dryRun,
144
+ warn: () => {}, // warnings surface once in the printed summary
145
+ ...(output ? { output } : {}),
146
+ ...(pluginRoot ? { pluginRoot } : {}),
147
+ });
148
+ printPiInstall(result);
149
+ if (output === undefined) {
150
+ const registration = await registerPi({
151
+ output: result.output,
152
+ dryRun,
153
+ requireRegister,
154
+ warn: (msg) => process.stdout.write(` ⚠ ${msg}\n`),
105
155
  });
106
- printPiInstall(result);
107
- if (parsed.output === undefined) {
108
- const registration = await registerPi({
109
- output: result.output,
110
- dryRun: parsed.dryRun ?? false,
111
- requireRegister: parsed.requireRegister ?? false,
112
- warn: (msg) => process.stdout.write(` ⚠ ${msg}\n`),
113
- });
114
- // The old installer's rendered copies are only stale once the staged
115
- // package is what pi loads — and nothing is deleted on a dry run. The
116
- // flag is passed through as well, so the sweep's own guarantee does not
117
- // depend on this guard staying in place.
118
- if (registration.registered && !(parsed.dryRun ?? false)) {
119
- printPiSweep(
120
- await sweepLegacyPiUserDirs({
121
- piUserDir: dirname(registration.settingsPath),
122
- dryRun: parsed.dryRun ?? false,
123
- }),
124
- );
125
- }
126
- } else {
127
- process.stdout.write(` → register it with pi: pi install ${result.output}\n`);
156
+ // The old installer's rendered copies are only stale once the staged
157
+ // package is what pi loads — and nothing is deleted on a dry run. The
158
+ // flag is passed through as well, so the sweep's own guarantee does not
159
+ // depend on this guard staying in place.
160
+ if (registration.registered && !dryRun) {
161
+ printPiSweep(
162
+ await sweepLegacyPiUserDirs({
163
+ piUserDir: dirname(registration.settingsPath),
164
+ dryRun,
165
+ }),
166
+ );
128
167
  }
129
- } catch (err) {
130
- process.stderr.write(`Install failed: ${/** @type {Error} */ (err).message}\n`);
131
- process.exit(1);
168
+ } else {
169
+ process.stdout.write(` → register it with pi: pi install ${result.output}\n`);
132
170
  }
133
- process.exit(0);
171
+ return describePiArtifacts(result);
134
172
  }
135
173
 
136
- process.stdout.write(`Installing agent-cortex for "${parsed.harness}" harness…\n`);
174
+ process.stdout.write(`Installing agent-cortex for "${harness}" harness…\n`);
137
175
 
138
176
  // claude is the first wired harness: a plain `agent-cortex install claude`
139
177
  // materialises the plugin into the home install root (~/.agent-cortex/claude)
@@ -144,40 +182,90 @@ if (parsed.command === "install") {
144
182
  // (marketplace add → install → update); `--output <dir>` is the generate-only
145
183
  // form (tests/CI validation), and the generated claude/ plugin subtree is
146
184
  // never committed — only the settings template is.
147
- if (parsed.harness === "claude") {
148
- // Avoid loading the installer on the help/summary paths and for other harnesses.
149
- const { installClaude, registerClaude } = await import("./installers/claude.mjs");
150
- try {
151
- const result = await installClaude({
152
- dryRun: parsed.dryRun ?? false,
153
- warn: () => {}, // warnings surface once via result.warnings
154
- ...(parsed.output ? { output: parsed.output } : {}),
155
- });
156
- for (const warning of result.warnings) {
157
- process.stdout.write(` ⚠ ${warning}\n`);
185
+
186
+ // Avoid loading the installer on the help/summary paths and for other harnesses.
187
+ const { installClaude, registerClaude } = await import("./installers/claude.mjs");
188
+ const result = await installClaude({
189
+ dryRun,
190
+ warn: () => {}, // warnings surface once via result.warnings
191
+ ...(output ? { output } : {}),
192
+ });
193
+ for (const warning of result.warnings) {
194
+ process.stdout.write(` ⚠ ${warning}\n`);
195
+ }
196
+ // Runtime registration happens only for the plain install — `--output`
197
+ // keeps generating only (documented in --help).
198
+ if (output === undefined) {
199
+ await registerClaude({
200
+ // A default install always materialises the marketplace manifest; the
201
+ // generate-only form (--output) is the only path where it is null, and
202
+ // this branch is not that path.
203
+ root: /** @type {string} */ (result.marketplaceRoot),
204
+ manifest: /** @type {object} */ (result.marketplaceManifest),
205
+ pluginVersion: result.pluginVersion,
206
+ // The enablement check reads the same file this install just merged:
207
+ // a plugin Claude Code does not enable loads nothing.
208
+ ...(result.settings ? { settingsPath: result.settings.path } : {}),
209
+ dryRun,
210
+ requireRegister,
211
+ });
212
+ }
213
+ return describeClaudeArtifacts(result);
214
+ }
215
+
216
+ /**
217
+ * `ctx update`: choose the harnesses, refresh each through its installer, report
218
+ * what changed. Every write happens inside `installHarness`'s call into the
219
+ * harness installer — this command adds selection and reporting only.
220
+ * @param {import("../lib/cli.mjs").ParsedArgs} parsed
221
+ * @param {string} programName
222
+ * @returns {Promise<number>} The exit code
223
+ */
224
+ async function runUpdateCommand(parsed, programName) {
225
+ if (parsed.optionError) {
226
+ process.stderr.write(`${parsed.optionError}\n`);
227
+ return 1;
228
+ }
229
+ try {
230
+ const cliVersion = await readPackageVersion();
231
+ const statuses = await collectHarnessStatus({ cli: cliVersion });
232
+ const all = parsed.all ?? false;
233
+ let harnesses = parsed.harnesses ?? [];
234
+
235
+ // Bare `ctx update` asks; a non-interactive shell gets the same table plus
236
+ // the flags that select without a prompt, and a non-zero exit so a script
237
+ // never mistakes "nothing chosen" for "nothing to do".
238
+ if (harnesses.length === 0 && !all) {
239
+ if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
240
+ process.stdout.write(`${formatHarnessTable(statuses)}\n\n${formatUpdateFlags(programName)}\n`);
241
+ return 1;
158
242
  }
159
- // Runtime registration happens only for the plain install — `--output`
160
- // keeps generating only (documented in --help).
161
- if (parsed.output === undefined) {
162
- await registerClaude({
163
- // A default install always materialises the marketplace manifest; the
164
- // generate-only form (--output) is the only path where it is null, and
165
- // this branch is not that path.
166
- root: /** @type {string} */ (result.marketplaceRoot),
167
- manifest: /** @type {object} */ (result.marketplaceManifest),
168
- pluginVersion: result.pluginVersion,
169
- // The enablement check reads the same file this install just merged:
170
- // a plugin Claude Code does not enable loads nothing.
171
- ...(result.settings ? { settingsPath: result.settings.path } : {}),
172
- dryRun: parsed.dryRun ?? false,
173
- requireRegister: parsed.requireRegister ?? false,
174
- });
243
+ harnesses = await promptHarnessSelection({ statuses });
244
+ if (harnesses.length === 0) {
245
+ process.stdout.write("No harness selected; nothing updated.\n");
246
+ return 0;
175
247
  }
176
- process.exit(0);
177
- } catch (err) {
178
- process.stderr.write(`Install failed: ${/** @type {Error} */ (err).message}\n`);
179
- process.exit(1);
180
248
  }
249
+
250
+ const plan = selectHarnesses({ statuses, harnesses, all, force: parsed.force ?? false });
251
+ if (plan.unknown.length > 0) {
252
+ const names = plan.unknown.map((name) => `"${name}"`).join(", ");
253
+ process.stderr.write(
254
+ `Unknown harness ${names}. Supported harnesses: ${SUPPORTED_HARNESSES.join(", ")}\nRun "${programName} --help" for usage.\n`,
255
+ );
256
+ return 1;
257
+ }
258
+
259
+ const entries = await runUpdate({
260
+ cli: cliVersion,
261
+ targets: plan.targets,
262
+ refresh: (harness) => installHarness(/** @type {"pi" | "claude"} */ (harness), {}),
263
+ });
264
+ process.stdout.write(`${formatUpdateReport({ cli: cliVersion, entries, skipped: plan.skipped })}\n`);
265
+ return 0;
266
+ } catch (err) {
267
+ process.stderr.write(`Update failed: ${/** @type {Error} */ (err).message}\n`);
268
+ return 1;
181
269
  }
182
270
  }
183
271
 
@@ -192,6 +280,24 @@ function printPiSweep({ removed }) {
192
280
  }
193
281
  }
194
282
 
283
+ if (parsed.command === "self-update") {
284
+ if (parsed.optionError) {
285
+ process.stderr.write(`${parsed.optionError}\n`);
286
+ process.exit(1);
287
+ }
288
+ try {
289
+ // The real path follows the global bin shim into the layout of the manager
290
+ // that installed this copy, which is what identifies the owner. Taking it
291
+ // from this file's own URL keeps detection on the running entry point, not
292
+ // on the cwd or on PATH.
293
+ const entryPoint = await realpath(fileURLToPath(import.meta.url));
294
+ process.exitCode = await runSelfUpdate({ entryPoint, programName, check: parsed.check ?? false });
295
+ } catch (err) {
296
+ process.stderr.write(`${/** @type {Error} */ (err).message}\n`);
297
+ process.exit(1);
298
+ }
299
+ }
300
+
195
301
  if (parsed.command === "migrate") {
196
302
  if (parsed.optionError) {
197
303
  process.stderr.write(`${parsed.optionError}\n`);
@@ -217,6 +323,9 @@ function printPiInstall(result) {
217
323
  process.stdout.write(` ✓ ${agent.name}.agent.md → ${agent.filePath}\n`);
218
324
  }
219
325
  process.stdout.write(` ✓ Substituted ${result.skills.md} markdown file(s) across ${result.skills.skills} skill(s) → ${result.skills.dir}\n`);
326
+ if (result.docs.files.length > 0) {
327
+ process.stdout.write(` ✓ Copied ${result.docs.files.length} doc(s) → ${result.docs.dir}\n`);
328
+ }
220
329
  process.stdout.write(` ✓ Copied ${result.extensions.files} extension file(s) → ${result.extensions.dir}\n`);
221
330
  for (const name of result.packages.bundled) {
222
331
  process.stdout.write(` ✓ Bundled pi package ${name} → ${join(result.packages.dir, name)}\n`);
@@ -22,6 +22,12 @@
22
22
  // layout (scripts/lib/skill-links.mjs),
23
23
  // so a grouped-tree handoff still
24
24
  // resolves in the plugin.
25
+ // <output>/docs/... documents the skills read after
26
+ // install (docs/skills/skill-anatomy.md),
27
+ // copied verbatim once per install — a
28
+ // skill names them by {{PATH:key}}, so
29
+ // the token map decides both location
30
+ // and shipment
25
31
  // <output>/hooks.json + hooks/<scripts> from hooks/claude/ (support files
26
32
  // referenced via ${CLAUDE_PLUGIN_ROOT})
27
33
  // <output>/.mcp.json + scripts/ hand-authored extras copied from
@@ -67,6 +73,7 @@ import { join, dirname, basename, resolve } from "node:path";
67
73
  import { fileURLToPath } from "node:url";
68
74
  import { homedir } from "node:os";
69
75
  import { composeAgent, loadTokenMap, substituteTokens } from "../../scripts/lib/compose-agent.mjs";
76
+ import { installDocs } from "../../scripts/lib/docs.mjs";
70
77
  import { isDirectory, isFile } from "../../scripts/lib/fs.mjs";
71
78
  import { flattenSkillLinks } from "../../scripts/lib/skill-links.mjs";
72
79
 
@@ -633,6 +640,26 @@ async function isSymlink(filePath) {
633
640
  }
634
641
  }
635
642
 
643
+ /**
644
+ * Result of generating (or planning) the Claude Code plugin.
645
+ * @typedef {object} InstallClaudeResult
646
+ * @property {string} output
647
+ * @property {string | null} marketplaceRoot
648
+ * @property {string | null} manifestPath
649
+ * @property {object | null} marketplaceManifest
650
+ * @property {string[]} agents
651
+ * @property {{ names: string[], md: number, files: number, dir: string }} skills
652
+ * @property {{ files: string[], dir: string }} docs
653
+ * @property {boolean} hooks
654
+ * @property {string[]} hookFiles
655
+ * @property {string[]} handAuthored
656
+ * @property {{ path: string, action: string, removedSymlink: boolean } | null} settings
657
+ * @property {{ path: string, action: string, removedSymlink: boolean } | null} instructions
658
+ * @property {string} pluginVersion
659
+ * @property {string[]} warnings
660
+ * @property {boolean} dryRun
661
+ */
662
+
636
663
  /**
637
664
  * Generate the Claude Code plugin subtree.
638
665
  *
@@ -654,13 +681,7 @@ async function isSymlink(filePath) {
654
681
  * explicitly materialises the section even
655
682
  * with --output — the test/CI seam.
656
683
  * @param {(msg: string) => void} [options.warn] Warning sink, also collected in `warnings`.
657
- * @returns {Promise<{ output: string, marketplaceRoot: string|null, manifestPath: string|null,
658
- * marketplaceManifest: object|null, agents: string[],
659
- * skills: {names: string[], md: number, files: number, dir: string},
660
- * hooks: boolean, hookFiles: string[], handAuthored: string[],
661
- * settings: {path: string, action: string, removedSymlink: boolean}|null,
662
- * instructions: {path: string, action: string, removedSymlink: boolean}|null,
663
- * pluginVersion: string, warnings: string[], dryRun: boolean }>}
684
+ * @returns {Promise<InstallClaudeResult>}
664
685
  */
665
686
  export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = false, pluginRoot, settingsPath, instructionsPath, warn = DEFAULT_WARN } = {}) {
666
687
  const tokenMap = await loadTokenMap(root);
@@ -690,7 +711,7 @@ export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = fals
690
711
  // or a tmp dir, never claude-extras/, but clearing the output first would
691
712
  // still destroy sources if they ever collided). They are re-copied fresh in
692
713
  // the write phase. In dry-run nothing is cleaned or written.
693
- const cleanable = ["agents", "skills", ".claude-plugin", "hooks", "scripts", "hooks.json", ".mcp.json"];
714
+ const cleanable = ["agents", "skills", "docs", ".claude-plugin", "hooks", "scripts", "hooks.json", ".mcp.json"];
694
715
  const handAuthored = await buildHandAuthored(root);
695
716
 
696
717
  // Regenerate the plugin children (removes stale output from earlier flows,
@@ -721,6 +742,7 @@ export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = fals
721
742
  const agents = await buildAgents(root);
722
743
  const skillOut = join(out, "skills");
723
744
  const skills = await buildSkills(root, skillOut, { dryRun, pluginRoot: rootPlugin, tokenMap, warn: warnSink });
745
+ const docs = await installDocs({ root, output: out, dryRun, tokenMap });
724
746
  const pluginJson = await buildPluginJson(root);
725
747
  const hooksSrc = join(root, "hooks", "claude", "hooks.json");
726
748
  const hooksJson = (await isFile(hooksSrc)) ? await readFile(hooksSrc, "utf-8") : null;
@@ -735,6 +757,7 @@ export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = fals
735
757
  pluginVersion: JSON.parse(pluginJson).version,
736
758
  agents: agents.files.map((f) => f.file.replace(/\.md$/, "")),
737
759
  skills: { names: skills.names, md: skills.md, files: skills.files, dir: skillOut },
760
+ docs,
738
761
  hooks: hooksJson !== null,
739
762
  hookFiles: hookFiles.map((f) => f.rel),
740
763
  handAuthored: handAuthored.map((f) => f.rel),
@@ -748,6 +771,7 @@ export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = fals
748
771
  console.log(`DRY-RUN — would generate into ${out}, no files written:`);
749
772
  console.log(` agents: ${summary.agents.join(", ")}`);
750
773
  console.log(` skills: ${summary.skills.names.length} copied → ${skillOut}/<name>/ (token-substituted)`);
774
+ if (docs.files.length) console.log(` docs: ${docs.files.join(", ")} → ${docs.dir}/ (verbatim copies)`);
751
775
  console.log(` .claude-plugin/plugin.json (version ${JSON.parse(pluginJson).version})`);
752
776
  if (isDefaultInstall && manifestPath) {
753
777
  console.log(` marketplace manifest → ${manifestPath} (plugin agent-cortex @ ./${basename(out)})`);
@@ -778,6 +802,7 @@ export async function installClaude({ root = PACKAGE_ROOT, output, dryRun = fals
778
802
  console.log(
779
803
  `Copied ${summary.skills.names.length} skill(s) → ${skillOut}/<name>/ (token-substituted, no symlinks)`,
780
804
  );
805
+ if (docs.files.length) console.log(`Copied ${docs.files.length} doc(s) → ${docs.dir}/ (verbatim)`);
781
806
 
782
807
  await mkdir(join(out, ".claude-plugin"), { recursive: true });
783
808
  await writeFile(join(out, ".claude-plugin", "plugin.json"), pluginJson);
@@ -22,6 +22,11 @@
22
22
  // <extension>/../../agents, i.e. this dir
23
23
  // <output>/skills/<group>/<name>/ token-substituted skill copies — real
24
24
  // files, never symlinks
25
+ // <output>/docs/... documents the skills read after install
26
+ // (docs/skills/skill-anatomy.md), copied
27
+ // verbatim once per install — a skill names
28
+ // them by {{PATH:key}}, so the token map
29
+ // decides both location and shipment
25
30
  // <output>/extensions/ copy of the repo extensions/ tree (loaded
26
31
  // by pi's package discovery)
27
32
  // <output>/token-map.json the map agent-modes reads from the package
@@ -115,6 +120,7 @@ import { join, dirname, resolve } from "node:path";
115
120
  import { fileURLToPath } from "node:url";
116
121
  import { homedir } from "node:os";
117
122
  import { composeAgent, loadTokenMap, substituteTokens, translateToolList } from "../../scripts/lib/compose-agent.mjs";
123
+ import { installDocs } from "../../scripts/lib/docs.mjs";
118
124
  import { isDirectory, isFile } from "../../scripts/lib/fs.mjs";
119
125
 
120
126
  const PI = "pi";
@@ -141,6 +147,7 @@ const PI = "pi";
141
147
  * @property {string} output
142
148
  * @property {{ name: string, filePath: string }[]} agents
143
149
  * @property {{ skills: number, md: number, files: number, dir: string }} skills
150
+ * @property {{ files: string[], dir: string }} docs
144
151
  * @property {{ files: number, dir: string }} extensions
145
152
  * @property {{ dir: string, bundled: string[], staged: string[], resources: PackageResources }} packages
146
153
  * @property {{ path: string, manifest: PiManifest }} manifest
@@ -181,7 +188,7 @@ const STAGED_NAME = "@jaybeeuu/agent-cortex-pi";
181
188
  // before every write so re-staging cannot leave stale copies behind (a skill
182
189
  // deleted in the repo, a package dropped from the manifest). Anything else in
183
190
  // the output directory is left alone.
184
- const OWNED_CHILDREN = ["package.json", "token-map.json", "agents", "skills", "extensions", "node_modules"];
191
+ const OWNED_CHILDREN = ["package.json", "token-map.json", "agents", "skills", "docs", "extensions", "node_modules"];
185
192
 
186
193
  const GENERATED_MARKER =
187
194
  "GENERATED by `agent-cortex install pi` — DO NOT EDIT; re-run the installer to update.";
@@ -242,6 +249,7 @@ const DEFAULT_LEGACY_SWEEP_RECORD = join(homedir(), ".agent-cortex", "legacy-swe
242
249
  * @param {(msg: string) => void} [options.warn] Warning sink, also collected in `warnings`
243
250
  * @returns {Promise<{ output: string, agents: {name: string, filePath: string}[],
244
251
  * skills: {skills: number, md: number, files: number, dir: string},
252
+ * docs: {files: string[], dir: string},
245
253
  * extensions: {files: number, dir: string},
246
254
  * packages: {dir: string, bundled: string[], staged: string[], resources: {extensions: string[], skills: string[]}},
247
255
  * manifest: {path: string, manifest: PiManifest},
@@ -285,6 +293,7 @@ export async function installPi(options = {}) {
285
293
 
286
294
  const agents = await installAgents({ root, agentsDir: join(output, "agents"), dryRun, pluginRoot, tokenMap, warn });
287
295
  const skills = await installSkills({ root, skillsDir: join(output, "skills"), dryRun, pluginRoot, tokenMap, warn });
296
+ const docs = await installDocs({ root, output, dryRun, tokenMap });
288
297
  const extensions = await installExtensions({ root, output, dryRun });
289
298
  const tokenMapFile = await installTokenMap({ output, tokenMap, dryRun });
290
299
  const packages = await stagePiPackages({ dependencies, output, resolvePackage, dryRun, warn });
@@ -301,6 +310,7 @@ export async function installPi(options = {}) {
301
310
  output,
302
311
  agents,
303
312
  skills,
313
+ docs,
304
314
  extensions,
305
315
  packages,
306
316
  manifest,