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
@@ -29,6 +29,12 @@
29
29
  * generic pattern path — its `pattern: ">"` would FP-match nearly
30
30
  * every redirect. Unresolved targets (variable/substitution) warn
31
31
  * on stderr (fail-visible) and never block.
32
+ * rule.type === 'path-delete' (#1401): decided by the DELETE/RENAME
33
+ * operand via firstDeniedDeleteTarget (same target-denylist globs,
34
+ * same compiler), likewise shadowing the generic pattern path. The
35
+ * redirect class answers "what does this command WRITE"; nothing
36
+ * answered "what does it REMOVE", so `rm -f <ledger>` was allowed
37
+ * while `> <ledger>` was denied — 53,896 lines of telemetry lost.
32
38
  * G6 no match → exit 0
33
39
  *
34
40
  * Telemetry (Epic #803 process-safety dimension): best-effort
@@ -46,8 +52,7 @@ import crypto from 'node:crypto';
46
52
  import { pathToFileURL } from 'node:url';
47
53
 
48
54
  import { shouldRunHook } from './_lib/profile-gate.mjs';
49
- // #211: exit 0 immediately (silent allow) when this hook is disabled via profile/env
50
- if (!shouldRunHook('pre-bash-destructive-guard')) process.exit(0);
55
+ import { isMainModule } from '../scripts/lib/is-main-module.mjs';
51
56
 
52
57
  // ---------------------------------------------------------------------------
53
58
  // #992 — late-bound repo dependencies
@@ -179,6 +184,8 @@ async function bootstrap() {
179
184
  'redirectSpanEnd',
180
185
  'resolveSegmentVerb',
181
186
  'splitChainSegments',
187
+ 'extractDeleteTargets',
188
+ 'firstDeniedDeleteTarget',
182
189
  ],
183
190
  },
184
191
  },
@@ -877,6 +884,61 @@ async function main() {
877
884
  continue; // unreachable (blockCommand never returns) — kept for clarity
878
885
  }
879
886
 
887
+ // #1401 — path-delete rules are decided by the DELETE/RENAME operand via
888
+ // firstDeniedDeleteTarget, never by the generic pattern path below (same
889
+ // full-shadow contract as redirect-truncate above: this rule's `pattern` is
890
+ // a human-readable label, and routing it through commandMatchesBlocked
891
+ // would match either nothing or every `rm`).
892
+ //
893
+ // Why a second surface at all: the redirect rule answers "what does this
894
+ // command WRITE". Nothing answered "what does it REMOVE" — so `rm -f
895
+ // .orchestrator/metrics/events.jsonl.1` was ALLOWED while `> ` on the same
896
+ // file was DENIED (measured 2026-09-19, #1401: 53,896 ledger lines lost).
897
+ if (rule.type === 'path-delete') {
898
+ const collected = blocker.extractDeleteTargets(command);
899
+ const cutOff = collected.filter(
900
+ (e) => e.unresolved && (e.reason === 'budget-exhausted' || e.reason === 'depth-exceeded')
901
+ );
902
+ if (cutOff.length > 0) {
903
+ // A recursion cap that drops payloads silently is a bypass, not a cap
904
+ // (#988 T2) — surface it on the VISIBLE channel (#995).
905
+ //
906
+ // Deliberately NARROWER than the redirect branch above, which also
907
+ // surfaces `variable/substitution` operands: `rm -rf "$WORK"` is the
908
+ // ORDINARY spelling of a temp cleanup, so notifying on it would fire on
909
+ // a large share of benign commands and train the operator to ignore the
910
+ // channel (host-resources.md § HR-101 — a signal may only warn if it is
911
+ // rare). A cut-off marker, by contrast, needs 32+ payload segments or
912
+ // 3 levels of nesting: pathological by construction. Unresolved
913
+ // operands stay fail-visible on stderr below; neither class ever blocks.
914
+ const reasons = [...new Set(cutOff.map((e) => e.reason))].join(', ');
915
+ const msg = `pre-bash-destructive-guard: delete-target traversal cut off (${reasons}) — not matched (fail-visible)`;
916
+ process.stderr.write(`⚠ ${msg}\n`);
917
+ notices.push(msg);
918
+ }
919
+ const unresolvedOperands = collected.filter(
920
+ (e) => e.unresolved && e.reason === 'variable/substitution'
921
+ );
922
+ if (unresolvedOperands.length > 0) {
923
+ process.stderr.write(
924
+ `ℹ pre-bash-destructive-guard: ${unresolvedOperands.length} delete target(s) carry a variable/substitution — not matched (rule: ${id})\n`
925
+ );
926
+ }
927
+ const hit = blocker.firstDeniedDeleteTarget(rule, collected, { repoRoot: projectDir });
928
+ if (!hit) continue;
929
+ if (severity !== 'block') {
930
+ const msg = `pre-bash-destructive-guard: protected delete target matched (rule: ${id}) — ${rationale}`;
931
+ process.stderr.write(`⚠ ${msg}\n`);
932
+ notices.push(msg);
933
+ continue;
934
+ }
935
+ // Reason stays short (stdout-budget): verb + target, never the command.
936
+ // The policy `rationale` is long by design (it carries the measurement
937
+ // trail); blockCommand puts only its first line in the systemMessage.
938
+ await blockCommand(`${hit.verb} ${hit.target}`, id, rationale, command, sessionId);
939
+ continue; // unreachable (blockCommand never returns) — kept for clarity
940
+ }
941
+
880
942
  // The rm-rf-destructive rule also fires for recursive+force rm flag variants
