session-orchestrator 5.2.0 → 5.3.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 (295) 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 +3 -1
  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/npm-publish/SKILL.md +4 -2
  41. package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
  42. package/.agents/skills/persona-panel/SKILL.md +5 -1
  43. package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
  44. package/.agents/skills/plan/SKILL.md +5 -1
  45. package/.agents/skills/plan/agents/openai.yaml +5 -0
  46. package/.agents/skills/playwright-driver/SKILL.md +3 -1
  47. package/.agents/skills/portfolio/SKILL.md +5 -1
  48. package/.agents/skills/portfolio/agents/openai.yaml +5 -0
  49. package/.agents/skills/quality-gates/SKILL.md +3 -1
  50. package/.agents/skills/reconcile/SKILL.md +5 -1
  51. package/.agents/skills/reconcile/agents/openai.yaml +5 -0
  52. package/.agents/skills/release/SKILL.md +5 -1
  53. package/.agents/skills/release/agents/openai.yaml +5 -0
  54. package/.agents/skills/remote-offload/SKILL.md +3 -1
  55. package/.agents/skills/repo-audit/SKILL.md +5 -1
  56. package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
  57. package/.agents/skills/session/SKILL.md +21 -0
  58. package/.agents/skills/session/agents/openai.yaml +5 -0
  59. package/.agents/skills/session-end/SKILL.md +3 -1
  60. package/.agents/skills/session-plan/SKILL.md +3 -1
  61. package/.agents/skills/session-start/SKILL.md +3 -1
  62. package/.agents/skills/spinout/SKILL.md +5 -1
  63. package/.agents/skills/spinout/agents/openai.yaml +5 -0
  64. package/.agents/skills/sunset-review/SKILL.md +5 -1
  65. package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
  66. package/.agents/skills/templates-ack/SKILL.md +21 -0
  67. package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
  68. package/.agents/skills/test/SKILL.md +5 -1
  69. package/.agents/skills/test/agents/openai.yaml +5 -0
  70. package/.agents/skills/test-runner/SKILL.md +3 -1
  71. package/.agents/skills/tmux-layout/SKILL.md +3 -1
  72. package/.agents/skills/using-orchestrator/SKILL.md +3 -1
  73. package/.agents/skills/ux-grill/SKILL.md +5 -1
  74. package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
  75. package/.agents/skills/vault-mirror/SKILL.md +3 -1
  76. package/.agents/skills/vault-sync/SKILL.md +3 -1
  77. package/.agents/skills/wave-executor/SKILL.md +3 -1
  78. package/.agents/skills/write-executable-plan/SKILL.md +3 -1
  79. package/.claude-plugin/marketplace.json +1 -1
  80. package/.claude-plugin/plugin.json +1 -1
  81. package/.codex-plugin/plugin.json +4 -4
  82. package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -3
  83. package/.codex-plugin/skills/eval/SKILL.md +1 -1
  84. package/.codex-plugin/skills/evolve/SKILL.md +1 -1
  85. package/.codex-plugin/skills/npm-publish/SKILL.md +1 -3
  86. package/.codex-plugin/skills/session/SKILL.md +1 -1
  87. package/.cursor/commands/eval.md +1 -1
  88. package/.cursor/commands/session.md +1 -1
  89. package/.cursor/rules/000-session-orchestrator.mdc +0 -2
  90. package/.cursor/rules/050-plan.mdc +1 -1
  91. package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
  92. package/.cursor/skills/eval/SKILL.md +1 -1
  93. package/.cursor/skills/npm-publish/SKILL.md +1 -0
  94. package/.cursor-plugin/plugin.json +1 -1
  95. package/.orchestrator/policy/blocked-commands.json +12 -3
  96. package/AGENTS.md +3 -2
  97. package/CHANGELOG.md +136 -0
  98. package/README.md +9 -9
  99. package/SECURITY.md +12 -0
  100. package/agents/dialectic-deriver.md +13 -10
  101. package/agents/eval-judge.md +67 -45
  102. package/agents/skill-applied-judge.md +34 -19
  103. package/commands/session.md +7 -3
  104. package/docs/baseline.md +12 -6
  105. package/docs/codex-setup.md +14 -2
  106. package/docs/components.md +7 -5
  107. package/docs/events-schema.md +56 -9
  108. package/docs/rule-authoring.md +58 -6
  109. package/docs/session-config-reference.md +100 -7
  110. package/docs/session-config-template.md +31 -2
  111. package/docs/telemetry.md +2 -0
  112. package/hooks/_lib/hook-import-set.json +85 -8
  113. package/hooks/_lib/subagent-transcript.mjs +582 -31
  114. package/hooks/config-protection.mjs +11 -3
  115. package/hooks/cwd-change-restore.mjs +11 -3
  116. package/hooks/enforce-commands.mjs +70 -23
  117. package/hooks/enforce-scope.mjs +143 -33
  118. package/hooks/hooks-codex.json +1 -1
  119. package/hooks/hooks.json +1 -1
  120. package/hooks/loop-guard.mjs +11 -3
  121. package/hooks/on-session-end.mjs +58 -23
  122. package/hooks/on-session-start.mjs +48 -11
  123. package/hooks/on-stop.mjs +168 -22
  124. package/hooks/operator-steer.mjs +11 -3
  125. package/hooks/post-bash-issue-budget-refund.mjs +18 -8
  126. package/hooks/post-bash-write-verify.mjs +3 -2
  127. package/hooks/post-edit-import-probe.mjs +17 -9
  128. package/hooks/post-edit-validate.mjs +13 -5
  129. package/hooks/post-subagent-discovery-validator.mjs +98 -13
  130. package/hooks/post-tool-batch-wave-signal.mjs +200 -38
  131. package/hooks/post-tool-failure-corrective-context.mjs +11 -5
  132. package/hooks/post-tooluse-frontend-slop.mjs +10 -4
  133. package/hooks/pre-auq-clarity.mjs +15 -2
  134. package/hooks/pre-bash-destructive-guard.mjs +80 -9
  135. package/hooks/pre-bash-issue-budget.mjs +16 -11
  136. package/hooks/pre-bash-memory-propose-audit.mjs +86 -54
  137. package/hooks/pre-bash-sessions-ledger-guard.mjs +391 -20
  138. package/hooks/pre-bash-staging-fence.mjs +335 -31
  139. package/hooks/pre-bash-templates-first.mjs +19 -14
  140. package/hooks/pre-task-scope-disjoint.mjs +233 -2
  141. package/hooks/subagent-telemetry.mjs +15 -19
  142. package/hooks/wave-scope-commit-guard.mjs +197 -100
  143. package/monitors/monitors.json +1 -1
  144. package/output-styles/wave-summary.md +1 -1
  145. package/package.json +1 -1
  146. package/pi/prompts/eval.md +1 -1
  147. package/pi/prompts/session.md +1 -1
  148. package/rules/README.md +1 -1
  149. package/rules/opt-in-domain/prompt-caching.md +1 -1
  150. package/rules/opt-in-stack/backend-data.md +1 -1
  151. package/rules/opt-in-stack/backend.md +3 -3
  152. package/rules/opt-in-stack/frontend.md +1 -1
  153. package/rules/opt-in-stack/security-web.md +3 -3
  154. package/rules/opt-in-stack/swift.md +1 -1
  155. package/scripts/autopilot.mjs +23 -2
  156. package/scripts/backfill-abandoned-sessions.mjs +117 -15
  157. package/scripts/check-sessions-integrity.mjs +300 -0
  158. package/scripts/dialectic-deriver.mjs +50 -13
  159. package/scripts/emit-session.mjs +75 -29
  160. package/scripts/eval-session.mjs +65 -3
  161. package/scripts/generate-agents-skills.mjs +102 -29
  162. package/scripts/generate-cursor-adapter.mjs +61 -16
  163. package/scripts/lib/agent-status.mjs +2 -31
  164. package/scripts/lib/auq/clarity.mjs +10 -2
  165. package/scripts/lib/auq/parse.mjs +12 -31
  166. package/scripts/lib/auq/schema.mjs +56 -41
  167. package/scripts/lib/auto-dialectic.mjs +304 -15
  168. package/scripts/lib/autopilot/flags.mjs +12 -1
  169. package/scripts/lib/autopilot/kill-switches.mjs +6 -3
  170. package/scripts/lib/autopilot/loop.mjs +14 -1
  171. package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
  172. package/scripts/lib/ci-status-banner.mjs +376 -16
  173. package/scripts/lib/command-blocker.mjs +275 -28
  174. package/scripts/lib/config/dialectic.mjs +12 -3
  175. package/scripts/lib/config/gate.mjs +74 -0
  176. package/scripts/lib/config/reaper.mjs +162 -0
  177. package/scripts/lib/config.mjs +14 -0
  178. package/scripts/lib/convergence-monitor.mjs +74 -11
  179. package/scripts/lib/ecosystem-health.mjs +11 -0
  180. package/scripts/lib/eval/engine.mjs +421 -53
  181. package/scripts/lib/eval/judge.mjs +463 -40
  182. package/scripts/lib/eval/schema.mjs +10 -1
  183. package/scripts/lib/events-rotation.mjs +221 -25
  184. package/scripts/lib/events-schema.mjs +114 -0
  185. package/scripts/lib/events.mjs +524 -5
  186. package/scripts/lib/frontmatter-guard.mjs +21 -10
  187. package/scripts/lib/gates/gate-baseline.mjs +27 -2
  188. package/scripts/lib/gates/gate-full.mjs +28 -3
  189. package/scripts/lib/gates/gate-helpers.mjs +243 -21
  190. package/scripts/lib/gates/gate-incremental.mjs +28 -3
  191. package/scripts/lib/gates/gate-per-file.mjs +27 -2
  192. package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
  193. package/scripts/lib/instruction-budget-guard.mjs +146 -4
  194. package/scripts/lib/io.mjs +42 -8
  195. package/scripts/lib/issue-close-strip-labels.mjs +207 -49
  196. package/scripts/lib/js-mask.mjs +197 -0
  197. package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
  198. package/scripts/lib/maintenance-due-banner.mjs +53 -88
  199. package/scripts/lib/orphan-reaper.mjs +1588 -0
  200. package/scripts/lib/peer-cards/merger.mjs +48 -10
  201. package/scripts/lib/peer-cards/reader.mjs +78 -2
  202. package/scripts/lib/process-group.mjs +899 -0
  203. package/scripts/lib/quality-gate.mjs +107 -28
  204. package/scripts/lib/reconcile/backlog.mjs +368 -0
  205. package/scripts/lib/reconcile/engine.mjs +55 -188
  206. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +302 -60
  207. package/scripts/lib/reconcile/sanitize.mjs +69 -3
  208. package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
  209. package/scripts/lib/resource-probe/parsers.mjs +31 -0
  210. package/scripts/lib/rule-loader.mjs +41 -12
  211. package/scripts/lib/scope-echo.mjs +39 -2
  212. package/scripts/lib/scope-gate.mjs +605 -1
  213. package/scripts/lib/session-close-backfill.mjs +33 -6
  214. package/scripts/lib/session-id.mjs +9 -20
  215. package/scripts/lib/session-invocation.mjs +20 -0
  216. package/scripts/lib/session-schema/constants.mjs +30 -2
  217. package/scripts/lib/session-schema/normalizer.mjs +56 -4
  218. package/scripts/lib/session-schema.mjs +8 -3
  219. package/scripts/lib/session-start-probes.mjs +95 -10
  220. package/scripts/lib/sessions-canonical.mjs +23 -0
  221. package/scripts/lib/sessions-integrity-banner.mjs +7 -1
  222. package/scripts/lib/sessions-staleness-banner.mjs +193 -51
  223. package/scripts/lib/skill-evidence-window.mjs +891 -0
  224. package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
  225. package/scripts/lib/skill-evolution/engine.mjs +18 -9
  226. package/scripts/lib/skill-judge.mjs +45 -3
  227. package/scripts/lib/tail-window.mjs +56 -0
  228. package/scripts/lib/telemetry/schema.mjs +30 -0
  229. package/scripts/lib/telemetry/sync.mjs +61 -6
  230. package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
  231. package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
  232. package/scripts/lib/tmux-layout/telemetry-stats.mjs +72 -13
  233. package/scripts/lib/user-invocable-skills.mjs +23 -3
  234. package/scripts/lib/ux-grill/reconcile.mjs +48 -22
  235. package/scripts/lib/validate/check-agents-skills.mjs +26 -15
  236. package/scripts/lib/validate/check-cursor-adapter.mjs +1 -0
  237. package/scripts/lib/validate/check-entry-guard.mjs +13 -50
  238. package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
  239. package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
  240. package/scripts/lib/validate/check-rules.mjs +7 -5
  241. package/scripts/lib/validate/check-skill-links.mjs +9 -1
  242. package/scripts/lib/validate/check-skill-script-paths.mjs +239 -27
  243. package/scripts/lib/validate/check-test-git-config-target.mjs +24 -34
  244. package/scripts/lib/validate/check-untracked-test-deps.mjs +7 -102
  245. package/scripts/lib/validate/check-unwired-features.mjs +130 -27
  246. package/scripts/lib/validate/check-validator-registration.mjs +34 -10
  247. package/scripts/lib/validate/confidential-names.mjs +10 -0
  248. package/scripts/lib/validate-vendored-rules.mjs +4 -3
  249. package/scripts/lib/vault-mirror/namespace.mjs +46 -8
  250. package/scripts/lib/vault-mirror/process.mjs +10 -3
  251. package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
  252. package/scripts/lib/vault-status/narrative-mirror.mjs +31 -7
  253. package/scripts/lib/vault-yaml.mjs +118 -0
  254. package/scripts/lib/worktree/lifecycle.mjs +153 -1
  255. package/scripts/release-session-lock.mjs +305 -0
  256. package/scripts/release.mjs +30 -5
  257. package/scripts/resolve-session-invocation.mjs +59 -0
  258. package/scripts/run-quality-gate.mjs +156 -17
  259. package/scripts/sweep-expired-rules.mjs +14 -3
  260. package/scripts/validate-plugin.mjs +12 -0
  261. package/scripts/validate-wave-scope.mjs +32 -105
  262. package/scripts/vault-mirror.mjs +9 -1
  263. package/skills/_shared/platform-tools.md +23 -11
  264. package/skills/autopilot/SKILL.md +22 -7
  265. package/skills/claude-md-drift-check/SKILL.md +1 -1
  266. package/skills/convergence-monitoring/README.md +8 -1
  267. package/skills/convergence-monitoring/SIGNALS.md +50 -6
  268. package/skills/convergence-monitoring/SKILL.md +15 -6
  269. package/skills/eval/SKILL.md +39 -24
  270. package/skills/eval/rubric-v1.md +1 -0
  271. package/skills/eval/rubric-v2.md +457 -0
  272. package/skills/evolve/SKILL.md +1 -1
  273. package/skills/evolve/references/evolve-dialectic-mode.md +42 -25
  274. package/skills/gitlab-ops/SKILL.md +3 -2
  275. package/skills/npm-publish/SKILL.md +1 -1
  276. package/skills/reconcile/SKILL.md +11 -0
  277. package/skills/session-end/SKILL.md +13 -16
  278. package/skills/session-end/discovery-scan.md +1 -1
  279. package/skills/session-end/phase-3-6-tail.md +55 -9
  280. package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
  281. package/skills/session-end/session-metrics-write.md +10 -0
  282. package/skills/session-plan/SKILL.md +17 -5
  283. package/skills/session-plan/references/session-plan-task-classification.md +2 -2
  284. package/skills/session-start/references/phase-4-ssot-environment-check.md +2 -1
  285. package/skills/ux-grill/SKILL.md +1 -1
  286. package/skills/wave-executor/SKILL.md +8 -4
  287. package/skills/wave-executor/circuit-breaker.md +2 -0
  288. package/skills/wave-executor/references/wave-executor-state-init.md +5 -3
  289. package/skills/wave-executor/references/wave-loop-dispatch.md +2 -1
  290. package/.codex-plugin/skills/convergence-monitoring/agents/openai.yaml +0 -5
  291. package/.codex-plugin/skills/npm-publish/agents/openai.yaml +0 -5
  292. package/.cursor/commands/convergence-monitoring.md +0 -13
  293. package/.cursor/commands/npm-publish.md +0 -13
  294. package/pi/prompts/convergence-monitoring.md +0 -11
  295. 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-FILTERskip 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
+ }