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
@@ -173,6 +173,53 @@
173
173
  * confused" as a bypass strategy. This is the one place the guard is closed
174
174
  * rather than open, and it is closed because the alternative is a silent skip.
175
175
  *
176
+ * ## Interpreter eval payloads (GitLab #1408)
177
+ *
178
+ * The blind spot this file used to NAME became the habitual shape. On
179
+ * 2026-09-19 a coordinator appended its own `sessions.jsonl` record with
180
+ * `node --input-type=module -e "… appendFileSync('.orchestrator/metrics/
181
+ * sessions.jsonl', JSON.stringify(rec) + '\n')"`; `emit-session.mjs` never ran
182
+ * that session, and the record landed with `ended_at` instead of `completed_at`
183
+ * and four required fields missing. Measured over 58 transcripts of this repo:
184
+ * 438 Bash commands mention the ledger, the SHELL write shapes above occurred
185
+ * **0 times**, and 46 are interpreter one-liners. Fleet-wide: 40 interpreter
186
+ * write calls, 38 with the ledger path as the FIRST argument. The guard was
187
+ * aimed at a shape nobody uses.
188
+ *
189
+ * What is covered now: ONE predicate (`findInterpreterLedgerWrite`), hung off
190
+ * the SAME per-segment loop as every other matcher here, so scoping is per
191
+ * STATEMENT — `node -e '<read>' ; echo ok` allows, `cmd && node -e '<append>'`
192
+ * denies on the second statement alone. It takes the eval-flag operand
193
+ * (`-e`/`--eval`/`-p`/`--print`, `python -c`, `perl -e`, `ruby -e`,
194
+ * `deno eval`, in separated / attached / `=`-joined spelling) and denies iff a
195
+ * write call from a CLOSED list (`appendFile(Sync)`, `writeFile(Sync)`,
196
+ * `createWriteStream`; Python `open(…, 'a'|'w'|'x'|…)`; Ruby `File.write` /
197
+ * `IO.write` / `File.open`; Perl's two `open` forms) has the literal basename
198
+ * `sessions.jsonl` in its FIRST ARGUMENT TEXT.
199
+ *
200
+ * The first-argument scoping is load-bearing, not an optimisation: keying on
201
+ * mere co-occurrence denies all three of the ledger READS measured in this
202
+ * repo's transcripts (each reads the ledger and writes
203
+ * `.orchestrator/tmp/session-entry.json` before piping into
204
+ * `emit-session.mjs`), and denies `readFileSync('…/sessions.jsonl')` +
205
+ * `writeFileSync('/tmp/x')` in one one-liner.
206
+ *
207
+ * KNOWN LIMITS — these stay limits, deliberately (BV-004):
208
+ * - The path held in a VARIABLE: `const p = '…/sessions.jsonl';
209
+ * appendFileSync(p, …)`. No constant propagation here; the first-argument
210
+ * text carries no basename.
211
+ * - A here-doc-fed interpreter (`node <<'EOF' … EOF`) — the body is skipped
212
+ * upstream by `scanCommand`, so no payload reaches this predicate.
213
+ * - An invoked SCRIPT FILE (`node tools/append.mjs`) — the ledger name is not
214
+ * in the command string at all.
215
+ * - A write API outside the closed list (`fs.open` + `write`, a stream from a
216
+ * helper, `Deno.writeTextFile`), and Python kwargs-only `open(file=…,
217
+ * mode='a')`.
218
+ * REVISIT TRIGGER: re-run the transcript census (`rg` for interpreter
219
+ * one-liners naming the ledger). If a shape from the four bullets above
220
+ * appears in ANY real transcript, widen the predicate then — not before. A
221
+ * guard widened against imagined shapes buys false positives at full price.
222
+ *
176
223
  * ## What this does NOT catch (stated plainly — the bounds are the contract)
177
224
  *
178
225
  * - Indirection through a variable: `>> "$LEDGER"`, `tee "$LEDGER"`,
