peaks-loop 4.1.2 → 4.1.3

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 (323) hide show
  1. package/.claude-plugin/marketplace.json +136 -47
  2. package/CHANGELOG.md +56 -0
  3. package/README-en.md +1 -1
  4. package/README.md +1 -1
  5. package/contracts/readonly-argv-whitelist.json +211 -0
  6. package/dist/cli/commands/audit-artifact-runners.d.ts +16 -0
  7. package/dist/cli/commands/audit-artifact-runners.js +170 -0
  8. package/dist/cli/commands/audit-command-shared.d.ts +97 -0
  9. package/dist/cli/commands/audit-command-shared.js +60 -0
  10. package/dist/cli/commands/audit-commands.d.ts +7 -31
  11. package/dist/cli/commands/audit-commands.js +64 -473
  12. package/dist/cli/commands/audit-goal-runners.d.ts +17 -0
  13. package/dist/cli/commands/audit-goal-runners.js +114 -0
  14. package/dist/cli/commands/audit-red-lines-runners.d.ts +11 -0
  15. package/dist/cli/commands/audit-red-lines-runners.js +43 -0
  16. package/dist/cli/commands/audit-refusal-data.d.ts +23 -0
  17. package/dist/cli/commands/audit-refusal-data.js +69 -0
  18. package/dist/cli/commands/audit-static-runners.d.ts +13 -0
  19. package/dist/cli/commands/audit-static-runners.js +139 -0
  20. package/dist/cli/commands/await-runners.d.ts +11 -0
  21. package/dist/cli/commands/await-runners.js +148 -0
  22. package/dist/cli/commands/code-auto-compact-command.d.ts +3 -0
  23. package/dist/cli/commands/code-auto-compact-command.js +124 -0
  24. package/dist/cli/commands/code-auto-compact-envelope.d.ts +22 -0
  25. package/dist/cli/commands/code-auto-compact-envelope.js +39 -0
  26. package/dist/cli/commands/code-context-audit-command.d.ts +3 -0
  27. package/dist/cli/commands/code-context-audit-command.js +68 -0
  28. package/dist/cli/commands/code-context-now-command.d.ts +3 -0
  29. package/dist/cli/commands/code-context-now-command.js +124 -0
  30. package/dist/cli/commands/code-context-now-envelope.d.ts +66 -0
  31. package/dist/cli/commands/code-context-now-envelope.js +136 -0
  32. package/dist/cli/commands/code-context-now-mapping.d.ts +27 -0
  33. package/dist/cli/commands/code-context-now-mapping.js +43 -0
  34. package/dist/cli/commands/code-emit-handoff-command.d.ts +3 -0
  35. package/dist/cli/commands/code-emit-handoff-command.js +99 -0
  36. package/dist/cli/commands/code-gate-step-08-command.d.ts +3 -0
  37. package/dist/cli/commands/code-gate-step-08-command.js +139 -0
  38. package/dist/cli/commands/code-post-compact-detect-command.d.ts +3 -0
  39. package/dist/cli/commands/code-post-compact-detect-command.js +50 -0
  40. package/dist/cli/commands/code-runtime-commands.d.ts +9 -43
  41. package/dist/cli/commands/code-runtime-commands.js +20 -672
  42. package/dist/cli/commands/code-runtime-session.d.ts +1 -0
  43. package/dist/cli/commands/code-runtime-session.js +15 -0
  44. package/dist/cli/commands/compact-command.d.ts +6 -2
  45. package/dist/cli/commands/compact-command.js +12 -559
  46. package/dist/cli/commands/compact-dry-run-command.d.ts +3 -0
  47. package/dist/cli/commands/compact-dry-run-command.js +87 -0
  48. package/dist/cli/commands/compact-force-command.d.ts +3 -0
  49. package/dist/cli/commands/compact-force-command.js +98 -0
  50. package/dist/cli/commands/compact-harness-window-command.d.ts +3 -0
  51. package/dist/cli/commands/compact-harness-window-command.js +174 -0
  52. package/dist/cli/commands/compact-history-command.d.ts +3 -0
  53. package/dist/cli/commands/compact-history-command.js +89 -0
  54. package/dist/cli/commands/compact-read-commands.d.ts +3 -0
  55. package/dist/cli/commands/compact-read-commands.js +123 -0
  56. package/dist/cli/commands/compact-settle-command.d.ts +3 -0
  57. package/dist/cli/commands/compact-settle-command.js +130 -0
  58. package/dist/cli/commands/compact-shared.d.ts +9 -0
  59. package/dist/cli/commands/compact-shared.js +26 -0
  60. package/dist/cli/commands/core/skill-command.d.ts +1 -44
  61. package/dist/cli/commands/core/skill-command.js +13 -541
  62. package/dist/cli/commands/core/skill-context-verdict.d.ts +38 -0
  63. package/dist/cli/commands/core/skill-context-verdict.js +63 -0
  64. package/dist/cli/commands/core/skill-generated-config-notice.d.ts +41 -0
  65. package/dist/cli/commands/core/skill-generated-config-notice.js +75 -0
  66. package/dist/cli/commands/core/skill-heartbeat-commands.d.ts +3 -0
  67. package/dist/cli/commands/core/skill-heartbeat-commands.js +50 -0
  68. package/dist/cli/commands/core/skill-presence-commands.d.ts +3 -0
  69. package/dist/cli/commands/core/skill-presence-commands.js +123 -0
  70. package/dist/cli/commands/core/skill-presence-housekeeping-commands.d.ts +3 -0
  71. package/dist/cli/commands/core/skill-presence-housekeeping-commands.js +165 -0
  72. package/dist/cli/commands/core/skill-registry-commands.d.ts +3 -0
  73. package/dist/cli/commands/core/skill-registry-commands.js +117 -0
  74. package/dist/cli/commands/core/skill-runbook-command.d.ts +3 -0
  75. package/dist/cli/commands/core/skill-runbook-command.js +29 -0
  76. package/dist/cli/commands/dispatch-command-spec.d.ts +2 -0
  77. package/dist/cli/commands/dispatch-command-spec.js +62 -0
  78. package/dist/cli/commands/dispatch-commands.js +159 -775
  79. package/dist/cli/commands/dispatch-detached-branch.d.ts +3 -0
  80. package/dist/cli/commands/dispatch-detached-branch.js +58 -0
  81. package/dist/cli/commands/dispatch-envelope.d.ts +36 -0
  82. package/dist/cli/commands/dispatch-envelope.js +104 -0
  83. package/dist/cli/commands/dispatch-isolation-lease.d.ts +17 -0
  84. package/dist/cli/commands/dispatch-isolation-lease.js +149 -0
  85. package/dist/cli/commands/dispatch-preflight.d.ts +7 -0
  86. package/dist/cli/commands/dispatch-preflight.js +105 -0
  87. package/dist/cli/commands/dispatch-prompt-blocks.d.ts +10 -0
  88. package/dist/cli/commands/dispatch-prompt-blocks.js +56 -0
  89. package/dist/cli/commands/dispatch-prompt-composition.d.ts +30 -0
  90. package/dist/cli/commands/dispatch-prompt-composition.js +171 -0
  91. package/dist/cli/commands/dispatch-record-write.d.ts +24 -0
  92. package/dist/cli/commands/dispatch-record-write.js +96 -0
  93. package/dist/cli/commands/dispatch-tool-call.d.ts +15 -0
  94. package/dist/cli/commands/dispatch-tool-call.js +128 -0
  95. package/dist/cli/commands/evolution-command-shared.d.ts +78 -0
  96. package/dist/cli/commands/evolution-command-shared.js +107 -0
  97. package/dist/cli/commands/evolution-commands.d.ts +11 -1
  98. package/dist/cli/commands/evolution-commands.js +19 -350
  99. package/dist/cli/commands/evolution-evaluate-command.d.ts +24 -0
  100. package/dist/cli/commands/evolution-evaluate-command.js +138 -0
  101. package/dist/cli/commands/evolution-propose-command.d.ts +23 -0
  102. package/dist/cli/commands/evolution-propose-command.js +145 -0
  103. package/dist/cli/commands/evolution-status-command.d.ts +17 -0
  104. package/dist/cli/commands/evolution-status-command.js +41 -0
  105. package/dist/cli/commands/evolution-verdict-commands.d.ts +30 -0
  106. package/dist/cli/commands/evolution-verdict-commands.js +100 -0
  107. package/dist/cli/commands/finalize-accumulator.d.ts +44 -0
  108. package/dist/cli/commands/finalize-accumulator.js +55 -0
  109. package/dist/cli/commands/finalize-branches.d.ts +34 -0
  110. package/dist/cli/commands/finalize-branches.js +119 -0
  111. package/dist/cli/commands/finalize-runners.d.ts +3 -0
  112. package/dist/cli/commands/finalize-runners.js +92 -0
  113. package/dist/cli/commands/finalize-selection.d.ts +65 -0
  114. package/dist/cli/commands/finalize-selection.js +35 -0
  115. package/dist/cli/commands/gate-commands.js +26 -8
  116. package/dist/cli/commands/job-checkpoint-commands.d.ts +3 -0
  117. package/dist/cli/commands/job-checkpoint-commands.js +157 -0
  118. package/dist/cli/commands/job-command-shared.d.ts +109 -0
  119. package/dist/cli/commands/job-command-shared.js +72 -0
  120. package/dist/cli/commands/job-commands.d.ts +4 -64
  121. package/dist/cli/commands/job-commands.js +42 -552
  122. package/dist/cli/commands/job-cost-check-command.d.ts +3 -0
  123. package/dist/cli/commands/job-cost-check-command.js +41 -0
  124. package/dist/cli/commands/job-init-status-commands.d.ts +3 -0
  125. package/dist/cli/commands/job-init-status-commands.js +149 -0
  126. package/dist/cli/commands/job-run-commands.d.ts +3 -0
  127. package/dist/cli/commands/job-run-commands.js +97 -0
  128. package/dist/cli/commands/job-state-root.d.ts +35 -0
  129. package/dist/cli/commands/job-state-root.js +76 -0
  130. package/dist/cli/commands/loop-eval-capture-index.d.ts +4 -0
  131. package/dist/cli/commands/loop-eval-capture-index.js +44 -0
  132. package/dist/cli/commands/loop-eval-commands.d.ts +8 -5
  133. package/dist/cli/commands/loop-eval-commands.js +12 -626
  134. package/dist/cli/commands/loop-eval-eval-command.d.ts +3 -0
  135. package/dist/cli/commands/loop-eval-eval-command.js +168 -0
  136. package/dist/cli/commands/loop-eval-monotonic-command.d.ts +3 -0
  137. package/dist/cli/commands/loop-eval-monotonic-command.js +90 -0
  138. package/dist/cli/commands/loop-eval-parents.d.ts +7 -0
  139. package/dist/cli/commands/loop-eval-parents.js +20 -0
  140. package/dist/cli/commands/loop-eval-run-code.d.ts +7 -0
  141. package/dist/cli/commands/loop-eval-run-code.js +40 -0
  142. package/dist/cli/commands/loop-eval-run-command.d.ts +3 -0
  143. package/dist/cli/commands/loop-eval-run-command.js +113 -0
  144. package/dist/cli/commands/loop-eval-spec-commands.d.ts +3 -0
  145. package/dist/cli/commands/loop-eval-spec-commands.js +165 -0
  146. package/dist/cli/commands/loop-eval-spec-lint-command.d.ts +3 -0
  147. package/dist/cli/commands/loop-eval-spec-lint-command.js +42 -0
  148. package/dist/cli/commands/loop-eval-workflow-commands.d.ts +3 -0
  149. package/dist/cli/commands/loop-eval-workflow-commands.js +141 -0
  150. package/dist/cli/commands/mcp-commands.d.ts +10 -0
  151. package/dist/cli/commands/mcp-commands.js +157 -0
  152. package/dist/cli/commands/request-command-options.d.ts +49 -0
  153. package/dist/cli/commands/request-command-options.js +1 -0
  154. package/dist/cli/commands/request-commands.d.ts +3 -9
  155. package/dist/cli/commands/request-commands.js +17 -522
  156. package/dist/cli/commands/request-init-command.d.ts +3 -0
  157. package/dist/cli/commands/request-init-command.js +100 -0
  158. package/dist/cli/commands/request-lint-command.d.ts +3 -0
  159. package/dist/cli/commands/request-lint-command.js +50 -0
  160. package/dist/cli/commands/request-list-command.d.ts +10 -0
  161. package/dist/cli/commands/request-list-command.js +53 -0
  162. package/dist/cli/commands/request-repair-status-command.d.ts +3 -0
  163. package/dist/cli/commands/request-repair-status-command.js +61 -0
  164. package/dist/cli/commands/request-show-command.d.ts +3 -0
  165. package/dist/cli/commands/request-show-command.js +56 -0
  166. package/dist/cli/commands/request-transition-command.d.ts +3 -0
  167. package/dist/cli/commands/request-transition-command.js +122 -0
  168. package/dist/cli/commands/request-transition-failures.d.ts +8 -0
  169. package/dist/cli/commands/request-transition-failures.js +90 -0
  170. package/dist/cli/commands/request-transition-hooks.d.ts +50 -0
  171. package/dist/cli/commands/request-transition-hooks.js +115 -0
  172. package/dist/cli/commands/request-transition-preflight.d.ts +10 -0
  173. package/dist/cli/commands/request-transition-preflight.js +107 -0
  174. package/dist/cli/commands/scan-command-shared.d.ts +96 -0
  175. package/dist/cli/commands/scan-command-shared.js +83 -0
  176. package/dist/cli/commands/scan-commands.d.ts +3 -2
  177. package/dist/cli/commands/scan-commands.js +23 -463
  178. package/dist/cli/commands/scan-inventory-commands.d.ts +3 -0
  179. package/dist/cli/commands/scan-inventory-commands.js +96 -0
  180. package/dist/cli/commands/scan-project-commands.d.ts +3 -0
  181. package/dist/cli/commands/scan-project-commands.js +65 -0
  182. package/dist/cli/commands/scan-scope-commands.d.ts +3 -0
  183. package/dist/cli/commands/scan-scope-commands.js +149 -0
  184. package/dist/cli/commands/scan-scope-helpers.d.ts +8 -0
  185. package/dist/cli/commands/scan-scope-helpers.js +54 -0
  186. package/dist/cli/commands/scan-structural-commands.d.ts +3 -0
  187. package/dist/cli/commands/scan-structural-commands.js +113 -0
  188. package/dist/cli/commands/share-commands.d.ts +12 -68
  189. package/dist/cli/commands/share-commands.js +47 -556
  190. package/dist/cli/commands/share-error-hints.d.ts +14 -0
  191. package/dist/cli/commands/share-error-hints.js +38 -0
  192. package/dist/cli/commands/share-identity.d.ts +13 -0
  193. package/dist/cli/commands/share-identity.js +22 -0
  194. package/dist/cli/commands/share-record-paths.d.ts +37 -0
  195. package/dist/cli/commands/share-record-paths.js +68 -0
  196. package/dist/cli/commands/share-runners.d.ts +3 -0
  197. package/dist/cli/commands/share-runners.js +158 -0
  198. package/dist/cli/commands/shared-read-runners.d.ts +3 -0
  199. package/dist/cli/commands/shared-read-runners.js +51 -0
  200. package/dist/cli/commands/slice-check-command.d.ts +3 -0
  201. package/dist/cli/commands/slice-check-command.js +54 -0
  202. package/dist/cli/commands/slice-commands.d.ts +2 -22
  203. package/dist/cli/commands/slice-commands.js +18 -517
  204. package/dist/cli/commands/slice-decompose-artifacts.d.ts +18 -0
  205. package/dist/cli/commands/slice-decompose-artifacts.js +79 -0
  206. package/dist/cli/commands/slice-decompose-command.d.ts +3 -0
  207. package/dist/cli/commands/slice-decompose-command.js +123 -0
  208. package/dist/cli/commands/slice-list-commands.d.ts +3 -0
  209. package/dist/cli/commands/slice-list-commands.js +182 -0
  210. package/dist/cli/commands/slice-pick-command.d.ts +3 -0
  211. package/dist/cli/commands/slice-pick-command.js +60 -0
  212. package/dist/cli/commands/slice-plan-command.d.ts +24 -0
  213. package/dist/cli/commands/slice-plan-command.js +111 -0
  214. package/dist/cli/commands/workflow-commands.d.ts +10 -1
  215. package/dist/cli/commands/workflow-commands.js +6 -594
  216. package/dist/cli/commands/workflow-gate-commands.d.ts +3 -0
  217. package/dist/cli/commands/workflow-gate-commands.js +116 -0
  218. package/dist/cli/commands/workflow-plan-helpers.d.ts +48 -0
  219. package/dist/cli/commands/workflow-plan-helpers.js +103 -0
  220. package/dist/cli/commands/workflow-planning-commands.d.ts +3 -0
  221. package/dist/cli/commands/workflow-planning-commands.js +56 -0
  222. package/dist/cli/commands/workflow-swarm-actions.d.ts +7 -0
  223. package/dist/cli/commands/workflow-swarm-actions.js +109 -0
  224. package/dist/cli/commands/workflow-swarm-commands.d.ts +3 -0
  225. package/dist/cli/commands/workflow-swarm-commands.js +138 -0
  226. package/dist/cli/commands/workflow-tech-actions.d.ts +10 -0
  227. package/dist/cli/commands/workflow-tech-actions.js +150 -0
  228. package/dist/cli/commands/workspace/init-actions.d.ts +10 -0
  229. package/dist/cli/commands/workspace/init-actions.js +199 -0
  230. package/dist/cli/commands/workspace/init-codegraph-stake.d.ts +10 -0
  231. package/dist/cli/commands/workspace/init-codegraph-stake.js +54 -0
  232. package/dist/cli/commands/workspace/init-command.d.ts +8 -124
  233. package/dist/cli/commands/workspace/init-command.js +59 -623
  234. package/dist/cli/commands/workspace/init-context.d.ts +58 -0
  235. package/dist/cli/commands/workspace/init-context.js +121 -0
  236. package/dist/cli/commands/workspace/init-envelope.d.ts +3 -0
  237. package/dist/cli/commands/workspace/init-envelope.js +34 -0
  238. package/dist/cli/commands/workspace/init-failures.d.ts +3 -0
  239. package/dist/cli/commands/workspace/init-failures.js +84 -0
  240. package/dist/cli/commands/workspace/init-hooks-decision.d.ts +24 -0
  241. package/dist/cli/commands/workspace/init-hooks-decision.js +189 -0
  242. package/dist/cli/commands/workspace/init-options.d.ts +102 -0
  243. package/dist/cli/commands/workspace/init-options.js +1 -0
  244. package/dist/cli/commands/worktree-lease-commands.d.ts +10 -6
  245. package/dist/cli/commands/worktree-lease-commands.js +42 -512
  246. package/dist/cli/commands/worktree-lease-gc-command.d.ts +12 -0
  247. package/dist/cli/commands/worktree-lease-gc-command.js +117 -0
  248. package/dist/cli/commands/worktree-lease-git.d.ts +22 -0
  249. package/dist/cli/commands/worktree-lease-git.js +48 -0
  250. package/dist/cli/commands/worktree-lease-list-command.d.ts +12 -0
  251. package/dist/cli/commands/worktree-lease-list-command.js +100 -0
  252. package/dist/cli/commands/worktree-lease-load.d.ts +37 -0
  253. package/dist/cli/commands/worktree-lease-load.js +28 -0
  254. package/dist/cli/commands/worktree-lease-release-command.d.ts +13 -0
  255. package/dist/cli/commands/worktree-lease-release-command.js +105 -0
  256. package/dist/cli/commands/worktree-lease-renew-command.d.ts +11 -0
  257. package/dist/cli/commands/worktree-lease-renew-command.js +99 -0
  258. package/dist/cli/commands/worktree-lease-session.d.ts +22 -0
  259. package/dist/cli/commands/worktree-lease-session.js +29 -0
  260. package/dist/cli/commands/worktree-lease-spawn-command.d.ts +11 -0
  261. package/dist/cli/commands/worktree-lease-spawn-command.js +119 -0
  262. package/dist/cli/commands/worktree-lease-status-command.d.ts +11 -0
  263. package/dist/cli/commands/worktree-lease-status-command.js +86 -0
  264. package/dist/cli/commands/worktree-lease-store.d.ts +19 -0
  265. package/dist/cli/commands/worktree-lease-store.js +24 -0
  266. package/dist/cli/program.js +5 -0
  267. package/dist/services/distribution/mcp-install-runner.d.ts +21 -0
  268. package/dist/services/distribution/mcp-install-runner.js +59 -0
  269. package/dist/services/distribution/mcp-install.d.ts +95 -0
  270. package/dist/services/distribution/mcp-install.js +179 -0
  271. package/dist/services/distribution/plugin-manifest.d.ts +97 -0
  272. package/dist/services/distribution/plugin-manifest.js +126 -0
  273. package/dist/services/hooks/mcp-surface-gate.d.ts +19 -0
  274. package/dist/services/hooks/mcp-surface-gate.js +87 -0
  275. package/dist/services/ide/adapters/claude-code-adapter.d.ts +4 -35
  276. package/dist/services/ide/adapters/claude-code-adapter.js +48 -171
  277. package/dist/services/ide/adapters/claude-code-local-state.d.ts +28 -0
  278. package/dist/services/ide/adapters/claude-code-local-state.js +112 -0
  279. package/dist/services/ide/adapters/claude-code-model-window.d.ts +37 -0
  280. package/dist/services/ide/adapters/claude-code-model-window.js +113 -0
  281. package/dist/services/ide/ide-mcp-install-types.d.ts +78 -0
  282. package/dist/services/ide/ide-mcp-install-types.js +104 -0
  283. package/dist/services/ide/ide-types.d.ts +45 -1
  284. package/dist/services/ide/mcp-tool-matcher.d.ts +34 -0
  285. package/dist/services/ide/mcp-tool-matcher.js +84 -0
  286. package/dist/services/lint/npx-resolver.js +7 -0
  287. package/dist/services/mcp/cli-executor.d.ts +36 -0
  288. package/dist/services/mcp/cli-executor.js +97 -0
  289. package/dist/services/mcp/json-rpc.d.ts +81 -0
  290. package/dist/services/mcp/json-rpc.js +116 -0
  291. package/dist/services/mcp/memory-tool.d.ts +3 -0
  292. package/dist/services/mcp/memory-tool.js +62 -0
  293. package/dist/services/mcp/server-main.d.ts +1 -0
  294. package/dist/services/mcp/server-main.js +66 -0
  295. package/dist/services/mcp/server.d.ts +22 -0
  296. package/dist/services/mcp/server.js +152 -0
  297. package/dist/services/mcp/surface.d.ts +62 -0
  298. package/dist/services/mcp/surface.js +198 -0
  299. package/dist/services/mcp/tool-core.d.ts +42 -0
  300. package/dist/services/mcp/tool-core.js +137 -0
  301. package/dist/services/mcp/tools.d.ts +13 -0
  302. package/dist/services/mcp/tools.js +58 -0
  303. package/dist/services/readonly-surface/argv-guard.d.ts +32 -0
  304. package/dist/services/readonly-surface/argv-guard.js +164 -0
  305. package/dist/services/readonly-surface/readonly-whitelist.d.ts +147 -0
  306. package/dist/services/readonly-surface/readonly-whitelist.js +107 -0
  307. package/dist/services/readonly-surface/whitelist-generator.d.ts +64 -0
  308. package/dist/services/readonly-surface/whitelist-generator.js +212 -0
  309. package/dist/services/skills/hooks-codegate-superpowers.d.ts +1 -0
  310. package/dist/services/skills/hooks-codegate-superpowers.js +26 -0
  311. package/dist/services/skills/hooks-settings-service.js +10 -2
  312. package/package.json +17 -6
  313. package/scripts/install-skills-bundles.mjs +233 -0
  314. package/scripts/install-skills-fs.mjs +148 -0
  315. package/scripts/install-skills-ide.mjs +279 -0
  316. package/scripts/install-skills-output-style-default.mjs +196 -0
  317. package/scripts/install-skills-output-styles.mjs +121 -0
  318. package/scripts/install-skills-report.mjs +42 -0
  319. package/scripts/install-skills-user-config.mjs +198 -0
  320. package/scripts/install-skills.mjs +36 -1066
  321. package/scripts/watch-tree.mjs +140 -0
  322. package/scripts/watch.mjs +4 -126
  323. package/skills/peaks-status/SKILL.md +31 -13
