session-orchestrator 5.3.0 → 5.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/.agents/skills/claude-md-drift-check/SKILL.md +1 -1
  2. package/.agents/skills/navigator/SKILL.md +22 -0
  3. package/.agents/skills/navigator/agents/openai.yaml +5 -0
  4. package/.agents/skills/test-audit/SKILL.md +21 -0
  5. package/.agents/skills/test-audit/agents/openai.yaml +5 -0
  6. package/.claude-plugin/marketplace.json +1 -1
  7. package/.claude-plugin/plugin.json +1 -1
  8. package/.codex-plugin/plugin.json +1 -1
  9. package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -1
  10. package/.codex-plugin/skills/navigator/SKILL.md +22 -0
  11. package/.codex-plugin/skills/navigator/agents/openai.yaml +5 -0
  12. package/.codex-plugin/skills/test-audit/SKILL.md +21 -0
  13. package/.codex-plugin/skills/test-audit/agents/openai.yaml +5 -0
  14. package/.cursor/commands/navigator.md +15 -0
  15. package/.cursor/commands/test-audit.md +14 -0
  16. package/.cursor/rules/060-evolve.mdc +27 -12
  17. package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
  18. package/.cursor/skills/navigator/SKILL.md +13 -0
  19. package/.cursor/skills/test-audit/SKILL.md +12 -0
  20. package/.cursor-plugin/plugin.json +1 -1
  21. package/.orchestrator/policy/blocked-commands.json +1 -1
  22. package/AGENTS.md +9 -31
  23. package/CHANGELOG.md +70 -0
  24. package/README.md +14 -10
  25. package/docs/agent-authoring.md +2 -2
  26. package/docs/baseline.md +10 -1
  27. package/docs/ci-setup.md +57 -4
  28. package/docs/components.md +12 -12
  29. package/docs/events-schema.md +11 -8
  30. package/docs/persona-panel.md +5 -4
  31. package/docs/rule-authoring.md +27 -12
  32. package/docs/scope-collision-guard.md +26 -4
  33. package/docs/session-config-reference.md +55 -6
  34. package/docs/session-config-template.md +11 -2
  35. package/hooks/_lib/hook-import-set.json +27 -23
  36. package/hooks/hooks-codex.json +1 -1
  37. package/hooks/hooks-cursor.json +5 -0
  38. package/hooks/hooks-pi.json +5 -0
  39. package/hooks/hooks.json +6 -1
  40. package/hooks/on-session-start.mjs +53 -0
  41. package/hooks/on-stop.mjs +3 -1
  42. package/hooks/post-tool-batch-wave-signal.mjs +3 -1
  43. package/hooks/pre-bash-memory-propose-audit.mjs +52 -3
  44. package/hooks/pre-bash-sessions-ledger-guard.mjs +28 -2
  45. package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
  46. package/hooks/pre-task-scope-disjoint.mjs +559 -72
  47. package/hooks/subagent-telemetry.mjs +96 -19
  48. package/package.json +3 -3
  49. package/pi/prompts/navigator.md +12 -0
  50. package/pi/prompts/test-audit.md +12 -0
  51. package/rules/README.md +6 -1
  52. package/rules/_index.md +3 -3
  53. package/rules/always-on/bash-harness-pitfalls.md +7 -1
  54. package/rules/opt-in-domain/prompt-caching.md +5 -1
  55. package/rules/opt-in-stack/backend-data.md +9 -1
  56. package/rules/opt-in-stack/backend.md +7 -1
  57. package/rules/opt-in-stack/frontend.md +6 -1
  58. package/rules/opt-in-stack/security-web.md +7 -1
  59. package/rules/opt-in-stack/swift.md +5 -1
  60. package/scripts/apply-session-learnings.mjs +492 -0
  61. package/scripts/archive-closed-prds.mjs +2 -1
  62. package/scripts/autopilot.mjs +49 -53
  63. package/scripts/backfill-abandoned-sessions.mjs +1 -1
  64. package/scripts/baseline-archetypes.mjs +1 -1
  65. package/scripts/check-package-manager.mjs +62 -5
  66. package/scripts/cursor-install.mjs +1 -4
  67. package/scripts/emit-session.mjs +296 -2
  68. package/scripts/export-hw-learnings.mjs +22 -3
  69. package/scripts/fleet-checkin.mjs +147 -0
  70. package/scripts/lib/agent-frontmatter.mjs +3 -3
  71. package/scripts/lib/auto-dream.mjs +7 -6
  72. package/scripts/lib/autopilot/flags.mjs +6 -1
  73. package/scripts/lib/autopilot/loop.mjs +10 -6
  74. package/scripts/lib/baseline-archetypes.mjs +41 -22
  75. package/scripts/lib/config/host-paths.mjs +79 -0
  76. package/scripts/lib/config/persona-gate-wave.mjs +3 -3
  77. package/scripts/lib/config/reaper.mjs +7 -6
  78. package/scripts/lib/config-schema.mjs +13 -0
  79. package/scripts/lib/config.mjs +71 -10
  80. package/scripts/lib/events.mjs +21 -2
  81. package/scripts/lib/fleet-protocol.mjs +270 -0
  82. package/scripts/lib/gates/gate-baseline.mjs +13 -3
  83. package/scripts/lib/gates/gate-full.mjs +7 -2
  84. package/scripts/lib/gates/gate-helpers.mjs +93 -2
  85. package/scripts/lib/gates/gate-incremental.mjs +23 -22
  86. package/scripts/lib/gates/gate-per-file.mjs +16 -7
  87. package/scripts/lib/instruction-budget-guard.mjs +69 -11
  88. package/scripts/lib/learnings/expiry-sweep.mjs +34 -3
  89. package/scripts/lib/learnings/io.mjs +154 -26
  90. package/scripts/lib/learnings/schema.mjs +163 -15
  91. package/scripts/lib/learnings/sizing-subject.mjs +2 -2
  92. package/scripts/lib/memory-proposals/schema.mjs +11 -5
  93. package/scripts/lib/named-baseline-resolver.mjs +53 -28
  94. package/scripts/lib/named-vault-resolver.mjs +104 -5
  95. package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
  96. package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
  97. package/scripts/lib/orphan-reaper/reaper-audit.mjs +291 -0
  98. package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
  99. package/scripts/lib/orphan-reaper/scan-throttle.mjs +92 -0
  100. package/scripts/lib/orphan-reaper/scan.mjs +506 -0
  101. package/scripts/lib/orphan-reaper.mjs +43 -1315
  102. package/scripts/lib/owner-yaml.mjs +104 -6
  103. package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
  104. package/scripts/lib/process-group.mjs +62 -11
  105. package/scripts/lib/project-hygiene.mjs +21 -1
  106. package/scripts/lib/quality-gate.mjs +45 -5
  107. package/scripts/lib/resource-probe.mjs +1 -1
  108. package/scripts/lib/rule-loader.mjs +100 -62
  109. package/scripts/lib/rules-sync.mjs +27 -12
  110. package/scripts/lib/scope-baseline.mjs +3 -4
  111. package/scripts/lib/session-record-repair.mjs +183 -26
  112. package/scripts/lib/session-registry.mjs +27 -2
  113. package/scripts/lib/session-schema/constants.mjs +30 -4
  114. package/scripts/lib/session-schema/validator.mjs +28 -1
  115. package/scripts/lib/session-token-rollup.mjs +245 -52
  116. package/scripts/lib/sessions-canonical.mjs +100 -9
  117. package/scripts/lib/sh-quote.mjs +29 -0
  118. package/scripts/lib/subagents-schema.mjs +30 -3
  119. package/scripts/lib/telemetry/pricing.mjs +118 -71
  120. package/scripts/lib/tests-src-ratio.mjs +140 -23
  121. package/scripts/lib/tmux-layout/layouts.mjs +1 -11
  122. package/scripts/lib/tmux-layout/tmux-shell.mjs +2 -11
  123. package/scripts/lib/ux-grill/reconcile.mjs +1 -1
  124. package/scripts/lib/validate/check-agents.mjs +1 -1
  125. package/scripts/lib/validate/check-hooks-symmetry.mjs +5 -0
  126. package/scripts/lib/validate/check-learnings-shell-writes.mjs +214 -0
  127. package/scripts/lib/validate/check-test-value-bans.mjs +87 -14
  128. package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
  129. package/scripts/lib/validate/check-unwired-features.mjs +1 -1
  130. package/scripts/lib/validate-vendored-rules.mjs +206 -81
  131. package/scripts/lib/vault-mirror/telemetry.mjs +8 -1
  132. package/scripts/lib/vault-status/board-writer.mjs +48 -11
  133. package/scripts/lib/vault-status/narrative-mirror.mjs +40 -9
  134. package/scripts/lib/wave-executor/dispatch-common.mjs +10 -18
  135. package/scripts/lib/wave-executor/foreign-dispatch.mjs +1 -1
  136. package/scripts/lib/wave-executor/remote-dispatch.mjs +1 -1
  137. package/scripts/lib/wave-resource-gate.mjs +34 -8
  138. package/scripts/memory-propose.mjs +18 -5
  139. package/scripts/release-session-lock.mjs +2 -1
  140. package/scripts/release.mjs +10 -3
  141. package/scripts/repair-invalid-sessions.mjs +37 -4
  142. package/scripts/run-migrate-v2-cross-repo.mjs +158 -30
  143. package/scripts/run-quality-gate.mjs +323 -302
  144. package/scripts/site-numbers.mjs +163 -5
  145. package/scripts/sweep-expired-learnings.mjs +56 -5
  146. package/scripts/sweep-expired-rules.mjs +2 -1
  147. package/scripts/validate-plugin.mjs +7 -0
  148. package/scripts/vault-integration-watcher.mjs +2 -1
  149. package/scripts/vault-mirror.mjs +72 -46
  150. package/scripts/wave-scope-binding.mjs +2 -1
  151. package/skills/_shared/config-reading.md +3 -3
  152. package/skills/_shared/fleet-protocol.md +131 -0
  153. package/skills/_shared/state-ownership.md +5 -0
  154. package/skills/autopilot/SKILL.md +25 -11
  155. package/skills/bootstrap/SKILL.md +1 -1
  156. package/skills/bootstrap/_shared-template.md +1 -1
  157. package/skills/bootstrap/fast-template.md +1 -1
  158. package/skills/bootstrap/private-contract.md +2 -1
  159. package/skills/bootstrap/public-fallback.md +6 -3
  160. package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
  161. package/skills/claude-md-drift-check/SKILL.md +10 -10
  162. package/skills/claude-md-drift-check/checker.mjs +26 -18
  163. package/skills/evolve/SKILL.md +2 -2
  164. package/skills/evolve/references/evolve-analyze-mode.md +10 -6
  165. package/skills/memory-cleanup/SKILL.md +1 -1
  166. package/skills/navigator/SKILL.md +193 -0
  167. package/skills/navigator/references/account-check.sh +87 -0
  168. package/skills/navigator/references/fleet-decisions.mjs +231 -0
  169. package/skills/navigator/references/slot-check.sh +114 -0
  170. package/skills/persona-panel/SKILL.md +2 -2
  171. package/skills/persona-panel/persona-format.md +2 -2
  172. package/skills/persona-panel/presets/designer-lens.md +1 -1
  173. package/skills/persona-panel/presets/engineer-lens.md +1 -1
  174. package/skills/persona-panel/presets/pm-lens.md +1 -1
  175. package/skills/plan/mode-retro.md +19 -4
  176. package/skills/quality-gates/SKILL.md +6 -4
  177. package/skills/remote-offload/SKILL.md +4 -2
  178. package/skills/session-end/learning-patterns.md +22 -12
  179. package/skills/session-end/metrics-collection.md +5 -6
  180. package/skills/session-end/references/phase-3-documentation-updates.md +2 -2
  181. package/skills/session-end/references/phase-4a-worktree-cleanup.md +2 -0
  182. package/skills/session-end/references/session-summary-template.md +1 -1
  183. package/skills/session-end/session-metrics-write.md +22 -60
  184. package/skills/session-plan/SKILL.md +0 -3
  185. package/skills/session-start/SKILL.md +6 -0
  186. package/skills/session-start/references/operations-contract.md +4 -0
  187. package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
  188. package/skills/session-start/references/phase-4-ssot-environment-check.md +2 -2
  189. package/skills/test-audit/SKILL.md +152 -0
  190. package/skills/test-audit/references/campaign.md +111 -0
  191. package/skills/test-audit/references/mutate.sh +50 -0
  192. package/skills/wave-executor/SKILL.md +1 -1
  193. package/skills/wave-executor/references/wave-executor-state-init.md +3 -0
  194. package/skills/wave-executor/references/wave-loop-dispatch.md +3 -1
  195. package/skills/wave-executor/references/wave-loop-review.md +3 -5
  196. package/templates/personas/README.md +5 -5
  197. package/templates/personas/accounting-compliance.v1.md +1 -1
  198. package/templates/personas/accounting-tax-advisor.v1.md +1 -1
  199. package/templates/personas/buyer-p1-cto.v1.md +1 -1
  200. package/templates/personas/buyer-p2-kanzlei.v1.md +1 -1
  201. package/templates/personas/buyer-p3-build.v1.md +1 -1
  202. package/templates/personas/buyer-p4-tech-veto.v1.md +1 -1
  203. package/templates/personas/buyer-p5-solo.v1.md +1 -1
  204. package/templates/personas/buyer-p6-ld.v1.md +1 -1
  205. package/templates/personas/klima-ai-expert.v1.md +1 -1
  206. package/templates/personas/klima-physicist.v1.md +1 -1