@@ -193,12 +240,29 @@
193
240
  * buried in a quoted substitution is not the accident shape this guard is
194
241
  * for — it is the "determined circumvention" line below.
195
242
  * - In-place editors: `sed -i`, `perl -i`, `ed`, an interactive editor.
196
- * - A write performed inside an interpreter: `node -e`, `python -c`,
197
- * `bash -c '… >> …/sessions.jsonl'`, or any script the command invokes that
198
- * appends the ledger itself. The one exception is a WRAPPER payload
199
- * `env -S 'tee -a …/sessions.jsonl'` which `resolveSegmentVerb` reports
200
- * as a payload and which this matcher re-enters (to MAX_PAYLOAD_DEPTH).
201
- * Interpreter `-c` payloads are a larger surface and stay out of scope.
243
+ * - A write performed by a SCRIPT FILE the command invokes
244
+ * (`node tools/append.mjs`) the ledger name never appears in the command
245
+ * string at all, so nothing here can see it. Interpreter EVAL payloads
246
+ * (`node -e`, `python3 -c`) used to sit in this bullet too; since #1408 they
247
+ * are covered see "Interpreter eval payloads" below for the exact bound.
248
+ * - A here-doc-fed interpreter (`node <<'EOF' EOF`): here-doc BODIES are
249
+ * skipped as the data they are, so the payload never reaches the matcher.
250
+ * - A shell `-c` payload the segment does not NAME a shell for. Since
251
+ * 2026-09-20 a `-c` operand following a `bash`/`sh`/`zsh`/`dash`/`ksh`/`su`
252
+ * TOKEN is re-entered by the same matcher (`bash -c '… >> …/sessions.jsonl'`
253
+ * and `xargs -I{} sh -c '…'` both deny), but three shapes stay out:
254
+ * `ssh host '… >> …/sessions.jsonl'` (a REMOTE ledger — not this repo's
255
+ * file, so out of scope by nature, not by omission); a payload whose shell
256
+ * is only reachable through expansion (`"$SHELL" -c …`, `eval "$CMD"`);
257
+ * and a payload nested past MAX_PAYLOAD_DEPTH (2) — that cut is marked, not
258
+ * silent, when the dropped payload mentions the ledger (#998 item 2).
259
+ * This bullet REPLACES the one #1408 deleted (it named
260
+ * `bash -c '… >> …/sessions.jsonl'` as uncovered, which was true, and the
261
+ * replacement bullet narrowed the claim to script files and here-docs while
262
+ * the hole stayed open — a guard may have limits, it may not misstate them).
263
+ * REVISIT TRIGGER: re-run the transcript census for interpreter one-liners
264
+ * naming the ledger; widen only if one of these three shapes appears in a
265
+ * REAL transcript.
202
266
  * - Obfuscation: `eval`, `base64 -d | sh`, a here-doc-fed shell. Here-doc
203
267
  * BODIES are skipped as the data they are — `bash <<EOF … EOF` therefore
204
268
  * hides its payload from this matcher by construction.
@@ -249,8 +313,7 @@ import path from 'node:path';
249
313
  import { pathToFileURL } from 'node:url';
250
314
 
251
315
  import { shouldRunHook } from './_lib/profile-gate.mjs';
252
- // #211: exit 0 immediately (silent allow) when this hook is disabled via profile/env
253
- if (!shouldRunHook('pre-bash-sessions-ledger-guard')) process.exit(0);
316
+ import { isMainModule } from '../scripts/lib/is-main-module.mjs';
254
317
 
255
318
  // ---------------------------------------------------------------------------
256
319
  // #993 — late-bound repo dependencies
@@ -324,8 +387,9 @@ const GUARD_CONSEQUENCE = {
324
387
  ' COMMITTED (HEAD) command lexer — any uncommitted change to that file is NOT in effect.',
325
388
  ],