881
943
  // the literal "rm -rf" pattern misses (`rm -r -f`, `rm -fr`) — #641 gap closure.
882
944
  const matched = id === 'rm-rf-destructive'
@@ -984,10 +1046,19 @@ try {
984
1046
  process.exit(0); // fail-open, but no longer fail-silent
985
1047
  }
986
1048
 
987
- // Top-level error handler never let exit 1 leak
988
- main().catch((e) => {
989
- process.stderr.write(
990
- `⚠ pre-bash-destructive-guard: internal error ${e?.message || e}\n`
991
- );
992
- process.exit(0); // fail-open on internal errors to avoid blocking legitimate work
993
- });
1049
+ // Entry guard (#1393): run only as the node script the harness execs — a bare
1050
+ // `import()` must run no handler and must not exit the importing process.
1051
+ // Placed strictly AFTER all decision logic: it gates whether main() is invoked,
1052
+ // never what main() decides, so no deny path can be short-circuited by it.
1053
+ if (isMainModule(import.meta.url)) {
1054
+ // #211: exit 0 immediately (silent allow) when this hook is disabled via profile/env
1055
+ if (!shouldRunHook('pre-bash-destructive-guard')) process.exit(0);
1056
+
1057
+ // Top-level error handler — never let exit 1 leak
1058
+ main().catch((e) => {
1059
+ process.stderr.write(
1060
+ `⚠ pre-bash-destructive-guard: internal error — ${e?.message || e}\n`
1061
+ );
1062
+ process.exit(0); // fail-open on internal errors to avoid blocking legitimate work
1063
+ });
1064
+ }
@@ -67,13 +67,10 @@ import {
67
67
  } from '../scripts/lib/issue-budget.mjs';
68
68
 
69
69
  import { shouldRunHook } from './_lib/profile-gate.mjs';
70
+ import { isMainModule } from '../scripts/lib/is-main-module.mjs';
70
71
  import { existsSync } from 'node:fs';
71
72
  import path from 'node:path';
72
73
 
73
- // Opt-out per session via SO_DISABLED_HOOKS=pre-bash-issue-budget; the
74
- // "minimal"/"off" profiles disable it like every other non-core hook.
75
- if (!shouldRunHook('pre-bash-issue-budget')) process.exit(0);
76
-
77
74
  // ---------------------------------------------------------------------------
78
75
  // Helpers
79
76
  // ---------------------------------------------------------------------------
@@ -500,10 +497,18 @@ async function main() {
500
497
  return emitAllow();
501
498
  }
