session-orchestrator 5.2.0 → 5.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (427) hide show
  1. package/.agents/skills/architecture/SKILL.md +3 -1
  2. package/.agents/skills/autopilot/SKILL.md +5 -1
  3. package/.agents/skills/autopilot/agents/openai.yaml +5 -0
  4. package/.agents/skills/bootstrap/SKILL.md +5 -1
  5. package/.agents/skills/bootstrap/agents/openai.yaml +5 -0
  6. package/.agents/skills/brainstorm/SKILL.md +5 -1
  7. package/.agents/skills/brainstorm/agents/openai.yaml +5 -0
  8. package/.agents/skills/claude-md-drift-check/SKILL.md +4 -2
  9. package/.agents/skills/close/SKILL.md +5 -1
  10. package/.agents/skills/close/agents/openai.yaml +5 -0
  11. package/.agents/skills/convergence-monitoring/SKILL.md +4 -2
  12. package/.agents/skills/debug/SKILL.md +5 -1
  13. package/.agents/skills/debug/agents/openai.yaml +5 -0
  14. package/.agents/skills/discovery/SKILL.md +5 -1
  15. package/.agents/skills/discovery/agents/openai.yaml +5 -0
  16. package/.agents/skills/dispatcher/SKILL.md +5 -1
  17. package/.agents/skills/dispatcher/agents/openai.yaml +5 -0
  18. package/.agents/skills/docs-orchestrator/SKILL.md +3 -1
  19. package/.agents/skills/ecosystem-health/SKILL.md +3 -1
  20. package/.agents/skills/eli5/SKILL.md +5 -1
  21. package/.agents/skills/eli5/agents/openai.yaml +5 -0
  22. package/.agents/skills/eval/SKILL.md +6 -2
  23. package/.agents/skills/eval/agents/openai.yaml +5 -0
  24. package/.agents/skills/evolve/SKILL.md +6 -2
  25. package/.agents/skills/evolve/agents/openai.yaml +5 -0
  26. package/.agents/skills/frontmatter-guard/SKILL.md +3 -1
  27. package/.agents/skills/gitlab-ops/SKILL.md +3 -1
  28. package/.agents/skills/gitlab-portfolio/SKILL.md +3 -1
  29. package/.agents/skills/go/SKILL.md +5 -1
  30. package/.agents/skills/go/agents/openai.yaml +5 -0
  31. package/.agents/skills/grill/SKILL.md +5 -1
  32. package/.agents/skills/grill/agents/openai.yaml +5 -0
  33. package/.agents/skills/harness-audit/SKILL.md +5 -1
  34. package/.agents/skills/harness-audit/agents/openai.yaml +5 -0
  35. package/.agents/skills/hook-development/SKILL.md +3 -1
  36. package/.agents/skills/mcp-builder/SKILL.md +3 -1
  37. package/.agents/skills/memory-cleanup/SKILL.md +5 -1
  38. package/.agents/skills/memory-cleanup/agents/openai.yaml +5 -0
  39. package/.agents/skills/mode-selector/SKILL.md +3 -1
  40. package/.agents/skills/navigator/SKILL.md +22 -0
  41. package/.agents/skills/navigator/agents/openai.yaml +5 -0
  42. package/.agents/skills/npm-publish/SKILL.md +4 -2
  43. package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
  44. package/.agents/skills/persona-panel/SKILL.md +5 -1
  45. package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
  46. package/.agents/skills/plan/SKILL.md +5 -1
  47. package/.agents/skills/plan/agents/openai.yaml +5 -0
  48. package/.agents/skills/playwright-driver/SKILL.md +3 -1
  49. package/.agents/skills/portfolio/SKILL.md +5 -1
  50. package/.agents/skills/portfolio/agents/openai.yaml +5 -0
  51. package/.agents/skills/quality-gates/SKILL.md +3 -1
  52. package/.agents/skills/reconcile/SKILL.md +5 -1
  53. package/.agents/skills/reconcile/agents/openai.yaml +5 -0
  54. package/.agents/skills/release/SKILL.md +5 -1
  55. package/.agents/skills/release/agents/openai.yaml +5 -0
  56. package/.agents/skills/remote-offload/SKILL.md +3 -1
  57. package/.agents/skills/repo-audit/SKILL.md +5 -1
  58. package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
  59. package/.agents/skills/session/SKILL.md +21 -0
  60. package/.agents/skills/session/agents/openai.yaml +5 -0
  61. package/.agents/skills/session-end/SKILL.md +3 -1
  62. package/.agents/skills/session-plan/SKILL.md +3 -1
  63. package/.agents/skills/session-start/SKILL.md +3 -1
  64. package/.agents/skills/spinout/SKILL.md +5 -1
  65. package/.agents/skills/spinout/agents/openai.yaml +5 -0
  66. package/.agents/skills/sunset-review/SKILL.md +5 -1
  67. package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
  68. package/.agents/skills/templates-ack/SKILL.md +21 -0
  69. package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
  70. package/.agents/skills/test/SKILL.md +5 -1
  71. package/.agents/skills/test/agents/openai.yaml +5 -0
  72. package/.agents/skills/test-audit/SKILL.md +21 -0
  73. package/.agents/skills/test-audit/agents/openai.yaml +5 -0
  74. package/.agents/skills/test-runner/SKILL.md +3 -1
  75. package/.agents/skills/tmux-layout/SKILL.md +3 -1
  76. package/.agents/skills/using-orchestrator/SKILL.md +3 -1
  77. package/.agents/skills/ux-grill/SKILL.md +5 -1
  78. package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
  79. package/.agents/skills/vault-mirror/SKILL.md +3 -1
  80. package/.agents/skills/vault-sync/SKILL.md +3 -1
  81. package/.agents/skills/wave-executor/SKILL.md +3 -1
  82. package/.agents/skills/write-executable-plan/SKILL.md +3 -1
  83. package/.claude-plugin/marketplace.json +1 -1
  84. package/.claude-plugin/plugin.json +1 -1
  85. package/.codex-plugin/plugin.json +4 -4
  86. package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -1
  87. package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -3
  88. package/.codex-plugin/skills/eval/SKILL.md +1 -1
  89. package/.codex-plugin/skills/evolve/SKILL.md +1 -1
  90. package/.codex-plugin/skills/navigator/SKILL.md +22 -0
  91. package/.codex-plugin/skills/{convergence-monitoring → navigator}/agents/openai.yaml +2 -2
  92. package/.codex-plugin/skills/npm-publish/SKILL.md +1 -3
  93. package/.codex-plugin/skills/session/SKILL.md +1 -1
  94. package/.codex-plugin/skills/test-audit/SKILL.md +21 -0
  95. package/.codex-plugin/skills/{npm-publish → test-audit}/agents/openai.yaml +1 -1
  96. package/.cursor/commands/eval.md +1 -1
  97. package/.cursor/commands/navigator.md +15 -0
  98. package/.cursor/commands/session.md +1 -1
  99. package/.cursor/commands/test-audit.md +14 -0
  100. package/.cursor/rules/000-session-orchestrator.mdc +0 -2
  101. package/.cursor/rules/050-plan.mdc +1 -1
  102. package/.cursor/rules/060-evolve.mdc +27 -12
  103. package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
  104. package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
  105. package/.cursor/skills/eval/SKILL.md +1 -1
  106. package/.cursor/skills/navigator/SKILL.md +13 -0
  107. package/.cursor/skills/npm-publish/SKILL.md +1 -0
  108. package/.cursor/skills/test-audit/SKILL.md +12 -0
  109. package/.cursor-plugin/plugin.json +1 -1
  110. package/.orchestrator/policy/blocked-commands.json +12 -3
  111. package/AGENTS.md +9 -30
  112. package/CHANGELOG.md +206 -0
  113. package/README.md +13 -9
  114. package/SECURITY.md +12 -0
  115. package/agents/dialectic-deriver.md +13 -10
  116. package/agents/eval-judge.md +67 -45
  117. package/agents/skill-applied-judge.md +34 -19
  118. package/commands/session.md +7 -3
  119. package/docs/agent-authoring.md +2 -2
  120. package/docs/baseline.md +22 -7
  121. package/docs/ci-setup.md +57 -4
  122. package/docs/codex-setup.md +14 -2
  123. package/docs/components.md +12 -10
  124. package/docs/events-schema.md +65 -15
  125. package/docs/persona-panel.md +5 -4
  126. package/docs/rule-authoring.md +84 -17
  127. package/docs/scope-collision-guard.md +26 -4
  128. package/docs/session-config-reference.md +154 -12
  129. package/docs/session-config-template.md +41 -3
  130. package/docs/telemetry.md +2 -0
  131. package/hooks/_lib/hook-import-set.json +87 -6
  132. package/hooks/_lib/subagent-transcript.mjs +582 -31
  133. package/hooks/config-protection.mjs +11 -3
  134. package/hooks/cwd-change-restore.mjs +11 -3
  135. package/hooks/enforce-commands.mjs +70 -23
  136. package/hooks/enforce-scope.mjs +143 -33
  137. package/hooks/hooks-codex.json +1 -1
  138. package/hooks/hooks-cursor.json +5 -0
  139. package/hooks/hooks-pi.json +5 -0
  140. package/hooks/hooks.json +6 -1
  141. package/hooks/loop-guard.mjs +11 -3
  142. package/hooks/on-session-end.mjs +58 -23
  143. package/hooks/on-session-start.mjs +101 -11
  144. package/hooks/on-stop.mjs +170 -22
  145. package/hooks/operator-steer.mjs +11 -3
  146. package/hooks/post-bash-issue-budget-refund.mjs +18 -8
  147. package/hooks/post-bash-write-verify.mjs +3 -2
  148. package/hooks/post-edit-import-probe.mjs +17 -9
  149. package/hooks/post-edit-validate.mjs +13 -5
  150. package/hooks/post-subagent-discovery-validator.mjs +98 -13
  151. package/hooks/post-tool-batch-wave-signal.mjs +202 -38
  152. package/hooks/post-tool-failure-corrective-context.mjs +11 -5
  153. package/hooks/post-tooluse-frontend-slop.mjs +10 -4
  154. package/hooks/pre-auq-clarity.mjs +15 -2
  155. package/hooks/pre-bash-destructive-guard.mjs +80 -9
  156. package/hooks/pre-bash-issue-budget.mjs +16 -11
  157. package/hooks/pre-bash-memory-propose-audit.mjs +137 -56
  158. package/hooks/pre-bash-sessions-ledger-guard.mjs +419 -22
  159. package/hooks/pre-bash-staging-fence.mjs +335 -31
  160. package/hooks/pre-bash-templates-first.mjs +19 -14
  161. package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
  162. package/hooks/pre-task-scope-disjoint.mjs +792 -74
  163. package/hooks/subagent-telemetry.mjs +111 -38
  164. package/hooks/wave-scope-commit-guard.mjs +197 -100
  165. package/monitors/monitors.json +1 -1
  166. package/output-styles/wave-summary.md +1 -1
  167. package/package.json +3 -3
  168. package/pi/prompts/eval.md +1 -1
  169. package/pi/prompts/navigator.md +12 -0
  170. package/pi/prompts/session.md +1 -1
  171. package/pi/prompts/test-audit.md +12 -0
  172. package/rules/README.md +7 -2
  173. package/rules/_index.md +3 -3
  174. package/rules/always-on/bash-harness-pitfalls.md +7 -1
  175. package/rules/opt-in-domain/prompt-caching.md +6 -2
  176. package/rules/opt-in-stack/backend-data.md +10 -2
  177. package/rules/opt-in-stack/backend.md +10 -4
  178. package/rules/opt-in-stack/frontend.md +7 -2
  179. package/rules/opt-in-stack/security-web.md +10 -4
  180. package/rules/opt-in-stack/swift.md +6 -2
  181. package/scripts/apply-session-learnings.mjs +492 -0
  182. package/scripts/archive-closed-prds.mjs +2 -1
  183. package/scripts/autopilot.mjs +70 -53
  184. package/scripts/backfill-abandoned-sessions.mjs +118 -16
  185. package/scripts/baseline-archetypes.mjs +1 -1
  186. package/scripts/check-package-manager.mjs +62 -5
  187. package/scripts/check-sessions-integrity.mjs +300 -0
  188. package/scripts/cursor-install.mjs +1 -4
  189. package/scripts/dialectic-deriver.mjs +50 -13
  190. package/scripts/emit-session.mjs +371 -31
  191. package/scripts/eval-session.mjs +65 -3
  192. package/scripts/export-hw-learnings.mjs +22 -3
  193. package/scripts/fleet-checkin.mjs +147 -0
  194. package/scripts/generate-agents-skills.mjs +102 -29
  195. package/scripts/generate-cursor-adapter.mjs +61 -16
  196. package/scripts/lib/agent-frontmatter.mjs +3 -3
  197. package/scripts/lib/agent-status.mjs +2 -31
  198. package/scripts/lib/auq/clarity.mjs +10 -2
  199. package/scripts/lib/auq/parse.mjs +12 -31
  200. package/scripts/lib/auq/schema.mjs +56 -41
  201. package/scripts/lib/auto-dialectic.mjs +304 -15
  202. package/scripts/lib/auto-dream.mjs +7 -6
  203. package/scripts/lib/autopilot/flags.mjs +18 -2
  204. package/scripts/lib/autopilot/kill-switches.mjs +6 -3
  205. package/scripts/lib/autopilot/loop.mjs +19 -2
  206. package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
  207. package/scripts/lib/baseline-archetypes.mjs +41 -22
  208. package/scripts/lib/ci-status-banner.mjs +376 -16
  209. package/scripts/lib/command-blocker.mjs +275 -28
  210. package/scripts/lib/config/dialectic.mjs +12 -3
  211. package/scripts/lib/config/gate.mjs +74 -0
  212. package/scripts/lib/config/host-paths.mjs +79 -0
  213. package/scripts/lib/config/persona-gate-wave.mjs +3 -3
  214. package/scripts/lib/config/reaper.mjs +163 -0
  215. package/scripts/lib/config-schema.mjs +13 -0
  216. package/scripts/lib/config.mjs +85 -10
  217. package/scripts/lib/convergence-monitor.mjs +74 -11
  218. package/scripts/lib/ecosystem-health.mjs +11 -0
  219. package/scripts/lib/eval/engine.mjs +421 -53
  220. package/scripts/lib/eval/judge.mjs +463 -40
  221. package/scripts/lib/eval/schema.mjs +10 -1
  222. package/scripts/lib/events-rotation.mjs +221 -25
  223. package/scripts/lib/events-schema.mjs +114 -0
  224. package/scripts/lib/events.mjs +545 -7
  225. package/scripts/lib/fleet-protocol.mjs +270 -0
  226. package/scripts/lib/frontmatter-guard.mjs +21 -10
  227. package/scripts/lib/gates/gate-baseline.mjs +40 -5
  228. package/scripts/lib/gates/gate-full.mjs +35 -5
  229. package/scripts/lib/gates/gate-helpers.mjs +336 -23
  230. package/scripts/lib/gates/gate-incremental.mjs +49 -23
  231. package/scripts/lib/gates/gate-per-file.mjs +42 -8
  232. package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
  233. package/scripts/lib/instruction-budget-guard.mjs +208 -8
  234. package/scripts/lib/io.mjs +42 -8
  235. package/scripts/lib/issue-close-strip-labels.mjs +207 -49
  236. package/scripts/lib/js-mask.mjs +197 -0
  237. package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
  238. package/scripts/lib/learnings/expiry-sweep.mjs +34 -3
  239. package/scripts/lib/learnings/io.mjs +154 -26
  240. package/scripts/lib/learnings/schema.mjs +163 -15
  241. package/scripts/lib/learnings/sizing-subject.mjs +2 -2
  242. package/scripts/lib/maintenance-due-banner.mjs +53 -88
  243. package/scripts/lib/memory-proposals/schema.mjs +11 -5
  244. package/scripts/lib/named-baseline-resolver.mjs +53 -28
  245. package/scripts/lib/named-vault-resolver.mjs +104 -5
  246. package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
  247. package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
  248. package/scripts/lib/orphan-reaper/reaper-audit.mjs +291 -0
  249. package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
  250. package/scripts/lib/orphan-reaper/scan-throttle.mjs +92 -0
  251. package/scripts/lib/orphan-reaper/scan.mjs +506 -0
  252. package/scripts/lib/orphan-reaper.mjs +316 -0
  253. package/scripts/lib/owner-yaml.mjs +104 -6
  254. package/scripts/lib/peer-cards/merger.mjs +48 -10
  255. package/scripts/lib/peer-cards/reader.mjs +78 -2
  256. package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
  257. package/scripts/lib/process-group.mjs +950 -0
  258. package/scripts/lib/project-hygiene.mjs +21 -1
  259. package/scripts/lib/quality-gate.mjs +152 -33
  260. package/scripts/lib/reconcile/backlog.mjs +368 -0
  261. package/scripts/lib/reconcile/engine.mjs +55 -188
  262. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +302 -60
  263. package/scripts/lib/reconcile/sanitize.mjs +69 -3
  264. package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
  265. package/scripts/lib/resource-probe/parsers.mjs +31 -0
  266. package/scripts/lib/resource-probe.mjs +1 -1
  267. package/scripts/lib/rule-loader.mjs +141 -74
  268. package/scripts/lib/rules-sync.mjs +27 -12
  269. package/scripts/lib/scope-baseline.mjs +3 -4
  270. package/scripts/lib/scope-echo.mjs +39 -2
  271. package/scripts/lib/scope-gate.mjs +605 -1
  272. package/scripts/lib/session-close-backfill.mjs +33 -6
  273. package/scripts/lib/session-id.mjs +9 -20
  274. package/scripts/lib/session-invocation.mjs +20 -0
  275. package/scripts/lib/session-record-repair.mjs +183 -26
  276. package/scripts/lib/session-registry.mjs +27 -2
  277. package/scripts/lib/session-schema/constants.mjs +56 -2
  278. package/scripts/lib/session-schema/normalizer.mjs +56 -4
  279. package/scripts/lib/session-schema/validator.mjs +28 -1
  280. package/scripts/lib/session-schema.mjs +8 -3
  281. package/scripts/lib/session-start-probes.mjs +95 -10
  282. package/scripts/lib/session-token-rollup.mjs +245 -52
  283. package/scripts/lib/sessions-canonical.mjs +118 -4
  284. package/scripts/lib/sessions-integrity-banner.mjs +7 -1
  285. package/scripts/lib/sessions-staleness-banner.mjs +193 -51
  286. package/scripts/lib/sh-quote.mjs +29 -0
  287. package/scripts/lib/skill-evidence-window.mjs +891 -0
  288. package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
  289. package/scripts/lib/skill-evolution/engine.mjs +18 -9
  290. package/scripts/lib/skill-judge.mjs +45 -3
  291. package/scripts/lib/subagents-schema.mjs +30 -3
  292. package/scripts/lib/tail-window.mjs +56 -0
  293. package/scripts/lib/telemetry/pricing.mjs +118 -71
  294. package/scripts/lib/telemetry/schema.mjs +30 -0
  295. package/scripts/lib/telemetry/sync.mjs +61 -6
  296. package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
  297. package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
  298. package/scripts/lib/tests-src-ratio.mjs +140 -23
  299. package/scripts/lib/tmux-layout/layouts.mjs +1 -11
  300. package/scripts/lib/tmux-layout/telemetry-stats.mjs +72 -13
  301. package/scripts/lib/tmux-layout/tmux-shell.mjs +2 -11
  302. package/scripts/lib/user-invocable-skills.mjs +23 -3
  303. package/scripts/lib/ux-grill/reconcile.mjs +48 -22
  304. package/scripts/lib/validate/check-agents-skills.mjs +26 -15
  305. package/scripts/lib/validate/check-agents.mjs +1 -1
  306. package/scripts/lib/validate/check-cursor-adapter.mjs +1 -0
  307. package/scripts/lib/validate/check-entry-guard.mjs +13 -50
  308. package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
  309. package/scripts/lib/validate/check-hooks-symmetry.mjs +5 -0
  310. package/scripts/lib/validate/check-learnings-shell-writes.mjs +214 -0
  311. package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
  312. package/scripts/lib/validate/check-rules.mjs +7 -5
  313. package/scripts/lib/validate/check-skill-links.mjs +9 -1
  314. package/scripts/lib/validate/check-skill-script-paths.mjs +239 -27
  315. package/scripts/lib/validate/check-test-git-config-target.mjs +24 -34
  316. package/scripts/lib/validate/check-test-value-bans.mjs +87 -14
  317. package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
  318. package/scripts/lib/validate/check-untracked-test-deps.mjs +7 -102
  319. package/scripts/lib/validate/check-unwired-features.mjs +130 -27
  320. package/scripts/lib/validate/check-validator-registration.mjs +34 -10
  321. package/scripts/lib/validate/confidential-names.mjs +10 -0
  322. package/scripts/lib/validate-vendored-rules.mjs +210 -84
  323. package/scripts/lib/vault-mirror/namespace.mjs +46 -8
  324. package/scripts/lib/vault-mirror/process.mjs +10 -3
  325. package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
  326. package/scripts/lib/vault-mirror/telemetry.mjs +8 -1
  327. package/scripts/lib/vault-status/board-writer.mjs +48 -11
  328. package/scripts/lib/vault-status/narrative-mirror.mjs +71 -16
  329. package/scripts/lib/vault-yaml.mjs +118 -0
  330. package/scripts/lib/wave-executor/dispatch-common.mjs +10 -18
  331. package/scripts/lib/wave-executor/foreign-dispatch.mjs +1 -1
  332. package/scripts/lib/wave-executor/remote-dispatch.mjs +1 -1
  333. package/scripts/lib/wave-resource-gate.mjs +34 -8
  334. package/scripts/lib/worktree/lifecycle.mjs +153 -1
  335. package/scripts/memory-propose.mjs +18 -5
  336. package/scripts/release-session-lock.mjs +306 -0
  337. package/scripts/release.mjs +40 -8
  338. package/scripts/repair-invalid-sessions.mjs +37 -4
  339. package/scripts/resolve-session-invocation.mjs +59 -0
  340. package/scripts/run-migrate-v2-cross-repo.mjs +158 -30
  341. package/scripts/run-quality-gate.mjs +357 -197
  342. package/scripts/site-numbers.mjs +163 -5
  343. package/scripts/sweep-expired-learnings.mjs +56 -5
  344. package/scripts/sweep-expired-rules.mjs +16 -4
  345. package/scripts/validate-plugin.mjs +19 -0
  346. package/scripts/validate-wave-scope.mjs +32 -105
  347. package/scripts/vault-integration-watcher.mjs +2 -1
  348. package/scripts/vault-mirror.mjs +81 -47
  349. package/scripts/wave-scope-binding.mjs +2 -1
  350. package/skills/_shared/config-reading.md +3 -3
  351. package/skills/_shared/fleet-protocol.md +131 -0
  352. package/skills/_shared/platform-tools.md +23 -11
  353. package/skills/_shared/state-ownership.md +5 -0
  354. package/skills/autopilot/SKILL.md +47 -18
  355. package/skills/bootstrap/SKILL.md +1 -1
  356. package/skills/bootstrap/_shared-template.md +1 -1
  357. package/skills/bootstrap/fast-template.md +1 -1
  358. package/skills/bootstrap/private-contract.md +2 -1
  359. package/skills/bootstrap/public-fallback.md +6 -3
  360. package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
  361. package/skills/claude-md-drift-check/SKILL.md +10 -10
  362. package/skills/claude-md-drift-check/checker.mjs +26 -18
  363. package/skills/convergence-monitoring/README.md +8 -1
  364. package/skills/convergence-monitoring/SIGNALS.md +50 -6
  365. package/skills/convergence-monitoring/SKILL.md +15 -6
  366. package/skills/eval/SKILL.md +39 -24
  367. package/skills/eval/rubric-v1.md +1 -0
  368. package/skills/eval/rubric-v2.md +457 -0
  369. package/skills/evolve/SKILL.md +3 -3
  370. package/skills/evolve/references/evolve-analyze-mode.md +10 -6
  371. package/skills/evolve/references/evolve-dialectic-mode.md +42 -25
  372. package/skills/gitlab-ops/SKILL.md +3 -2
  373. package/skills/memory-cleanup/SKILL.md +1 -1
  374. package/skills/navigator/SKILL.md +193 -0
  375. package/skills/navigator/references/account-check.sh +87 -0
  376. package/skills/navigator/references/fleet-decisions.mjs +231 -0
  377. package/skills/navigator/references/slot-check.sh +114 -0
  378. package/skills/npm-publish/SKILL.md +1 -1
  379. package/skills/persona-panel/SKILL.md +2 -2
  380. package/skills/persona-panel/persona-format.md +2 -2
  381. package/skills/persona-panel/presets/designer-lens.md +1 -1
  382. package/skills/persona-panel/presets/engineer-lens.md +1 -1
  383. package/skills/persona-panel/presets/pm-lens.md +1 -1
  384. package/skills/plan/mode-retro.md +19 -4
  385. package/skills/quality-gates/SKILL.md +6 -4
  386. package/skills/reconcile/SKILL.md +11 -0
  387. package/skills/remote-offload/SKILL.md +4 -2
  388. package/skills/session-end/SKILL.md +13 -16
  389. package/skills/session-end/discovery-scan.md +1 -1
  390. package/skills/session-end/learning-patterns.md +22 -12
  391. package/skills/session-end/metrics-collection.md +5 -6
  392. package/skills/session-end/phase-3-6-tail.md +55 -9
  393. package/skills/session-end/references/phase-3-documentation-updates.md +2 -2
  394. package/skills/session-end/references/phase-4a-worktree-cleanup.md +2 -0
  395. package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
  396. package/skills/session-end/references/session-summary-template.md +1 -1
  397. package/skills/session-end/session-metrics-write.md +32 -60
  398. package/skills/session-plan/SKILL.md +17 -8
  399. package/skills/session-plan/references/session-plan-task-classification.md +2 -2
  400. package/skills/session-start/SKILL.md +6 -0
  401. package/skills/session-start/references/operations-contract.md +4 -0
  402. package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
  403. package/skills/session-start/references/phase-4-ssot-environment-check.md +4 -3
  404. package/skills/test-audit/SKILL.md +152 -0
  405. package/skills/test-audit/references/campaign.md +111 -0
  406. package/skills/test-audit/references/mutate.sh +50 -0
  407. package/skills/ux-grill/SKILL.md +1 -1
  408. package/skills/wave-executor/SKILL.md +9 -5
  409. package/skills/wave-executor/circuit-breaker.md +2 -0
  410. package/skills/wave-executor/references/wave-executor-state-init.md +8 -3
  411. package/skills/wave-executor/references/wave-loop-dispatch.md +5 -2
  412. package/skills/wave-executor/references/wave-loop-review.md +3 -5
  413. package/templates/personas/README.md +5 -5
  414. package/templates/personas/accounting-compliance.v1.md +1 -1
  415. package/templates/personas/accounting-tax-advisor.v1.md +1 -1
  416. package/templates/personas/buyer-p1-cto.v1.md +1 -1
  417. package/templates/personas/buyer-p2-kanzlei.v1.md +1 -1
  418. package/templates/personas/buyer-p3-build.v1.md +1 -1
  419. package/templates/personas/buyer-p4-tech-veto.v1.md +1 -1
  420. package/templates/personas/buyer-p5-solo.v1.md +1 -1
  421. package/templates/personas/buyer-p6-ld.v1.md +1 -1
  422. package/templates/personas/klima-ai-expert.v1.md +1 -1
  423. package/templates/personas/klima-physicist.v1.md +1 -1
  424. package/.cursor/commands/convergence-monitoring.md +0 -13
  425. package/.cursor/commands/npm-publish.md +0 -13
  426. package/pi/prompts/convergence-monitoring.md +0 -11
  427. package/pi/prompts/npm-publish.md +0 -11
