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
@@ -1,547 +1,19 @@
1
- import { runDoctor } from '../../../services/doctor/index.js';
2
- import { listSkills } from '../../../services/skills/skill-registry.js';
3
- import { runSkillSync, SYNC_PLATFORMS } from '../../../services/skills/sync-service.js';
4
- import { inspectSkillRunbook } from '../../../services/skills/skill-runbook-service.js';
5
- import { setSkillPresence, clearSkillPresence, getSkillPresence, isSkillPresenceMode, touchSkillHeartbeat, checkStalePresence } from '../../../services/skills/skill-presence-service.js';
6
- import { detectPresenceMarker } from '../../../services/hooks/presence-marker-detector.js';
7
- import { findProjectRoot } from '../../../services/config/config-safety.js';
8
- import { generateProjectContext } from '../../../services/memory/project-context-service.js';
9
- import { getSessionId, setSessionMeta } from '../../../services/session/session-manager.js';
10
- import { resolveCallerProjection } from '../../../services/session/resolve-caller-id.js';
11
- import { readContextPercent } from '../../../services/context/auto-compact-reader.js';
12
- import { resolveOuterSessionId } from '../../../services/session/binding-status-service.js';
13
- import { evaluateCompactTrigger } from '../../../services/code/auto-compact-orchestrator.js';
14
- import { resolveAutoCompactProfile } from '../../../services/mode/mode-status-service.js';
15
- import { gcStalePresenceLeases } from '../../../services/skills/presence-lease-service.js';
16
- import { fail, ok } from 'peaks-loop-shared/result';
17
- import { stableRealPath } from '../../../shared/path-utils.js';
18
- import { detectStaleGeneratedArtifacts } from '../../../services/workspace/generated-artifacts-stamp.js';
19
- /**
20
- * Canonicalize a user-supplied `--project <path>` value.
21
- *
22
- * Git Bash on Windows hands us forward-slash paths
23
- * (`C:/Users/.../peaks-loop`) while `peaks workspace init` writes the
24
- * backslash form, and either side may carry a trailing separator or
25
- * differing case. Resolving to the real path here means every
26
- * downstream consumer (`getSessionId`, `setSessionMeta`,
27
- * `setSkillPresence`) sees one stable form.
28
- *
29
- * Returns the input unchanged when it cannot be resolved (path does
30
- * not exist yet, or is not readable) so a bad `--project` still
31
- * reaches the existing error handling rather than throwing here.
32
- */
33
- /**
34
- * The loop-hygiene verdict attached to every ACTIVE `skill.presence` read.
35
- *
36
- * Why it lives here instead of only in a SKILL.md body: the zero-pause
37
- * contract ("auto-compact is the skill's own action, never the user's")
38
- * was already written into `peaks-code/SKILL.md`, including an explicit
39
- * ban on the exact phrasing "prompt the user to run `/compact`" — and it
40
- * was still violated. A SKILL.md body is loaded once and is then
41
- * compacted away, so the rule stops governing at precisely the moment
42
- * context pressure makes it matter. `skill.presence` is the one call
43
- * EVERY skill makes on EVERY turn, in EVERY consumer project and EVERY
44
- * mode, so a value carried here cannot be forgotten, and no skill has to
45
- * be edited for the obligation to reach the model.
46
- *
47
- * READ-ONLY by contract. It probes and reports; it never compacts.
48
- * Executing `peaks code auto-compact` stays the skill's own action — a
49
- * side effect here would fire on every single turn.
50
- *
51
- * Failure swallows to `null`: the hygiene verdict is advisory and must
52
- * never be able to break the presence read itself.
53
- */
54
- export function buildContextVerdict(ratio, mode) {
55
- const trigger = evaluateCompactTrigger(ratio, mode);
56
- const ratioPct = `${(ratio * 100).toFixed(1)}%`;
57
- // `auto-fire` belongs in the in-zone set: it is the tier where
58
- // peaks-loop preempts rather than asking the LLM to decide.
59
- const inZone = trigger.kind === 'auto-fire' || trigger.kind === 'pre-compact' || trigger.kind === 'red-line';
60
- return {
61
- context: { ratioPct, action: trigger.kind, mode },
62
- nextActions: inZone
63
- ? [
64
- `Context at ${ratioPct} is in the '${trigger.kind}' zone (mode=${mode}).`,
65
- 'Run `peaks code auto-compact --project .` YOURSELF now, then continue. Do NOT ask the user to run /compact — that is the regression the zero-pause contract forbids.'
66
- ]
67
- : []
68
- };
69
- }
70
- function contextVerdict(projectRoot) {
71
- try {
72
- const sessionId = getSessionId(projectRoot);
73
- if (sessionId === null)
74
- return { context: null, nextActions: [] };
75
- const mode = resolveAutoCompactProfile(projectRoot);
76
- // `outerSessionId` is NOT optional in practice: without it the
77
- // adapter cannot find the harness transcript and falls back to
78
- // conservative-zero, reporting a serene "0.0%" that never crosses
79
- // any threshold. Same resolution order the orchestrator uses.
80
- const outerSessionId = resolveOuterSessionId(projectRoot, sessionId, process.env);
81
- const probe = readContextPercent({ projectRoot, sessionId, outerSessionId, env: process.env });
82
- return buildContextVerdict(probe.ratio, mode);
83
- }
84
- catch {
85
- return { context: null, nextActions: [] };
86
- }
87
- }
88
- function canonicalizeProjectOption(project) {
89
- if (project === undefined)
90
- return undefined;
91
- try {
92
- return stableRealPath(project);
93
- }
94
- catch {
95
- return project;
96
- }
97
- }
98
- /**
99
- * D1 (2026-09-15) — generated-config staleness, carried on `skill presence`.
100
- *
101
- * WHY THIS CALL. `npm i -g peaks-loop@<newer>` upgrades the CLI and leaves the
102
- * project's generated config exactly as the OLD release wrote it:
103
- * `initWorkspace` is the only writer and `ensureSession` early-returns once a
104
- * session is bound, so nothing re-runs the generator. `.claude/settings.local.json`
105
- * is drift-checked — but only on an init that never comes. The user found the
106
- * previous instance of this by deleting their `.claude/*.json` and restarting;
107
- * nothing in the product told them to.
108
- *
109
- * `peaks skill presence` is the ONE peaks call every skill makes in every turn
110
- * (CLAUDE.md mandates it at the start of every response), so it is the only
111
- * channel guaranteed to carry a drift notice to the LLM that can act on it —
112
- * the same reasoning as the loop-hygiene verdict attached one screen down. A
113
- * rule that lives only in a SKILL.md body is compacted away; a warning that
114
- * rides the per-turn tool output is not.
115
- *
116
- * The field is additive and present only when stale, so no existing consumer
117
- * of the envelope changes shape. `null` projectRoot means the caller had no
118
- * project to inspect — skipped, not assumed stale.
119
- */
120
- function generatedConfigNotice(projectRoot) {
121
- if (projectRoot === undefined)
122
- return { field: {}, warnings: [] };
123
- const staleness = detectStaleGeneratedArtifacts(projectRoot);
124
- if (!staleness.stale)
125
- return { field: {}, warnings: [] };
126
- const onDiskVersion = staleness.onDisk?.packageVersion ?? '(unstamped)';
127
- return {
128
- field: {
129
- generatedArtifacts: {
130
- stale: true,
131
- reasons: staleness.reasons,
132
- onDiskPackageVersion: staleness.onDisk?.packageVersion ?? null,
133
- installedPackageVersion: staleness.expected.packageVersion
134
- }
135
- },
136
- warnings: [
137
- `Generated config at '${projectRoot}' was produced by peaks-loop ${onDiskVersion} ` +
138
- `but ${staleness.expected.packageVersion} is installed (${staleness.reasons.join(', ')}). ` +
139
- `Re-run \`peaks workspace init\` ` +
140
- `to regenerate .claude/settings.local.json and the offline template copy.`
141
- ]
142
- };
143
- }
144
- import { addJsonOption, getErrorMessage, printResult } from '../../cli-helpers.js';
145
- // `peaks skill search` is the CLI primitive that feeds the
146
- // peaks-solo dispatcher (S1). Adding a single import + register call
147
- // here keeps the change surgical and leaves all existing skill
148
- // subcommands (list / doctor / sync / runbook / presence / heartbeat)
149
- // untouched. See
150
- // and the S0 plan under docs/superpowers/plans/.
151
1
  import { registerSkillSearchCommand } from '../skill-search-commands.js';
