@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
@@ -115,6 +115,48 @@ import { isDirectory, isFile } from "../../scripts/lib/fs.mjs";
115
115
 
116
116
  const PI = "pi";
117
117
 
118
+ /** @typedef {import("../../scripts/lib/compose-agent.mjs").TokenMap} TokenMap */
119
+ /** @typedef {import("../../scripts/lib/compose-agent.mjs").ComposedAgent} ComposedAgent */
120
+ /** @typedef {(msg: string) => void} Warn */
121
+ /** @typedef {(name: string, fromDir?: string) => string | Promise<string>} ResolvePackage */
122
+ /** @typedef {{ name: string, dir: string }} ResolvedPackage */
123
+ /** @typedef {{ extensions: string[], skills: string[] }} PackageResources */
124
+ /** @typedef {{ packages: ResolvedPackage[], resources: PackageResources }} PiDependencies */
125
+ /** @typedef {(content: string) => string} ContentTransform */
126
+
127
+ /**
128
+ * The generated package's pi manifest (`//` carries the generated-file marker).
129
+ * @typedef {{ "//": string, name: string, version: string, private: boolean,
130
+ * description: string, type: string, keywords: string[],
131
+ * pi: { extensions: string[], skills: string[] } }} PiManifest
132
+ */
133
+
134
+ /**
135
+ * Staged-package summary returned by installPi.
136
+ * @typedef {object} InstallPiResult
137
+ * @property {string} output
138
+ * @property {{ name: string, filePath: string }[]} agents
139
+ * @property {{ skills: number, md: number, files: number, dir: string }} skills
140
+ * @property {{ files: number, dir: string }} extensions
141
+ * @property {{ dir: string, bundled: string[], staged: string[], resources: PackageResources }} packages
142
+ * @property {{ path: string, manifest: PiManifest }} manifest
143
+ * @property {string} tokenMap
144
+ * @property {string[]} warnings
145
+ * @property {boolean} dryRun
146
+ */
147
+
148
+ /**
149
+ * Registration plan/outcome returned by registerPi.
150
+ * @typedef {object} RegisterPiResult
151
+ * @property {string} output
152
+ * @property {string} piBin
153
+ * @property {string} settingsPath
154
+ * @property {boolean} registered
155
+ * @property {string[]} removed
156
+ * @property {string[][]} commands
157
+ * @property {boolean} dryRun
158
+ */
159
+
118
160
  // The token-map contract version this installer implements (token-map.json
119
161
  // "version" field). A higher map version is rejected: the contract must be
120
162
  // extended before this installer can trust it.
@@ -152,7 +194,7 @@ const DEFAULT_SETTINGS_PATH = join(homedir(), ".pi", "agent", "settings.json");
152
194
  // npm:, git:, https/github URLs, and built-in extension names).
153
195
  const NON_LOCAL_PREFIXES = ["npm:", "git:", "github:", "http:", "https:", "ssh:", "builtin:"];
154
196
 
155
- const DEFAULT_WARN = (msg) => console.warn(`[pi-installer] ${msg}`);
197
+ const DEFAULT_WARN = (/** @type {string} */ msg) => console.warn(`[pi-installer] ${msg}`);
156
198
 
157
199
  // Artifacts the pre-staged-model installer wrote into pi's own user directory
158
200
  // that the package root can no longer name on its own: the orphaned
@@ -197,7 +239,7 @@ const DEFAULT_LEGACY_SWEEP_RECORD = join(homedir(), ".agent-cortex", "legacy-swe
197
239
  * skills: {skills: number, md: number, files: number, dir: string},
198
240
  * extensions: {files: number, dir: string},
199
241
  * packages: {dir: string, bundled: string[], staged: string[], resources: {extensions: string[], skills: string[]}},
200
- * manifest: {path: string, manifest: object},
242
+ * manifest: {path: string, manifest: PiManifest},
201
243
  * tokenMap: string, warnings: string[], dryRun: boolean }>}
202
244
  */