502
499
 
503
- // Top-level error handler fail open, same posture as the sibling hooks.
504
- main().catch((e) => {
505
- process.stderr.write(
506
- `⚠ pre-bash-issue-budget: internal error — ${e?.message || e}\n`,
507
- );
508
- process.exit(0);
509
- });
500
+ // Entry guard (#1393): run only as the node script the harness execs — a bare
501
+ // `import()` must run no handler and must not exit the importing process.
502
+ if (isMainModule(import.meta.url)) {
503
+ // Opt-out per session via SO_DISABLED_HOOKS=pre-bash-issue-budget; the
504
+ // "minimal"/"off" profiles disable it like every other non-core hook.
505
+ if (!shouldRunHook('pre-bash-issue-budget')) process.exit(0);
506
+
507
+ // Top-level error handler — fail open, same posture as the sibling hooks.
508
+ main().catch((e) => {
509
+ process.stderr.write(
510
+ `⚠ pre-bash-issue-budget: internal error — ${e?.message || e}\n`,
511
+ );
512
+ process.exit(0);
513
+ });
514
+ }
@@ -13,7 +13,8 @@
13
13
  * G3 — regex match /\bnode\b.*\bmemory-propose\.mjs\b/i (no match → exit 0)
14
14
  * G4 — resolve session_id (from stdin payload or .orchestrator/current-session.json)
15
15
  * G5 — resolve wave (from the active wave-scope.json `wave` field, default 0)
16
- * G6 — redact argv: strip --insight/--subject/--evidence/--content/--reason values
16
+ * G6 — derive a NON-REVERSIBLE argv summary: sha256 command_hash + known flag
17
+ * NAMES + raw length. The command text itself never leaves this process.
17
18
  * G7 — append event JSON line to .orchestrator/metrics/events.jsonl
18
19
  * Always: exit 0 (never block)
19
20
  *
@@ -22,6 +23,7 @@
22
23
  */
23
24
 
24
25
  import { readStdin, emitAllow } from '../scripts/lib/io.mjs';
26
+ import crypto from 'node:crypto';
25
27
  import { existsSync } from 'node:fs';
26
28
  import { readFile } from 'node:fs/promises';
27
29
  import path from 'node:path';
@@ -29,8 +31,7 @@ import { emitEvent } from '../scripts/lib/events.mjs';
29
31
  import { findScopeFile } from '../scripts/lib/hardening.mjs';
30
32
 
31
33
  import { shouldRunHook } from './_lib/profile-gate.mjs';
32
- // exit 0 immediately when this hook is disabled via profile/env
33
- if (!shouldRunHook('pre-bash-memory-propose-audit')) process.exit(0);
34
+ import { isMainModule } from '../scripts/lib/is-main-module.mjs';
34
35
 
35
36
  // ---------------------------------------------------------------------------
36
37
  // Constants
@@ -48,59 +49,78 @@ if (!shouldRunHook('pre-bash-memory-propose-audit')) process.exit(0);
48
49
  const MEMORY_PROPOSE_REGEX = /\bnode\b.*\bmemory-propose\.mjs\b/i;
49
50
 
50
51
  /**
51
- * Flags whose VALUES are privacy-sensitive and must be redacted from the log.
52
- * The flag name itself is preserved; only the value is replaced with [REDACTED].
52
+ * The flag NAMES `scripts/memory-propose.mjs` accepts (its `parseArgs` options
53
+ * block, plus `--help`). CLOSED LIST BY DESIGN: `flags_present` is built by
54
+ * intersecting the command with this constant, never by scraping `--\S+` tokens
55
+ * out of the command. A scrape would re-open the leak this file just closed —
56
+ * `--insight --my-secret-value` would publish the value as if it were a flag.
53
57
  *
54
- * Matches both forms:
55
- * --insight=value → --insight=[REDACTED]
56
- * --insight "quoted value" → --insight [REDACTED]
57
- * --insight unquoted → --insight [REDACTED]
58
+ * A flag the CLI does not know is simply not reported. That is the correct
59
+ * trade: the field exists to say WHICH of the audited CLI's switches were used,
60
+ * not to mirror the command line.
58
61
  */
