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
@@ -22,23 +22,32 @@
22
22
  * "host": "string",
23
23
  * "started_at": "2026-05-23T21:30:00.000Z",
24
24
  * "staged_paths": [
25
- * { "command": "git add src/foo.ts", "timestamp": "2026-…" },
25
+ * { "paths": ["src/foo.ts"], "command_hash": "a635c66ec7e7bf56",
26
+ * "timestamp": "2026-…" },
26
27
  * ...
27
28
  * ]
28
29
  * }
29
30
  *
30
- * The hook does NOT parse the `git add` argv into paths — argv parsing is
31
- * fragile (globs, pathspec magic, `-A`, `-u`, `--`). Instead it records the
32
- * raw command string; the commit-guard reconciles staged paths via
33
- * `git diff --cached --name-only` and cross-checks them against ALL sibling
34
- * fence files at commit time (where the staged set is unambiguous).
31
+ * The entry records the PATH OPERANDS, never the command text (#1404): a
32
+ * staging command can carry a secret (`GIT_TOKEN=… git add x`), and the raw
33
+ * string was persisted verbatim to disk — the same defect 8f15f77b removed
34
+ * from hooks/enforce-commands.mjs. Paths are what the only reader
35
+ * (hooks/wave-scope-commit-guard.mjs) actually needs; `command_hash` keeps
36
+ * the entry countable and groupable without the text.
37
+ *
38
+ * `paths: ["*"]` means "stages a set no operand names" (`git add -A`, `.`,
39
+ * `-u`) and overlaps EVERY staged path — the case the reader's old raw-text
40
+ * regex could not see at all, because `-A` contains no path to match.
35
41
  *
36
42
  * Decision flow (G1-G6 early-return ladder):
37
43
  * G1 tool filter — Bash only
38
44
  * G2 command must be a non-empty string
39
- * G3 regex match /\bgit\s+add\b/ — only `git add` invocations
45
+ * G3 cheap regex PRE-FILTER — skip the tokenizer for plainly unrelated
46
+ * commands (see GIT_ADD_REGEX; it does not decide the question)
40
47
  * G4 context gate — isWaveAgentContext(); exit 0 immediately for
41
48
  * coordinator/manual commits (AC5 safe-default no-op)
49
+ * G4b tokenize + extract path operands; exit 0 when no `git add` statement
50
+ * resolves (a pre-filter false positive writes nothing)
42
51
  * G5 derive agent_id, build fence dir + path
43
52
  * G6 read-modify-write the fence file (append the staging-intent entry).
44
53
  * Errors are warned + swallowed — the hook NEVER blocks the Bash call.
@@ -64,29 +73,303 @@ import os from 'node:os';
64
73
  import crypto from 'node:crypto';
65
74
 
66
75
  import { shouldRunHook } from './_lib/profile-gate.mjs';
76
+ import { isMainModule } from '../scripts/lib/is-main-module.mjs';
67
77
  import { stableHostname } from '../scripts/lib/host-identity.mjs';
68
- if (!shouldRunHook('pre-bash-staging-fence')) process.exit(0);
69
78
 
70
79
  // ---------------------------------------------------------------------------
71
80
  // Constants
72
81
  // ---------------------------------------------------------------------------
73
82
 
74
83
  /**
75
- * Matches `git add ...` invocations with word boundaries on both sides so
76
- * `gitadd` and `git addremote` (hypothetical) do not match. Anchored to the
77
- * `git` token, so `echo "git add"` is not matched (echo ≠ verb position).
78
- *
79
- * We deliberately accept `git add` anywhere in the command string — operators
80
- * sometimes prefix env-vars (`GIT_COMMITTER_NAME=foo git add ...`) or chain
81
- * commands (`cd repo && git add ...`). False positives are tolerable here
82
- * (the hook only writes a log line, never blocks).
84
+ * Cheap PRE-FILTER for the staging-command gate (G3). It answers "is it worth
85
+ * loading the tokenizer for this command?", never "is this a staging
86
+ * command?" — since #1404 that verdict belongs to {@link extractStagedPaths},
87
+ * which resolves the verb and subcommand properly.
88
+ *
89
+ * `git` and `add` must sit in the same statement (the negated class stops at
90
+ * `;`, `&`, `|` and newline) within 120 characters of each other, so global
91
+ * flags between them are tolerated: `git -C sub add x` reaches the extractor,
92
+ * where `\bgit\s+add\b` used to drop it silently. `\badd\b` keeps
93
+ * `git addremote` out.
94
+ *
95
+ * False positives are cheap AND inert: `git commit -m "add x"` matches here,
96
+ * resolves to no `git add` statement, and writes nothing (G4b).
97
+ */
98
+ const GIT_ADD_REGEX = /\bgit\b[^;&|\n]{0,120}\badd\b/;
99
+
100
+ /**
101
+ * Recorded in place of a path list when the staging command names NO explicit
102
+ * path operand but stages a set we cannot enumerate — `git add -A`,
103
+ * `git add .`, `git add -u`, `git add --pathspec-from-file=<f>`. The reader
104
+ * (hooks/wave-scope-commit-guard.mjs) treats it as "overlaps every staged
105
+ * path". This is the case the previous raw-command regex silently MISSED:
106
+ * `-A` contains no path text, so `pathRegex(ours).test('git add -A')` was
107
+ * always false and the widest staging command of all fenced nothing.
83
108
  */
84
- const GIT_ADD_REGEX = /\bgit\s+add\b/;
109
+ const ALL_PATHS_MARKER = '*';
110
+
111
+ /** git GLOBAL flags (before the subcommand) that consume the NEXT token. */
112
+ const GIT_GLOBAL_VALUE_FLAGS = new Set([
113
+ '-C', '-c', '--git-dir', '--work-tree', '--namespace', '--exec-path', '--config-env',
114
+ ]);
115
+
116
+ /** `git add` flags that consume the NEXT token as their value. */
117
+ const GIT_ADD_VALUE_FLAGS = new Set(['--chmod']);
118
+
119
+ /** `git add` long flags that stage a set no path operand names. */
120
+ const GIT_ADD_ALL_FLAGS = new Set(['--all', '--no-ignore-removal', '--update']);
121
+
122
+ /**
123
+ * Shells whose `-c <payload>` operand is a command line the shell EXECUTES, so
124
+ * a `git add` inside it stages this working copy for real.
125
+ *
126
+ * DELIBERATE DUPLICATE of `DASH_C_SHELLS` in scripts/lib/command-blocker.mjs —
127
+ * that module does not export it (nor its `dashCPayloads`), and #1404's own
128
+ * rule for this hook is to consume the tokenizer, not to edit it (5 other
129
+ * consumers). Change one, change both.
130
+ */
131
+ const DASH_C_SHELLS = new Set(['bash', 'sh', 'zsh', 'dash', 'ksh', 'su']);
132
+
133
+ /**
134
+ * Payload recursion depth for {@link scanForStagedPaths}. ONE level: it covers
135
+ * `bash -c "git add x"` and `xargs -I{} sh -c "git add x"`, which is the whole
136
+ * measured regression class (2026-09-20), and keeps a hook that runs on EVERY
137
+ * Bash call from re-tokenizing an arbitrarily nested string.
138
+ *
139
+ * NAMED CEILING (BV-004): `bash -c "bash -c 'git add x'"` is not recorded.
140
+ * REVISIT TRIGGER: raise to 2 if a doubly-nested staging command is ever
141
+ * observed in a wave transcript.
142
+ */
143
+ const MAX_PAYLOAD_DEPTH = 1;
85
144
 
86
145
  // ---------------------------------------------------------------------------
87
146
  // Helpers
88
147
  // ---------------------------------------------------------------------------
89
148
 
149
+ /**
150
+ * sha256(command) truncated to 16 hex chars.
151
+ *
152
+ * DELIBERATE DUPLICATE of hooks/enforce-commands.mjs (itself a copy of
153
+ * hooks/pre-bash-destructive-guard.mjs) — see the 8f15f77b commit message:
154
+ * the three guards stay import-free of each other on purpose, so a shared
155
+ * helper is NOT introduced. The hash keeps a fence entry COUNTABLE and
156
+ * GROUPABLE without persisting command text that may carry a secret
157
+ * (`GIT_TOKEN=… git add x`) — issue #1404.
158
+ *
159
+ * @param {string} command
160
+ * @returns {string}
161
+ */
162
+ function hashCommand(command) {
163
+ return crypto.createHash('sha256').update(command).digest('hex').slice(0, 16);
164
+ }
165
+
166
+ /**
167
+ * Normalise a path token so writer and reader compare the same spelling.
168
+ *
169
+ * DELIBERATE DUPLICATE of the identically-named function in
170
+ * hooks/wave-scope-commit-guard.mjs — the reader is a husky pre-commit hook
171
+ * outside the Claude-Code hook import set, and the two must agree byte for
172
+ * byte. Change one, change both.
173
+ *
174
+ * @param {string} raw
175
+ * @returns {string}
176
+ */
177
+ function normalizeStagedPath(raw) {
178
+ let p = String(raw).trim();
179
+ while (p.startsWith('./')) p = p.slice(2);
180
+ p = p.replace(/\/{2,}/g, '/');
181
+ while (p.length > 1 && p.endsWith('/')) p = p.slice(0, -1);
182
+ return p;
183
+ }
184
+
185
+ /**
186
+ * Extract the PATH OPERANDS of every `git add` statement in a Bash command.
187
+ *
188
+ * Reuses the repo's own shell tokenizer (scripts/lib/command-blocker.mjs) —
189
+ * no second shell grammar is invented here. The import is LAZY: this hook
190
+ * runs on every Bash call of every session, and only a `git add` command
191
+ * (G3) ever reaches this function, so the tokenizer's module graph must not
192
+ * be paid for by the other 99% of calls.
193
+ *
194
+ * Handled: `git add -- a b`, quoted operands with spaces, `git -C <dir> add x`
195
+ * (relative `-C` is prefixed onto the operand), chained statements
196
+ * (`cd x && git add y` — each statement is its own segment), leading env
197
+ * assignments, and the enumerate-nothing flags that yield ALL_PATHS_MARKER.
198
+ *
199
+ * NAMED CEILINGS (BV-004):
200
+ * - Operands are recorded as WRITTEN, relative to the invoking cwd. A
201
+ * `cd subdir && git add foo.ts` records `foo.ts` while the staged path is
202
+ * `subdir/foo.ts` → a miss. This is exactly the pre-#1404 behaviour (the
203
+ * raw-command regex missed it too), not a new gap; revisit if a wave agent
204
+ * is ever observed staging from a subdirectory.
205
+ * - An ABSOLUTE operand (or an absolute `-C`) is recorded verbatim and will
206
+ * not match a repo-relative staged path. Same ceiling, same trigger.
207
+ * - A command the tokenizer resolves to no `git add` statement (`echo "git
208
+ * add x"` — G3's documented, tolerated false positive) yields an EMPTY
209
+ * list, never the marker: over-reporting here would turn a log line into a
210
+ * blocked commit, which the hook's fail-safe posture forbids. This is also
211
+ * why a blanket "pre-filter hit but nothing resolved → `paths: ['*']`"
212
+ * floor was REJECTED at the 2026-09-20 fix: `git commit -m "add x"` matches
213
+ * {@link GIT_ADD_REGEX}, and the marker overlaps EVERY staged path, so that
214
+ * floor manufactures a collision for a command that stages nothing.
215
+ * - A staging command a shell never executes locally is not recorded, and
216
+ * should not be: `ssh host 'git add x'` stages a REMOTE index, and
217
+ * `git submodule foreach "git add x"` stages the SUBMODULE's — neither can
218
+ * collide with a sibling agent in this working copy.
219
+ * REVISIT TRIGGER: if the commit-guard ever reconciles submodule indexes.
220
+ *
221
+ * @param {string} command
222
+ * @returns {Promise<{ recognised: boolean, paths: string[] }>}
223
+ */
224
+ async function extractStagedPaths(command) {
225
+ let lib;
226
+ try {
227
+ lib = await import('../scripts/lib/command-blocker.mjs');
228
+ } catch {
229
+ return { recognised: false, paths: [] };
230
+ }
231
+
232
+ const paths = new Set();
233
+ const state = { recognised: false };
234
+ scanForStagedPaths(command, lib, paths, state, 0);
235
+ return { recognised: state.recognised, paths: [...paths] };
236
+ }
237
+
238
+ /**
239
+ * Collect the command lines a shell nested in this segment will EXECUTE.
240
+ *
241
+ * Two sources, unioned:
242
+ * - wrapper payloads `resolveSegmentVerb` already reports (`env -S '…'`),
243
+ * across both readings of an ambiguous dual parse (#1000);
244
+ * - a `-c`-style operand following a {@link DASH_C_SHELLS} token ANYWHERE in
245
+ * the segment, not only in resolved-verb position. The wider scan is what
246
+ * covers `xargs -I{} sh -c "git add x"` and `find … -exec sh -c "…" \;`,
247
+ * whose resolved verb is `xargs`/`find`; keying on the shell TOKEN instead
248
+ * of the verb also keeps `grep -c <pattern>` out, because `grep` is not a
249
+ * shell name.
250
+ *
251
+ * @param {Array<{ text: string, quoted: boolean }>} segment
252
+ * @param {{ payloads: string[], alt?: { payloads: string[] } }} resolved
253
+ * @returns {string[]} distinct payload strings
254
+ */
255
+ function nestedShellPayloads(segment, resolved) {
256
+ const out = new Set();
257
+ for (const reading of [resolved, resolved.alt]) {
258
+ if (!reading) continue;
259
+ for (const p of reading.payloads) out.add(p);
260
+ }
261
+ for (let i = 0; i < segment.length - 1; i++) {
262
+ const tok = segment[i];
263
+ if (tok.quoted || !DASH_C_SHELLS.has(path.basename(tok.text))) continue;
264
+ for (let j = i + 1; j < segment.length - 1; j++) {
265
+ if (/^-[A-Za-z]*c$/.test(segment[j].text)) {
266
+ out.add(segment[j + 1].text);
267
+ break;
268
+ }
269
+ }
270
+ }
271
+ return [...out];
272
+ }
273
+
274
+ /**
275
+ * One tokenize + per-segment pass of {@link extractStagedPaths}, re-entered
276
+ * once per nested shell payload (bounded by {@link MAX_PAYLOAD_DEPTH}).
277
+ *
278
+ * Accumulates into the caller's `paths` set and `state.recognised` flag rather
279
+ * than returning, so a nested hit and a top-level hit compose:
280
+ * `git add a && bash -c "git add b"` records BOTH.
281
+ *
282
+ * @param {string} command
283
+ * @param {{ tokenizeCommand: Function, splitChainSegments: Function,
284
+ * resolveSegmentVerb: Function }} lib
285
+ * @param {Set<string>} paths - OUT
286
+ * @param {{ recognised: boolean }} state - OUT
287
+ * @param {number} depth
288
+ * @returns {void}
289
+ */
290
+ function scanForStagedPaths(command, lib, paths, state, depth) {
291
+ let segments;
292
+ try {
293
+ segments = lib.splitChainSegments(lib.tokenizeCommand(command));
294
+ } catch {
295
+ return;
296
+ }
297
+
298
+ for (const segment of segments) {
299
+ const resolved = lib.resolveSegmentVerb(segment);
300
+
301
+ // A staging command nested in `bash -c "…"` resolves to the verb `bash`,
302
+ // so the loop below never sees it. Before #1404 the raw-text regex matched
303
+ // it INSIDE the quotes and the reader's path regex found the collision;
304
+ // the tokenizer rewrite dropped the whole class silently (measured
305
+ // 2026-09-20: `bash -c "git add src/foo.ts"` → no fence file at all). The
306
+ // fence is cross-agent collision detection, so a miss is silent in BOTH
307
+ // directions — re-enter the same extraction on the payload.
308
+ if (depth < MAX_PAYLOAD_DEPTH) {
309
+ for (const payload of nestedShellPayloads(segment, resolved)) {
310
+ // The G3 pre-filter again, on the payload: same cheap question, same
311
+ // tolerated false positives, and it keeps an unrelated `-c` operand
312
+ // from paying for a second tokenize.
313
+ if (typeof payload === 'string' && GIT_ADD_REGEX.test(payload)) {
314
+ scanForStagedPaths(payload, lib, paths, state, depth + 1);
315
+ }
316
+ }
317
+ }
318
+
319
+ const { verb, index } = resolved;
320
+ if (verb !== 'git' || index < 0) continue;
321
+
322
+ let i = index + 1;
323
+ let chdir = null;
324
+
325
+ // git GLOBAL flags sit between `git` and the subcommand.
326
+ while (i < segment.length) {
327
+ const tok = segment[i];
328
+ if (tok.quoted || !tok.text.startsWith('-') || tok.text === '-') break;
329
+ if (GIT_GLOBAL_VALUE_FLAGS.has(tok.text)) {
330
+ if (tok.text === '-C' && i + 1 < segment.length) chdir = segment[i + 1].text;
331
+ i += 2;
332
+ continue;
333
+ }
334
+ i += 1; // boolean global flag, or attached `--git-dir=<p>`
335
+ }
336
+
337
+ if (i >= segment.length || segment[i].quoted || segment[i].text !== 'add') continue;
338
+ state.recognised = true;
339
+ i += 1;
340
+
341
+ let endOfFlags = false;
342
+ let stagesAll = false;
343
+
344
+ for (; i < segment.length; i++) {
345
+ const tok = segment[i];
346
+ const text = tok.text;
347
+
348
+ if (!endOfFlags && !tok.quoted && text.startsWith('-') && text !== '-') {
349
+ if (text === '--') { endOfFlags = true; continue; }
350
+ if (GIT_ADD_VALUE_FLAGS.has(text)) { i += 1; continue; }
351
+ if (text === '--pathspec-from-file' || text.startsWith('--pathspec-from-file=')) {
352
+ stagesAll = true;
353
+ if (text === '--pathspec-from-file') i += 1;
354
+ continue;
355
+ }
356
+ if (GIT_ADD_ALL_FLAGS.has(text)) { stagesAll = true; continue; }
357
+ // Short-flag cluster: `-A`, `-u`, `-An`, `-vu` all stage a set no
358
+ // operand names.
359
+ if (!text.startsWith('--') && /[Au]/.test(text.slice(1))) stagesAll = true;
360
+ continue;
361
+ }
362
+
363
+ if (text === '.' || text === './') { stagesAll = true; continue; }
364
+ const operand = chdir && !chdir.startsWith('/') ? `${chdir}/${text}` : text;
365
+ const normalised = normalizeStagedPath(operand);
366
+ if (normalised.length > 0) paths.add(normalised);
367
+ }
368
+
369
+ if (stagesAll) paths.add(ALL_PATHS_MARKER);
370
+ }
371
+ }
372
+
90
373
  /**
91
374
  * Derive a per-agent identifier for the fence filename.
92
375
  *
@@ -136,10 +419,10 @@ function resolveProjectDir() {
136
419
  * #558 M1). The first call for a given agent_id creates the file with a
137
420
  * fresh body.
138
421
  *
139
- * @param {{ fenceFile: string, agentId: string, command: string }} args
422
+ * @param {{ fenceFile: string, agentId: string, command: string, paths: string[] }} args
140
423
  * @returns {{ ok: true } | { ok: false, reason: 'fs-error', error: string }}
141
424
  */
142
- function appendIntent({ fenceFile, agentId, command }) {
425
+ function appendIntent({ fenceFile, agentId, command, paths }) {
143
426
  const timestamp = new Date().toISOString();
144
427
 
145
428
  let body;
@@ -169,7 +452,14 @@ function appendIntent({ fenceFile, agentId, command }) {
169
452
  };
170
453
  }
171
454
 
172
- body.staged_paths.push({ command: command.slice(0, 512), timestamp });
455
+ // #1404 — persist what the READER needs (path operands), never the command
456
+ // text: a staging command can carry a secret in a leading env assignment.
457
+ // The hash keeps the entry countable/groupable, mirroring enforce-commands.
458
+ body.staged_paths.push({
459
+ paths,
460
+ command_hash: hashCommand(command),
461
+ timestamp,
462
+ });
173
463
 
174
464
  return writeJsonAtomicSync(fenceFile, body, { tmpPrefix: '.fence.tmp' });
175
465
  }
@@ -189,12 +479,20 @@ async function main() {
189
479
  const command = input?.tool_input?.command;
190
480
  if (typeof command !== 'string' || command.length === 0) return emitAllow();
191
481
 
192
- // G3 — regex gate. Non-`git add` commands pass through unconditionally.
482
+ // G3 — cheap regex PRE-FILTER. Commands that cannot possibly be a staging
483
+ // command pass through without loading the tokenizer.
193
484
  if (!GIT_ADD_REGEX.test(command)) return emitAllow();
194
485
 
195
486
  // G4 — context gate. Coordinator/manual commits are not fenced.
196
487
  if (!isWaveAgentContext()) return emitAllow();
197
488
 
489
+ // G4b — the PRECISE gate (#1404). The tokenizer, not the regex, decides
490
+ // whether this command really stages anything: a pre-filter hit with no
491
+ // resolvable `git add` statement (`git commit -m "add x"`) writes NO fence
492
+ // file, where the pre-#1404 hook logged its raw text.
493
+ const { recognised, paths } = await extractStagedPaths(command);
494
+ if (!recognised) return emitAllow();
495
+
198
496
  // G5 — derive paths.
199
497
  const projectDir = resolveProjectDir();
200
498
  const agentId = deriveAgentId();
@@ -206,7 +504,7 @@ async function main() {
206
504
  );
207
505
 
208
506
  // G6 — append intent. Never blocks the Bash call on failure.
209
- const result = appendIntent({ fenceFile, agentId, command });
507
+ const result = appendIntent({ fenceFile, agentId, command, paths });
210
508
  if (!result.ok) {
211
509
  process.stderr.write(
212
510
  `⚠ pre-bash-staging-fence: failed to write fence file — ${result.error}\n`,
@@ -216,12 +514,18 @@ async function main() {
216
514
  return emitAllow();
217
515
  }
218
516
 
219
- // Top-level error handler — never let exit 1 leak. Fail-open on internal
220
- // errors to avoid blocking legitimate work (mirrors the destructive-guard
221
- // posture).
222
- main().catch((e) => {
223
- process.stderr.write(
224
- `⚠ pre-bash-staging-fence: internal error — ${e?.message || e}\n`,
225
- );
226
- process.exit(0);
227
- });
517
+ // Entry guard (#1393): run only as the node script the harness execs — a bare
518
+ // `import()` must run no handler and must not exit the importing process.
519
+ if (isMainModule(import.meta.url)) {
520
+ if (!shouldRunHook('pre-bash-staging-fence')) process.exit(0);
521
+
522
+ // Top-level error handler — never let exit 1 leak. Fail-open on internal
523
+ // errors to avoid blocking legitimate work (mirrors the destructive-guard
524
+ // posture).
525
+ main().catch((e) => {
526
+ process.stderr.write(
527
+ `⚠ pre-bash-staging-fence: internal error — ${e?.message || e}\n`,
528
+ );
529
+ process.exit(0);
530
+ });
531
+ }
@@ -44,14 +44,10 @@ import { hasReadInSession } from './_lib/transcript-history.mjs';
44
44
  import { resolveHost, matchesBypass } from './_lib/vcs-create-matcher.mjs';
45
45
 
46
46
  import { shouldRunHook } from './_lib/profile-gate.mjs';
47
+ import { isMainModule } from '../scripts/lib/is-main-module.mjs';
47
48
  import { existsSync, readdirSync, lstatSync } from 'node:fs';
48
49
  import path from 'node:path';
49
50
 
50
- // #519: opt-in via profile/env. Default profile "full" enables this hook;
51
- // minimal/off disable. SO_DISABLED_HOOKS=pre-bash-templates-first opts out
52
- // per session.
53
- if (!shouldRunHook('pre-bash-templates-first')) process.exit(0);
54
-
55
51
  // ---------------------------------------------------------------------------
56
52
  // Constants
57
53
  // ---------------------------------------------------------------------------
@@ -426,12 +422,21 @@ async function main() {
426
422
  });
427
423
  }
428
424
 
429
- // Top-level error handler — never let exit 1 leak. Same posture as
430
- // pre-bash-destructive-guard.mjs: fail-open on internal errors to avoid
431
- // blocking legitimate work.
432
- main().catch((e) => {
433
- process.stderr.write(
434
- `⚠ pre-bash-templates-first: internal error — ${e?.message || e}\n`,
435
- );
436
- process.exit(0);
437
- });
425
+ // Entry guard (#1393): run only as the node script the harness execs — a bare
426
+ // `import()` must run no handler and must not exit the importing process.
427
+ if (isMainModule(import.meta.url)) {
428
+ // #519: opt-in via profile/env. Default profile "full" enables this hook;
429
+ // minimal/off disable. SO_DISABLED_HOOKS=pre-bash-templates-first opts out
430
+ // per session.
431
+ if (!shouldRunHook('pre-bash-templates-first')) process.exit(0);
432
+
433
+ // Top-level error handler — never let exit 1 leak. Same posture as
434
+ // pre-bash-destructive-guard.mjs: fail-open on internal errors to avoid
435
+ // blocking legitimate work.
436
+ main().catch((e) => {
437
+ process.stderr.write(
438
+ `⚠ pre-bash-templates-first: internal error — ${e?.message || e}\n`,
439
+ );
440
+ process.exit(0);
441
+ });
442
+ }
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * pre-edit-sessions-ledger-guard.mjs — PreToolUse `Edit|Write|MultiEdit` hook:
4
+ * denies a TOOL write into the sessions ledger
5
+ * (`.orchestrator/metrics/sessions.jsonl`, plus rotated `sessions.jsonl.<N>`).
6
+ *
7
+ * The sibling `pre-bash-sessions-ledger-guard.mjs` covers shell writes only; the
8
+ * Edit/Write/MultiEdit tools were a second, unguarded route to the same file
9
+ * (GitLab #1443 item 4). Editing the ledger in place skips the validating writer
10
+ * (`scripts/emit-session.mjs`) and can silently rewrite records that are already
11
+ * there — the ledger is append-only by contract.
12
+ *
13
+ * ## Fleet safety — allow is the default, deny is the exception
14
+ *
15
+ * This hook runs on EVERY Edit/Write/MultiEdit in every session on the host, so
16
+ * it denies exactly one thing and allows everything else, including every
17
+ * internal failure: empty or unparsable stdin, a missing `tool_input`, an
18
+ * unknown `tool_name`, an absent `file_path`, or a repo module that fails to
19
+ * load all exit 0 with no decision (the harness proceeds). Only a positively
20
+ * identified ledger path is denied.
21
+ *
22
+ * ## Path scope — narrower than the Bash guard, on purpose
23
+ *
24
+ * The Bash guard matches on BASENAME (`sessions.jsonl` in any directory), because
25
+ * a shell `cd .orchestrator/metrics && echo … >> sessions.jsonl` leaves only the
26
+ * basename to see. The Edit/Write tools always carry the full target path, so
27
+ * here the path must END in `.orchestrator/metrics/sessions.jsonl` (optionally
28
+ * `.<digits>` for a rotated archive). Tmp-dir fixtures outside a
29
+ * `.orchestrator/metrics/` directory, `sessions.jsonl.bak`, `learnings.jsonl` and
30
+ * `events.jsonl` all allow. The comparison is case-insensitive because the
31
+ * default macOS filesystem is: `Sessions.JSONL` there IS the ledger.
32
+ *
33
+ * Not covered (by design — a guard against the accident, not a containment
34
+ * boundary): a symlink that points at the ledger under another name, and the
35
+ * `NotebookEdit` tool (not in the matcher; it cannot target a `.jsonl` file
36
+ * meaningfully).
37
+ *
38
+ * Named limits (measured 2026-09-25, security review of #1443):
39
+ *
40
+ * - The TRACKED fixture `tests/fixtures/harness-audit/clean-repo/.orchestrator/
41
+ * metrics/sessions.jsonl` matches the suffix and is DENIED (the Bash guard
42
+ * denies it too, by basename). No `tests/fixtures/` exemption on purpose: a
43
+ * path-prefix carve-out is a second hole, not a narrower guard. Editing that
44
+ * fixture is intentional maintenance — run with
45
+ * `SO_DISABLED_HOOKS=pre-edit-sessions-ledger-guard`.
46
+ * - A payload over 1 MB fails OPEN: `readStdin` (`scripts/lib/io.mjs`) rejects
47
+ * past 1,048,576 bytes and the catch below allows. A whole-ledger `Write`
48
+ * carries the file as `content`, so it bypasses the guard once the ledger
49
+ * nears that size. Largest ledger on the reference host: 481,543 bytes
50
+ * (`wc -c` over all 31 `.orchestrator/metrics/sessions.jsonl` under the
51
+ * projects root). Revisit when any ledger passes ~900 KB.
52
+ * - The case-insensitive match can DENY a distinct file on a case-sensitive
53
+ * filesystem (Linux): `.Orchestrator/METRICS/Sessions.JSONL` is not the
54
+ * ledger there, but is refused. Fails closed; same override as above.
55
+ *
56
+ * ## Platforms
57
+ *
58
+ * Cursor and Pi reach this hook through their bridges
59
+ * (`scripts/lib/cursor-hook-bridge.mjs`, `scripts/lib/pi-hook-bridge.mjs`), which
60
+ * rewrite native tool names to `Edit`/`Write`/`MultiEdit` and populate
61
+ * `tool_input.file_path`. Codex is a documented gap in
62
+ * `scripts/lib/validate/check-hooks-symmetry.mjs` (no Claude tool-name vocabulary).
63
+ *
64
+ * ## Override
65
+ *
66
+ * SO_DISABLED_HOOKS=pre-edit-sessions-ledger-guard (session-level)
67
+ * SO_HOOK_PROFILE=minimal|off
68
+ */
69
+
70
+ import path from 'node:path';
71
+
72
+ import { shouldRunHook } from './_lib/profile-gate.mjs';
73
+ import { isMainModule } from '../scripts/lib/is-main-module.mjs';
74
+
75
+ /** This hook's name — the `SO_DISABLED_HOOKS` key. */
76
+ const HOOK_NAME = 'pre-edit-sessions-ledger-guard';
77
+
78
+ /** The tools this hook judges. Anything else allows. */
79
+ const GUARDED_TOOLS = new Set(['Edit', 'Write', 'MultiEdit']);
80
+
81
+ /**
82
+ * The ledger, or a rotated archive of it, at the END of a normalized path.
83
+ * Anchored on a separator (or the start) so `x.orchestrator/metrics/…` does not match.
84
+ */
85
+ const LEDGER_PATH_RE = /(?:^|\/)\.orchestrator\/metrics\/sessions\.jsonl(?:\.\d+)?$/i;
86
+
87
+ /**
88
+ * Upper bound on the echoed target in the deny reason. `emitDeny` clamps the
89
+ * whole reason, so an unbounded path on line 1 would push the writer/reader
90
+ * instructions off the end (same reasoning as the Bash guard's TARGET_ECHO_MAX).
91
+ */
92
+ const TARGET_ECHO_MAX = 200;
93
+
94
+ /**
95
+ * Does this `file_path` name the sessions ledger (or a rotated archive)?
96
+ *
97
+ * Backslashes are normalized first so a Windows-spelled path is not read as one
98
+ * long filename (mirrors the Bash guard's `refersToLedger`), then
99
+ * `path.posix.normalize` folds `..` and `//` so `metrics/../metrics/sessions.jsonl`
100
+ * cannot sidestep the suffix match.
101
+ *
102
+ * @param {unknown} filePath
103
+ * @returns {boolean}
104
+ */
105
+ export function isLedgerPath(filePath) {
106
+ if (typeof filePath !== 'string' || filePath.length === 0) return false;
107
+ const normalized = path.posix.normalize(filePath.replace(/\\/g, '/'));
108
+ return LEDGER_PATH_RE.test(normalized);
109
+ }
110
+
111
+ /**
112
+ * Read the payload and decide. Every non-ledger outcome returns without output
113
+ * (exit 0 = no decision = allow).
114
+ *
115
+ * @returns {Promise<void>}
116
+ */
117
+ async function main() {
118
+ // Late-bound so a broken io.mjs lands in the fail-open catch below instead of
119
+ // an ESM link-time crash, and so the stdin reader is the repo's shared one.
120
+ const { readStdin, emitDeny } = await import('../scripts/lib/io.mjs');
121
+
122
+ let input;
123
+ try {
124
+ input = await readStdin();
125
+ } catch {
126
+ return; // unparsable / oversized / timed-out stdin → allow
127
+ }
128
+ if (!input || typeof input !== 'object') return;
129
+ if (!GUARDED_TOOLS.has(input.tool_name)) return;
130
+
131
+ const filePath = input.tool_input?.file_path;
132
+ if (!isLedgerPath(filePath)) return;
133
+
134
+ const shown = filePath.length > TARGET_ECHO_MAX ? `${filePath.slice(0, TARGET_ECHO_MAX)}…` : filePath;
135
+ emitDeny(
136
+ [
137
+ `${input.tool_name} on the sessions ledger blocked: '${shown}'`,
138
+ `The ledger is append-only through its validating writer:`,
139
+ ` node scripts/emit-session.mjs --entry '<json>' (or pipe the JSON on stdin)`,
140
+ `Read or verify it without writing:`,
141
+ ` node scripts/check-sessions-integrity.mjs`,
142
+ `Editing the file in place skips schema validation and can rewrite existing records.`,
143
+ `Override (intentional maintenance only): run the session with`,
144
+ `SO_DISABLED_HOOKS=${HOOK_NAME}`,
145
+ `See: GitLab #1443, skills/session-end/session-metrics-write.md`,
146
+ ].join('\n'),
147
+ );
148
+ }
149
+
150
+ // Entry guard (#1393): run only as the node script the harness execs — a bare
151
+ // `import()` must run no handler and must not exit the importing process.
152
+ if (isMainModule(import.meta.url)) {
153
+ if (!shouldRunHook(HOOK_NAME)) process.exit(0);
154
+
155
+ // Fail-OPEN on any internal error: exit 0 with no stdout is "no decision".
156
+ main()
157
+ .then(() => process.exit(0))
158
+ .catch((e) => {
159
+ process.stderr.write(`⚠ ${HOOK_NAME}: internal error — ${e?.message || e}\n`);
160
+ process.exit(0);
161
+ });
162
+ }