@@ -0,0 +1,395 @@
1
+ /**
2
+ * orphan-reaper/reaper-decide.mjs — the PURE half of the reaper: which processes
3
+ * are reapable orphans, decided from a `ps` snapshot and the gate-process ledger.
4
+ *
5
+ * ## Why PPID 1 is necessary and never sufficient
6
+ *
7
+ * Measured on this host 2026-09-21: 538 of 784 processes (68.6%) have PPID 1 —
8
+ * on macOS launchd is the parent of nearly everything. A reaper keyed on PPID 1
9
+ * alone would be a weapon pointed at the operating system. So a candidate is a
10
+ * CONJUNCTION: in our own gate-process ledger, PPID 1, old enough, a read-only
11
+ * gate command, and an identity that still verifies against the ledger record.
12
+ *
13
+ * ## Coverage: the ledger is joined by PID **and** by PGID
14
+ *
15
+ * The binding `ps` format (`PS_ARGS` in `ps-snapshot.mjs`) publishes `pgid`, so a row joins the
16
+ * ledger either as the recorded LEADER (`row.pid === record.pid`) or as any
17
+ * member of the recorded GROUP (`row.pgid === record.pgid`). The group join is
18
+ * what covers the measured grandchild case: `npm run typecheck` (82507) spawns
19
+ * `node scripts/typecheck.mjs` (82591) which spawns `tsgo`, all three in pgid
20
+ * 82507 — when only the leader dies, 82591 sits at PPID 1 and the ledger knows
21
+ * its group but not its pid. Named ceiling (BV-004): the join FINDS every
22
+ * member, but a member is only reaped when its own command passes the read-only
23
+ * allowlist — `tsgo`/`vitest` descendants are, a repo-local `node scripts/…`
24
+ * runner is NOT and is reported as `not-read-only` (measured 2026-09-22); the
25
+ * group still dies through its allowlisted sibling. Revisit trigger: an orphaned
26
+ * node runner that outlives every allowlisted sibling.
27
+ *
28
+ * A non-leader cannot be identified the leader's way: its start time is its own
29
+ * and its command line is not the recorded one. Its identity is therefore
30
+ * {@link verifyGroupMemberIdentity} — born no earlier than its group leader
31
+ * (a descendant is never older than the process that forked it), plus either
32
+ * the record's signature token or a command name the read-only allowlist knows.
33
+ *
34
+ * The KILL TARGET is the group in both cases (`-pgid`), so one record still
35
+ * covers its whole group with one ladder.
36
+ *
37
+ * Named ceiling (BV-004): a process that `setsid`-ed out of its group carries no
38
+ * ledger identity in this format and is not a candidate BY DESIGN (PRD
39
+ * § Umfangsgrenze: orphan-confidence via PPID history is Stufe 2 / C4). Revisit
40
+ * if such a case is ever observed for a process the ledger DID record.
41
+ */
42
+
43
+ import { verifyProcessIdentity } from '../process-group.mjs';
44
+ import { REAPER_DEFAULTS } from './defaults.mjs';
45
+
46
+ /** @typedef {import('./ps-snapshot.mjs').PsRow} PsRow */
47
+
48
+ /**
49
+ * The read-only gate command class — an ALLOWLIST, which is why no denylist is
50
+ * needed: a dev server or MCP server matches nothing here and is therefore never
51
+ * a candidate, orphaned or not (PRD § Out-of-Scope: two orphaned `browser-kit`
52
+ * servers were reported, not killed).
53
+ *
54
+ * IN — commands that only read the working copy and can be re-run at will:
55
+ * `tsgo`, `tsc`, `vitest`, `eslint`, `node …vitest…`,
56
+ * `npm test` / `npm run typecheck` / `npm run lint`.
57
+ * DELIBERATELY OUT — anything that holds state or serves a port: dev servers,
58
+ * MCP servers, `npm run build`, `git` (writes the index), database processes.
59
+ *
60
+ * Matched per STATEMENT (see {@link isReadOnlyCommand}), with a `(^|[\s/])`
61
+ * boundary so both `tsgo --noEmit` and `/opt/homebrew/bin/tsgo --noEmit` and
62
+ * `npx tsgo` hit.
63
+ *
64
+ * `eslint` carries a negative lookahead for its WRITING flags: `eslint . --fix`
65
+ * is this repo's own `lint:fix` script and rewrites the working copy, so it is
66
+ * not re-runnable at will and has no business on a read-only allowlist.
67
+ */
68
+ export const READ_ONLY_COMMAND_PATTERNS = Object.freeze([
69
+ /(^|[\s/])tsgo(\s|$)/,
70
+ /(^|[\s/])tsc(\s|$)/,
71
+ /(^|[\s/])vitest(\s|$)/,
72
+ /(^|[\s/])eslint(?!\S)(?!.*\s--(fix|fix-dry-run|output-file)(\s|=|$))/,
73
+ /(^|[\s/])node\s+\S*vitest/,
74
+ /(^|[\s/])npm\s+(run\s+)?(test|typecheck|lint)(\s|$)/,
75
+ ]);
76
+
77
+ /**
78
+ * Shell operators that separate one STATEMENT from the next in a `ps` `args`
79
+ * line. `ps` prints argv joined by spaces with the quoting already stripped, so
80
+ * a quote-aware lexer (`command-blocker.mjs` `splitChainSegments`) would have
81
+ * nothing left to be aware of here — and it would put 2.190 lines into the
82
+ * static import closure of a module that is destined for a hot-path hook
83
+ * (#1432) and lazily imports even `events.mjs` for that reason.
84
+ */
85
+ const STATEMENT_SEPARATOR_RE = /\s*(?:&&|\|\||[;|&])\s*/;
86
+
87
+ /**
88
+ * True when EVERY statement of `args` is a read-only gate command per
89
+ * {@link READ_ONLY_COMMAND_PATTERNS}.
90
+ *
91
+ * Per-statement and ALL, not the whole string and ANY: judged over the whole
92
+ * line, `sh -c npm run build && npm test` matches the `npm test` pattern as a
93
+ * SUBSTRING and a build — which writes — is allowlisted by the read-only half
94
+ * of its own command. This is the destructive-guard's per-statement rule
95
+ * (`.claude/rules/guard-design.md` § "Widening a matcher without narrowing its
96
+ * bypass"), applied in the other direction: there one appended statement lifts
97
+ * a block, here one appended statement must be able to REVOKE a permission.
98
+ *
99
+ * An empty statement (a trailing `&&`, a doubled separator) is skipped rather
100
+ * than counted as a failure; a line with no statement at all is not read-only.
101
+ *
102
+ * @param {string} args
103
+ * @param {readonly RegExp[]} [patterns]
104
+ * @returns {boolean}
105
+ */
106
+ export function isReadOnlyCommand(args, patterns = READ_ONLY_COMMAND_PATTERNS) {
107
+ const s = typeof args === 'string' ? args : '';
108
+ if (s.trim().length === 0) return false;
109
+ const statements = s.split(STATEMENT_SEPARATOR_RE).filter((part) => part.trim().length > 0);
110
+ if (statements.length === 0) return false;
111
+ return statements.every((statement) => patterns.some((re) => re.test(statement)));
112
+ }
113
+
114
+ /**
115
+ * Identity check for a NON-LEADER member of a recorded process group — PURE.
116
+ *
117
+ * {@link verifyProcessIdentity} cannot serve here: it compares the row's start
118
+ * time against the LEADER's and the row's first token against the LEADER's
119
+ * signature, and a descendant matches neither (`node scripts/typecheck.mjs` was
120
+ * forked by `npm run typecheck` seconds after it).
121
+ *
122
+ * Both checks must hold, and both are one-sided on purpose:
123
+ * 1. NOT OLDER than the group leader, within `toleranceMs`. A process that
124
+ * existed BEFORE the leader cannot be its descendant, so an older row in the
125
+ * same pgid is a recycled group id, never a grandchild.
126
+ * 2. Either the leader's signature token prefixes the row's command (the
127
+ * descendant re-execs the same binary — `npm` → `npm`), or the row's command
128
+ * is itself on the read-only allowlist (`tsgo`, `node …vitest` — NOT a
129
+ * repo-local `node scripts/typecheck.mjs`, see the module head).
130
+ *
131
+ * @param {PsRow|null} row
132
+ * @param {import('../process-group.mjs').GateProcessRecord} record
133
+ * @param {object} [opts]
134
+ * @param {number} [opts.nowMs]
135
+ * @param {number} [opts.toleranceMs]
136
+ * @param {readonly RegExp[]} [opts.readOnlyPatterns]
137
+ * @returns {{match: boolean, reason: 'ok'|'gone'|'start-time-mismatch'|'signature-mismatch'}}
138
+ */
139
+ export function verifyGroupMemberIdentity(row, record, {
140
+ nowMs = Date.now(),
141
+ toleranceMs = 2000,
142
+ readOnlyPatterns = READ_ONLY_COMMAND_PATTERNS,
143
+ } = {}) {
144
+ if (!row || typeof row !== 'object') return { match: false, reason: 'gone' };
145
+ const etimeSeconds = typeof row.etimeSeconds === 'number' && Number.isFinite(row.etimeSeconds)
146
+ ? row.etimeSeconds
147
+ : null;
148
+ // An unmeasurable age is a refusal, never a pass — the same fail-closed
149
+ // direction `verifyProcessIdentity` takes for the leader.
150
+ if (etimeSeconds === null) return { match: false, reason: 'start-time-mismatch' };
151
+ const observedStart = nowMs - etimeSeconds * 1000;
152
+ const leaderStart = Number(record?.startTime);
153
+ if (!Number.isFinite(leaderStart)) return { match: false, reason: 'start-time-mismatch' };
154
+ if (observedStart < leaderStart - toleranceMs) {
155
+ return { match: false, reason: 'start-time-mismatch' };
156
+ }
157
+
158
+ const args = typeof row.args === 'string' ? row.args : '';
159
+ const token = signatureTokenOf(record?.commandSignature);
160
+ const firstToken = args.trim().split(/\s+/)[0] ?? '';
161
+ const tokenOk = token.length > 0 && firstToken === token;
162
+ if (!tokenOk && !isReadOnlyCommand(args, readOnlyPatterns)) {
163
+ return { match: false, reason: 'signature-mismatch' };
164
+ }
165
+ return { match: true, reason: 'ok' };
166
+ }
167
+
168
+ /**
169
+ * The command-name half of a command signature (`npm:6f1c…` → `npm`).
170
+ * A local four-liner rather than an import: `process-group.mjs` keeps its own
171
+ * copy private, and re-exporting it for one caller would widen that module's
172
+ * interface for no second consumer.
173
+ *
174
+ * @param {unknown} signature
175
+ * @returns {string}
176
+ */
177
+ function signatureTokenOf(signature) {
178
+ const s = typeof signature === 'string' ? signature : '';
179
+ const i = s.lastIndexOf(':');
180
+ return i === -1 ? s : s.slice(0, i);
181
+ }
182
+
183
+ /**
184
+ * Decide which processes are reapable orphans — PURE.
185
+ *
186
+ * No I/O, no signal, no clock of its own: `nowMs` is an argument. The only
187
+ * imports it reaches are {@link verifyProcessIdentity} and
188
+ * {@link verifyGroupMemberIdentity}, both themselves pure.
189
+ *
190
+ * Exactly ONE verdict per examined row, at a FIXED priority, so a row can never
191
+ * appear twice and a trigger is never ambiguous:
192
+ *
193
+ * 1. not in the ledger → `rejected: not-in-ledger`
194
+ * 2. record with `pgid !== pid` → `rejected: pgid-mismatch`
195
+ * 3. PPID !== 1 → `rejected: has-parent`
196
+ * 4. younger than `minAgeSeconds` → `rejected: too-young`
197
+ * 5. no `sessionId` on the record → `reported: unattributed` (never killed)
198
+ * 6. foreign session, live or of unmeasurable liveness
199
+ * → `reported: foreign-live-session`
200
+ * / `foreign-session-liveness-unknown` (never killed)
201
+ * 7. not a read-only command → `reported: not-read-only` (never killed)
202
+ * 8. identity does not verify → `rejected: identity-mismatch | signature-mismatch`
203
+ * 9. otherwise → `candidates` with `trigger: 'orphan-ppid1'`
204
+ *
205
+ * Step 2 is a LEDGER-INTEGRITY rejection, not a property of the row: under
206
+ * `detached: true` the leader IS its own group, so `pgid === pid` is the
207
+ * documented invariant (`process-group.mjs` — "the child calls setsid, so it IS
208
+ * its own group leader"). A record violating it describes a group this module
209
+ * did not create, and its `pgid` is the value that gets negated and signalled.
210
+ *
211
+ * Step 5 is fail-closed and was inert until 2026-09-22: `sessionId` was `null`
212
+ * in 377 of 377 live records because the gate runner passed none, and a null
213
+ * owner was silently read as "nobody's, therefore mine". An unattributed process
214
+ * is REPORTED — it is not evidence of ownership in either direction.
215
+ *
216
+ * Step 6 precedes step 7 on purpose: a foreign live session's process is
217
+ * reported for its OWNER, whether or not it also happens to be read-only
218
+ * (PRD FA3: "gemeldet, aber nicht automatisch getötet").
219
+ *
220
+ * Named ceiling (BV-004) on the `rejected` volume: every PPID-1 row that is not
221
+ * in the ledger is materialised as `not-in-ledger` — ~538 entries on this
222
+ * 784-process host. That is deliberate (the AC requires the rejection reason to
223
+ * be traceable in the RESULT) and bounded, because only candidates and reports
224
+ * are ever persisted to the audit; `rejected` stays in memory for one scan.
225
+ * Rows with a live parent that are ALSO not in the ledger are skipped entirely —
226
+ * they are neither orphan-shaped nor ours, and materialising them would double
227
+ * the array for no signal. Revisit if a consumer ever persists `rejected`.
228
+ *
229
+ * @param {PsRow[]} snapshot
230
+ * @param {import('../process-group.mjs').GateProcessRecord[]} ledgerRecords
231
+ * @param {number} nowMs
232
+ * @param {object} [opts]
233
+ * @param {string|null} [opts.ownSessionId]
234
+ * @param {string[]|null} [opts.livePeerSessionIds] Session ids `detectPeers()`
235
+ * reports LIVE — or `null` when the probe could not measure at all.
236
+ * @param {number} [opts.minAgeSeconds]
237
+ * @param {readonly RegExp[]} [opts.readOnlyPatterns]
238
+ * @param {number} [opts.identityToleranceMs]
239
+ * @returns {{candidates: object[], reported: object[], rejected: object[]}}
240
+ */
241
+ export function decideReapCandidates(snapshot, ledgerRecords, nowMs, {
242
+ ownSessionId = null,
243
+ livePeerSessionIds = [],
244
+ minAgeSeconds = REAPER_DEFAULTS.minAgeSeconds,
245
+ readOnlyPatterns = READ_ONLY_COMMAND_PATTERNS,
246
+ identityToleranceMs = 2000,
247
+ } = {}) {
248
+ const rows = Array.isArray(snapshot) ? snapshot : [];
249
+ const records = Array.isArray(ledgerRecords) ? ledgerRecords : [];
250
+ // THREE states, not two: a measured list, an empty measured list, or `null`
251
+ // for "liveness could not be measured". A failed peer probe must not read as
252
+ // "no peers are alive" — that would turn an unmeasured foreign session into a
253
+ // kill target (a missing measurement must never look like a zero).
254
+ const peers = livePeerSessionIds === null || livePeerSessionIds === undefined
255
+ ? null
256
+ : new Set((Array.isArray(livePeerSessionIds) ? livePeerSessionIds : [])
257
+ .filter((id) => typeof id === 'string' && id.length > 0));
258
+
259
+ /** Leader index: the pid the ledger recorded. @type {Map<number, object>} */
260
+ const byPid = new Map();
261
+ /** Group index: every member of a recorded group joins through this.
262
+ * @type {Map<number, object>} */
263
+ const byPgid = new Map();
264
+ for (const rec of records) {
265
+ if (!rec || typeof rec.pid !== 'number') continue;
266
+ byPid.set(rec.pid, rec);
267
+ // First record wins for a pgid: a recycled group id would otherwise let a
268
+ // newer record claim an older group's members.
269
+ if (typeof rec.pgid === 'number' && !byPgid.has(rec.pgid)) byPgid.set(rec.pgid, rec);
270
+ }
271
+
272
+ const candidates = [];
273
+ const reported = [];
274
+ const rejected = [];
275
+
276
+ for (const row of rows) {
277
+ // A `pid` hit is the LEADER; a `pgid` hit is any other member of its group
278
+ // (the measured grandchild case — see the module header). The leader wins,
279
+ // so a row is never judged by the weaker of the two identities.
280
+ const leaderRecord = byPid.get(row.pid) ?? null;
281
+ const record = leaderRecord
282
+ ?? (typeof row.pgid === 'number' ? byPgid.get(row.pgid) ?? null : null);
283
+ const isLeader = leaderRecord !== null;
284
+ const orphanShaped = row.ppid === 1;
285
+
286
+ if (!record) {
287
+ if (orphanShaped) {
288
+ rejected.push({
289
+ pid: row.pid, ppid: row.ppid, ageSeconds: row.etimeSeconds, reason: 'not-in-ledger',
290
+ });
291
+ }
292
+ continue;
293
+ }
294
+
295
+ const recordSessionId = typeof record.sessionId === 'string' && record.sessionId.length > 0
296
+ ? record.sessionId
297
+ : null;
298
+ const base = {
299
+ pid: row.pid,
300
+ pgid: record.pgid,
301
+ ppid: row.ppid,
302
+ isLeader,
303
+ ageSeconds: row.etimeSeconds,
304
+ rssKb: row.rssKb,
305
+ cpuPct: row.cpuPct,
306
+ commandSignature: record.commandSignature ?? null,
307
+ // Normalised to `null`: an empty-string owner is an ABSENT owner, and
308
+ // leaving `''` here would let a downstream truthiness check read it as one.
309
+ sessionId: recordSessionId,
310
+ };
311
+
312
+ // Ledger integrity before anything else: `record.pgid` is the value that
313
+ // gets NEGATED and signalled, and `pgid === pid` is the invariant every
314
+ // record this module writes satisfies.
315
+ if (record.pgid !== record.pid) {
316
+ rejected.push({ ...base, reason: 'pgid-mismatch' });
317
+ continue;
318
+ }
319
+
320
+ if (!orphanShaped) {
321
+ rejected.push({ ...base, reason: 'has-parent' });
322
+ continue;
323
+ }
324
+ if (row.etimeSeconds < minAgeSeconds) {
325
+ rejected.push({ ...base, reason: 'too-young', threshold: { minAgeSeconds } });
326
+ continue;
327
+ }
328
+
329
+ if (recordSessionId === null) {
330
+ // Fail-closed: an unowned record is not an unowned PROCESS. Report it and
331
+ // let the operator (or a fixed producer) decide.
332
+ reported.push({ ...base, reason: 'unattributed' });
333
+ continue;
334
+ }
335
+ const foreignSessionId = recordSessionId !== ownSessionId ? recordSessionId : null;
336
+ if (foreignSessionId !== null) {
337
+ if (peers === null) {
338
+ // Unmeasurable liveness → report, never reap. The only foreign process
339
+ // this function reaps is one whose session a SUCCESSFUL probe proved dead.
340
+ reported.push({ ...base, reason: 'foreign-session-liveness-unknown' });
341
+ continue;
342
+ }
343
+ if (peers.has(foreignSessionId)) {
344
+ // `args` ONLY for a command the allowlist already cleared — the same
345
+ // rule `ARGS_HEAD_CHARS` (`reaper-audit.mjs`) states, applied at the source rather than at
346
+ // the audit writer, so a foreign dev server's command line (paths,
347
+ // tokens) never enters the result in the first place.
348
+ reported.push({
349
+ ...base,
350
+ ...(isReadOnlyCommand(row.args, readOnlyPatterns) ? { args: row.args } : {}),
351
+ reason: 'foreign-live-session',
352
+ });
353
+ continue;
354
+ }
355
+ }
356
+
357
+ if (!isReadOnlyCommand(row.args, readOnlyPatterns)) {
358
+ // No `args` here: a command the allowlist did not recognise is not ours to
359
+ // copy around (dev servers, MCP servers, anything with a command line we
360
+ // have no reason to retain).
361
+ reported.push({ ...base, reason: 'not-read-only' });
362
+ continue;
363
+ }
364
+
365
+ const identity = isLeader
366
+ ? verifyProcessIdentity(
367
+ row.pid,
368
+ { startTime: record.startTime, commandSignature: record.commandSignature },
369
+ { snapshotLine: row, nowMs, toleranceMs: identityToleranceMs },
370
+ )
371
+ : verifyGroupMemberIdentity(row, record, {
372
+ nowMs, toleranceMs: identityToleranceMs, readOnlyPatterns,
373
+ });
374
+ if (!identity.match) {
375
+ rejected.push({
376
+ ...base,
377
+ reason: identity.reason === 'signature-mismatch' ? 'signature-mismatch' : 'identity-mismatch',
378
+ identity: { match: identity.match, reason: identity.reason },
379
+ });
380
+ continue;
381
+ }
382
+
383
+ candidates.push({
384
+ ...base,
385
+ args: row.args,
386
+ ledgerRecord: record,
387
+ identity: { match: identity.match, reason: identity.reason },
388
+ trigger: 'orphan-ppid1',
389
+ threshold: { minAgeSeconds },
390
+ actual: { ageSeconds: row.etimeSeconds },
391
+ });
392
+ }
393
+
394
+ return { candidates, reported, rejected };
395
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * orphan-reaper/scan-throttle.mjs — the B4 throttle the two trigger hooks call.
3
+ *
4
+ * This is the reaper's only hot-path surface: `hooks/on-stop.mjs` and
5
+ * `hooks/post-tool-batch-wave-signal.mjs` import THIS module, not the scan, so a
6
+ * tool batch pays for one `stat` and two tiny modules — never for the ledger,
7
+ * `ps` or kill-ladder code the detached scan child loads.
8
+ */
9
+
10
+ import { lstatSync, mkdirSync, statSync, writeFileSync } from 'node:fs';
11
+ import path from 'node:path';
12
+
13
+ import { REAPER_DEFAULTS, underRepo } from './defaults.mjs';
14
+
15
+ /** Relative path of the scan-throttle marker (B4). Gitignored via
16
+ * `.gitignore:141` (`.orchestrator/tmp/`). */
17
+ export const SCAN_MARKER_RELPATH = '.orchestrator/tmp/reaper-last-scan';
18
+
19
+ /**
20
+ * Throttle gate (B4): may a scan run now?
21
+ *
22
+ * Reads the marker's mtime; a missing or unreadable marker means "yes" — the
23
+ * first scan of a host must not be blocked by the absence of its own throttle
24
+ * file. Writing the marker is the CALLER's job ({@link touchScanMarker}), so
25
+ * this stays a read-only predicate a hook can call cheaply.
26
+ *
27
+ * @param {string} markerPath Absolute path — build it with {@link scanMarkerPath}.
28
+ * @param {number} nowMs
29
+ * @param {number} [minIntervalSeconds]
30
+ * @param {object} [opts]
31
+ * @param {(p: string) => {mtimeMs: number}} [opts.statFn]
32
+ * @returns {boolean}
33
+ */
34
+ export function shouldScanNow(markerPath, nowMs, minIntervalSeconds = REAPER_DEFAULTS.minScanIntervalSeconds, {
35
+ statFn = statSync,
36
+ } = {}) {
37
+ if (typeof markerPath !== 'string' || markerPath.length === 0) return false;
38
+ let mtimeMs;
39
+ try {
40
+ mtimeMs = statFn(markerPath)?.mtimeMs;
41
+ } catch {
42
+ return true; // no marker yet → first scan
43
+ }
44
+ if (typeof mtimeMs !== 'number' || Number.isNaN(mtimeMs)) return true;
45
+ return (nowMs - mtimeMs) >= minIntervalSeconds * 1000;
46
+ }
47
+
48
+ /**
49
+ * Stamp the throttle marker. Best-effort and never throws — a marker that could
50
+ * not be written means the next scan runs, which is the safe direction for a
51
+ * read-only probe.
52
+ *
53
+ * A marker path that is a symlink or not a regular file is left alone and
54
+ * reported as not written: `writeFileSync` follows a link, so a marker linked to
55
+ * any file would overwrite that file on every hook (CWE-59). Silent, like every
56
+ * other failure here — this runs inside a 50 ms hook budget.
57
+ * Named ceiling (BV-004): lstat-then-write leaves a check-to-use window of one
58
+ * syscall; a link planted inside it is still written through. Acceptable for a
59
+ * gitignored throttle stamp; revisit (O_NOFOLLOW open) if the marker ever
60
+ * carries data.
61
+ *
62
+ * @param {string} markerPath
63
+ * @param {object} [opts]
64
+ * @param {(p: string, data: string) => void} [opts.writeFn]
65
+ * @returns {boolean} whether the marker was written
66
+ */
67
+ export function touchScanMarker(markerPath, { writeFn } = {}) {
68
+ try {
69
+ if (writeFn) {
70
+ writeFn(markerPath, `${new Date().toISOString()}\n`);
71
+ return true;
72
+ }
73
+ let existing = null;
74
+ try {
75
+ existing = lstatSync(markerPath);
76
+ } catch {
77
+ /* no marker yet — the first stamp creates it */
78
+ }
79
+ if (existing && !existing.isFile()) return false;
80
+ mkdirSync(path.dirname(markerPath), { recursive: true });
81
+ writeFileSync(markerPath, `${new Date().toISOString()}\n`, 'utf8');
82
+ return true;
83
+ } catch {
84
+ return false;
85
+ }
86
+ }
87
+
88
+ /** Absolute path of the throttle marker for a repo. One constant per path.
89
+ * @param {string} repoRoot @returns {string} */
90
+ export function scanMarkerPath(repoRoot) {
91
+ return underRepo(repoRoot, SCAN_MARKER_RELPATH);
92
+ }