2
+ import { registerSkillRegistryCommands } from './skill-registry-commands.js';
3
+ import { registerSkillRunbookCommand } from './skill-runbook-command.js';
4
+ import { registerSkillPresenceCommands } from './skill-presence-commands.js';
5
+ import { registerSkillPresenceHousekeepingCommands } from './skill-presence-housekeeping-commands.js';
6
+ import { registerSkillHeartbeatCommands } from './skill-heartbeat-commands.js';
7
+ // The loop-hygiene verdict stays importable from this module's original path:
8
+ // `tests/unit/cli/skill-presence-loop-hygiene.test.ts` imports it from here.
9
+ export { buildContextVerdict } from './skill-context-verdict.js';
152
10
  export function registerSkillCommand(program, io) {
153
11
  const skill = program.command('skill').description('Manage Peaks skills');
154
- addJsonOption(skill
155
- .command('list')
156
- .description('List skills derived from skills/*/SKILL.md')
157
- .option('--include-internal', 'include skills with visibility: internal (default: hide them)')).action(async (options) => {
158
- let skills = await listSkills();
159
- if (options.includeInternal !== true) {
160
- skills = skills.filter((s) => s.visibility !== 'internal');
161
- }
162
- if (options.json === true) {
163
- printResult(io, ok('skill.list', { skills }), true);
164
- }
165
- else {
166
- const sorted = [...skills].sort((a, b) => {
167
- // door) — list it FIRST so users discover the dispatcher before
168
- // any specific leaf. Followed by peaks-sop (current default
169
- // runbook showcase) and peaks-code (canonical code-domain
170
- // orchestrator); everything else falls back to alphabetical.
171
- if (a.name === 'peaks-solo')
172
- return -1;
173
- if (b.name === 'peaks-solo')
174
- return 1;
175
- if (a.name === 'peaks-sop')
176
- return -1;
177
- if (b.name === 'peaks-sop')
178
- return 1;
179
- if (a.name === 'peaks-code')
180
- return -1;
181
- if (b.name === 'peaks-code')
182
- return 1;
183
- return a.name.localeCompare(b.name);
184
- });
185
- for (const skill of sorted) {
186
- io.stdout(` ${skill.name.padEnd(14)}${skill.description}`);
187
- }
188
- io.stdout(`\n Invoke any skill by typing its name in conversation (e.g. \`peaks-sop\`).`);
189
- }
190
- });
191
- addJsonOption(skill.command('doctor').description('Run skill-related doctor checks')).action(async (options) => {
192
- const report = await runDoctor();
193
- const skillChecks = report.checks.filter((check) => check.id.startsWith('skill'));
194
- const failed = skillChecks.filter((check) => !check.ok).length;
195
- if (options.json === true) {
196
- printResult(io, ok('skill.doctor', { checks: skillChecks, ok: failed === 0 }), true);
197
- }
198
- else {
199
- for (const check of skillChecks) {
200
- const icon = check.ok ? '+' : '×';
201
- io.stdout(` ${icon} ${check.message}`);
202
- }
203
- io.stdout(`\n ${skillChecks.length - failed} passed, ${failed} failed`);
204
- if (failed > 0) {
205
- io.stderr('\nOne or more skill checks failed.');
206
- }
207
- }
208
- if (failed > 0) {
209
- process.exitCode = 1;
210
- }
211
- });
212
- // Slice #12 final piece (per spec §9 line 1105):
213
- // `peaks skills sync 8 平台分发`. Idempotent: re-running is a
214
- // no-op when the symlinks are already correct.
215
- addJsonOption(skill
216
- .command('sync')
217
- .description(`Sync the peaks-* skill family to one or all of the 8 supported LLM-CLI platforms (${SYNC_PLATFORMS.join(', ')}). Idempotent.`)
218
- .option('--platform <id>', `sync only one platform (default: --all). Valid: ${SYNC_PLATFORMS.join(', ')}`)
219
- .option('--all', 'sync all 8 platforms (default if --platform is omitted)')
220
- .option('--dry-run', 'do not write; emit the same shape with applied=false')
221
- .option('--reconcile-junctions', 'repair Peaks-managed skill Junctions whose targets were deleted with a host worktree')
222
- .option('--project <path>', 'project root (default: cwd)')).action(async (options) => {
223
- try {
224
- const projectRoot = options.project ?? process.cwd();
225
- const platforms = options.platform !== undefined ? [options.platform] : undefined;
226
- const result = await runSkillSync({
227
- projectRoot,
228
- ...(platforms !== undefined ? { platforms } : {}),
229
- ...(options.dryRun === true ? { dryRun: true } : {}),
230
- ...(options.reconcileJunctions === true ? { reconcileJunctions: true } : {})
231
- });
232
- const envelope = ok('skill.sync', result, [], [
233
- `syncedCount: ${result.syncedCount}/${result.perPlatform.length} platforms`,
234
- `totalInstalled: ${result.totalInstalled} skill symlinks`,
235
- result.failedCount > 0
236
- ? `failedCount: ${result.failedCount} (run \`peaks skill status\` for details)`
237
- : 'no failures'
238
- ]);
239
- printResult(io, envelope, options.json);
240
- if (result.failedCount > 0) {
241
- process.exitCode = 1;
242
- }
243
- }
244
- catch (error) {
245
- const message = getErrorMessage(error);
246
- printResult(io, fail('skill.sync', 'SKILL_SYNC_FAILED', message, { applied: false }, [message]), options.json);
247
- process.exitCode = 1;
248
- }
249
- });
250
- addJsonOption(skill
251
- .command('runbook <name>')
252
- .description('Inspect a skill Default runbook section and its --apply authorization-note status')).action(async (name, options) => {
253
- try {
254
- const inspection = await inspectSkillRunbook(name);
255
- const result = inspection.ok
256
- ? ok('skill.runbook', inspection)
257
- : fail('skill.runbook', inspection.hasRunbook ? 'SKILL_RUNBOOK_APPLY_UNGATED' : 'SKILL_RUNBOOK_MISSING', inspection.hasRunbook
258
- ? `Skill ${inspection.name} has ${inspection.destructiveApplyLines.length} destructive --apply command(s) without an authorization/dry-run note`
259
- : `Skill ${inspection.name} is missing a ## Default runbook section`, inspection, inspection.hasRunbook
260
- ? [
261
- 'Add an authorization or --dry-run note next to destructive --apply lines in the runbook section'
262
- ]
263
- : ['Add a `## Default runbook` section to the skill SKILL.md']);
264
- printResult(io, result, options.json);
265
- if (!inspection.ok) {
266
- process.exitCode = 1;
267
- }
268
- }
269
- catch (error) {
270
- printResult(io, fail('skill.runbook', 'SKILL_NOT_FOUND', getErrorMessage(error), { name }), options.json);
271
- process.exitCode = 1;
272
- }
273
- });
274
- addJsonOption(skill
275
- .command('presence')
276
- .description('Show the currently active Peaks skill (alias: presence:get)')
277
- .option('--check-stale', 'slice 002 (v2.15.0): also report whether the recorded outer session id still matches the current one. Default false (back-compat).')
278
- .option('--project <path>', 'project root (default: cwd)')).action((options) => {
279
- const projectOption = canonicalizeProjectOption(options.project);
280
- const generatedConfig = generatedConfigNotice(projectOption ?? findProjectRoot(process.cwd()) ?? process.cwd());
281
- const presence = getSkillPresence(projectOption);
282
- if (presence === null) {
283
- printResult(io, ok('skill.presence', { active: false, ...generatedConfig.field }, generatedConfig.warnings), options.json);
284
- return;
285
- }
286
- // Loop-hygiene verdict: attached to every ACTIVE read, so the
287
- // zero-pause contract travels with the one call every skill already
288
- // makes — in every mode and every consumer project.
289
- const verdict = contextVerdict(projectOption ?? process.cwd());
290
- if (options.checkStale === true) {
291
- // check so callers (peaks-code Step 1, statusline) get both
292
- // pieces of info from a single CLI invocation. The presence
293
- // is returned UNCHANGED — `--check-stale` is a read-only flag,
294
- // not a clear.
295
- const staleness = checkStalePresence({ projectRootOverride: projectOption });
296
- printResult(io, ok('skill.presence', {
297
- active: true,
298
- ...presence,
299
- stale: staleness.stale,
300
- staleReason: staleness.reason,
301
- currentOuterSessionId: staleness.currentOuterSessionId,
302
- recordedOuterSessionId: staleness.recordedOuterSessionId,
303
- ...(verdict.context !== null ? { context: verdict.context } : {}),
304
- ...generatedConfig.field
305
- }, generatedConfig.warnings, verdict.nextActions), options.json);
306
- return;
307
- }
308
- printResult(io, ok('skill.presence', {
309
- active: true,
310
- ...presence,
311
- ...(verdict.context !== null ? { context: verdict.context } : {}),
312
- ...generatedConfig.field
313
- }, generatedConfig.warnings, verdict.nextActions), options.json);
314
- });
315
- addJsonOption(skill
316
- .command('presence:set <name>')
317
- .description('Set the currently active Peaks skill for session-wide visibility. Slice 4.0.8: requires a bound session and an adapter-resolved caller id (fail-closed); raw unlink is rejected.')
318
- .option('--mode <mode>', 'execution mode')
319
- .option('--gate <gate>', 'current gate')
320
- .option('--project <path>', 'project root path (auto-detected from cwd when omitted)')).action((name, options) => {
321
- const projectOption = canonicalizeProjectOption(options.project);
322
- const projectRoot = projectOption ?? findProjectRoot(process.cwd()) ?? process.cwd();
323
- if (options.mode !== undefined && !isSkillPresenceMode(options.mode)) {
324
- printResult(io, fail('skill.presence:set', 'INVALID_MODE', `Invalid mode: ${options.mode} (expected one of: full-auto, assisted, strict, 24h)`, { name, mode: options.mode }, ['Use a valid mode: full-auto, assisted, strict, or 24h']), options.json);
325
- process.exitCode = 1;
326
- return;
327
- }
328
- // Slice 4.0.8 (D1 + D2): `presence:set` is fail-closed. We refuse
329
- // any write when (a) no peaks session is bound, or (b) the active
330
- // IDE adapter cannot resolve a valid callerId. Both failures
331
- // surface BEFORE any filesystem write. The legacy
332
- // `setSkillPresence` wrapper is kept as a compat shim for tests
333
- // and CLI flows that intentionally do not need a session — but
334
- // production CLI traffic must go through this gate.
335
- const boundSessionId = getSessionId(projectRoot);
336
- if (boundSessionId === null) {
337
- printResult(io, fail('skill.presence:set', 'PEAKS_SESSION_NOT_BOUND', 'No canonical peaks session is bound for this project (RD §3 D1).', { projectRoot, name }, [
338
- 'Run `peaks workspace init --project <p>` first, then re-run `peaks skill presence:set`.'
339
- ]), options.json);
340
- process.exitCode = 1;
341
- return;
342
- }
343
- try {
344
- resolveCallerProjection({ projectRoot });
345
- }
346
- catch (err) {
347
- const message = err instanceof Error ? err.message : String(err);
348
- printResult(io, fail('skill.presence:set', 'PEAKS_CALLER_NOT_RESOLVED', `Active IDE adapter could not resolve a callerId (RD §3 D1): ${message}`, { projectRoot, name }, [
349
- 'Ensure the active IDE is detected by `peaks` and the IDE session variable is set.',
350
- 'Or set PEAKS_CALLER_ID=<id> in the environment for scripted usage.'
351
- ]), options.json);
352
- process.exitCode = 1;
353
- return;
354
- }
355
- const presence = setSkillPresence(name, options.mode, options.gate, projectOption);
356
- // Session metadata is updated when a session is bound (read-only
357
- // path: `getSessionId`). We do not auto-spawn a session.
358
- if (boundSessionId !== null) {
359
- setSessionMeta(projectRoot, boundSessionId, {
360
- skill: name,
361
- ...(options.mode ? { mode: options.mode } : {}),
362
- ...(options.gate ? { gate: options.gate } : {})
363
- });
364
- }
365
- printResult(io, ok('skill.presence:set', { active: true, ...presence }), options.json);
366
- });
367
- addJsonOption(skill
368
- .command('presence:clear')
369
- .description('Unlink the DEPRECATED pre-4.0.11 single-slot presence marker files (`.peaks/_runtime/active-skill.json`, `.peaks/.active-skill.json`). It does NOT terminalize a live presence lease — a workflow-bound lease terminalizes through `peaks workflow terminalize --workflow <id> --reason <reason>`, an ad-hoc lease only at session exit, and raw unlink is FORBIDDEN for both. The envelope reports what is still active.')
370
- .option('--project <path>', 'project root path (auto-detected from cwd when omitted)')).action(async (options) => {
371
- const projectRoot = options.project ?? findProjectRoot(process.cwd()) ?? process.cwd();
372
- // What this command actually is: a stale-marker cleanup for projects
373
- // carrying a pre-4.0.11 single-slot file, and nothing more. The previous
374
- // description claimed it "routes workflow leases through `workflow
375
- // terminalize`" and that a "compat wrapper re-routes [it] to
376
- // `terminalizePresenceLease` when a workflow binding is present" — there is
377
- // no such wrapper, and no such routing happens here or in
378
- // `clearSkillPresence`. A caller who read that line and ran this command
379
- // expecting a live workflow lease to terminalize got a successful exit code
380
- // and a still-running lease.
381
- const removed = clearSkillPresence(options.project);
382
- // Auto-update project context so future sessions have up-to-date history.
383
- // bootstraps `.peaks/project-scan/` (idempotent). Await the async
384
- // signature; failure is still non-fatal so we don't block the clear.
385
- try {
386
- await generateProjectContext(projectRoot);
387
- }
388
- catch {
389
- // TODO(g2): legacy silent catch — grace: 1 minor release (v2.14.0)
390
- // non-fatal: context update failure should not block presence clear
391
- }
392
- // Report the state the project is actually in, not the state this command
393
- // intended to reach. `clearSkillPresence` unlinks only the DEPRECATED
394
- // single-slot marker files; the canonical sid-scoped lease is deliberately
395
- // left alive for an AD-HOC lease (raw unlink is FORBIDDEN — only session
396
- // exit may terminalize one; workflow leases route through `workflow
397
- // terminalize`). So after a `presence:set` the very next `peaks skill
398
- // presence` call still reads `active: true`. A hardcoded `active: false`
399
- // therefore reported a state the command never reached, and the caller had
400
- // no way to tell that from a real clear.
401
- //
402
- // Re-reading through `getSkillPresence` — the same projection the
403
- // `skill presence` command serves — is the only derivation that cannot
404
- // drift from what the next command prints: it is the same read path, on
405
- // the same project root, evaluated after every write this command makes.
406
- const active = getSkillPresence(projectRoot) !== null;
407
- // `cleared` is the live question the caller is actually asking ("is it gone
408
- // now?"), which `removed` could never answer: a legacy marker being unlinked
409
- // says nothing about the lease the next command will read. When it is false
410
- // the envelope says WHY, in the two routes that can terminalize a lease —
411
- // without this the caller saw `active: true, removed: false` and had no way
412
- // to tell "you ran the wrong command" from "this command has no opinion".
413
- const cleared = !active;
414
- printResult(io, ok('skill.presence:clear', {
415
- active,
416
- removed,
417
- cleared,
418
- ...(cleared ? {} : { reason: 'live-lease-survives-presence-clear' }),
419
- projectContextUpdated: true
420
- }, [], cleared
421
- ? []
422
- : [
423
- 'A workflow-bound lease terminalizes through `peaks workflow terminalize --workflow <id> --reason <reason>`.',
424
- 'An ad-hoc lease (`peaks skill presence:set`) is terminalizable only at session exit; `presence:clear` will not remove it, and raw unlink is FORBIDDEN.',
425
- 'Run `peaks skill presence --json` to read the lease that survived.'
426
- ]), options.json);
427
- });
428
- // Slice 4.0.8 (RD §4): manual lease GC primitive. LLM-coordinated;
429
- // never a user-typed requirement. The user / LLM runner invokes
430
- // `peaks skill lease gc --project <p>` to drain leases that meet
431
- // both stale predicates (24h start AND 1h heartbeat) for the
432
- // canonical project. Returns a typed envelope so the runner can
433
- // branch on the aggregate counters.
434
- // Note: `.command('lease gc')` implicitly creates the `lease`
435
- // parent under `skill`; we MUST NOT also call `.command('lease')`
436
- // separately, or Commander.js throws "cannot add command 'lease' as
437
- // already have command 'lease'" at startup.
438
- addJsonOption(skill
439
- .command('lease gc')
440
- .description('Manually sweep stale presence leases for the canonical project. Both predicates required: now - lastHeartbeat > 1h AND now - startedAt > 24h. Drained leases are classified; corrupt graphs surface as PEAKS_GRAPH_REF_BROKEN warnings and are excluded.')
441
- .option('--project <path>', 'project root (default: cwd)')
442
- .option('--now <iso>', 'override the current time (test seam)')).action(async (options) => {
443
- const projectRoot = options.project ?? findProjectRoot(process.cwd()) ?? process.cwd();
444
- try {
445
- const result = await gcStalePresenceLeases({
446
- projectRoot,
447
- ...(options.now !== undefined ? { now: options.now } : {}),
448
- trigger: 'manual'
449
- });
450
- printResult(io, ok('skill.lease.gc', {
451
- envelopeVersion: '4.0.8',
452
- removed: result.removed,
453
- retained: result.retained,
454
- trigger: result.trigger,
455
- inFlightBatch: result.inFlightBatch,
456
- warnings: result.warnings,
457
- errors: result.errors
458
- }, result.warnings.map((w) => `${w.code}: ${w.message}`), [
459
- 'GC predicate: now - lastHeartbeat > 1h AND now - startedAt > 24h (RD §3 D2 + D3).',
460
- 'Re-run `peaks workspace init` or `peaks skill presence:set` to sweep the same project on the bound trigger.'
461
- ]), options.json);
462
- }
463
- catch (err) {
464
- printResult(io, fail('skill.lease.gc', 'PEAKS_LEASE_GC_FAILED', getErrorMessage(err), { projectRoot }, [
465
- 'Re-run with a valid --project and ensure the session is bound.'
466
- ]), options.json);
467
- process.exitCode = 1;
468
- }
469
- });
470
- // peaks-code Step 1 (and `peaks code should-pause --step
471
- // step-1-mode-select`) calls this to decide whether the recorded
472
- // `mode` field can be trusted or whether the LLM must AskUserQuestion.
473
- addJsonOption(skill
474
- .command('presence:check-stale')
475
- .description('Slice 002 (v2.15.0) AC-1: report whether the recorded presence outer session id still matches the current outer session id. ' +
476
- 'Returns { stale: boolean, reason: "outer-session-mismatch" | "no-presence" | null }. ' +
477
- 'Pure read-only — does NOT clear the presence (use `peaks skill presence:clear` for that).')
478
- .option('--project <path>', 'project root (default: cwd)')
479
- .option('--current-outer <id>', 'override the current outer session id (test seam; default: read from PEAKS_OUTER_SESSION_ID / CLAUDE_CODE_SESSION_ID)')).action((options) => {
480
- // when the user omits the flag. The service-layer branch
481
- // `'currentOuter' in opts` returns true for an explicit
482
- // `undefined` (the key exists on the spread object literal),
483
- // which would skip the env-var fallback and pin `current =
484
- // undefined` — always reading the presence as stale. Build a
485
- // sparse opts object so the service can fall back to
486
- // `getCurrentOuterSessionId()` (reads PEAKS_OUTER_SESSION_ID /
487
- // CLAUDE_CODE_SESSION_ID).
488
- const checkOpts = options.project !== undefined ? { projectRootOverride: options.project } : {};
489
- if (options.currentOuter !== undefined) {
490
- checkOpts.currentOuter = options.currentOuter;
491
- }
492
- const result = checkStalePresence(checkOpts);
493
- // Always emit `currentOuterSessionId` in the JSON envelope (even
494
- // tooling (statusline, sub-agent dispatch) reads the field by
495
- // name, never by `data.currentOuterSessionId ?? ''`. JSON.stringify
496
- // drops `undefined` properties, so we coerce to '' before
497
- // wrapping in the envelope.
498
- const data = {
499
- stale: result.stale,
500
- reason: result.reason,
501
- presence: result.presence,
502
- currentOuterSessionId: result.currentOuterSessionId ?? '',
503
- recordedOuterSessionId: result.recordedOuterSessionId ?? ''
504
- };
505
- printResult(io, ok('skill.presence:check-stale', data), options.json);
506
- });
507
- addJsonOption(skill.command('heartbeat').description('Show the heartbeat status of the active Peaks skill')).action((options) => {
508
- const presence = getSkillPresence();
509
- if (presence === null) {
510
- printResult(io, ok('skill.heartbeat', { active: false, heartbeat: 'none' }), options.json);
511
- return;
512
- }
513
- printResult(io, ok('skill.heartbeat', {
514
- active: true,
515
- skill: presence.skill,
516
- gate: presence.gate ?? null,
517
- lastHeartbeat: presence.lastHeartbeat ?? presence.setAt,
518
- setAt: presence.setAt
519
- }), options.json);
520
- });
521
- addJsonOption(skill
522
- .command('heartbeat:touch')
523
- .description('Update the heartbeat timestamp (called by the LLM each turn to confirm peaks skill context is alive)')).action((options) => {
524
- const updated = touchSkillHeartbeat();
525
- if (updated === null) {
526
- printResult(io, ok('skill.heartbeat:touch', { active: false, heartbeat: 'none' }), options.json);
527
- return;
528
- }
529
- printResult(io, ok('skill.heartbeat:touch', {
530
- active: true,
531
- skill: updated.skill,
532
- lastHeartbeat: updated.lastHeartbeat
533
- }), options.json);
534
- });
535
- addJsonOption(skill
536
- .command('detect-marker-loss')
537
- .description('Detect whether the latest assistant message lost the Peaks-Loop status header while a peaks skill is still active (slice 028 detection primitive).')
538
- .option('--project <path>', 'project root path (auto-detected from cwd when omitted)')
539
- .option('--message <text>', 'latest assistant message text to scan (defaults to reading the most recent LLM response from the stdin pipe, or empty string when no pipe is attached)')).action((options) => {
540
- const projectRoot = options.project ?? findProjectRoot(process.cwd()) ?? process.cwd();
541
- const message = options.message ?? '';
542
- const result = detectPresenceMarker({ project: projectRoot, latestAssistantMessage: message });
543
- printResult(io, ok('skill.detect-marker-loss', result), options.json);
544
- });
12
+ registerSkillRegistryCommands(skill, io);
13
+ registerSkillRunbookCommand(skill, io);
14
+ registerSkillPresenceCommands(skill, io);
15
+ registerSkillPresenceHousekeepingCommands(skill, io);
16
+ registerSkillHeartbeatCommands(skill, io);
545
17
  // list / runbook / presence; preserves the existing surface
546
18
  // (HC-10 — 老入口保留).
547
19
  registerSkillSearchCommand(program, io);
@@ -0,0 +1,38 @@
1
+ import type { AutoCompactMode } from '../../../services/code/auto-compact-modes.js';
2
+ /**
3
+ * The loop-hygiene verdict attached to every ACTIVE `skill.presence` read.
4
+ *
5
+ * Why it lives here instead of only in a SKILL.md body: the zero-pause
6
+ * contract ("auto-compact is the skill's own action, never the user's")
7
+ * was already written into `peaks-code/SKILL.md`, including an explicit
8
+ * ban on the exact phrasing "prompt the user to run `/compact`" — and it
9
+ * was still violated. A SKILL.md body is loaded once and is then
10
+ * compacted away, so the rule stops governing at precisely the moment
11
+ * context pressure makes it matter. `skill.presence` is the one call
12
+ * EVERY skill makes on EVERY turn, in EVERY consumer project and EVERY
13
+ * mode, so a value carried here cannot be forgotten, and no skill has to
14
+ * be edited for the obligation to reach the model.
15
+ *
16
+ * READ-ONLY by contract. It probes and reports; it never compacts.
17
+ * Executing `peaks code auto-compact` stays the skill's own action — a
18
+ * side effect here would fire on every single turn.
19
+ *
20
+ * Failure swallows to `null`: the hygiene verdict is advisory and must
21
+ * never be able to break the presence read itself.
22
+ */
23
+ export declare function buildContextVerdict(ratio: number, mode: AutoCompactMode): {
24
+ context: {
25
+ ratioPct: string;
26
+ action: string;
27
+ mode: string;
28
+ };
29
+ nextActions: string[];
30
+ };
31
+ export declare function contextVerdict(projectRoot: string): {
32
+ context: {
33
+ ratioPct: string;
34
+ action: string;
35
+ mode: string;
36
+ } | null;
37
+ nextActions: string[];
38
+ };
@@ -0,0 +1,63 @@
1
+ // Split out of `core/skill-command.ts`. The
2
+ // loop-hygiene verdict is a pure projection: it probes context pressure and
3
+ // renders the advisory `nextActions`; it never compacts.
4
+ import { readContextPercent } from '../../../services/context/auto-compact-reader.js';
5
+ import { evaluateCompactTrigger } from '../../../services/code/auto-compact-orchestrator.js';
6
+ import { resolveAutoCompactProfile } from '../../../services/mode/mode-status-service.js';
7
+ import { getSessionId } from '../../../services/session/session-manager.js';
8
+ import { resolveOuterSessionId } from '../../../services/session/binding-status-service.js';
9
+ /**
10
+ * The loop-hygiene verdict attached to every ACTIVE `skill.presence` read.
11
+ *
12
+ * Why it lives here instead of only in a SKILL.md body: the zero-pause
13
+ * contract ("auto-compact is the skill's own action, never the user's")
14
+ * was already written into `peaks-code/SKILL.md`, including an explicit
15
+ * ban on the exact phrasing "prompt the user to run `/compact`" — and it
16
+ * was still violated. A SKILL.md body is loaded once and is then
17
+ * compacted away, so the rule stops governing at precisely the moment
18
+ * context pressure makes it matter. `skill.presence` is the one call
19
+ * EVERY skill makes on EVERY turn, in EVERY consumer project and EVERY
20
+ * mode, so a value carried here cannot be forgotten, and no skill has to
21
+ * be edited for the obligation to reach the model.
22
+ *
23
+ * READ-ONLY by contract. It probes and reports; it never compacts.
24
+ * Executing `peaks code auto-compact` stays the skill's own action — a
25
+ * side effect here would fire on every single turn.
26
+ *
27
+ * Failure swallows to `null`: the hygiene verdict is advisory and must
28
+ * never be able to break the presence read itself.
29
+ */
30
+ export function buildContextVerdict(ratio, mode) {
31
+ const trigger = evaluateCompactTrigger(ratio, mode);
32
+ const ratioPct = `${(ratio * 100).toFixed(1)}%`;
33
+ // `auto-fire` belongs in the in-zone set: it is the tier where
34
+ // peaks-loop preempts rather than asking the LLM to decide.
35
+ const inZone = trigger.kind === 'auto-fire' || trigger.kind === 'pre-compact' || trigger.kind === 'red-line';
36
+ return {
37
+ context: { ratioPct, action: trigger.kind, mode },
38
+ nextActions: inZone
39
+ ? [
40
+ `Context at ${ratioPct} is in the '${trigger.kind}' zone (mode=${mode}).`,
41
+ 'Run `peaks code auto-compact --project .` YOURSELF now, then continue. Do NOT ask the user to run /compact — that is the regression the zero-pause contract forbids.'
42
+ ]
43
+ : []
44
+ };
45
+ }
46
+ export function contextVerdict(projectRoot) {
47
+ try {
48
+ const sessionId = getSessionId(projectRoot);
49
+ if (sessionId === null)
50
+ return { context: null, nextActions: [] };
51
+ const mode = resolveAutoCompactProfile(projectRoot);
52
+ // `outerSessionId` is NOT optional in practice: without it the
53
+ // adapter cannot find the harness transcript and falls back to
54
+ // conservative-zero, reporting a serene "0.0%" that never crosses
55
+ // any threshold. Same resolution order the orchestrator uses.
56
+ const outerSessionId = resolveOuterSessionId(projectRoot, sessionId, process.env);
57
+ const probe = readContextPercent({ projectRoot, sessionId, outerSessionId, env: process.env });
58
+ return buildContextVerdict(probe.ratio, mode);
59
+ }
60
+ catch {
61
+ return { context: null, nextActions: [] };
62
+ }
63
+ }