59
- // CAVEAT (#554 A3, low severity): The regex operates on the unparsed Bash
60
- // tool_input.command string. Exotic shell forms bypass redaction by hiding
61
- // the value behind shell expansion the regex cannot see:
62
- // --insight=$VAR (env-var indirection)
63
- // --insight=$(cat secret) (command substitution)
64
- // --insight <<<heredoc (here-string)
65
- // The literal token (e.g. `$VAR`) is logged, NOT the resolved value — so an
66
- // audit-trail leak requires the resolved value to also appear literally
67
- // elsewhere in the command. Acceptable per the project's local-trust model.
68
- //
69
- // Issue #546: `\S+` would match an opening quote `"` or `'` (non-whitespace),
70
- // and on malformed inputs (e.g. unbalanced or shell-already-unescaped values)
71
- // could partial-match the value, leaving the tail unredacted. The negative
72
- // lookahead `(?!["'])` forces `\S+` to be tried only on values that do not
73
- // start with a quote, preserving the quoted-alt as the sole path for quoted
74
- // values and preventing tail-leaks on malformed inputs.
75
- const SENSITIVE_FLAGS_REGEX = /--(?:insight|subject|evidence|content|reason)(?:=("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|(?!["'])\S+)|\s+("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|(?!["'])\S+))/g;
62
+ const KNOWN_FLAGS = Object.freeze([
63
+ 'type',
64
+ 'subject',
65
+ 'insight',
66
+ 'evidence',
67
+ 'confidence',
68
+ 'dry-run',
69
+ 'file-paths',
70
+ 'help',
71
+ ]);
76
72
 
77
73
  // ---------------------------------------------------------------------------
78
74
  // Helpers
79
75
  // ---------------------------------------------------------------------------
80
76
 
81
77
  /**
82
- * Redact values of privacy-sensitive flags from a command string.
83
- * Preserves flag names; replaces values with [REDACTED].
78
+ * sha256(command), truncated to 16 hex characters.
79
+ *
80
+ * WORD-FOR-WORD the `hashCommand()` of `hooks/enforce-commands.mjs:69` (itself
81
+ * taken from `hooks/pre-bash-destructive-guard.mjs:253`, which mirrors
82
+ * `loop-guard.mjs` `hashArgs()`). Deliberately duplicated rather than shared:
83
+ * a PreToolUse hook must not gain another loadable module that can fail at
84
+ * start — three lines are cheaper than a load error on the hot path. Verified
85
+ * before duplicating that no helper is EXPORTED at any of those sites.
86
+ *
87
+ * WHY THE HASH IS HERE (#1415, 2026-09-21): until today this hook wrote a
88
+ * merely flag-redacted `argv_truncated` (512 chars of command text) into
89
+ * `orchestrator.memory.propose_invoked` — a TRACKED `events.jsonl` line and,
90
+ * with the webhook configured, a network payload. Redaction covered only the
91
+ * five `--insight/--subject/--evidence/--content/--reason` values and said so
92
+ * in its own caveat: `$VAR`, `$(cat secret)` and any value written elsewhere
93
+ * on the line passed through. This was the THIRD and last open site of the
94
+ * class closed by `8f15f77b` (enforce-commands) and #1404 (staging-fence).
95
+ *
96
+ * The hash keeps the event COUNTABLE and GROUPABLE — repeat invocations of the
97
+ * same command still collapse to one key — which is the only property any
98
+ * consumer needed. Measured 2026-09-21 (`rg argv_truncated` over `scripts/
99
+ * hooks/ skills/ docs/`): ZERO production readers, the field's only consumers
100
+ * were this hook's own tests.
84
101
  *
85
102
  * @param {string} command
86
103
  * @returns {string}
87
104
  */