@@ -0,0 +1,212 @@
1
+ /**
2
+ * The schema introspector (spec §3 row 2): turns the hand-authored surface
3
+ * declaration into the generated whitelist artifact by READING the live Commander
4
+ * registry - never by restating it.
5
+ *
6
+ * Everything the registry can answer is taken from it:
7
+ * - does the command path exist,
8
+ * - does each `--flag` in the template exist on that command,
9
+ * - is that flag mandatory, and does it take a value,
10
+ * - which positionals does the command declare, and are they required.
11
+ * A template that disagrees with the registry is a BUILD FAILURE, not a warning:
12
+ * that is the whole point of generating the artifact instead of typing it.
13
+ *
14
+ * What the registry CANNOT answer is the constraint on a placeholder's value
15
+ * (`^[a-z0-9-]+$` for a request id). Those constraints come from the surface file
16
+ * and are marked `constraintSource: 'handwritten'`, so the one place drift can
17
+ * enter is visible in the artifact. When a command later declares `.choices()`,
18
+ * the same field flips to `introspected` with no artifact edit.
19
+ *
20
+ * DETERMINISM: no timestamps, no `Date`, no unordered iteration. Record
21
+ * keys follow argv order and entries follow the surface file's order, so two
22
+ * consecutive runs are byte-identical.
23
+ */
24
+ import { readFileSync } from 'node:fs';
25
+ import { join } from 'node:path';
26
+ import { z } from 'zod';
27
+ import { repoRoot } from 'peaks-loop-shared/paths';
28
+ import { READONLY_WHITELIST_RELATIVE_PATH } from './readonly-whitelist.js';
29
+ /** The hand-authored input, relative to the package root. */
30
+ export const READONLY_SURFACE_RELATIVE_PATH = 'contracts/readonly-surface.json';
31
+ /** The schema version of the artifact this module emits. */
32
+ export const READONLY_WHITELIST_SCHEMA_VERSION = 1;
33
+ const SurfaceParamSchema = z.object({
34
+ type: z.enum(['slug', 'enum', 'token', 'integer', 'path']),
35
+ pattern: z.string().min(1).optional(),
36
+ values: z.array(z.string().min(1)).min(1).optional(),
37
+ min: z.number().int().optional(),
38
+ max: z.number().int().optional(),
39
+ default: z.number().int().optional()
40
+ });
41
+ const SurfaceEntrySchema = z.object({
42
+ id: z.string().min(1),
43
+ commandPath: z.array(z.string().min(1)).min(1),
44
+ argv: z.array(z.string().min(1)).min(1),
45
+ params: z.record(z.string(), SurfaceParamSchema)
46
+ });
47
+ export const ReadOnlySurfaceSchema = z.object({
48
+ schemaVersion: z.literal(1),
49
+ readOnlyDefinition: z.string().min(1),
50
+ note: z.array(z.string()).default([]),
51
+ tools: z.array(z.object({ tool: z.string().min(1), entries: z.array(SurfaceEntrySchema).min(1) }))
52
+ });
53
+ /** `<name>` marks a placeholder token in a surface `argv` element. */
54
+ const PLACEHOLDER_TOKEN = /^<([a-z][a-z0-9-]*)>$/;
55
+ class SurfaceDriftError extends Error {
56
+ constructor(message) {
57
+ super(message);
58
+ this.name = 'SurfaceDriftError';
59
+ }
60
+ }
61
+ function resolveCommand(program, path) {
62
+ let current = program;
63
+ for (const name of path) {
64
+ current = current.commands.find((candidate) => candidate.name() === name);
65
+ if (current === undefined) {
66
+ throw new SurfaceDriftError(`No command registered at path: ${path.join(' ')}`);
67
+ }
68
+ }
69
+ return current;
70
+ }
71
+ function hasOption(command, token) {
72
+ return command.options.some((option) => option.long === token || option.short === token);
73
+ }
74
+ function optionTakingValue(command, token) {
75
+ return command.options.find((option) => {
76
+ if (option.long !== token && option.short !== token)
77
+ return false;
78
+ return /[<[].+[>\]]/.test(option.flags);
79
+ });
80
+ }
81
+ /** Placeholders the registry supplies a constraint for are marked as such. */
82
+ function constraintSourceOf(declared, option) {
83
+ if (declared.type === 'enum' && option?.argChoices !== undefined)
84
+ return 'introspected';
85
+ if (option?.defaultValue !== undefined && declared.default !== undefined)
86
+ return 'introspected';
87
+ return 'handwritten';
88
+ }
89
+ /** The constraint fields the surface declares, copied without inventing absent ones. */
90
+ function constraintsOf(declared) {
91
+ return {
92
+ type: declared.type,
93
+ ...(declared.pattern === undefined ? {} : { pattern: declared.pattern }),
94
+ ...(declared.values === undefined ? {} : { values: declared.values }),
95
+ ...(declared.min === undefined ? {} : { min: declared.min }),
96
+ ...(declared.max === undefined ? {} : { max: declared.max }),
97
+ ...(declared.default === undefined ? {} : { default: declared.default })
98
+ };
99
+ }
100
+ /**
101
+ * Bind one placeholder either to the value-taking flag before it or to the next
102
+ * positional the command declares. Membership in the template is what makes a
103
+ * value required; `binding.required` carries the REGISTRY's opinion, which is a
104
+ * different fact (`--limit` is not mandatory, yet the template pins it).
105
+ */
106
+ function bindPlaceholder(slot) {
107
+ const { entryId, name, declared, command, previousToken, cursor } = slot;
108
+ if (declared === undefined) {
109
+ throw new SurfaceDriftError(`Entry ${entryId}: placeholder <${name}> has no params spec`);
110
+ }
111
+ const boundOption = previousToken === undefined ? undefined : optionTakingValue(command, previousToken);
112
+ if (boundOption !== undefined && previousToken !== undefined) {
113
+ return {
114
+ ...constraintsOf(declared),
115
+ binding: { kind: 'option', token: previousToken, required: boundOption.mandatory },
116
+ constraintSource: constraintSourceOf(declared, boundOption)
117
+ };
118
+ }
119
+ const registered = command.registeredArguments[cursor.positionalIndex];
120
+ if (registered === undefined) {
121
+ throw new SurfaceDriftError(`Entry ${entryId}: <${name}> is positional ${cursor.positionalIndex + 1} but the command declares ${command.registeredArguments.length}`);
122
+ }
123
+ cursor.positionalIndex += 1;
124
+ cursor.positionalNames.push(registered.name());
125
+ return {
126
+ ...constraintsOf(declared),
127
+ binding: { kind: 'positional', token: registered.name(), required: registered.required },
128
+ constraintSource: 'handwritten'
129
+ };
130
+ }
131
+ /** The non-flag words a template opens with, which must be the command path. */
132
+ function leadingLiteralsOf(argv) {
133
+ return argv.flatMap((segment) => segment.kind === 'literal' && !segment.value.startsWith('-') ? [segment.value] : []);
134
+ }
135
+ function assertTemplateDrivesCommand(surface, command, argv, cursor) {
136
+ const leading = leadingLiteralsOf(argv);
137
+ if (leading.join('\u0000') !== surface.commandPath.join('\u0000')) {
138
+ throw new SurfaceDriftError(`Entry ${surface.id}: argv opens with [${leading.join(', ')}] but commandPath is [${surface.commandPath.join(', ')}]`);
139
+ }
140
+ if (cursor.positionalIndex !== command.registeredArguments.length) {
141
+ throw new SurfaceDriftError(`Entry ${surface.id}: template fills ${cursor.positionalIndex} positional(s); ${surface.commandPath.join(' ')} declares ${command.registeredArguments.length}`);
142
+ }
143
+ }
144
+ function buildEntry(program, tool, surface) {
145
+ const command = resolveCommand(program, surface.commandPath);
146
+ const optionTokens = [];
147
+ const argv = [];
148
+ const params = {};
149
+ const cursor = { positionalIndex: 0, positionalNames: [] };
150
+ for (let index = 0; index < surface.argv.length; index += 1) {
151
+ const token = surface.argv[index];
152
+ if (token === undefined)
153
+ continue;
154
+ const placeholder = PLACEHOLDER_TOKEN.exec(token);
155
+ const name = placeholder?.[1];
156
+ if (placeholder !== null && name !== undefined) {
157
+ params[name] = bindPlaceholder({
158
+ entryId: surface.id,
159
+ name,
160
+ declared: surface.params[name],
161
+ command,
162
+ previousToken: index > 0 ? surface.argv[index - 1] : undefined,
163
+ cursor
164
+ });
165
+ argv.push({ kind: 'param', name });
166
+ continue;
167
+ }
168
+ if (token.startsWith('-')) {
169
+ if (!hasOption(command, token)) {
170
+ throw new SurfaceDriftError(`Entry ${surface.id}: no such option ${token} on ${surface.commandPath.join(' ')}`);
171
+ }
172
+ optionTokens.push(token);
173
+ }
174
+ argv.push({ kind: 'literal', value: token });
175
+ }
176
+ assertTemplateDrivesCommand(surface, command, argv, cursor);
177
+ return {
178
+ id: surface.id,
179
+ tool,
180
+ commandPath: [...surface.commandPath],
181
+ argv,
182
+ params,
183
+ introspection: { optionTokens, positionalCount: cursor.positionalNames.length }
184
+ };
185
+ }
186
+ /** Build the artifact object from a surface declaration and a live program. */
187
+ export function generateReadOnlyWhitelist(surface, program) {
188
+ const entries = surface.tools.flatMap((group) => group.entries.map((entry) => buildEntry(program, group.tool, entry)));
189
+ const ids = new Set(entries.map((entry) => entry.id));
190
+ if (ids.size !== entries.length) {
191
+ throw new SurfaceDriftError('Entry ids must be unique across the whole surface');
192
+ }
193
+ return {
194
+ schemaVersion: READONLY_WHITELIST_SCHEMA_VERSION,
195
+ source: READONLY_SURFACE_RELATIVE_PATH,
196
+ readOnlyDefinition: surface.readOnlyDefinition,
197
+ entries
198
+ };
199
+ }
200
+ /** Byte-stable serialization: two runs over the same input MUST match. */
201
+ export function serializeReadOnlyWhitelist(whitelist) {
202
+ return `${JSON.stringify(whitelist, null, 2)}\n`;
203
+ }
204
+ /** Read + validate the hand-authored surface declaration. */
205
+ export function loadReadOnlySurface(root = repoRoot) {
206
+ const text = readFileSync(join(root, READONLY_SURFACE_RELATIVE_PATH), 'utf8');
207
+ return ReadOnlySurfaceSchema.parse(JSON.parse(text));
208
+ }
209
+ /** Absolute path of the generated artifact. */
210
+ export function readonlyWhitelistPath(root = repoRoot) {
211
+ return join(root, READONLY_WHITELIST_RELATIVE_PATH);
212
+ }
@@ -15,6 +15,7 @@ interface ResolvedHookSpec {
15
15
  readonly hookEnforceSentinel: string;
16
16
  readonly hookEnforceMatcher: string;
17
17
  readonly hookEnforceEvent: string;
18
+ readonly hookEnforceMcpMatcher: string | undefined;
18
19
  /**
19
20
  * True when the gate-enforce entry must be materialized into the IDE's
20
21
  * MACHINE-LOCAL settings file rather than the shared one (see
@@ -8,6 +8,7 @@
8
8
  * type contract re-exported by the caller.
9
9
  */
10
10
  import { getAdapter } from '../ide/ide-registry.js';
11
+ import { mcpHookMatcher } from '../ide/mcp-tool-matcher.js';
11
12
  import { HOOK_COMPACT_SETTLE_COMMAND, HOOK_COMPACT_SETTLE_EVENT, HOOK_COMPACT_SETTLE_MATCHER, HOOK_COMPACT_SETTLE_SENTINEL, HOOK_OUTER_CACHE_COMMAND, HOOK_OUTER_CACHE_EVENT, HOOK_OUTER_CACHE_SENTINEL, HOOK_POST_COMPACT_REINJECT_COMMAND, HOOK_POST_COMPACT_REINJECT_EVENT, HOOK_POST_COMPACT_REINJECT_MATCHER, HOOK_POST_COMPACT_REINJECT_SENTINEL, HOOK_WORKSPACE_INIT_COMMAND, HOOK_WORKSPACE_INIT_EVENT, HOOK_WORKSPACE_INIT_SENTINEL } from './session-start-hook-constants.js';
12
13
  /** Sentinel substring identifying a Claude-Code gate-enforce hook entry. */
13
14
  export const HOOK_ENFORCE_SENTINEL = 'peaks gate enforce';
@@ -87,6 +88,7 @@ export function resolveHookSpec(ide) {
87
88
  hookEnforceSentinel: spec.sentinel,
88
89
  hookEnforceMatcher: adapter.toolMatcher,
89
90
  hookEnforceEvent: adapter.hookEvent,
91
+ hookEnforceMcpMatcher: mcpHookMatcher(adapter),
90
92
  // Only Claude Code has the machine-local sibling settings file the
91
93
  // routing depends on, and only Claude Code's hook schema accepts a
92
94
  // `shell` key.
@@ -105,6 +107,27 @@ export const HOOK_CODE_GATE_SENTINEL = 'peaks code-gate';
105
107
  export const HOOK_CODE_GATE_MATCHER = 'Edit|Write|MultiEdit';
106
108
  export const HOOK_CODE_GATE_EVENT = 'PreToolUse';
107
109
  export const HOOK_CODE_GATE_COMMAND = `peaks code-gate --json`;
110
+ /**
111
+ * The read-only MCP surface as a hook entry: the same command under the
112
+ * adapter's MCP matcher, or `undefined` for a harness that declares no MCP
113
+ * naming. A hook fires only for the matchers its OWN entry names, so without this
114
+ * entry the fail-closed branch in `mcp-surface-gate.ts` never runs on a live
115
+ * harness. A second entry rather than a wider matcher on the Bash one: that entry
116
+ * is mirrored by the workspace-init template, and widening it would put installer
117
+ * and template out of step.
118
+ */
119
+ function mcpMatcherEntry(spec) {
120
+ if (spec.hookEnforceMcpMatcher === undefined)
121
+ return undefined;
122
+ return {
123
+ sentinel: spec.hookEnforceSentinel,
124
+ matcher: spec.hookEnforceMcpMatcher,
125
+ command: spec.hookEnforceCommand,
126
+ event: spec.hookEnforceEvent,
127
+ machineLocal: spec.hookEnforceMachineLocal,
128
+ ...(spec.hookEnforceShell !== undefined ? { shell: spec.hookEnforceShell } : {})
129
+ };
130
+ }
108
131
  export function resolveHookEntries(ide, _skipProgress = false) {
109
132
  const spec = resolveHookSpec(ide);
110
133
  const entries = [
@@ -117,6 +140,9 @@ export function resolveHookEntries(ide, _skipProgress = false) {
117
140
  ...(spec.hookEnforceShell !== undefined ? { shell: spec.hookEnforceShell } : {})
118
141
  }
119
142
  ];
143
+ const mcpEntry = mcpMatcherEntry(spec);
144
+ if (mcpEntry !== undefined)
145
+ entries.push(mcpEntry);
120
146
  // ── Why the three SessionStart entries below carry NO `shell` pin ─────────
121
147
  //
122
148
  // The gate-enforce entry above is shell-pinned on Windows (see
@@ -358,8 +358,16 @@ function shapeMatchesDesired(settings, entries, allPeaksSentinels) {
358
358
  // source" — so a file written before matchers were explicit converges
359
359
  // once instead of churning on every install.
360
360
  for (const desired of entries.filter((e) => e.event === eventKey)) {
361
- const onDisk = peaksPresent.find((entry) => (entry.hooks ?? []).some((h) => String(h.command ?? '').includes(desired.sentinel)));
362
- if (onDisk === undefined || (onDisk.matcher ?? '') !== desired.matcher)
361
+ // The identity of a desired entry is the PAIR (sentinel, matcher), not the
362
+ // sentinel alone: one command may legitimately be installed under several
363
+ // matchers (the read-only MCP surface rides `peaks gate enforce` under its
364
+ // own matcher set). Matching on the sentinel first found that first entry
365
+ // every time, so the second one never looked installed and every install
366
+ // rewrote the file. A wrong matcher is still un-repaired — it fails to
367
+ // match and the caller rewrites.
368
+ const onDisk = peaksPresent.find((entry) => (entry.matcher ?? '') === desired.matcher &&
369
+ (entry.hooks ?? []).some((h) => String(h.command ?? '').includes(desired.sentinel)));
370
+ if (onDisk === undefined)
363
371
  return false;
364
372
  }
365
373
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peaks-loop",
3
- "version": "4.1.2",
3
+ "version": "4.1.3",
4
4
  "description": "Loop Engineering CLI — workflow primitive / loop guards / evaluators / slice orchestration",
5
5
  "author": "SquabbyZ",
6
6
  "keywords": [
@@ -75,16 +75,25 @@
75
75
  "scripts/copy-templates.mjs",
76
76
  "scripts/sync-version.mjs",
77
77
  "scripts/install-skills.mjs",
78
+ "scripts/install-skills-fs.mjs",
79
+ "scripts/install-skills-report.mjs",
80
+ "scripts/install-skills-user-config.mjs",
81
+ "scripts/install-skills-ide.mjs",
82
+ "scripts/install-skills-bundles.mjs",
83
+ "scripts/install-skills-output-styles.mjs",
84
+ "scripts/install-skills-output-style-default.mjs",
78
85
  "scripts/canonical-store.mjs",
79
86
  "scripts/canonical-store-link.mjs",
80
87
  "scripts/canonical-store-prune.mjs",
81
88
  "scripts/watch.mjs",
89
+ "scripts/watch-tree.mjs",
82
90
  "skills/**",
83
91
  "!skills/**/test-prompts.json",
84
92
  "!skills/**/.DS_Store",
85
93
  "output-styles/**",
86
94
  "agents/**",
87
95
  "schemas/*.json",
96
+ "contracts/readonly-argv-whitelist.json",
88
97
  ".claude-plugin/**",
89
98
  "config/eslint/.peaks-rules.cjs",
90
99
  "config/eslint/tsconfig.lint.json",
@@ -120,10 +129,10 @@
120
129
  "picomatch": "4.0.4",
121
130
  "yaml": "^2.9.0",
122
131
  "zod": "^4.4.3",
123
- "peaks-loop-internal-runtime": "0.0.42",
124
- "peaks-loop-shared-channel": "0.0.59",
125
- "peaks-loop-mut": "0.1.56",
126
- "peaks-loop-shared": "0.0.91"
132
+ "peaks-loop-internal-runtime": "0.0.43",
133
+ "peaks-loop-mut": "0.1.57",
134
+ "peaks-loop-shared-channel": "0.0.60",
135
+ "peaks-loop-shared": "0.0.92"
127
136
  },
128
137
  "devDependencies": {
129
138
  "@changesets/cli": "2.31.1",
@@ -148,7 +157,9 @@
148
157
  "vitest": "^4.1.10"
149
158
  },
150
159
  "scripts": {
151
- "build": "node ./scripts/sync-version.mjs && node ./scripts/clean-dist.mjs && pnpm -r --filter \"./packages/*\" run build && node ./scripts/write-package-dist-stamps.mjs && tsc -p tsconfig.build.json && node ./scripts/copy-templates.mjs && node ./scripts/check-build-integrity.mjs && node ./scripts/write-dist-stamp.mjs",
160
+ "build": "node ./scripts/sync-version.mjs && node ./scripts/clean-dist.mjs && pnpm -r --filter \"./packages/*\" run build && node ./scripts/write-package-dist-stamps.mjs && node node_modules/tsx/dist/cli.mjs scripts/generate-readonly-whitelist.ts && node node_modules/tsx/dist/cli.mjs scripts/generate-plugin-manifest.ts && tsc -p tsconfig.build.json && node ./scripts/copy-templates.mjs && node ./scripts/check-build-integrity.mjs && node ./scripts/write-dist-stamp.mjs",
161
+ "readonly:whitelist": "node node_modules/tsx/dist/cli.mjs scripts/generate-readonly-whitelist.ts",
162
+ "plugin:manifest": "node node_modules/tsx/dist/cli.mjs scripts/generate-plugin-manifest.ts",
152
163
  "prepublish": "node ./scripts/sync-version.mjs",
153
164
  "postinstall": "node ./scripts/install-skills.mjs",
154
165
  "predev": "node ./scripts/sync-version.mjs",
@@ -0,0 +1,233 @@
1
+ // scripts/install-skills-bundles.mjs
2
+ //
3
+ // The two installers that read a directory of bundled assets out of the package and
4
+ // reconcile each entry into an IDE directory: the DIRECTORY-shaped family (skills) and
5
+ // the FILE-shaped one (agents). Each owns its dispatch precedence and its repair rule.
6
+ //
7
+ // Split out of `scripts/install-skills.mjs` (rid-043). Every line below was moved
8
+ // VERBATIM from that file; only this header, the import block, and the `export`
9
+ // keyword on names a sibling imports are new.
10
+
11
+ import { existsSync, lstatSync, mkdirSync, readdirSync } from 'node:fs';
12
+ import { join } from 'node:path';
13
+
14
+ import { reconcileCanonicalEntry } from './canonical-store.mjs';
15
+ import { reconcileCanonicalFileEntry } from './canonical-store-link.mjs';
16
+ import { pruneBundledEntries } from './canonical-store-prune.mjs';
17
+ import { createInstallResult } from './install-skills-report.mjs';
18
+ import { createInstallRootValidator, resolvePackageRoot } from './install-skills-fs.mjs';
19
+ import { resolveBundleTargetRoot } from './install-skills-ide.mjs';
20
+
21
+ /**
22
+ * THE INSTALL CANDIDATE LIST — every skill the package ships, as
23
+ * `{ skillName, sourcePath }`.
24
+ *
25
+ * After the v2.13.0 bee-demote (commit de0872b), the role skills (peaks-prd,
26
+ * peaks-rd, peaks-qa, peaks-ui, peaks-sc, peaks-txt) moved under
27
+ * `skills/bee/<role>/` while user-facing helpers stayed at `skills/<name>/`. So the
28
+ * list is built by walking top-level entries (user-facing helpers) AND
29
+ * `skills/bee/<role>` (demoted role skills). Each candidate is installed under its
30
+ * basename, so the postinstall links `~/.claude/skills/peaks-rd` to
31
+ * `skills/bee/peaks-rd` rather than to `skills/bee`.
32
+ *
33
+ * De-dupe rule: a top-level helper with the same name WINS, which preserves the
34
+ * existing install contract for any helper sharing a name with a demoted role skill
35
+ * (legacy overlap).
36
+ *
37
+ * @param {string} skillsRoot
38
+ * @returns {Array<{ skillName: string, sourcePath: string }>}
39
+ */
40
+ function collectSkillCandidates(skillsRoot) {
41
+ const beeRoot = join(skillsRoot, 'bee');
42
+ /** @type {Array<{ skillName: string, sourcePath: string }>} */
43
+ const candidates = [];
44
+ for (const skillName of readdirSync(skillsRoot)) {
45
+ if (skillName === 'bee') continue;
46
+ const sourcePath = join(skillsRoot, skillName);
47
+ if (!isSkillSource(sourcePath)) continue;
48
+ candidates.push({ skillName, sourcePath });
49
+ }
50
+ if (existsSync(beeRoot) && isDirectoryEntry(beeRoot)) {
51
+ for (const skillName of readdirSync(beeRoot)) {
52
+ const sourcePath = join(beeRoot, skillName);
53
+ if (!isSkillSource(sourcePath)) continue;
54
+ if (candidates.some((c) => c.skillName === skillName)) continue;
55
+ candidates.push({ skillName, sourcePath });
56
+ }
57
+ }
58
+ return candidates;
59
+ }
60
+
61
+ /** A real directory entry — `lstat`, so a symlink to one is NOT one. */
62
+ function isDirectoryEntry(entryPath) {
63
+ return lstatSync(entryPath).isDirectory();
64
+ }
65
+
66
+ /** A directory entry that is a skill: a real directory holding a `SKILL.md`. */
67
+ function isSkillSource(sourcePath) {
68
+ return isDirectoryEntry(sourcePath) && existsSync(join(sourcePath, 'SKILL.md'));
69
+ }
70
+
71
+ export function installBundledSkills(options = {}) {
72
+ const packageRoot = resolvePackageRoot(options);
73
+ const skillsRoot = join(packageRoot, 'skills');
74
+
75
+ if (process.env.PEAKS_SKIP_SKILL_INSTALL === '1' || !existsSync(skillsRoot)) {
76
+ return createInstallResult();
77
+ }
78
+
79
+ // Slice #011: IDE-aware dispatch — the precedence itself now lives in ONE place
80
+ // (`resolveBundleTargetRoot` in `install-skills-ide.mjs`), because all three
81
+ // families stated it and only this one also warns.
82
+ const targetRoot = resolveBundleTargetRoot(options, {
83
+ profileField: 'skillsDir',
84
+ envVar: 'PEAKS_CLAUDE_SKILLS_DIR',
85
+ fallback: '.claude/skills',
86
+ warn: true
87
+ });
88
+
89
+ const installed = [];
90
+ const skipped = [];
91
+ mkdirSync(targetRoot, { recursive: true });
92
+ const validateSkillsRoot = createInstallRootValidator(targetRoot, 'Peaks skills');
93
+
94
+ const candidates = collectSkillCandidates(skillsRoot);
95
+
96
+ for (const { skillName, sourcePath } of candidates) {
97
+ const targetPath = join(targetRoot, skillName);
98
+
99
+ // Slice 2 (`agents-canonical-store`) — the entry points at the CANONICAL
100
+ // STORE (`~/.peaks/skills/<name>`), not at `<packageRoot>/skills/<name>`,
101
+ // because `<packageRoot>` changes on every `npm i -g peaks-loop@latest` and
102
+ // the links must bind to a PATH peaks-loop owns, not to a VERSION. Repair is
103
+ // the only behaviour: an entry peaks-loop owns (sidecar matches, or the link
104
+ // dangles) is relinked onto the store; only a real directory the user authored
105
+ // is left alone and reported `skipped`. `options.reconcileJunctions` is
106
+ // therefore READ NOWHERE here — still accepted (`sync-service.ts` forwards it)
107
+ // so passing it changes nothing, and removing it is a CLI-surface change this
108
+ // slice does not own.
109
+ validateSkillsRoot();
110
+ const reconciled = reconcileCanonicalEntry({
111
+ kind: 'skills',
112
+ name: skillName,
113
+ sourcePath,
114
+ linkPath: targetPath
115
+ });
116
+ validateSkillsRoot();
117
+
118
+ if (reconciled.linkAction === 'skipped') {
119
+ skipped.push(skillName);
120
+ continue;
121
+ }
122
+ installed.push(skillName);
123
+ }
124
+
125
+ // Slice 3 — PRUNE. The loop above only ever ADDS. A skill removed from the
126
+ // package left its canonical copy, every IDE link and every sidecar on the
127
+ // user's machine forever, because nothing walked the DESTINATIONS. This does,
128
+ // and deletes only what `isManagedEntry` proves is ours.
129
+ const { pruned } = pruneBundledEntries({
130
+ kind: 'skills',
131
+ keepNames: candidates.map((candidate) => candidate.skillName),
132
+ linkDirs: [targetRoot]
133
+ });
134
+
135
+ return { installed, skipped, pruned };
136
+ }
137
+
138
+ /**
139
+ * Slice 7/7 — bundled agents (Claude Code sub-agent prompts).
140
+ *
141
+ * Writes into the IDE's sub-agent loader directory (`~/.claude/agents/` for Claude
142
+ * Code, and whatever `agentsDir` the detected profile declares). The bytes live in
143
+ * the canonical store (`~/.peaks/agents/<name>`) and the IDE entry is a link to it.
144
+ *
145
+ * Ownership policy — ONE predicate, in `canonical-store-link.mjs::isOwnedFileEntry`,
146
+ * and it is deliberately not a string comparison:
147
+ * - entry resolves to the canonical copy → leave it (idempotent run)
148
+ * - entry is a link we wrote (sidecar agrees, dangles) → relink onto the store
149
+ * - entry is a real file with OUR sidecar beside it → re-point it at the store
150
+ * - entry is a real file with a LEGACY sidecar whose
151
+ * recorded package path no longer resolves → same: repair it
152
+ * - entry is a real file we never wrote (no sidecar) → skip (user-authored)
153
+ * - entry is a real file whose sidecar names a path
154
+ * that STILL resolves → skip (a second live install)
155
+ *
156
+ * The two "skip" arms above are the ones the old code got wrong in opposite
157
+ * directions: it skipped whenever the package path merely DIFFERED, which froze
158
+ * every entry on the first upgrade, silently and permanently. See
159
+ * `isOwnedFileEntry` for why a vanished package path is proof of an earlier install.
160
+ */
161
+ export function installBundledAgents(options = {}) {
162
+ const packageRoot = resolvePackageRoot(options);
163
+ const agentsRoot = join(packageRoot, 'agents');
164
+
165
+ // Per-IDE env-var override (claude-code only today): PEAKS_CLAUDE_AGENTS_DIR.
166
+ // Universal escape hatch: PEAKS_SKIP_AGENT_INSTALL=1 (parallel to
167
+ // PEAKS_SKIP_SKILL_INSTALL).
168
+ if (
169
+ process.env.PEAKS_SKIP_SKILL_INSTALL === '1' ||
170
+ process.env.PEAKS_SKIP_AGENT_INSTALL === '1' ||
171
+ !existsSync(agentsRoot)
172
+ ) {
173
+ return createInstallResult();
174
+ }
175
+
176
+ // Slice #011: IDE-aware dispatch — same precedence as the other two families,
177
+ // resolved by the same helper (`resolveBundleTargetRoot`).
178
+ const targetRoot = resolveBundleTargetRoot(options, {
179
+ profileField: 'agentsDir',
180
+ envVar: 'PEAKS_CLAUDE_AGENTS_DIR',
181
+ fallback: '.claude/agents'
182
+ });
183
+
184
+ const installed = [];
185
+ const skipped = [];
186
+ const fallbacks = [];
187
+ mkdirSync(targetRoot, { recursive: true });
188
+ const validateAgentsRoot = createInstallRootValidator(targetRoot, 'Peaks agents');
189
+
190
+ // Slice 4 (`agents-canonical-store`) — the canonical store owns the bytes and the
191
+ // IDE entry points at it. Everything this loop used to decide about ownership is
192
+ // now `reconcileCanonicalFileEntry`'s, and the reason is the defect the brief
193
+ // names as this slice's core: the old test was
194
+ // `resolve(marker.sourcePath) === resolve(sourcePath)`, a STRING comparison against
195
+ // the package path of the installing version — a path with a node version inside it
196
+ // (`…\nvm\v24.21.0\node_modules\peaks-loop\agents\…`). It went false the first time
197
+ // the package moved and stayed false, so the agent silently never updated again.
198
+ const bundledAgentNames = [];
199
+ for (const agentFileName of readdirSync(agentsRoot)) {
200
+ const sourcePath = join(agentsRoot, agentFileName);
201
+ if (!lstatSync(sourcePath).isFile() || !agentFileName.endsWith('.md')) continue;
202
+ bundledAgentNames.push(agentFileName);
203
+
204
+ validateAgentsRoot();
205
+ const reconciled = reconcileCanonicalFileEntry({
206
+ kind: 'agents',
207
+ name: agentFileName,
208
+ sourcePath,
209
+ linkPath: join(targetRoot, agentFileName),
210
+ createFileLink: options.createFileLink
211
+ });
212
+ validateAgentsRoot();
213
+
214
+ if (reconciled.linkAction === 'skipped') {
215
+ skipped.push(agentFileName);
216
+ continue;
217
+ }
218
+ installed.push(agentFileName);
219
+ if (reconciled.fallback !== null) {
220
+ fallbacks.push({ name: agentFileName, ...reconciled.fallback });
221
+ }
222
+ }
223
+
224
+ // The delete half: an agent the package stops shipping used to leave its canonical
225
+ // copy, its IDE entry and both sidecars on the machine forever.
226
+ const { pruned } = pruneBundledEntries({
227
+ kind: 'agents',
228
+ keepNames: bundledAgentNames,
229
+ linkDirs: [targetRoot]
230
+ });
231
+
232
+ return { installed, skipped, pruned, fallbacks };
233
+ }