@@ -1,5 +1,5 @@
1
1
  /**
2
- * pricing.mjs — USD price table for token-bucket cost estimation (#1244).
2
+ * pricing.mjs — USD price table for token-bucket cost estimation (#1244, #1470).
3
3
  *
4
4
  * The subagent ledger records four token buckets per stop record
5
5
  * (`token_input_uncached`, `token_cache_read`, `token_cache_creation`,
@@ -8,23 +8,39 @@
8
8
  * an order of magnitude on a cache-heavy run — which is the whole reason this
9
9
  * table exists rather than a single `$/token` constant.
10
10
  *
11
- * ## What is measured and what is derived
11
+ * ## Every rate is a documented figure
12
12
  *
13
- * `input` and `output` are the first-party Anthropic API list prices as
14
- * published in the bundled model table, sourced on the date `PRICING_TABLE_DATE`
15
- * carries (2026-09-09) — that constant is the SSOT, never a second date repeated
16
- * in this prose. `cache_read` and `cache_creation` are DERIVED from the
17
- * documented ephemeral-cache multipliers (read ≈ 0.1×, 5-minute write ≈ 1.25×
18
- * the input rate) except where a row carries a documented figure — Claude Fable
19
- * 5.1 publishes $0.25/MTok cache reads directly, which is NOT 0.1× its $10
20
- * input rate. Every row therefore declares its own `cache_source` so a reader
21
- * never has to guess which numbers were read off a price list and which were
22
- * multiplied out here.
13
+ * All four rates of every row are first-party Anthropic API list prices, read
14
+ * off the price page each row names in `source_url` on the date
15
+ * `PRICING_TABLE_DATE` carries — that constant is the SSOT, never a second date
16
+ * repeated in this prose. Nothing here is multiplied out. `cache_read` in
17
+ * particular is never derived from `input`: the ratio is not a constant (Opus
18
+ * 5.5 reads at 0.05× its input rate, Fable 5.1 at 0.025×, Fable 5 at 0.1×), so
19
+ * a multiplier would misprice exactly the cache-heavy runs this table exists
20
+ * for. Each row carries its own provenance fields, so a consumer quoting a
21
+ * dollar figure can cite where it came from without reading this file.
23
22
  *
24
- * A row with `verified: false` carries a PLACEHOLDER: its numbers are a
25
- * best-effort stand-in, never a quoted price. `costUsd()` still prices such a
26
- * row (an estimate is more useful than a null when it is labelled), but any
27
- * consumer publishing a dollar figure must surface the flag.
23
+ * `verified: false` would mark a PLACEHOLDER row — a best-effort stand-in,
24
+ * never a quoted price. No current row carries it; a consumer publishing a
25
+ * dollar figure must still surface the flag.
26
+ *
27
+ * ## Only exact ids resolve
28
+ *
29
+ * `priceFor()` accepts an exact table key or an explicit `MODEL_ALIASES` entry
30
+ * and nothing else. Until #1470 it fell back to the longest table key an id
31
+ * started with, which handed an unlisted generation the rates of its nearest
32
+ * neighbour, marked verified.
33
+ *
34
+ * ## Series break (#1470, 2026-09-29)
35
+ *
36
+ * Session reports written before this fix priced records through that
37
+ * fallback: `claude-opus-5-5` at `claude-opus-5` rates, `claude-sonnet-5-5` at
38
+ * `claude-sonnet-5` rates (identical numbers, by coincidence rather than
39
+ * evidence), and `claude-fable-5` / `claude-opus-4-8` not at all. Nothing
40
+ * rewrites that history — the metrics ledgers are append-only. Measured
41
+ * 2026-09-29: 0 of the 9 priced sessions.jsonl reports in this repo contained
42
+ * such records (the first one reached subagents.jsonl after the last priced
43
+ * report was written).
28
44
  *
29
45
  * ## Null means UNKNOWN MODEL, never zero
30
46
  *
@@ -39,68 +55,93 @@
39
55
  */