88
- function redactArgv(command) {
89
- return command.replace(SENSITIVE_FLAGS_REGEX, (match, eqValue, spaceValue) => {
90
- if (eqValue !== undefined) {
91
- // --insight=value form: preserve up to and including '=', replace value
92
- const eqIndex = match.indexOf('=');
93
- return match.slice(0, eqIndex + 1) + '[REDACTED]';
94
- }
95
- if (spaceValue !== undefined) {
96
- // --insight value form: preserve flag + whitespace, replace value
97
- const valueIndex = match.lastIndexOf(spaceValue);
98
- return match.slice(0, valueIndex) + '[REDACTED]';
99
- }
100
- // Fallback: keep flag name, drop value
101
- const flagMatch = match.match(/^(--[\w-]+)/);
102
- return flagMatch ? flagMatch[1] + ' [REDACTED]' : match;
103
- });
105
+ function hashCommand(command) {
106
+ return crypto.createHash('sha256').update(command).digest('hex').slice(0, 16);
107
+ }
108
+
109
+ /**
110
+ * The subset of KNOWN_FLAGS that appears in the command, in KNOWN_FLAGS order.
111
+ *
112
+ * Names only — a value can never reach this array, because the array is built
113
+ * from the constant above and the command is only ever TESTED against it. Both
114
+ * spellings count (`--insight=x` and `--insight x`); the trailing boundary
115
+ * `(?![\w-])` keeps `--insightful` from reporting `insight`.
116
+ *
117
+ * @param {string} command
118
+ * @returns {string[]}
119
+ */
120
+ function flagsPresent(command) {
121
+ return KNOWN_FLAGS.filter((flag) =>
122
+ new RegExp(`--${flag}(?![\\w-])`).test(command),
123
+ );
104
124
  }
105
125
 
106
126
  /**
@@ -180,8 +200,11 @@ async function main() {
180
200
  // G5 — resolve wave
181
201
  const wave = await resolveWave(projectDir);
182
202
 
183
- // G6 — redact argv: strip sensitive flag values, keep flag names
184
- const argvRedacted = redactArgv(command);
203
+ // G6 — derive the non-reversible argv summary (#1415). Nothing downstream
204
+ // ever sees the command text: hash for grouping, known flag NAMES for
205
+ // shape, raw length for size — no operands, no paths, no values.
206
+ const commandHash = hashCommand(command);
207
+ const flags = flagsPresent(command);
185
208
 
186
209
  // G7 — emit canonical event via emitEvent (single emission path: schema + webhook,
187
210
  // replacing the local hand-rolled appendFileSync bypass).
@@ -193,7 +216,9 @@ async function main() {
193
216
  await emitEvent('orchestrator.memory.propose_invoked', {
194
217
  session_id: sessionId,
195
218
  wave,
196
- argv_truncated: argvRedacted.slice(0, 512),
219
+ command_hash: commandHash,
220
+ flags_present: flags,
221
+ argv_length: command.length,
197
222
  cwd: process.cwd(),
198
223
  exit_code: null,
199
224
  });
@@ -203,10 +228,17 @@ async function main() {
203
228
  return emitAllow();
204
229
  }
205
230
 
206
- // Top-level error handler never let exit 1 leak
207
- main().catch((e) => {
208
- process.stderr.write(
209
- `⚠ pre-bash-memory-propose-audit: internal error ${e?.message || e}\n`,
210
- );
211
- process.exit(0); // fail-open on internal errors
212
- });
231
+ // Entry guard (#1393): run only as the node script the harness execs — a bare
232
+ // `import()` must run no handler and must not exit the importing process.
233
+ if (isMainModule(import.meta.url)) {
234
+ // exit 0 immediately when this hook is disabled via profile/env
235
+ if (!shouldRunHook('pre-bash-memory-propose-audit')) process.exit(0);
236
+
237
+ // Top-level error handler — never let exit 1 leak
238
+ main().catch((e) => {
239
+ process.stderr.write(
240
+ `⚠ pre-bash-memory-propose-audit: internal error — ${e?.message || e}\n`,
241
+ );
242
+ process.exit(0); // fail-open on internal errors
243
+ });
244
+ }