203
245
  export async function installPi(options = {}) {
@@ -205,8 +247,9 @@ export async function installPi(options = {}) {
205
247
  const output = options.output ?? DEFAULT_OUTPUT;
206
248
  const dryRun = options.dryRun ?? false;
207
249
  const resolvePackage = options.resolvePackage ?? resolveFromNode;
250
+ /** @type {string[]} */
208
251
  const warnings = [];
209
- const warn = (msg) => {
252
+ const warn = (/** @type {string} */ msg) => {
210
253
  warnings.push(msg);
211
254
  (options.warn ?? DEFAULT_WARN)(msg);
212
255
  };
@@ -222,7 +265,7 @@ export async function installPi(options = {}) {
222
265
  );
223
266
  }
224
267
 
225
- const pluginRoot = options.pluginRoot ?? tokenMap.paths.plugin_root?.[PI];
268
+ const pluginRoot = options.pluginRoot ?? /** @type {string | undefined} */ (tokenMap.paths.plugin_root?.[PI]);
226
269
 
227
270
  // Resolve the bundled packages before clearing the output: an unresolvable
228
271
  // dependency must fail the install without destroying the previous staging.
@@ -264,6 +307,11 @@ export async function installPi(options = {}) {
264
307
 
265
308
  // ─── Agents ──────────────────────────────────────────────────────────────────
266
309
 
310
+ /**
311
+ * @param {{ root: string, agentsDir: string, dryRun: boolean, pluginRoot: string | undefined,
312
+ * tokenMap: TokenMap, warn: Warn }} options
313
+ * @returns {Promise<{ name: string, filePath: string }[]>}
314
+ */
267
315
  async function installAgents({ root, agentsDir, dryRun, pluginRoot, tokenMap, warn }) {
268
316
  const written = [];
269
317
  const agentsSrc = join(root, "agents");
@@ -295,6 +343,12 @@ async function installAgents({ root, agentsDir, dryRun, pluginRoot, tokenMap, wa
295
343
  return written;
296
344
  }
297
345
 
346
+ /**
347
+ * @param {string} name
348
+ * @param {ComposedAgent} fm
349
+ * @param {string[]} tools
350
+ * @returns {string}
351
+ */
298
352
  function renderPiAgent(name, fm, tools) {
299
353
  const lines = [
300
354
  "---",
@@ -316,10 +370,16 @@ function renderPiAgent(name, fm, tools) {
316
370
 
317
371
  // ─── Skills ──────────────────────────────────────────────────────────────────
318
372
 
373
+ /**
374
+ * @param {{ root: string, skillsDir: string, dryRun: boolean, pluginRoot: string | undefined,
375
+ * tokenMap: TokenMap, warn: Warn }} options
376
+ * @returns {Promise<{ skills: number, md: number, files: number, dir: string }>}
377
+ */
319
378
  async function installSkills({ root, skillsDir, dryRun, pluginRoot, tokenMap, warn }) {
320
379
  const skillsSrc = join(root, "skills");
321
380
  if (!(await isDirectory(skillsSrc))) return { skills: 0, md: 0, files: 0, dir: skillsDir };
322
381
 
382
+ /** @param {string} content @returns {string} */
323
383
  const transform = (content) =>
324
384
  substituteTokens(content, PI, tokenMap, { dropNullTools: true, pluginRoot, resolveRelativePaths: true, warn });
325
385
 
@@ -349,6 +409,8 @@ async function installSkills({ root, skillsDir, dryRun, pluginRoot, tokenMap, wa
349
409
  * dependency tree is needed. `node_modules` (pnpm workspace links) and symlinks
350
410
  * are never copied: the staged package owns its dependencies and must contain
351
411
  * real files only.
412
+ * @param {{ root: string, output: string, dryRun: boolean }} options
413
+ * @returns {Promise<{ files: number, dir: string }>}
352
414
  */
353
415
  async function installExtensions({ root, output, dryRun }) {
354
416
  const src = join(root, "extensions");
@@ -363,6 +425,8 @@ async function installExtensions({ root, output, dryRun }) {
363
425
  /**
364
426
  * Ship token-map.json alongside agents/ — agent-modes loads it from the package
365
427
  * root, so the staged extensions keep the same source of truth as the checkout.
428
+ * @param {{ output: string, tokenMap: TokenMap, dryRun: boolean }} options
429
+ * @returns {Promise<string>}
366
430
  */
367
431
  async function installTokenMap({ output, tokenMap, dryRun }) {
368
432
  const path = join(output, "token-map.json");
@@ -381,10 +445,13 @@ async function installTokenMap({ output, tokenMap, dryRun }) {
381
445
  * generated manifest loads them. Ordinary dependencies are left to the usual
382
446
  * module resolution.
383
447
  *
448
+ * @param {{ root: string, resolvePackage: ResolvePackage }} options
384
449
  * @returns {Promise<{packages: {name: string, dir: string}[], resources: {extensions: string[], skills: string[]}}>}
385
450
  */
386
451
  async function resolvePiPackages({ root, resolvePackage }) {
452
+ /** @type {ResolvedPackage[]} */
387
453
  const packages = [];
454
+ /** @type {PackageResources} */
388
455
  const resources = { extensions: [], skills: [] };
389
456
 
390
457
  for (const name of await readDirectDependencies(root)) {
@@ -393,7 +460,7 @@ async function resolvePiPackages({ root, resolvePackage }) {
393
460
  if (manifest === null || manifest.pi === undefined) continue;
394
461
 
395
462
  packages.push({ name, dir });
396
- for (const type of ["extensions", "skills"]) {
463
+ for (const type of /** @type {("extensions" | "skills")[]} */ (["extensions", "skills"])) {
397
464
  for (const entry of asStringList(manifest.pi[type])) {
398
465
  resources[type].push(`node_modules/${name}/${stripRelative(entry)}`);
399
466
  }
@@ -410,13 +477,17 @@ async function resolvePiPackages({ root, resolvePackage }) {
410
477
  * source layout is irrelevant. Copies are real files (never symlinks) and stay
411
478
  * flat, matching what pi loads from a local package; a package that resolves to
412
479
  * two different versions keeps the first and warns.
480
+ * @param {{ dependencies: PiDependencies, output: string, resolvePackage: ResolvePackage,
481
+ * dryRun: boolean, warn: Warn }} options
482
+ * @returns {Promise<{ dir: string, bundled: string[], staged: string[], resources: PackageResources }>}
413
483
  */
414
484
  async function stagePiPackages({ dependencies, output, resolvePackage, dryRun, warn }) {
485
+ /** @type {Map<string, string>} */
415
486
  const staged = new Map();
416
487
  const pending = dependencies.packages.map(({ name, dir }) => ({ name, dir }));
417
488
 
418
489
  while (pending.length > 0) {
419
- const { name, dir } = pending.shift();
490
+ const { name, dir } = /** @type {ResolvedPackage} */ (pending.shift());
420
491
  const seen = staged.get(name);
421
492
  if (seen !== undefined) {
422
493
  if (seen !== dir) warn(`pi package "${name}" resolves to two directories (${seen}, ${dir}) — keeping the first`);
@@ -445,9 +516,13 @@ async function stagePiPackages({ dependencies, output, resolvePackage, dryRun, w
445
516
  * construction. A package's `package.json` is not always exported, so the
446
517
  * package directory is found by resolving an entry point and walking up to the
447
518
  * enclosing manifest that names it.
519
+ * @param {string} name
520
+ * @param {string} [fromDir]
521
+ * @returns {Promise<string>}
448
522
  */
449
523
  async function resolveFromNode(name, fromDir = PACKAGE_ROOT) {
450
524
  const req = createRequire(join(fromDir, "package.json"));
525
+ /** @type {string | null} */
451
526
  let entry = null;
452
527
  for (const specifier of [name, `${name}/package.json`]) {
453
528
  try {
@@ -471,11 +546,19 @@ async function resolveFromNode(name, fromDir = PACKAGE_ROOT) {
471
546
  }
472
547
  }
473
548
 
549
+ /**
550
+ * @param {string} root
551
+ * @returns {Promise<string[]>}
552
+ */
474
553
  async function readDirectDependencies(root) {
475
554
  const manifest = await readJsonObject(join(root, "package.json"));
476
555
  return manifest === null ? [] : Object.keys(manifest.dependencies ?? {});
477
556
  }
478
557
 
558
+ /**
559
+ * @param {string} dir
560
+ * @returns {Promise<string[]>}
561
+ */
479
562
  async function readDependencyNames(dir) {
480
563
  const manifest = await readJsonObject(join(dir, "package.json"));
481
564
  return manifest === null ? [] : Object.keys(manifest.dependencies ?? {});
@@ -515,8 +598,7 @@ async function readDependencyNames(dir) {
515
598
  * @param {boolean} [options.dryRun] Plan and report without spawning anything
516
599
  * @param {boolean} [options.requireRegister] Throw when registration cannot run
517
600
  * @param {(msg: string) => void} [options.warn] Warning sink (default: console.warn)
518
- * @returns {Promise<{ output: string, piBin: string, settingsPath: string, registered: boolean,
519
- * removed: string[], commands: string[][], dryRun: boolean }>}
601
+ * @returns {Promise<RegisterPiResult>}
520
602
  */
521
603
  export async function registerPi({
522
604
  root = PACKAGE_ROOT,
@@ -536,7 +618,7 @@ export async function registerPi({
536
618
  // runs it: drop the superseded sources, then register the staged package.
537
619
  const manualCommands = [...stale.map(({ path }) => `${piBin} remove ${path}`), `${piBin} install ${stagedPath}`];
538
620
 
539
- const summary = { output: stagedPath, piBin, settingsPath, registered: false, removed: [], commands: [], dryRun };
621
+ const summary = /** @type {RegisterPiResult} */ ({ output: stagedPath, piBin, settingsPath, registered: false, removed: [], commands: [], dryRun });
540
622
  for (const { path } of stale) summary.commands.push(["remove", path]);
541
623
  if (!registered) summary.commands.push(["install", stagedPath]);
542
624
 
@@ -583,7 +665,7 @@ export async function registerPi({
583
665
  // checkout and the staged package at once, and every re-run would repeat
584
666
  // the same non-converging cycle. Fail instead of reporting success.
585
667
  throw new Error(
586
- `${err.message} — the superseded source is still configured: ${path}. ` +
668
+ `${/** @type {Error} */ (err).message} — the superseded source is still configured: ${path}. ` +
587
669
  `Remove it with "${piBin} remove ${path}" and re-run`,
588
670
  );
589
671
  }
@@ -612,6 +694,8 @@ export async function registerPi({
612
694
  /**
613
695
  * Local package sources configured in pi's settings file, in order. pi stores
614
696
  * each entry as a source string or a `{ source, … }` filter object.
697
+ * @param {string} settingsPath
698
+ * @returns {Promise<string[]>}
615
699
  */
616
700
  async function readConfiguredPackages(settingsPath) {
617
701
  const settings = await readJsonObject(settingsPath);
@@ -620,11 +704,19 @@ async function readConfiguredPackages(settingsPath) {
620
704
  return packages.map(packageSource).filter((source) => source !== undefined);
621
705
  }
622
706
 
707
+ /**
708
+ * @param {any} entry
709
+ * @returns {string | undefined}
710
+ */
623
711
  function packageSource(entry) {
624
712
  if (typeof entry === "string") return entry;
625
713
  return typeof entry?.source === "string" ? entry.source : undefined;
626
714
  }
627
715
 
716
+ /**
717
+ * @param {string} source
718
+ * @returns {boolean}
719
+ */
628
720
  function isLocalSource(source) {
629
721
  return !NON_LOCAL_PREFIXES.some((prefix) => source.trim().startsWith(prefix));
630
722
  }
@@ -632,6 +724,9 @@ function isLocalSource(source) {
632
724
  /**
633
725
  * Resolve a configured source the way pi does: relative to the directory
634
726
  * holding the settings file, with `~` expanded.
727
+ * @param {string} source
728
+ * @param {string} settingsDir
729
+ * @returns {string}
635
730
  */
636
731
  function resolveSourcePath(source, settingsDir) {
637
732
  const trimmed = source.trim();
@@ -645,9 +740,12 @@ function resolveSourcePath(source, settingsDir) {
645
740
  * whose directory is gone is left alone: a missing path cannot be told apart
646
741
  * from a temporarily absent one, and removing it could drop a package the user
647
742
  * still wants.
743
+ * @param {{ sources: string[], settingsPath: string, stagedPath: string, rawPackageName: string | undefined }} options
744
+ * @returns {Promise<{ registered: boolean, stale: { path: string }[] }>}
648
745
  */
649
746
  async function classifySources({ sources, settingsPath, stagedPath, rawPackageName }) {
650
747
  const settingsDir = dirname(settingsPath);
748
+ /** @type {{ path: string }[]} */
651
749
  const stale = [];
652
750
  let registered = false;
653
751
  for (const source of sources) {
@@ -666,6 +764,8 @@ async function classifySources({ sources, settingsPath, stagedPath, rawPackageNa
666
764
  /**
667
765
  * Probe whether the `pi` CLI can be run at all, with output suppressed.
668
766
  * Resolves `{ ok: true }` or `{ ok: false, detail }` naming the failure.
767
+ * @param {string} piBin
768
+ * @returns {Promise<{ ok: true } | { ok: false, detail: string }>}
669
769
  */
670
770
  function probePi(piBin) {
671
771
  return new Promise((resolve) => {
@@ -683,7 +783,12 @@ function probePi(piBin) {
683
783
  });
684
784
  }
685
785
 
686
- /** Run one pi package command, inheriting its output; rejects on failure. */
786
+ /**
787
+ * Run one pi package command, inheriting its output; rejects on failure.
788
+ * @param {string} piBin
789
+ * @param {string[]} args
790
+ * @returns {Promise<void>}
791
+ */
687
792
  function runPiCommand(piBin, args) {
688
793
  const label = `${piBin} ${args.join(" ")}`;
689
794
  return new Promise((resolve, reject) => {
@@ -747,15 +852,23 @@ export async function sweepLegacyPiUserDirs({
747
852
  recordPath = DEFAULT_LEGACY_SWEEP_RECORD,
748
853
  dryRun = false,
749
854
  warn = DEFAULT_WARN,
750
- } = {}) {
855
+ }) {
856
+ /** @type {string[]} */
751
857
  const removed = [];
858
+ /** @type {string[]} */
752
859
  const kept = [];
860
+ /** @type {string[]} */
753
861
  const failed = [];
754
862
 
755
863
  if (!(await isDirectory(piUserDir))) return { removed, kept };
756
864
  if (await hasMigrated({ recordPath, piUserDir, warn })) return { removed, kept };
757
865
 
758
866
  // Commands that record what happened and either delete the path or keep it.
867
+ /**
868
+ * @param {string} path
869
+ * @param {boolean} isLegacy
870
+ * @returns {Promise<void>}
871
+ */
759
872
  const sweep = async (path, isLegacy) => {
760
873
  if (!isLegacy) {
761
874
  kept.push(path);
@@ -769,7 +882,7 @@ export async function sweepLegacyPiUserDirs({
769
882
  await rm(path, { recursive: true, force: true });
770
883
  removed.push(path);
771
884
  } catch (err) {
772
- warn(`could not remove the legacy artifact ${path}: ${err.message}`);
885
+ warn(`could not remove the legacy artifact ${path}: ${/** @type {Error} */ (err).message}`);
773
886
  kept.push(path);
774
887
  failed.push(path);
775
888
  }
@@ -782,6 +895,7 @@ export async function sweepLegacyPiUserDirs({
782
895
  // attribute what it cannot see, so it warns and counts the directory as a
783
896
  // failure — which is what keeps the migration unrecorded, so the next install
784
897
  // retries once the permissions allow it.
898
+ /** @param {string} dir */
785
899
  const unreadable = (dir) => {
786
900
  warn(
787
901
  `could not list ${dir} — the legacy sweep cannot tell its contents from the user's, ` +
@@ -792,6 +906,10 @@ export async function sweepLegacyPiUserDirs({
792
906
 
793
907
  // Entries of a sweep site: none for a directory that is absent, and a recorded
794
908
  // failure for one that exists but cannot be read.
909
+ /**
910
+ * @param {string} dir
911
+ * @returns {Promise<string[]>}
912
+ */
795
913
  const list = async (dir) => {
796
914
  const entries = await listDir(dir);
797
915
  if (entries === null) unreadable(dir);
@@ -861,14 +979,16 @@ export async function sweepLegacyPiUserDirs({
861
979
  * A record that exists but cannot be read is treated as "already migrated":
862
980
  * the state is ambiguous and a deletion is not recoverable, so the sweep never
863
981
  * runs on an unreadable record.
982
+ * @param {{ recordPath: string, piUserDir: string, warn: Warn }} options
983
+ * @returns {Promise<boolean>}
864
984
  */
865
985
  async function hasMigrated({ recordPath, piUserDir, warn }) {
866
986
  let raw;
867
987
  try {
868
988
  raw = await readFile(recordPath, "utf-8");
869
989
  } catch (err) {
870
- if (err.code === "ENOENT") return false;
871
- warn(`could not read the legacy sweep record ${recordPath}: ${err.message} — the migration will not run again`);
990
+ if (/** @type {NodeJS.ErrnoException} */ (err).code === "ENOENT") return false;
991
+ warn(`could not read the legacy sweep record ${recordPath}: ${/** @type {Error} */ (err).message} — the migration will not run again`);
872
992
  return true;
873
993
  }
874
994
  const entries = parseRecordEntries(raw);
@@ -884,6 +1004,8 @@ async function hasMigrated({ recordPath, piUserDir, warn }) {
884
1004
  * — another pi config may have migrated already and must not be re-run. A write
885
1005
  * failure warns instead of throwing: registration already succeeded, so an
886
1006
  * unwritable record is not a reason to fail the install.
1007
+ * @param {{ recordPath: string, piUserDir: string, warn: Warn }} options
1008
+ * @returns {Promise<void>}
887
1009
  */
888
1010
  async function recordMigration({ recordPath, piUserDir, warn }) {
889
1011
  const entries = await readRecordEntries(recordPath);
@@ -893,12 +1015,16 @@ async function recordMigration({ recordPath, piUserDir, warn }) {
893
1015
  await writeFile(recordPath, `${JSON.stringify(entries, null, 2)}\n`);
894
1016
  } catch (err) {
895
1017
  warn(
896
- `could not record the legacy sweep at ${recordPath}: ${err.message} — a later install may run the migration again`,
1018
+ `could not record the legacy sweep at ${recordPath}: ${/** @type {Error} */ (err).message} — a later install may run the migration again`,
897
1019
  );
898
1020
  }
899
1021
  }
900
1022
 
901
- /** Record entries keyed by pi user directory; `{}` when it is absent or unusable. */
1023
+ /**
1024
+ * Record entries keyed by pi user directory; `{}` when it is absent or unusable.
1025
+ * @param {string} recordPath
1026
+ * @returns {Promise<Record<string, any>>}
1027
+ */
902
1028
  async function readRecordEntries(recordPath) {
903
1029
  try {
904
1030
  return parseRecordEntries(await readFile(recordPath, "utf-8")) ?? {};
@@ -907,7 +1033,11 @@ async function readRecordEntries(recordPath) {
907
1033
  }
908
1034
  }
909
1035
 
910
- /** The record's `{ "<piUserDir>": entry }` object, or null when the text is not one. */
1036
+ /**
1037
+ * The record's `{ "<piUserDir>": entry }` object, or null when the text is not one.
1038
+ * @param {string} raw
1039
+ * @returns {Record<string, any> | null}
1040
+ */
911
1041
  function parseRecordEntries(raw) {
912
1042
  let parsed;
913
1043
  try {
@@ -918,7 +1048,11 @@ function parseRecordEntries(raw) {
918
1048
  return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null;
919
1049
  }
920
1050
 
921
- /** Whether `path` is a directory without following a symlink to one. */
1051
+ /**
1052
+ * Whether `path` is a directory without following a symlink to one.
1053
+ * @param {string} path
1054
+ * @returns {Promise<boolean>}
1055
+ */
922
1056
  async function isRealDirectory(path) {
923
1057
  try {
924
1058
  return (await lstat(path)).isDirectory();
@@ -927,10 +1061,15 @@ async function isRealDirectory(path) {
927
1061
  }
928
1062
  }
929
1063
 
930
- /** Composable agent names the package root ships (`agents/<name>/agent.md`), or null when it cannot be listed. */
1064
+ /**
1065
+ * Composable agent names the package root ships (`agents/<name>/agent.md`), or null when it cannot be listed.
1066
+ * @param {string} root
1067
+ * @returns {Promise<Set<string> | null>}
1068
+ */
931
1069
  async function shippedAgentNames(root) {
932
1070
  const entries = await listDir(join(root, "agents"));
933
1071
  if (entries === null) return null;
1072
+ /** @type {Set<string>} */
934
1073
  const names = new Set();
935
1074
  for (const entry of entries) {
936
1075
  if (await isFile(join(root, "agents", entry, "agent.md"))) names.add(entry);
@@ -938,10 +1077,15 @@ async function shippedAgentNames(root) {
938
1077
  return names;
939
1078
  }
940
1079
 
941
- /** Skill paths the package root ships (`skills/<group>/<name>/SKILL.md`), or null when it cannot be listed. */
1080
+ /**
1081
+ * Skill paths the package root ships (`skills/<group>/<name>/SKILL.md`), or null when it cannot be listed.
1082
+ * @param {string} root
1083
+ * @returns {Promise<Set<string> | null>}
1084
+ */
942
1085
  async function shippedSkillPaths(root) {
943
1086
  const groups = await listDir(join(root, "skills"));
944
1087
  if (groups === null) return null;
1088
+ /** @type {Set<string>} */
945
1089
  const paths = new Set();
946
1090
  for (const group of groups) {
947
1091
  const names = await listDir(join(root, "skills", group));
@@ -960,12 +1104,15 @@ async function shippedSkillPaths(root) {
960
1104
  * The two must not collapse into one answer: the sweep records a finished
961
1105
  * migration, so a listing failure read as an empty directory would record one
962
1106
  * that removed nothing and never retry it.
1107
+ * @param {string} dir
1108
+ * @returns {Promise<string[] | null>}
963
1109
  */
964
1110
  async function listDir(dir) {
965
1111
  try {
966
1112
  return (await readdir(dir)).sort();
967
1113
  } catch (err) {
968
- if (err.code === "ENOENT" || err.code === "ENOTDIR") return [];
1114
+ const code = /** @type {NodeJS.ErrnoException} */ (err).code;
1115
+ if (code === "ENOENT" || code === "ENOTDIR") return [];
969
1116
  return null;
970
1117
  }
971
1118
  }
@@ -978,6 +1125,8 @@ async function listDir(dir) {
978
1125
  * pi's package rules do the loading: conventional dirs are declared explicitly
979
1126
  * so the bundled packages' resources are included alongside ./extensions and
980
1127
  * ./skills (one pi-visible source, no tokenised repo copies).
1128
+ * @param {{ root: string, dependencies: PiDependencies }} options
1129
+ * @returns {Promise<PiManifest>}
981
1130
  */
982
1131
  async function buildManifest({ root, dependencies }) {
983
1132
  const pkg = await readJsonObject(join(root, "package.json"));
@@ -996,6 +1145,10 @@ async function buildManifest({ root, dependencies }) {
996
1145
  };
997
1146
  }
998
1147
 
1148
+ /**
1149
+ * @param {{ output: string, manifest: PiManifest }} options
1150
+ * @returns {Promise<{ path: string, manifest: PiManifest }>}
1151
+ */
999
1152
  async function writeManifest({ output, manifest }) {
1000
1153
  const path = join(output, "package.json");
1001
1154
  await writeFile(path, `${JSON.stringify(manifest, null, 2)}\n`);
@@ -1006,9 +1159,12 @@ async function writeManifest({ output, manifest }) {
1006
1159
  * Prove the manifest references real resources before reporting a success: a
1007
1160
  * staged package whose declared extension or skill path does not exist would
1008
1161
  * load silently short of the tools the agents expect.
1162
+ * @param {{ output: string, manifest: PiManifest }} options
1163
+ * @returns {Promise<void>}
1009
1164
  */
1010
1165
  async function verifyManifestResources({ output, manifest }) {
1011
1166
  const declared = [...manifest.pi.extensions, ...manifest.pi.skills];
1167
+ /** @type {string[]} */
1012
1168
  const missing = [];
1013
1169
  for (const rel of declared) {
1014
1170
  if (!(await pathExists(join(output, rel)))) missing.push(rel);
@@ -1024,6 +1180,10 @@ async function verifyManifestResources({ output, manifest }) {
1024
1180
  * Recursively copy a tree, optionally transforming file contents. `.md` files
1025
1181
  * go through `transform` when supplied; everything else is copied verbatim.
1026
1182
  * `node_modules` and symlinks are never copied.
1183
+ * @param {string} src
1184
+ * @param {string} dest
1185
+ * @param {{ dryRun?: boolean, transform?: ContentTransform | null }} [options]
1186
+ * @returns {Promise<{ files: number, md: number }>}
1027
1187
  */
1028
1188
  async function copyTree(src, dest, { dryRun = false, transform = null } = {}) {
1029
1189
  let files = 0;
@@ -1052,21 +1212,33 @@ async function copyTree(src, dest, { dryRun = false, transform = null } = {}) {
1052
1212
 
1053
1213
  // ─── Small helpers ───────────────────────────────────────────────────────────
1054
1214
 
1055
- /** `./skills/` → `skills` — a package resource path relative to its own root. */
1215
+ /**
1216
+ * `./skills/` → `skills` — a package resource path relative to its own root.
1217
+ * @param {string} entry
1218
+ * @returns {string}
1219
+ */
1056
1220
  function stripRelative(entry) {
1057
1221
  return entry.replace(/^\.\//, "").replace(/\/$/, "");
1058
1222
  }
1059
1223
 
1224
+ /**
1225
+ * @param {unknown} value
1226
+ * @returns {string[]}
1227
+ */
1060
1228
  function asStringList(value) {
1061
1229
  return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
1062
1230
  }
1063
1231
 
1232
+ /**
1233
+ * @param {string} filePath
1234
+ * @returns {Promise<Record<string, any> | null>}
1235
+ */
1064
1236
  async function readJsonObject(filePath) {
1065
1237
  let raw;
1066
1238
  try {
1067
1239
  raw = await readFile(filePath, "utf-8");
1068
1240
  } catch (err) {
1069
- if (err.code === "ENOENT") return null;
1241
+ if (/** @type {NodeJS.ErrnoException} */ (err).code === "ENOENT") return null;
1070
1242
  throw err;
1071
1243
  }
1072
1244
  try {
@@ -1077,6 +1249,10 @@ async function readJsonObject(filePath) {
1077
1249
  }
1078
1250
  }
1079
1251
 
1252
+ /**
1253
+ * @param {string} filePath
1254
+ * @returns {Promise<boolean>}
1255
+ */
1080
1256
  async function pathExists(filePath) {
1081
1257
  try {
1082
1258
  await stat(filePath);
@@ -2,7 +2,7 @@
2
2
 
3
3
  Appends behavioural circuit-breakers to the PI system prompt once per user prompt.
4
4
 
5
- This is the first slice of `docs/ideas/improve-pi-system-prompt.md`: the highest-leverage
5
+ This is the first slice of the *Improve PI System Prompt* idea: the highest-leverage
6
6
  token reduction is behavioural, not structural. The extension injects a stable block via
7
7
  the `before_agent_start` hook — which fires once per user prompt, before the agent loop — so
8
8
  the rules apply to every turn of the run without rewriting Pi's default prompt.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Behavioural guardrails for the PI system prompt.
3
3
  *
4
- * The first slice of docs/ideas/improve-pi-system-prompt.md: hard circuit-breakers
4
+ * The first slice of the *Improve PI System Prompt* idea: hard circuit-breakers
5
5
  * against wasted retries, explicit check-in triggers, and atomic-change discipline.
6
6
  * Kept as a stable constant so it can be appended to every turn's system prompt
7
7
  * without invalidating the prompt-cache prefix.
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Appends behavioural circuit-breakers to the PI system prompt on every turn:
5
5
  * hard retry limits, explicit check-in triggers, and atomic-change discipline
6
- * (the first slice of docs/ideas/improve-pi-system-prompt.md).
6
+ * (the first slice of the *Improve PI System Prompt* idea).
7
7
  *
8
8
  * The block is stable text appended after any chained system-prompt changes, so
9
9
  * it keeps the prompt-cache prefix intact while remaining authoritative for the
@@ -98,14 +98,14 @@ describe("discoverAgents (composable source format)", () => {
98
98
  it("discovers agents from <name>/agent.md + <name>/pi/frontmatter.json", () => {
99
99
  const fx = track(makeFixture());
100
100
  writeComposableAgent(fx, "ralph", {
101
- body: "# ralph\nUse {{TOOL:task}} to spawn subagents.\n\n{{SECTION:polling}}",
101
+ body: "# ralph\nUse {{TOOL:task}} to spawn subagents.\n\n{{SECTION:wake}}",
102
102
  frontmatter: {
103
103
  name: "agent-cortex:ralph",
104
104
  description: "Runs beads end-to-end",
105
105
  tools: ["bash", "view", "rg", "glob", "task", "read_agent"],
106
106
  },
107
107
  sections: {
108
- polling: "## PI polling\nPoll with task + read_agent.",
108
+ wake: "## PI wake\nPoll with task + read_agent.",
109
109
  },
110
110
  });
111
111
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  Appends the always-on reply shape to the PI system prompt once per user prompt.
4
4
 
5
- This is the second slice of `docs/ideas/improve-pi-system-prompt.md`: the first
5
+ This is the second slice of the "Improve PI System Prompt" idea (bead `agnt-ctx-s5hn`): the first
6
6
  (`agent-guardrails`) stops wasted work; this one stops the answer getting buried in
7
7
  it. The extension injects a stable block via the `before_agent_start` hook — which
8
8
  fires once per user prompt, before the agent loop — so the rules apply to every
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Appends the always-on reply shape to the PI system prompt on every turn: lead
5
5
  * with the action, write in Simplified Technical English (ASD-STE100), restate the
6
- * context the reader cannot remember (the second slice of
7
- * docs/ideas/improve-pi-system-prompt.md).
6
+ * context the reader cannot remember (the second slice of the "Improve PI
7
+ * System Prompt" idea, bead `agnt-ctx-s5hn`).
8
8
  *
9
9
  * The block is stable text appended after any chained system-prompt changes, so it
10
10
  * keeps the prompt-cache prefix intact while remaining authoritative for the model.
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Output style for the PI system prompt.
3
3
  *
4
- * The second slice of docs/ideas/improve-pi-system-prompt.md: the reply shape, not
5
- * the task discipline. Three concerns, in order — lead with the action, write in
4
+ * The second slice of the "Improve PI System Prompt" idea (bead `agnt-ctx-s5hn`): the reply
5
+ * shape, not the task discipline. Three concerns, in order — lead with the action, write in
6
6
  * Simplified Technical English, restate the context the reader cannot remember.
7
7
  *
8
8
  * The prose lives in output-style.md so one file feeds both harnesses: this