40
56
 
41
57
  /**
42
- * The date the `input`/`output` rates in PRICING_TABLE were sourced.
43
- * Any consumer quoting a dollar figure should quote this date beside it — a
44
- * price without its measurement date ages silently.
58
+ * The date the rates in PRICING_TABLE were read off the price page. Every row's
59
+ * `checked_at` equals it. Any consumer quoting a dollar figure should quote this
60
+ * date beside it — a price without its measurement date ages silently.
45
61
  * @type {string}
46
62
  */
47
- export const PRICING_TABLE_DATE = '2026-09-09';
63
+ export const PRICING_TABLE_DATE = '2026-09-29';
48
64
 
49
65
  /**
50
66
  * @typedef {Object} PricingRow
51
67
  * @property {number} input - USD per 1M uncached prompt tokens.
52
68
  * @property {number} cache_read - USD per 1M tokens served from the prompt cache.
53
- * @property {number} cache_creation - USD per 1M tokens written to the prompt cache.
69
+ * @property {number} cache_creation - USD per 1M tokens written to the prompt cache
70
+ * (the `cache_write_ttl` rate).
54
71
  * @property {number} output - USD per 1M completion tokens.
55
72
  * @property {boolean} verified - false ⇒ the row is a labelled placeholder, not a quoted price.
56
73
  * @property {'documented'|'derived-multiplier'} cache_source - provenance of the two cache rates.
74
+ * Every current row is 'documented'; 'derived-multiplier' is kept for a future row whose cache
75
+ * rates are multiplied out and must say so.
76
+ * @property {string} source_url - the page all four rates were read from.
77
+ * @property {string} checked_at - ISO date the rates were read; equals PRICING_TABLE_DATE.
78
+ * @property {'anthropic'} provider
79
+ * @property {'5m'} cache_write_ttl - which cache-write rate `cache_creation` is.
80
+ * @property {string} unit - 'USD per 1M tokens'.
81
+ * @property {string} surface - the billing surface and speed the rates apply to.
82
+ * @property {'standard'} tier - the service tier the rates apply to.
83
+ */
84
+
85
+ /** Provenance shared by every current row — all read off one page on one date. */
86
+ const PROVENANCE = Object.freeze({
87
+ verified: true,
88
+ cache_source: 'documented',
89
+ source_url: 'https://platform.claude.com/docs/en/about-claude/pricing',
90
+ checked_at: PRICING_TABLE_DATE,
91
+ provider: 'anthropic',
92
+ cache_write_ttl: '5m',
93
+ unit: 'USD per 1M tokens',
94
+ surface: 'Claude API (first-party), standard speed',
95
+ tier: 'standard',
96
+ });
97
+
98
+ /**
99
+ * One frozen documented row: four USD rates plus the shared provenance. The
100
+ * rates are spread last, so provenance can never overwrite a price.
101
+ * @param {{input: number, cache_read: number, cache_creation: number, output: number}} rates
102
+ * @returns {PricingRow}
57
103
  */
