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,506 @@
1
+ /**
2
+ * orphan-reaper/scan.mjs — one orphan scan end to end: `ps` → decide → (unless
3
+ * `dryRun`) re-verify identity before every signal → kill the group → audit.
4
+ *
5
+ * The IMPURE half of the reaper. Everything impure reaches the pure decision
6
+ * (`reaper-decide.mjs`) through `deps` ({@link resolveDeps}), the
7
+ * `lock-reaper.mjs` split of `evaluateRepo()` / `archiveLock()`.
8
+ */
9
+
10
+ import {
11
+ killProcessGroup,
12
+ pruneGateProcessLedger,
13
+ readGateProcessLedger,
14
+ verifyProcessIdentity,
15
+ } from '../process-group.mjs';
16
+ import { REAPER_DEFAULTS } from './defaults.mjs';
17
+ import { parsePsSnapshot, parsePsSnapshotDetailed, runPs, runPsPid } from './ps-snapshot.mjs';
18
+ import {
19
+ FALSE_ALARM_SUSPECT_RATE,
20
+ appendAuditRecord,
21
+ buildAuditRecord,
22
+ falseAlarmRate,
23
+ pruneReaperAudit,
24
+ readAuditRecords,
25
+ } from './reaper-audit.mjs';
26
+ import { decideReapCandidates, verifyGroupMemberIdentity } from './reaper-decide.mjs';
27
+
28
+ /** Name const for the one event this module emits. @see docs/events-schema.md */
29
+ export const REAPER_SCAN_EVENT = 'orchestrator.reaper.scan_completed';
30
+
31
+ /** Default own-session-id reader. Lazily imported so the static import closure
32
+ * of this module stays small — it is destined for a hot-path hook (#1432).
33
+ * @param {string} repoRoot @returns {Promise<string|null>} */
34
+ async function defaultReadOwnSessionId(repoRoot) {
35
+ try {
36
+ const mod = await import('../session-identity/own-session.mjs');
37
+ const ids = mod.readOwnSessionIds(repoRoot);
38
+ for (const id of ids) return id;
39
+ return null;
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Default live-peer probe. Lazily imported for the same reason as
47
+ * {@link defaultReadOwnSessionId}.
48
+ *
49
+ * Returns `null` — never `[]` — when the registry cannot be read. The two mean
50
+ * opposite things to `decideReapCandidates`: `[]` is a MEASUREMENT ("no
51
+ * peer is alive", so a foreign session's leftovers are reapable), `null` is the
52
+ * absence of one ("report it, do not touch it").
53
+ *
54
+ * @param {string|null} sessionId
55
+ * @returns {Promise<string[]|null>}
56
+ */
57
+ async function defaultDetectPeers(sessionId) {
58
+ try {
59
+ const mod = await import('../session-registry.mjs');
60
+ const peers = await mod.detectPeers({ sessionId: sessionId ?? undefined });
61
+ if (!Array.isArray(peers)) return null;
62
+ return peers.map((p) => p?.session_id).filter((v) => typeof v === 'string' && v.length > 0);
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Default event emitter. Lazily imported for the same reason as
70
+ * {@link defaultReadOwnSessionId}: `events.mjs` pulls in the schema validator,
71
+ * the attribution chain and the webhook client, and this module is destined for
72
+ * a hot-path hook (#1432) whose static import closure is measured.
73
+ *
74
+ * Never throws and never rejects — telemetry that can fail a scan would fail the
75
+ * hook the scan runs in.
76
+ *
77
+ * @param {string} type
78
+ * @param {object} payload
79
+ * @param {{repoRoot?: string}} [opts]
80
+ * @returns {Promise<boolean>} whether the record was written
81
+ */
82
+ async function defaultEmitEvent(type, payload, opts = {}) {
83
+ try {
84
+ const mod = await import('../events.mjs');
85
+ await mod.emitEvent(type, payload, opts);
86
+ return true;
87
+ } catch {
88
+ return false;
89
+ }
90
+ }
91
+
92
+ /** @param {number} ms @returns {Promise<void>} */
93
+ function defaultSleep(ms) {
94
+ return new Promise((resolve) => { setTimeout(resolve, ms); });
95
+ }
96
+
97
+ /**
98
+ * Resolve the adapter, defaulting every seam to the real implementation — the
99
+ * `resolveDeps()` pattern from `lock-reaper.mjs:77-100`.
100
+ *
101
+ * `killProcessGroup` HAS a real default, and that is safe only because
102
+ * {@link runOrphanScan} defaults `dryRun: true`. Tests ALWAYS inject it
103
+ * (`.claude/rules/testing.md`: never a real process as a kill target).
104
+ *
105
+ * @param {object} [deps]
106
+ * @returns {object}
107
+ */
108
+ export function resolveDeps(deps = {}) {
109
+ const d = deps ?? {};
110
+ return {
111
+ runPs: d.runPs ?? runPs,
112
+ runPsPid: d.runPsPid ?? runPsPid,
113
+ readLedger: d.readLedger ?? readGateProcessLedger,
114
+ verifyIdentity: d.verifyIdentity ?? verifyProcessIdentity,
115
+ killProcessGroup: d.killProcessGroup ?? killProcessGroup,
116
+ detectPeers: d.detectPeers ?? defaultDetectPeers,
117
+ readOwnSessionId: d.readOwnSessionId ?? defaultReadOwnSessionId,
118
+ appendAudit: d.appendAudit ?? appendAuditRecord,
119
+ readAuditRecords: d.readAuditRecords ?? readAuditRecords,
120
+ emitEvent: d.emitEvent ?? defaultEmitEvent,
121
+ now: d.now ?? Date.now,
122
+ sleep: d.sleep ?? defaultSleep,
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Map an identity verdict's reason to the audit's `reason` vocabulary.
128
+ *
129
+ * Three outcomes, never two: `gone` (measured, the process is not there),
130
+ * `signature-mismatch` / `identity-mismatch` (measured, it is a DIFFERENT
131
+ * process) and `unmeasured` (the probe itself could not answer). The third is
132
+ * the one a two-state mapping loses, and losing it is how an absent measurement
133
+ * starts reading like a clean verdict.
134
+ *
135
+ * @param {string|null|undefined} identityReason
136
+ * @returns {'gone'|'signature-mismatch'|'identity-mismatch'|'unmeasured'}
137
+ */
138
+ function rejectReasonFor(identityReason) {
139
+ if (identityReason === 'gone') return 'gone';
140
+ if (identityReason === 'unmeasured') return 'unmeasured';
141
+ if (identityReason === 'signature-mismatch') return 'signature-mismatch';
142
+ return 'identity-mismatch';
143
+ }
144
+
145
+ /**
146
+ * Run one orphan scan: `ps` → decide → (unless `dryRun`) re-verify identity
147
+ * against a FRESH snapshot → kill the group → audit.
148
+ *
149
+ * NO-THROW contract: every failure degrades into `skipped: '<reason>'` or a
150
+ * `rejected` entry, because this runs from a hook and must never fail it
151
+ * (PRD FA4: "bei jedem Fehler degradiert er lautlos").
152
+ *
153
+ * The pre-signal re-check is the TOCTOU defence and the whole point of FA3: a
154
+ * PID recycled between decision and signal must NOT be signalled. It runs a
155
+ * TARGETED `ps` (`psPidArgs`, `ps-snapshot.mjs`) through `deps.runPsPid` and feeds the row to
156
+ * {@link verifyProcessIdentity} — immediately before SIGTERM **and** immediately
157
+ * before SIGKILL, via `killProcessGroup`'s `beforeSignal` gate, because the
158
+ * ladder's 10 s grace is itself a recycling window. On any mismatch no further
159
+ * signal is sent and the candidate is recorded as `decision: 'reject'` with the
160
+ * reason that withdrew it. Such a candidate stays in `candidates` (that WAS the
161
+ * decision) and additionally appears in `rejected` (that is the withdrawal) —
162
+ * the two arrays answer different questions and collapsing them would lose the
163
+ * TOCTOU event.
164
+ *
165
+ * Success is proven the same way, never from an exit code: after the ladder the
166
+ * scan waits `verifyWaitMs` and re-measures (B6). `ok` is true only for a
167
+ * process that is GONE; one that outlived SIGKILL is booked
168
+ * `survivedSigkill: true`, and one the probe could not measure is `verified:
169
+ * 'unmeasured'` — neither is ever a success.
170
+ *
171
+ * `dryRun` defaults to TRUE: arming happens at the CALL SITE (#1432), so a
172
+ * caller that forgets the flag scans and reports instead of killing.
173
+ *
174
+ * @param {object} opts
175
+ * @param {string} opts.repoRoot
176
+ * @param {number} [opts.now]
177
+ * @param {boolean} [opts.dryRun]
178
+ * @param {object} [opts.deps]
179
+ * @param {number} [opts.minAgeSeconds]
180
+ * @param {number} [opts.killGraceMs]
181
+ * @param {number} [opts.verifyWaitMs]
182
+ * @param {number} [opts.falseAlarmWindow] Rolling window of audit DECISIONS the
183
+ * HR-101 rate is judged over (`reaper.false-alarm-window`).
184
+ * @returns {Promise<{scanned: number, candidates: object[], reported: object[],
185
+ * rejected: object[], killed: object[], unattributed: number,
186
+ * peerLiveness: 'measured'|'unmeasured'|null, skipped?: string, malformed: number,
187
+ * durationMs: number, instrumentSuspect: boolean|null, falseAlarmRate: number|null,
188
+ * falseAlarmWindowN: number}>}
189
+ * `instrumentSuspect`/`falseAlarmRate` are `null` on a degraded (`skipped`) scan
190
+ * and `falseAlarmRate` is `null` below the 10-decision floor — in both cases a
191
+ * measurement that does not exist, never a measured zero. `peerLiveness` is the
192
+ * same distinction for the peer probe, and `null` only on a degraded scan.
193
+ */
194
+ export async function runOrphanScan({
195
+ repoRoot,
196
+ now,
197
+ dryRun = true,
198
+ deps,
199
+ minAgeSeconds = REAPER_DEFAULTS.minAgeSeconds,
200
+ killGraceMs = REAPER_DEFAULTS.killGraceMs,
201
+ verifyWaitMs = REAPER_DEFAULTS.verifyWaitMs,
202
+ falseAlarmWindow = REAPER_DEFAULTS.falseAlarmWindow,
203
+ } = {}) {
204
+ const d = resolveDeps(deps);
205
+ const startedAt = typeof now === 'number' ? now : d.now();
206
+ // A degraded scan measured NOTHING — including the instrument's own health.
207
+ // `null` rather than `false`/`0` for both instrument fields: a scan that never
208
+ // ran must not report a healthy instrument it never looked at.
209
+ const empty = (skipped, malformed = 0) => ({
210
+ scanned: 0,
211
+ candidates: [],
212
+ reported: [],
213
+ rejected: [],
214
+ killed: [],
215
+ unattributed: 0,
216
+ // Nothing was measured, so peer liveness was not measured either — `null`
217
+ // rather than the string, because "unmeasured" is a MEASUREMENT OUTCOME and
218
+ // a degraded scan never got as far as the probe.
219
+ peerLiveness: null,
220
+ skipped,
221
+ malformed,
222
+ durationMs: Math.max(0, d.now() - startedAt),
223
+ instrumentSuspect: null,
224
+ falseAlarmRate: null,
225
+ falseAlarmWindowN: 0,
226
+ });
227
+
228
+ if (typeof repoRoot !== 'string' || repoRoot.length === 0) return empty('no-repo-root');
229
+
230
+ let text;
231
+ try {
232
+ text = await d.runPs();
233
+ } catch {
234
+ return empty('ps-failed');
235
+ }
236
+ if (text === null || text === undefined) return empty('ps-failed');
237
+
238
+ const { rows, malformed } = parsePsSnapshotDetailed(text);
239
+
240
+ let ledger;
241
+ try {
242
+ ledger = d.readLedger(repoRoot, { nowMs: startedAt });
243
+ } catch {
244
+ return empty('ledger-unreadable', malformed);
245
+ }
246
+
247
+ let ownSessionId = null;
248
+ try {
249
+ ownSessionId = await d.readOwnSessionId(repoRoot);
250
+ } catch {
251
+ ownSessionId = null;
252
+ }
253
+
254
+ /** @type {string[]|null} */
255
+ let livePeerSessionIds;
256
+ try {
257
+ livePeerSessionIds = await d.detectPeers(ownSessionId);
258
+ } catch {
259
+ // null = unmeasured, NOT "no peers" — see decideReapCandidates.
260
+ livePeerSessionIds = null;
261
+ }
262
+
263
+ const { candidates, reported, rejected } = decideReapCandidates(
264
+ rows,
265
+ ledger?.records ?? [],
266
+ startedAt,
267
+ { ownSessionId, livePeerSessionIds, minAgeSeconds },
268
+ );
269
+
270
+ const timestamp = new Date(startedAt).toISOString();
271
+ const audit = (entry, decision, extra = {}) => {
272
+ try {
273
+ d.appendAudit(repoRoot, buildAuditRecord(entry, decision, {
274
+ timestamp, sessionId: ownSessionId, ...extra,
275
+ }));
276
+ } catch {
277
+ /* the audit is an aid, never a precondition */
278
+ }
279
+ };
280
+
281
+ for (const entry of reported) audit(entry, 'report');
282
+
283
+ /** @type {object[]} */
284
+ const killed = [];
285
+
286
+ /**
287
+ * Close the scan: measure the instrument's own health (HR-101), emit at most
288
+ * one event, return the result. Both real exits go through here so the rate
289
+ * and the event can never be computed twice or forgotten once.
290
+ * @returns {Promise<object>}
291
+ */
292
+ const complete = async () => {
293
+ let auditRecords;
294
+ try {
295
+ auditRecords = (await d.readAuditRecords(repoRoot, falseAlarmWindow)) ?? [];
296
+ } catch {
297
+ auditRecords = [];
298
+ }
299
+ const fa = falseAlarmRate(auditRecords, falseAlarmWindow);
300
+ // HR-101: the rate re-aims the instrument, it never re-thresholds it — so
301
+ // this flag is REPORTED and nothing here branches on it.
302
+ const instrumentSuspect = typeof fa.rate === 'number' && fa.rate > FALSE_ALARM_SUSPECT_RATE;
303
+ const survivedSigkill = killed.filter((k) => k.survivedSigkill === true).length;
304
+ const durationMs = Math.max(0, d.now() - startedAt);
305
+ // Two counts that must not hide inside `reported`: an unattributed record is
306
+ // a PRODUCER defect (the gate runner passed no session id), and an
307
+ // unmeasured peer probe is an INSTRUMENT gap. Folded into the generic
308
+ // `reported` number, both are invisible — which is how the foreign-session
309
+ // guard stayed inert across 377 of 377 records without anything saying so.
310
+ const unattributed = reported.filter((r) => r.reason === 'unattributed').length;
311
+ const peerLiveness = livePeerSessionIds === null ? 'unmeasured' : 'measured';
312
+
313
+ // HR-101 again, in the other direction: a signal that fires on every hook
314
+ // is noise nobody reads. A scan that found nothing emits nothing — the
315
+ // absence of a record IS the healthy state, and `instrumentSuspect` is the
316
+ // one finding that must surface even from an empty scan.
317
+ if (candidates.length + reported.length + killed.length > 0 || instrumentSuspect) {
318
+ try {
319
+ await d.emitEvent(REAPER_SCAN_EVENT, {
320
+ scanned: rows.length,
321
+ candidates: candidates.length,
322
+ reported: reported.length,
323
+ rejected: rejected.length,
324
+ killed: killed.length,
325
+ unattributed,
326
+ peer_liveness: peerLiveness,
327
+ survived_sigkill: survivedSigkill,
328
+ dry_run: dryRun === true,
329
+ duration_ms: durationMs,
330
+ instrument_suspect: instrumentSuspect,
331
+ // OMITTED below the 10-decision floor, never 0: absence means "no
332
+ // population yet", and a fabricated zero would read as a clean
333
+ // instrument (`falseAlarmRate` returns `{rate: null}` there).
334
+ ...(typeof fa.rate === 'number' ? { false_alarm_rate: fa.rate } : {}),
335
+ }, { repoRoot });
336
+ } catch {
337
+ /* telemetry that can fail a scan would fail the hook the scan runs in */
338
+ }
339
+ }
340
+
341
+ // Housekeeping, last and best-effort: the ledger is append-only and grew
342
+ // monotonically (measured 2026-09-22: 369 lines/day) because nothing in
343
+ // production ever called the pruner.
344
+ try {
345
+ pruneGateProcessLedger(repoRoot);
346
+ // The audit is pruned AFTER it was read for the rate above, so a prune
347
+ // can never shrink the population this scan judged itself on.
348
+ pruneReaperAudit(repoRoot);
349
+ } catch {
350
+ /* housekeeping, never a precondition of a scan */
351
+ }
352
+
353
+ return {
354
+ scanned: rows.length,
355
+ candidates,
356
+ reported,
357
+ rejected,
358
+ killed,
359
+ unattributed,
360
+ peerLiveness,
361
+ malformed,
362
+ durationMs,
363
+ instrumentSuspect,
364
+ falseAlarmRate: fa.rate,
365
+ falseAlarmWindowN: fa.n,
366
+ };
367
+ };
368
+
369
+ if (dryRun) {
370
+ for (const c of candidates) audit(c, 'dry-run');
371
+ return complete();
372
+ }
373
+
374
+ /**
375
+ * ONE fresh, targeted identity measurement for one candidate (B3).
376
+ *
377
+ * There is deliberately no second, bulk `ps` pass any more: two TOCTOU
378
+ * instruments measuring the same property at different freshness is how the
379
+ * stale one silently wins. This is the only pre-signal measurement, and it is
380
+ * taken immediately before each signal rather than once for the whole pass.
381
+ *
382
+ * @param {object} c
383
+ * @returns {Promise<{match: boolean, reason: string, observed?: object}>}
384
+ */
385
+ const freshIdentity = async (c) => {
386
+ let text;
387
+ try {
388
+ text = await d.runPsPid(c.pid);
389
+ } catch {
390
+ text = null;
391
+ }
392
+ // `ps -p` cannot distinguish "gone" from "could not run"; both must refuse
393
+ // the signal, and only the second is an instrument gap worth its own reason.
394
+ if (text === null || text === undefined) return { match: false, reason: 'unmeasured' };
395
+ const row = parsePsSnapshot(text).find((r) => r.pid === c.pid) ?? null;
396
+ try {
397
+ // A group MEMBER is re-verified the way it was decided — by the leader's
398
+ // seam it would fail every time (its own start time, its own command),
399
+ // and a pre-signal check that always refuses is a disarmed feature, not a
400
+ // strict one.
401
+ if (c.isLeader === false) {
402
+ return verifyGroupMemberIdentity(row, c.ledgerRecord, { nowMs: d.now() });
403
+ }
404
+ return d.verifyIdentity(
405
+ c.pid,
406
+ { startTime: c.ledgerRecord.startTime, commandSignature: c.ledgerRecord.commandSignature },
407
+ { snapshotLine: row, nowMs: d.now() },
408
+ );
409
+ } catch {
410
+ return { match: false, reason: 'gone' };
411
+ }
412
+ };
413
+
414
+ // ONE ladder per GROUP, not per row: with the pgid join a single group can
415
+ // contribute several candidate rows (leader + descendants), and they all name
416
+ // the same kill target. Leaders first, so the target of a group is the process
417
+ // the ledger actually recorded whenever it is still alive.
418
+ const killTargets = [];
419
+ const coveredPgids = new Set();
420
+ for (const c of [...candidates.filter((x) => x.isLeader), ...candidates.filter((x) => !x.isLeader)]) {
421
+ if (coveredPgids.has(c.pgid)) continue;
422
+ coveredPgids.add(c.pgid);
423
+ killTargets.push(c);
424
+ }
425
+
426
+ for (const c of killTargets) {
427
+ /** Set by the gate below when it refuses; `null` means every signal was permitted. */
428
+ let withdrawal = null;
429
+ const beforeSignal = async (signal) => {
430
+ const identity = await freshIdentity(c);
431
+ if (identity?.match === true) return true;
432
+ withdrawal = { signal, identity, reason: rejectReasonFor(identity?.reason) };
433
+ return false;
434
+ };
435
+
436
+ let result;
437
+ try {
438
+ result = await d.killProcessGroup(c.pgid, {
439
+ killGraceMs, verifyWaitMs, sleepFn: d.sleep, beforeSignal,
440
+ });
441
+ } catch (err) {
442
+ result = { ok: false, signalsSent: [], survivors: [c.pgid], error: err?.code ?? null, aborted: null };
443
+ }
444
+ const signalsSent = Array.isArray(result?.signalsSent) ? result.signalsSent : [];
445
+
446
+ if (withdrawal !== null) {
447
+ // The candidate was withdrawn between decision and signal. It stays in
448
+ // `candidates` (that WAS the decision) and appears in `rejected` (that is
449
+ // the withdrawal) — collapsing the two would lose the TOCTOU event.
450
+ // `signalsSent` is carried even here: a withdrawal before the ESCALATION
451
+ // still means a SIGTERM went out, and an audit that hid it would
452
+ // under-report what this reaper did to the host.
453
+ rejected.push({
454
+ ...c, reason: withdrawal.reason, identity: withdrawal.identity, signalsSent,
455
+ });
456
+ audit(c, 'reject', {
457
+ reason: withdrawal.reason,
458
+ result: {
459
+ ok: false,
460
+ signalsSent,
461
+ survivors: [],
462
+ survivedSigkill: false,
463
+ verifiedAfterMs: 0,
464
+ verified: 'withdrawn',
465
+ },
466
+ });
467
+ continue;
468
+ }
469
+
470
+ // B6: prove the EFFECT, after a wait, from a fresh measurement of the
471
+ // process table. An exit code and a sent signal prove nothing — on
472
+ // 2026-09-20 a probe with no wait reported "still alive" for dead processes.
473
+ let verified = 'unverified';
474
+ if (signalsSent.length > 0) {
475
+ await d.sleep(verifyWaitMs);
476
+ const after = await freshIdentity(c);
477
+ if (after?.match === true) verified = 'alive';
478
+ else if (after?.reason === 'unmeasured') verified = 'unmeasured';
479
+ else verified = 'gone';
480
+ }
481
+ const ok = verified === 'gone';
482
+ const survivedSigkill = verified === 'alive' && signalsSent.includes('SIGKILL');
483
+ const survivors = ok ? [] : [c.pgid];
484
+ const verifiedAfterMs = signalsSent.length > 0 ? verifyWaitMs : 0;
485
+
486
+ killed.push({
487
+ pid: c.pid,
488
+ pgid: c.pgid,
489
+ // Every candidate row this one ladder covers — the group join means one
490
+ // signal can end several candidates, and a `killed` list that named only
491
+ // the target would under-report what the reaper did to the host.
492
+ groupMemberPids: candidates.filter((x) => x.pgid === c.pgid).map((x) => x.pid),
493
+ ok,
494
+ signalsSent,
495
+ survivors,
496
+ survivedSigkill,
497
+ verified,
498
+ verifiedAfterMs,
499
+ });
500
+ audit(c, 'kill', {
501
+ result: { ok, signalsSent, survivors, survivedSigkill, verifiedAfterMs, verified },
502
+ });
503
+ }
504
+
505
+ return complete();
506
+ }