326
389
  inactive: [
327
- ' Consequence: a direct shell write into .orchestrator/metrics/sessions.jsonl',
328
- ' (>, >>, tee, dd of=, cp/mv destination) is NOT being blocked. This hook is a',
390
+ ' Consequence: a direct write into .orchestrator/metrics/sessions.jsonl',
391
+ ' (>, >>, tee, dd of=, cp/mv destination, or an interpreter payload such as',
392
+ ' node -e / python -c — #1408) is NOT being blocked. This hook is a',
329
393
  ' fail-open nudge, not a security boundary — but repair it so the #958 corruption',
330
394
  ' class stays caught.',
331
395
  ],
@@ -859,6 +923,293 @@ function scanCommand(command) {
859
923
  return { targets, sanitized: out, balanced: true };
860
924
  }
861
925
 
926
+ // ---------------------------------------------------------------------------
927
+ // Interpreter eval payloads (#1408)
928
+ //
929
+ // ONE predicate, hung off the EXISTING per-segment loop in findWriteVerbTarget.
930
+ // Deliberately NOT a Bash parser and deliberately NOT in command-blocker.mjs:
931
+ // that module has five other consumers, and a JS payload re-entered by its SHELL
932
+ // matcher would be misread as shell.
933
+ // ---------------------------------------------------------------------------
934
+
935
+ /**
936
+ * Interpreter verbs whose eval payload is read, mapped to the DIALECT its code
937
+ * is judged in. `node`/`bun`/`deno` share the `fs` API surface, so one dialect
938
+ * covers all three.
939
+ */
940
+ const INTERPRETER_FAMILY = new Map([
941
+ ['node', 'js'],
942
+ ['nodejs', 'js'],
943
+ ['bun', 'js'],
944
+ ['deno', 'js'],
945
+ ['python', 'python'],
946
+ ['python2', 'python'],
947
+ ['python3', 'python'],
948
+ ['perl', 'perl'],
949
+ ['ruby', 'ruby'],
950
+ ]);
951
+
952
+ /** Flags whose operand is CODE, not a script path — per dialect. */
953
+ const INTERPRETER_EVAL_FLAGS = {
954
+ js: new Set(['-e', '--eval', '-p', '--print']),
955
+ python: new Set(['-c']),
956
+ perl: new Set(['-e', '-E']),
957
+ ruby: new Set(['-e']),
958
+ };
959
+
960
+ /**
961
+ * CLOSED list of write calls per dialect. Nothing outside it is matched — a
962
+ * matcher keyed on "the payload mentions the ledger AND writes something" was
963
+ * measured to false-positive on all three real read one-liners in this repo's
964
+ * transcripts (they read the ledger and write `.orchestrator/tmp/…`).
965
+ */
966
+ const INTERPRETER_WRITE_CALLS = {
967
+ js: ['appendFileSync', 'appendFile', 'writeFileSync', 'writeFile', 'createWriteStream'],
968
+ python: ['open'],
969
+ perl: ['open'],
970
+ ruby: ['File.write', 'IO.write', 'File.open'],
971
+ };
972
+
973
+ /**
974
+ * Collect the eval-payload operands of one interpreter invocation.
975
+ *
976
+ * `deno` is the odd one: its eval mode is a SUBCOMMAND (`deno eval '<code>'`),
977
+ * not a flag. Everything else takes `-e`/`-c`-style flags in three spellings —
978
+ * separated (`-e '<code>'`), attached (`-e'<code>'`) and `=`-joined
979
+ * (`--eval='<code>'`).
980
+ *
981
+ * A QUOTED `-e` token is still treated as the flag: bash passes `'-e'` and `-e`
982
+ * identically, so skipping the quoted spelling would be a one-character bypass.
983
+ *
984
+ * @param {string} verb
985
+ * @param {string} family
986
+ * @param {Array<{ text: string, quoted: boolean }>} args - tokens after the verb
987
+ * @returns {string[]}
988
+ */
989
+ function collectCodeOperands(verb, family, args) {
990
+ const operands = [];
991
+ if (verb === 'deno') {
992
+ const at = args.findIndex((a) => a.text === 'eval');
993
+ if (at === -1) return operands;
994
+ for (const a of args.slice(at + 1)) {
995
+ if (!a.quoted && a.text.startsWith('-')) continue;
996
+ operands.push(a.text);
997
+ break;
998
+ }
999
+ return operands;
1000
+ }
1001
+
1002
+ const flags = INTERPRETER_EVAL_FLAGS[family];
1003
+ if (!flags) return operands;
1004
+ for (let i = 0; i < args.length; i++) {
1005
+ const text = args[i].text;
1006
+ const eq = text.indexOf('=');
1007
+ if (eq > 0 && flags.has(text.slice(0, eq))) { operands.push(text.slice(eq + 1)); continue; }
1008
+ if (flags.has(text)) {
1009
+ if (i + 1 < args.length) { operands.push(args[i + 1].text); i++; }
1010
+ continue;
1011
+ }
1012
+ for (const flag of flags) {
1013
+ if (flag.length === 2 && !text.startsWith('--') && text.length > 2 && text.startsWith(flag)) {
1014
+ operands.push(text.slice(2));
1015
+ break;
1016
+ }
1017
+ }
1018
+ }
1019
+ return operands;
1020
+ }
1021
+
1022
+ /**
1023
+ * Indices of the `(` that OPENS a call to `name` in `code`.
1024
+ *
1025
+ * Identifier-boundary aware on the left, so `appendFile` does not match inside
1026
+ * `appendFileSync` (which is a separate list entry) and `myWriteFile` is not a
1027
+ * `writeFile`. A receiver prefix is fine — `.` is not an identifier char, so
1028
+ * `fs.appendFileSync` and `require('fs').appendFileSync` both resolve.
1029
+ *
1030
+ * @param {string} code
1031
+ * @param {string} name
1032
+ * @returns {number[]}
1033
+ */
1034
+ function findCallOpenings(code, name) {
1035
+ const out = [];
1036
+ for (let from = 0; ;) {
1037
+ const idx = code.indexOf(name, from);
1038
+ if (idx === -1) return out;
1039
+ from = idx + name.length;
1040
+ const before = idx > 0 ? code[idx - 1] : '';
1041
+ if (before && /[A-Za-z0-9_$]/.test(before)) continue;
1042
+ let j = from;
1043
+ while (j < code.length && (code[j] === ' ' || code[j] === '\t' || code[j] === '\n')) j++;
1044
+ if (code[j] === '(') out.push(j);
1045
+ }
1046
+ }
1047
+
1048
+ /**
1049
+ * Split the argument list opening at `open` on TOP-LEVEL commas.
1050
+ *
1051
+ * Quote- and nesting-aware, single pass, no backtracking regex — this runs on
1052
+ * every Bash call of every session, and a path that can throw or hang reads as
1053
+ * NO decision under the exit-0 protocol, i.e. fail-OPEN in the wrong direction.
1054
+ * Returns `null` when the list never closes (judged as "no hit", never a throw).
1055
+ *
1056
+ * @param {string} code
1057
+ * @param {number} open - index of the opening `(`
1058
+ * @returns {string[]|null}
1059
+ */
1060
+ function splitCallArgs(code, open) {
1061
+ const args = [];
1062
+ let cur = '';
1063
+ let depth = 0;
1064
+ let quote = null;
1065
+ for (let i = open; i < code.length; i++) {
1066
+ const ch = code[i];
1067
+ if (quote !== null) {
1068
+ if (ch === '\\' && i + 1 < code.length) { cur += ch + code[i + 1]; i++; continue; }
1069
+ if (ch === quote) quote = null;
1070
+ cur += ch;
1071
+ continue;
1072
+ }
1073
+ if (ch === '"' || ch === "'" || ch === '`') { quote = ch; cur += ch; continue; }
1074
+ if (ch === '(' || ch === '[' || ch === '{') {
1075
+ depth++;
1076
+ if (depth > 1) cur += ch;
1077
+ continue;
1078
+ }
1079
+ if (ch === ')' || ch === ']' || ch === '}') {
1080
+ depth--;
1081
+ if (depth === 0) { args.push(cur); return args; }
1082
+ cur += ch;
1083
+ continue;
1084
+ }
1085
+ if (ch === ',' && depth === 1) { args.push(cur); cur = ''; continue; }
1086
+ cur += ch;
1087
+ }
1088
+ return null;
1089
+ }
1090
+
1091
+ /**
1092
+ * Does any operand after the first name a WRITE mode?
1093
+ *
1094
+ * `open(p, 'a')` / `open(p, mode='w')` / `File.open(p, 'a')` write; `open(p)`
1095
+ * and `open(p, 'r')` read. `+` counts (`'r+'` writes).
1096
+ *
1097
+ * @param {string[]} args
1098
+ * @returns {boolean}
1099
+ */
1100
+ function hasWriteModeOperand(args) {
1101
+ for (let i = 1; i < args.length; i++) {
1102
+ const m = /^\s*(?:mode\s*=\s*)?(['"])([^'"]*)\1\s*$/.exec(args[i]);
1103
+ if (m && /[awx+]/.test(m[2])) return true;
1104
+ }
1105
+ return false;
1106
+ }
1107
+
1108
+ /**
1109
+ * Judge ONE resolved write call. The FIRST-ARGUMENT scoping is load-bearing:
1110
+ * keying on mere co-occurrence of a write verb and the ledger basename anywhere
1111
+ * in the payload denies `readFileSync('…/sessions.jsonl')` +
1112
+ * `writeFileSync('/tmp/x')` in one one-liner — the shape all three measured
1113
+ * ledger READS in this repo's transcripts actually have.
1114
+ *
1115
+ * Perl is the exception, by its own grammar: `open(FH, '>>', $p)` puts the path
1116
+ * THIRD and `open(FH, ">>$p")` glues mode to path. A read open never carries a
1117
+ * `>`, so "the ledger is an operand AND some operand carries `>`" is exact
1118
+ * rather than a guess.
1119
+ *
1120
+ * @param {string} family
1121
+ * @param {string} name
1122
+ * @param {string[]} args
1123
+ * @returns {string|null} the offending argument text, or null
1124
+ */
1125
+ function judgeInterpreterCall(family, name, args) {
1126
+ if (args.length === 0) return null;
1127
+ if (family === 'perl') {
1128
+ const hit = args.find((a) => a.includes(LEDGER_BASENAME));
1129
+ if (!hit) return null;
1130
+ return args.some((a) => a.includes('>')) ? hit : null;
1131
+ }
1132
+ if (!args[0].includes(LEDGER_BASENAME)) return null;
1133
+ if (family === 'python' || name === 'File.open') {
1134
+ return hasWriteModeOperand(args) ? args[0] : null;
1135
+ }
1136
+ return args[0];
1137
+ }
1138
+
1139
+ /**
1140
+ * The #1408 predicate: does this interpreter invocation write the ledger from
1141
+ * INSIDE its eval payload?
1142
+ *
1143
+ * Called from the per-segment loop of {@link findWriteVerbTarget}, so scoping is
1144
+ * per STATEMENT: `node -e '<read>' ; echo ok` is judged on the statement that
1145
+ * hit, and `cmd && node -e '<append>'` denies on the second one alone.
1146
+ *
1147
+ * @param {string} verb - resolved segment verb (basename, wrappers already peeled)
1148
+ * @param {Array<{ text: string, quoted: boolean }>} args - tokens after the verb
1149
+ * @returns {string|null} the offending target, or null
1150
+ */
1151
+ function findInterpreterLedgerWrite(verb, args) {
1152
+ const family = INTERPRETER_FAMILY.get(verb);
1153
+ if (!family) return null;
1154
+ for (const code of collectCodeOperands(verb, family, args)) {
1155
+ if (typeof code !== 'string' || !code.includes(LEDGER_BASENAME)) continue;
1156
+ for (const name of INTERPRETER_WRITE_CALLS[family]) {
1157
+ for (const open of findCallOpenings(code, name)) {
1158
+ const callArgs = splitCallArgs(code, open);
1159
+ if (!callArgs) continue;
1160
+ const hit = judgeInterpreterCall(family, name, callArgs);
1161
+ if (hit) return `${hit.trim()} (${name}(…) inside a ${verb} eval payload)`;
1162
+ }
1163
+ }
1164
+ }
1165
+ return null;
1166
+ }
1167
+
1168
+ /**
1169
+ * Shells whose `-c <payload>` operand is a command line the shell EXECUTES.
1170
+ *
1171
+ * DELIBERATE DUPLICATE of `DASH_C_SHELLS` in scripts/lib/command-blocker.mjs,
1172
+ * which does not export it (nor its `dashCPayloads`). Change one, change both.
1173
+ */
1174
+ const DASH_C_SHELLS = new Set(['bash', 'sh', 'zsh', 'dash', 'ksh', 'su']);
1175
+
1176
+ /**
1177
+ * Collect the command lines a shell nested in this segment will EXECUTE, so
1178
+ * {@link findWriteVerbTarget} can re-enter the SAME matcher on them.
1179
+ *
1180
+ * `resolveSegmentVerb` reports WRAPPER payloads (`env -S '…'`) and this hook
1181
+ * has recursed on those since #982 — but a `bash -c` payload is not a wrapper
1182
+ * payload, and command-blocker's own `dedupedSegmentPayloads` (the piece that
1183
+ * adds them) is internal to that module. So the guard saw `bash -c` as the
1184
+ * verb `bash` with an inert quoted operand and ALLOWED, while the identical
1185
+ * unwrapped command DENIED — one word of bypass, measured 2026-09-20.
1186
+ *
1187
+ * Keyed on a shell TOKEN anywhere in the segment rather than on the resolved
1188
+ * verb: that is what reaches `xargs -I{} sh -c "…"` (verb `xargs`) and
1189
+ * `find … -exec sh -c "…" \;`, and it keeps `grep -c <pattern>` out, because
1190
+ * `grep` is not a shell name. Recursion is bounded by MAX_PAYLOAD_DEPTH, and a
1191
+ * payload that carries no write intent still ALLOWs — the false-positive floor
1192
+ * (`bash -c "jq . <ledger>"`, `bash -c "tail -1 <ledger> | jq ."`) is judged by
1193
+ * the same matcher as its unwrapped form, not by a second, weaker one.
1194
+ *
1195
+ * @param {Array<{ text: string, quoted: boolean }>} segment
1196
+ * @returns {string[]} distinct payload strings
1197
+ */
1198
+ function dashCShellPayloads(segment) {
1199
+ const out = new Set();
1200
+ for (let i = 0; i < segment.length - 1; i++) {
1201
+ const tok = segment[i];
1202
+ if (tok.quoted || !DASH_C_SHELLS.has(path.basename(tok.text))) continue;
1203
+ for (let j = i + 1; j < segment.length - 1; j++) {
1204
+ if (/^-[A-Za-z]*c$/.test(segment[j].text)) {
1205
+ out.add(segment[j + 1].text);
1206
+ break;
1207
+ }
1208
+ }
1209
+ }
1210
+ return [...out];
1211
+ }
1212
+
862
1213
  /**
863
1214
  * Find a non-redirect write verb (`tee`, `dd of=`, `cp`/`mv` destination)
864
1215
  * whose target is the ledger.
@@ -923,6 +1274,12 @@ function findWriteVerbTarget(command, depth = 0, marks = []) {
923
1274
  }
924
1275
  }
925
1276
 
1277
+ // A shell `-c` payload is a command line too, and until 2026-09-20 it was
1278
+ // the guard's one-word bypass: `bash -c "echo x >> <ledger>"` ALLOWED while
1279
+ // `echo x >> <ledger>` DENIED. See dashCShellPayloads for why this is keyed
1280
+ // on a shell token rather than the resolved verb.
1281
+ for (const p of dashCShellPayloads(segment)) payloadSet.add(p);
1282
+
926
1283
  // `env -S 'tee -a <ledger>'` hides a whole command line in one operand.
927
1284
  // Recurse on the payload with the SAME matcher rather than a second,
928
1285
  // weaker one — bounded by MAX_PAYLOAD_DEPTH. Kept BEFORE the verb dispatch
@@ -951,6 +1308,12 @@ function findWriteVerbTarget(command, depth = 0, marks = []) {
951
1308
  // other's (the lib docblock's positional-walk caveat).
952
1309
  const args = segment.slice(index + 1);
953
1310
 
1311
+ // #1408 — the write happens INSIDE an interpreter payload (`node -e`,
1312
+ // `python3 -c`, …), where there is no shell write intent to resolve. One
1313
+ // predicate, judged on THIS statement; see findInterpreterLedgerWrite.
1314
+ const interpreterTarget = findInterpreterLedgerWrite(verb, args);
1315
+ if (interpreterTarget) return interpreterTarget;
1316
+
954
1317
  // The repair CLI mutates the ledger from inside Node, so there is no
955
1318
  // redirect target for the structural matcher to see. Match only its
956
1319
  // effective apply contract at the resolved token seam: `node`, the
@@ -1163,8 +1526,9 @@ async function main() {
1163
1526
  `Direct write to the sessions ledger blocked: '${shown}'`,
1164
1527
  `The ledger is append-only through its validating writer:`,
1165
1528
  ` node scripts/emit-session.mjs --entry '<json>' (or pipe the JSON on stdin)`,
1166
- `Hand-composing a record and appending it with a shell redirect skips schema`,
1167
- `validation that is exactly how the malformed record in GitLab #958 landed.`,
1529
+ `Hand-composing a record and appending it with a shell redirect, or from`,
1530
+ `inside an interpreter (node -e / python -c) skips schema validation. That is`,
1531
+ `exactly how the malformed records in GitLab #958 and #1408 landed.`,
1168
1532
  `Override (intentional maintenance only): run the session with`,
1169
1533
  `SO_DISABLED_HOOKS=pre-bash-sessions-ledger-guard`,
1170
1534
  `See: GitLab #958, skills/session-end/session-metrics-write.md`,
@@ -1203,11 +1567,18 @@ try {
1203
1567
  process.exit(0); // fail-open, but no longer fail-silent
1204
1568
  }
1205
1569
 
1206
- // Top-level error handler fail-OPEN (see the module docblock). Never let a
1207
- // non-zero exit leak: on this protocol exit 0 + empty stdout is "no decision".
1208
- main().catch((e) => {
1209
- process.stderr.write(
1210
- `⚠ pre-bash-sessions-ledger-guard: internal error — ${e?.message || e}\n`,
1211
- );
1212
- process.exit(0);
1213
- });
1570
+ // Entry guard (#1393): run only as the node script the harness execs — a bare
1571
+ // `import()` must run no handler and must not exit the importing process.
1572
+ if (isMainModule(import.meta.url)) {
1573
+ // #211: exit 0 immediately (silent allow) when this hook is disabled via profile/env
1574
+ if (!shouldRunHook('pre-bash-sessions-ledger-guard')) process.exit(0);
1575
+
1576
+ // Top-level error handler — fail-OPEN (see the module docblock). Never let a
1577
+ // non-zero exit leak: on this protocol exit 0 + empty stdout is "no decision".
1578
+ main().catch((e) => {
1579
+ process.stderr.write(
1580
+ `⚠ pre-bash-sessions-ledger-guard: internal error — ${e?.message || e}\n`,
1581
+ );
1582
+ process.exit(0);
1583
+ });
1584
+ }