104
+ function usd(rates) {
105
+ return Object.freeze({ ...PROVENANCE, ...rates });
106
+ }
58
107
 
59
108
  /**
60
109
  * Per-model USD-per-million-token rates.
61
110
  * @type {Readonly<Record<string, PricingRow>>}
62
111
  */
63
112
  export const PRICING_TABLE = Object.freeze({
64
- // Claude Fable 5.1 — $10 / $50 list; cache reads documented at $0.25/MTok
65
- // (NOT the usual 0.1× multiplier), cache writes derived at 1.25× input.
66
- 'claude-fable-5-1': Object.freeze({
67
- input: 10.0,
68
- cache_read: 0.25,
69
- cache_creation: 12.5,
70
- output: 50.0,
71
- verified: true,
72
- cache_source: 'documented',
73
- }),
74
- 'claude-opus-5': Object.freeze({
75
- input: 5.0,
76
- cache_read: 0.5,
77
- cache_creation: 6.25,
78
- output: 25.0,
79
- verified: true,
80
- cache_source: 'derived-multiplier',
81
- }),
82
- 'claude-sonnet-5': Object.freeze({
83
- input: 2.0,
84
- cache_read: 0.2,
85
- cache_creation: 2.5,
86
- output: 10.0,
87
- verified: true,
88
- cache_source: 'derived-multiplier',
89
- }),
90
- 'claude-haiku-4-5': Object.freeze({
91
- input: 1.0,
92
- cache_read: 0.1,
93
- cache_creation: 1.25,
94
- output: 5.0,
95
- verified: true,
96
- cache_source: 'derived-multiplier',
97
- }),
113
+ // Ceiling: only the 5-minute cache-write rate is priced; a 1-hour write costs 2× input
114
+ // (Opus 5.5: $8/MTok) and would be under-priced here. Measured 2026-09-29 over 803,788
115
+ // assistant usage blocks in 10,745 subagent transcripts: `usage.cache_creation
116
+ // .ephemeral_1h_input_tokens > 0` on 0 blocks. Revisit when the first such block appears.
117
+ //
118
+ // Ceiling: list price only — fast mode, `inference_geo: "us"` (1.1×) and batch discounts are
119
+ // not modelled. Measured 2026-09-29: `usage.speed` = "fast" on 0 of the 374,331 blocks that
120
+ // carry the field; `inference_geo` = "not_available" on 803,479 and no other value seen.
121
+ // Revisit when either field shows another value.
122
+ //
123
+ // Ceiling: a model absent from this table prices as null, never at a neighbour's rate, so the
124
+ // rollup reports `total_cost_usd: null` and `cost_records_priced < cost_records_total` — the
125
+ // intended way missing evidence shows up. That ratio also drops below 1 for records no row can
126
+ // fix: a token-less record (transcript over the hook's size cap, without usage turns, or
127
+ // unreadable) and a record with a token-bearing turn that names no model (#1475). Revisit when
128
+ // an unpriced record names a model id absent from this table: add that model as its own
129
+ // evidenced row, never as a family prefix or an alias to another generation.
130
+ 'claude-fable-5-1': usd({ input: 10.0, cache_read: 0.25, cache_creation: 12.5, output: 50.0 }),
131
+ 'claude-fable-5': usd({ input: 10.0, cache_read: 1.0, cache_creation: 12.5, output: 50.0 }),
132
+ 'claude-opus-5-5': usd({ input: 4.0, cache_read: 0.2, cache_creation: 5.0, output: 20.0 }),
133
+ 'claude-opus-5': usd({ input: 5.0, cache_read: 0.5, cache_creation: 6.25, output: 25.0 }),
134
+ 'claude-opus-4-8': usd({ input: 5.0, cache_read: 0.5, cache_creation: 6.25, output: 25.0 }),
135
+ 'claude-sonnet-5-5': usd({ input: 2.0, cache_read: 0.2, cache_creation: 2.5, output: 10.0 }),
136
+ 'claude-sonnet-5': usd({ input: 2.0, cache_read: 0.2, cache_creation: 2.5, output: 10.0 }),
137
+ 'claude-haiku-4-5': usd({ input: 1.0, cache_read: 0.1, cache_creation: 1.25, output: 5.0 }),
98
138
  });
99
139
 
100
140
  /**
101
141
  * Explicit aliases for model ids that appear in transcripts but are not the
102
- * canonical table key — dated snapshots and the `[1m]` context-window suffix
103
- * the harness stamps on long-context sessions.
142
+ * canonical table key — a dated snapshot of the SAME model and the `[1m]`
143
+ * context-window suffix the harness stamps on long-context sessions. An alias
144
+ * never points at another generation.
104
145
  * @type {Readonly<Record<string, string>>}
105
146
  */
106
147
  export const MODEL_ALIASES = Object.freeze({
@@ -113,10 +154,19 @@ export const MODEL_ALIASES = Object.freeze({
113
154
  /**
114
155
  * Look up the rate row for a model id.
115
156
  *
116
- * Resolution order: exact key → explicit alias → longest matching table key
117
- * that the id starts with (so `claude-opus-5-20260401` and `claude-opus-5[1m]`
118
- * both resolve to `claude-opus-5`). Longest-prefix wins so a future
119
- * `claude-opus-5-1` row is never shadowed by `claude-opus-5`.
157
+ * Resolution: exact `PRICING_TABLE` key → explicit `MODEL_ALIASES` entry → `null`,
158
+ * after trimming surrounding whitespace.
159
+ * Nothing else resolves — no prefix, no pattern, no family fallback — so
160
+ * `claude-opus-5-20260401`, `claude-opus-5-99` and `claude-haiku-4-5-20251002` are
161
+ * all UNKNOWN even though each starts with a table key.
162
+ *
163
+ * Why no prefix: from the 4.6 generation on, Anthropic ships dateless pinned
164
+ * model ids (https://platform.claude.com/docs/en/about-claude/models/model-ids-and-versions,
165
+ * retrieved 2026-09-29). The ids that extend a current table key are therefore
166
+ * other generations, not snapshots of it — `claude-opus-5-5` extends
167
+ * `claude-opus-5` — and a prefix match priced them at another generation's
168
+ * rates, marked verified (#1470). A future generation gets its own evidenced
169
+ * row, never a family prefix.
120
170
  *
121
171
  * @param {string|null|undefined} modelId
122
172
  * @returns {PricingRow|null} the row, or null when the model is UNKNOWN
@@ -129,14 +179,10 @@ export function priceFor(modelId) {
129
179
 
130
180
  if (Object.hasOwn(PRICING_TABLE, id)) return PRICING_TABLE[id];
131
181
 
132
- const aliased = MODEL_ALIASES[id];
133
- if (aliased && Object.hasOwn(PRICING_TABLE, aliased)) return PRICING_TABLE[aliased];
134
-
135
- let best = null;
136
- for (const key of Object.keys(PRICING_TABLE)) {
137
- if (id.startsWith(key) && (best === null || key.length > best.length)) best = key;
182
+ if (Object.hasOwn(MODEL_ALIASES, id) && Object.hasOwn(PRICING_TABLE, MODEL_ALIASES[id])) {
183
+ return PRICING_TABLE[MODEL_ALIASES[id]];
138
184
  }
139
- return best === null ? null : PRICING_TABLE[best];
185
+ return null;
140
186
  }
141
187
 
142
188
  /**
@@ -161,9 +207,10 @@ function num(n) {
161
207
  /**
162
208
  * Estimate the USD cost of one record's four token buckets.
163
209
  *
164
- * Each bucket is multiplied by ITS OWN rate — a cache read is ~10× cheaper
165
- * than an uncached prompt token and a cache write ~1.25× more expensive, so a
166
- * blended rate is not an approximation of this, it is a different number.
210
+ * Each bucket is multiplied by ITS OWN rate — depending on the row, a cache
211
+ * read costs a tenth to a fortieth of an uncached prompt token and a 5-minute
212
+ * cache write 1.25× as much, so a blended rate is not an approximation of
213
+ * this, it is a different number.
167
214
  *
168
215
  * @param {object} opts
169
216
  * @param {string|null|undefined} opts.model - model id from the transcript
@@ -22,8 +22,11 @@
22
22
  * Here `src` is defined by NEGATION, so a new top-level directory joins the
23
23
  * denominator the moment it is committed, with no rule edit and no re-derivation.
24
24
  *
25
- * universe `git ls-files`, filtered to CODE_EXTENSIONS
26
- * numerator tracked code under `tests/`
25
+ * universe `git ls-files`, filtered to CODE_EXTENSIONS, minus declaration
26
+ * files (`*.d.ts`, `*.d.mts`, `*.d.cts` — types, not executable code)
27
+ * numerator tracked code under a top-level `tests/`, plus co-located tests
28
+ * anywhere: basename `*.test.<ext>` / `*.spec.<ext>`, or any file
29
+ * under a `__tests__/` directory
27
30
  * denominator every OTHER tracked code file
28
31
  *
29
32
  * ## The four questions, answered in code rather than left open
@@ -37,8 +40,8 @@
37
40
  * Only git-tracked? YES — reproducible at a SHA, and it is
38
41
  * what keeps node_modules/, coverage/ and
39
42
  * untracked scratch out without an ignore list.
40
- * Do `tests/fixtures/` count as test LOC? YES when they are code (`.mjs`/`.js`/
41
- * `.cjs`). A code fixture is maintained
43
+ * Do `tests/fixtures/` count as test LOC? YES when they are code (any of
44
+ * CODE_EXTENSIONS). A code fixture is maintained
42
45
  * code that exists only to serve the suite —
43
46
  * that is test-corpus cost. Non-code
44
47
  * fixtures (`.json`, `.jsonl`, `.md`) are
@@ -62,7 +65,9 @@
62
65
  * <repo-root> defaults to process.cwd()
63
66
  * --json emit a single JSON object on stdout, nothing else
64
67
  * --check exit 1 when the ratio exceeds the ceiling (consolidation
65
- * wave required); exit 0 when inside the corridor
68
+ * wave required); exit 3 when the ratio is not measurable
69
+ * (`reason: 'no-tests-found'`, #1451); exit 0 when inside
70
+ * the corridor
66
71
  * --ceiling <n> override the TV-003 ceiling (default 1.60)
67
72
  * --stdin take newline-separated paths from stdin instead of
68
73
  * enumerating via `git ls-files` (test seam / staged-only mode)
@@ -71,6 +76,12 @@
71
76
  * 0 — measurement completed (and, under --check, ratio is within the corridor)
72
77
  * 1 — --check only: ratio exceeds the ceiling
73
78
  * 2 — tool error (missing/unreadable root, bad argv)
79
+ * 3 — --check only: ratio not measurable — 0 test files counted while test-like
80
+ * files ARE tracked (`reason: 'no-tests-found'`, e.g. a Python repo whose
81
+ * `tests/test_*.py` sit outside CODE_EXTENSIONS). Fail-closed since #1451:
82
+ * before, such a repo read `ratio 0, withinCorridor true` — "not found"
83
+ * reported as "in corridor". TypeScript tests count since the 2026-10-01
84
+ * owner decision on #1451; other stacks still get "not measurable".
74
85
  */
75
86
 
76
87
  import { readFileSync, existsSync, statSync } from 'node:fs';
@@ -90,23 +101,65 @@ import { isMainModule } from './is-main-module.mjs';
90
101
  * Applied symmetrically to numerator and denominator by construction — a file
91
102
  * type can never inflate one side while being invisible to the other.
92
103
  */
93
- export const CODE_EXTENSIONS = Object.freeze(['.mjs', '.js', '.cjs']);
104
+ export const CODE_EXTENSIONS = Object.freeze([
105
+ '.mjs',
106
+ '.js',
107
+ '.cjs',
108
+ '.jsx',
109
+ '.ts',
110
+ '.tsx',
111
+ '.mts',
112
+ '.cts',
113
+ ]);
94
114
 
95
- /** Path prefix that makes a tracked code file part of the TEST corpus. */
115
+ /**
116
+ * TypeScript declaration files end in a CODE_EXTENSION but carry only types —
117
+ * no executable line a test could exercise. Excluded from BOTH sides, so a
118
+ * generated `.d.ts` can never move the ratio.
119
+ */
120
+ export const DECLARATION_SUFFIXES = Object.freeze(['.d.ts', '.d.mts', '.d.cts']);
121
+
122
+ /** Top-level path prefix that makes a tracked code file part of the TEST corpus. */
96
123
  export const TEST_PREFIX = 'tests/';
97
124
 
125
+ /**
126
+ * Co-located test files, wherever they sit: a `*.test.<ext>` / `*.spec.<ext>`
127
+ * basename (the vitest/jest default include shape) or a `__tests__/` segment.
128
+ */
129
+ const COLOCATED_TEST_RE = /\.(test|spec)\.[^./]+$|(^|\/)__tests__\//;
130
+
131
+ /**
132
+ * A tracked path that LOOKS like a test, counted or not: a `*.test.*` / `*.spec.*`
133
+ * JS/TS file, anything under a `__tests__/` segment, or any file under `tests/`
134
+ * regardless of extension. Used only to tell "this repo has no tests" apart from
135
+ * "this repo has tests the recipe cannot see" (#1451) — e.g. `tests/test_x.py`.
136
+ */
137
+ const TEST_LIKE_RE = /\.(test|spec)\.[cm]?[jt]sx?$/;
138
+
139
+ /**
140
+ * @param {string} relPath repo-relative path
141
+ * @returns {boolean}
142
+ */
143
+ export function isTestLikePath(relPath) {
144
+ const p = String(relPath).replace(/\\/g, '/').replace(/^\.\//, '');
145
+ return TEST_LIKE_RE.test(p) || p.startsWith(TEST_PREFIX) || /(^|\/)__tests__\//.test(p);
146
+ }
147
+
98
148
  /** The TV-003 ceiling. Exceeding it switches the consolidation rule on. */
99
149
  export const DEFAULT_CEILING = 1.6;
100
150
 
101
151
  /** Machine-readable schema tag for the --json envelope. */
102
- export const SCHEMA = 'tests-src-ratio/1';
152
+ export const SCHEMA = 'tests-src-ratio/2';
103
153
 
104
154
  /**
105
155
  * Which bucket a repo-relative path belongs to.
106
156
  *
107
- * `src` is defined by NEGATION — every tracked code file that is not under
108
- * `tests/`. That is what makes the two buckets a partition rather than two
109
- * globs that can overlap or leave a gap.
157
+ * `src` is defined by NEGATION — every tracked code file that is not a test
158
+ * (under `tests/`, or co-located per COLOCATED_TEST_RE). That is what makes the
159
+ * two buckets a partition rather than two globs that can overlap or leave a gap.
160
+ *
161
+ * Known blind spot: a nested `pkg/tests/helper.ts` that is not itself named
162
+ * `*.test.*` counts as src — only the TOP-LEVEL `tests/` is a test directory.
110
163
  *
111
164
  * @param {string} relPath repo-relative path, `/`-separated
112
165
  * @returns {'test'|'src'|null} null = outside the metric entirely
@@ -114,7 +167,8 @@ export const SCHEMA = 'tests-src-ratio/1';
114
167
  export function classifyPath(relPath) {
115
168
  const p = String(relPath).replace(/\\/g, '/').replace(/^\.\//, '');
116
169
  if (!CODE_EXTENSIONS.some((ext) => p.endsWith(ext))) return null;
117
- return p === TEST_PREFIX.slice(0, -1) || p.startsWith(TEST_PREFIX) ? 'test' : 'src';
170
+ if (DECLARATION_SUFFIXES.some((ext) => p.endsWith(ext))) return null;
171
+ return p.startsWith(TEST_PREFIX) || COLOCATED_TEST_RE.test(p) ? 'test' : 'src';
118
172
  }
119
173
 
120
174
  /**
@@ -154,7 +208,8 @@ export function countPhysicalLines(content) {
154
208
  * @param {(relPath: string) => string|null} opts.readFile returns content, or null when unreadable
155
209
  * @param {number} [opts.ceiling]
156
210
  * @returns {{testFiles:number,testLoc:number,srcFiles:number,srcLoc:number,ratio:number|null,
157
- * ceiling:number,withinCorridor:boolean,consolidationWaveRequired:boolean,skipped:number}}
211
+ * ceiling:number,withinCorridor:boolean|null,consolidationWaveRequired:boolean|null,
212
+ * skipped:number,testLikeFilesSeen:number,reason:'no-tests-found'|null}}
158
213
  */
159
214
  export function measure({ files, readFile, ceiling = DEFAULT_CEILING }) {
160
215
  let testFiles = 0;
@@ -162,8 +217,10 @@ export function measure({ files, readFile, ceiling = DEFAULT_CEILING }) {
162
217
  let srcFiles = 0;
163
218
  let srcLoc = 0;
164
219
  let skipped = 0;
220
+ let testLikeFilesSeen = 0;
165
221
 
166
222
  for (const rel of files) {
223
+ if (isTestLikePath(rel)) testLikeFilesSeen++;
167
224
  const bucket = classifyPath(rel);
168
225
  if (bucket === null) continue;
169
226
  const content = readFile(rel);
@@ -183,6 +240,26 @@ export function measure({ files, readFile, ceiling = DEFAULT_CEILING }) {
183
240
 
184
241
  // A repo with no src code has an undefined ratio, not an infinite one. Saying
185
242
  // `null` keeps the consumer from reading Infinity as a corridor breach.
243
+ // Fail closed (#1451): a zero numerator next to tracked test-like files means
244
+ // the recipe cannot SEE this repo's tests (e.g. `tests/test_x.py`), not that
245
+ // there are none. Reporting ratio 0 / withinCorridor true there turned "not found"
246
+ // into "green". The verdict is unknown, so every verdict field is null.
247
+ if (testFiles === 0 && testLikeFilesSeen > 0) {
248
+ return {
249
+ testFiles,
250
+ testLoc,
251
+ srcFiles,
252
+ srcLoc,
253
+ ratio: null,
254
+ ceiling,
255
+ withinCorridor: null,
256
+ consolidationWaveRequired: null,
257
+ skipped,
258
+ testLikeFilesSeen,
259
+ reason: 'no-tests-found',
260
+ };
261
+ }
262
+
186
263
  const ratio = srcLoc === 0 ? null : Number((testLoc / srcLoc).toFixed(4));
187
264
  const withinCorridor = ratio === null ? true : ratio <= ceiling;
188
265
 
@@ -196,6 +273,8 @@ export function measure({ files, readFile, ceiling = DEFAULT_CEILING }) {
196
273
  withinCorridor,
197
274
  consolidationWaveRequired: !withinCorridor,
198
275
  skipped,
276
+ testLikeFilesSeen,
277
+ reason: null,
199
278
  };
200
279
  }
201
280
 
@@ -208,11 +287,16 @@ export function definitionOf(source) {
208
287
  return {
209
288
  source: source === 'stdin' ? 'stdin path list' : 'git ls-files (tracked files only)',
210
289
  codeExtensions: [...CODE_EXTENSIONS],
211
- numerator: `tracked code files under ${TEST_PREFIX}`,
290
+ declarationsExcluded: [...DECLARATION_SUFFIXES],
291
+ numerator:
292
+ `tracked code files under a top-level ${TEST_PREFIX}, plus co-located tests anywhere: ` +
293
+ 'basename *.test.<ext> / *.spec.<ext>, or any file under a __tests__/ directory',
212
294
  denominator: `every OTHER tracked code file (src defined by negation, not by a directory list)`,
213
295
  lineRule: 'physical lines; blank + comment lines counted; EOF-newline-insensitive',
214
296
  excluded:
215
- 'non-code extensions (.md, .json, .jsonl, .yml) on BOTH sides; untracked files (node_modules/, coverage/, scratch)',
297
+ `declaration files (${DECLARATION_SUFFIXES.map((x) => `*${x}`).join(', ')}: types, not ` +
298
+ 'executable code) and non-code extensions (.md, .json, .jsonl, .yml) on BOTH sides; ' +
299
+ 'untracked files (node_modules/, coverage/, scratch)',
216
300
  };
217
301
  }
218
302
 
@@ -322,7 +406,11 @@ const USAGE =
322
406
  * because a measurement problem must not block a session start.
323
407
  *
324
408
  * @param {{ repoRoot?: string, ceiling?: number }} [opts]
325
- * @returns {{ severity: 'warn', message: string, ratio: number, ceiling: number } | null}
409
+ * The one non-null path besides a breach is `reason: 'no-tests-found'` (#1451):
410
+ * a repo whose tests the recipe cannot see gets a WARN, never silence — silence
411
+ * is what made the unmeasurable ratio read as green fleet-wide.
412
+ *
413
+ * @returns {{ severity: 'warn', message: string, ratio: number|null, ceiling: number, reason?: string } | null}
326
414
  */
327
415
  export function checkTestsSrcRatio({ repoRoot, ceiling = DEFAULT_CEILING } = {}) {
328
416
  try {
@@ -342,6 +430,18 @@ export function checkTestsSrcRatio({ repoRoot, ceiling = DEFAULT_CEILING } = {})
342
430
  };
343
431
 
344
432
  const result = measure({ files, readFile, ceiling });
433
+ if (result.reason === 'no-tests-found') {
434
+ return {
435
+ severity: 'warn',
436
+ ratio: null,
437
+ ceiling: result.ceiling,
438
+ reason: result.reason,
439
+ message:
440
+ `⚠ tests:src ratio nicht messbar — 0 Testdateien gezählt, ` +
441
+ `${result.testLikeFilesSeen} test-artige Dateien getrackt (#1451). ` +
442
+ `Detail: node scripts/lib/tests-src-ratio.mjs --json`,
443
+ };
444
+ }
345
445
  if (result.ratio === null || result.withinCorridor) return null;
346
446
 
347
447
  const dirty = isDirty(root);
@@ -402,17 +502,20 @@ function main() {
402
502
  console.log(USAGE);
403
503
  console.log('');
404
504
  console.log('The canonical tests:src LOC measurement for TV-003 (test-value.md).');
405
- console.log(' numerator tracked code under tests/');
505
+ console.log(' numerator tracked code under tests/, plus *.test.* / *.spec.* / __tests__/ anywhere');
406
506
  console.log(' denominator every other tracked code file (src by negation)');
407
507
  console.log(` code exts ${CODE_EXTENSIONS.join(' ')} (.md / .json never counted)`);
508
+ console.log(` excluded declaration files ${DECLARATION_SUFFIXES.join(' ')} on both sides`);
408
509
  console.log(' lines physical; blanks + comments counted; EOF-newline-insensitive');
409
510
  console.log('');
410
511
  console.log(' --json machine-readable envelope on stdout');
411
- console.log(' --check exit 1 when the ratio exceeds the ceiling');
512
+ console.log(' --check exit 1 when the ratio exceeds the ceiling, 3 when not measurable');
412
513
  console.log(` --ceiling <n> override the TV-003 ceiling (default ${DEFAULT_CEILING})`);
413
514
  console.log(' --stdin read newline-separated paths instead of git ls-files');
414
515
  console.log('');
415
- console.log('Exit: 0 ok / within corridor · 1 (--check) ceiling exceeded · 2 tool error');
516
+ console.log(
517
+ 'Exit: 0 ok / within corridor · 1 (--check) ceiling exceeded · 2 tool error · 3 (--check) not measurable',
518
+ );
416
519
  process.exit(0);
417
520
  }
418
521
 
@@ -460,10 +563,17 @@ function main() {
460
563
  // fail-open class is not worth re-litigating per payload size.
461
564
  writeStdoutLineSync(JSON.stringify(envelope, null, 2));
462
565
  } else {
463
- const r = result.ratio === null ? 'n/a (no src code)' : result.ratio.toFixed(4);
464
- const verdict = result.withinCorridor
465
- ? 'within corridor'
466
- : `ABOVE ceiling ${result.ceiling} — TV-003 consolidation wave required`;
566
+ const notMeasurable = result.reason === 'no-tests-found';
567
+ const r = notMeasurable
568
+ ? 'n/a (no tests found)'
569
+ : result.ratio === null
570
+ ? 'n/a (no src code)'
571
+ : result.ratio.toFixed(4);
572
+ const verdict = notMeasurable
573
+ ? `NOT MEASURABLE — ${result.testLikeFilesSeen} test-like files tracked but 0 counted (#1451)`
574
+ : result.withinCorridor
575
+ ? 'within corridor'
576
+ : `ABOVE ceiling ${result.ceiling} — TV-003 consolidation wave required`;
467
577
  const at = envelope.ref ? ` @ ${envelope.ref}${dirty ? '+dirty' : ''}` : '';
468
578
  writeStdoutLineSync(
469
579
  `tests:src = ${r} (${result.testLoc} test LOC / ${result.srcLoc} src LOC` +
@@ -472,6 +582,13 @@ function main() {
472
582
  );
473
583
  }
474
584
 
585
+ if (args.check && result.reason === 'no-tests-found') {
586
+ console.error(
587
+ `tests:src ratio not measurable — 0 test files counted but ${result.testLikeFilesSeen} ` +
588
+ `test-like files tracked (no-tests-found); see #1451`,
589
+ );
590
+ process.exit(3);
591
+ }
475
592
  process.exit(args.check && !result.withinCorridor ? 1 : 0);
476
593
  }
477
594
 
@@ -25,6 +25,7 @@
25
25
 
26
26
  import path from 'node:path';
27
27
  import { resolveStateDir } from '../platform.mjs';
28
+ import { shellQuote } from '../sh-quote.mjs';
28
29
  import { detectVcsCommand } from './vcs-detector.mjs';
29
30
  import { isSessionCollision } from './tmux-shell.mjs';
30
31
 
@@ -48,17 +49,6 @@ try {
48
49
  // Internal helpers
49
50
  // ---------------------------------------------------------------------------
50
51
 
51
- /**
52
- * Single-quote a shell command string for use as a tmux send-keys argument.
53
- * Escapes embedded single-quotes using the POSIX `'...' '\'' '...'` pattern.
54
- *
55
- * @param {string} s
56
- * @returns {string} e.g. 'tail -F /path/to/STATE.md'
57
- */
58
- function shellQuote(s) {
59
- return "'" + String(s).replace(/'/g, "'\\''") + "'";
60
- }
61
-
62
52
  /** Poll interval (seconds) of the agent-status pane loop. */
63
53
  const STATUS_PANE_POLL_SECONDS = 2;
64
54
 
@@ -6,6 +6,8 @@
6
6
 
7
7
  import { execSync } from 'node:child_process';
8
8
 
9
+ import { shellQuote } from '../sh-quote.mjs';
10
+
9
11
  const MIN_MAJOR = 3;
10
12
  const MIN_MINOR = 0;
11
13
 
@@ -69,14 +71,3 @@ export function isSessionCollision(sessionName) {
69
71
  return { exists: false, error: `tmux has-session failed: ${err.message}` };
70
72
  }
71
73
  }
72
-
73
- /**
74
- * Shell-quote a string for safe interpolation into a shell command.
75
- * Uses single-quote wrapping with embedded single-quote escaping.
76
- *
77
- * @param {string} s
78
- * @returns {string}
79
- */
80
- function shellQuote(s) {
81
- return "'" + String(s).replace(/'/g, "'\\''") + "'";
82
- }
@@ -187,7 +187,7 @@ export function buildIssueBody(finding, context) {
187
187
  if (!buildIssueBodyDeprecationWarned) {
188
188
  buildIssueBodyDeprecationWarned = true;
189
189
  console.warn(
190
- '[deprecated] buildIssueBody from scripts/lib/ux-grill/reconcile.mjs is deprecated since 5.3.0 ' +
190
+ '[deprecated] buildIssueBody from scripts/lib/ux-grill/reconcile.mjs is deprecated since the 5.3 line ' +
191
191
  'and will be removed in 6.0.0 — use buildUxGrillIssueBody instead.',
192
192
  );
193
193
  }
@@ -327,7 +327,7 @@ if (mdFiles.length === 0) {
327
327
  if (modelVal === null || (!ALLOWED_MODEL_ALIASES.has(modelVal) && !MODEL_ID_RE.test(modelVal))) {
328
328
  const got = modelVal ?? '';
329
329
  const aliasList = [...ALLOWED_MODEL_ALIASES].join('|');
330
- fail(`${agentName}: model must be ${aliasList} or a full model ID like 'claude-opus-4-7' or 'claude-sonnet-5' (got: '${got}')`);
330
+ fail(`${agentName}: model must be ${aliasList} or a full model ID like 'claude-opus-5-5' or 'claude-sonnet-5' (got: '${got}')`);
331
331
  }
332
332
  }
333
333
 
@@ -113,6 +113,11 @@ const DOCUMENTED_ASYMMETRIES = {
113
113
  'skill-invocation-telemetry.mjs',
114
114
  'enforce-scope.mjs',
115
115
  'config-protection.mjs',
116
+ // pre-edit-sessions-ledger-guard (#1443): same Edit/Write/MultiEdit tool-name
117
+ // gate as enforce-scope/config-protection above — Codex writes files through
118
+ // `apply_patch`, a name this hook never matches. Wired, it would run and allow
119
+ // everything: a silent no-op, not enforcement. Gap registered, not faked.
120
+ 'pre-edit-sessions-ledger-guard.mjs',
116
121
  'pre-bash-destructive-guard.mjs',
117
122
  'pre-bash-staging-fence.mjs',
118
123
  'pre-bash-memory-propose-audit.mjs',