session-orchestrator 5.3.0 → 5.5.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 (260) hide show
  1. package/.agents/skills/claude-md-drift-check/SKILL.md +1 -1
  2. package/.agents/skills/test-audit/SKILL.md +21 -0
  3. package/.agents/skills/test-audit/agents/openai.yaml +5 -0
  4. package/.claude-plugin/marketplace.json +1 -1
  5. package/.claude-plugin/plugin.json +1 -1
  6. package/.codex-plugin/plugin.json +1 -1
  7. package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -1
  8. package/.codex-plugin/skills/test-audit/SKILL.md +21 -0
  9. package/.codex-plugin/skills/test-audit/agents/openai.yaml +5 -0
  10. package/.cursor/commands/test-audit.md +14 -0
  11. package/.cursor/rules/060-evolve.mdc +28 -13
  12. package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
  13. package/.cursor/skills/test-audit/SKILL.md +12 -0
  14. package/.cursor-plugin/plugin.json +1 -1
  15. package/.orchestrator/policy/blocked-commands.json +1 -1
  16. package/AGENTS.md +9 -31
  17. package/CHANGELOG.md +162 -0
  18. package/README.md +14 -10
  19. package/docs/agent-authoring.md +2 -2
  20. package/docs/baseline.md +10 -1
  21. package/docs/ci-setup.md +154 -4
  22. package/docs/components.md +11 -11
  23. package/docs/events-schema.md +25 -19
  24. package/docs/install.md +14 -2
  25. package/docs/migration-v4.md +24 -17
  26. package/docs/persona-panel.md +5 -4
  27. package/docs/plugin-architecture-v3.md +1 -1
  28. package/docs/rule-authoring.md +27 -12
  29. package/docs/scope-collision-guard.md +40 -16
  30. package/docs/session-config-reference.md +61 -12
  31. package/docs/session-config-template.md +11 -2
  32. package/docs/vault-docs-architecture.md +8 -4
  33. package/hooks/_lib/hook-import-set.json +42 -29
  34. package/hooks/_lib/lock-bootstrap.mjs +118 -26
  35. package/hooks/config-protection.mjs +1 -1
  36. package/hooks/enforce-commands.mjs +28 -13
  37. package/hooks/enforce-scope.mjs +201 -23
  38. package/hooks/hooks-codex.json +1 -1
  39. package/hooks/hooks-cursor.json +5 -0
  40. package/hooks/hooks-pi.json +5 -0
  41. package/hooks/hooks.json +6 -1
  42. package/hooks/on-session-start.mjs +57 -2
  43. package/hooks/on-stop.mjs +70 -168
  44. package/hooks/post-bash-write-verify.mjs +98 -17
  45. package/hooks/post-edit-validate.mjs +15 -5
  46. package/hooks/post-tool-batch-wave-signal.mjs +32 -152
  47. package/hooks/pre-bash-memory-propose-audit.mjs +71 -10
  48. package/hooks/pre-bash-sessions-ledger-guard.mjs +28 -2
  49. package/hooks/pre-bash-staging-fence.mjs +29 -8
  50. package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
  51. package/hooks/pre-task-scope-disjoint.mjs +1143 -279
  52. package/hooks/subagent-telemetry.mjs +96 -19
  53. package/hooks/wave-scope-commit-guard.mjs +168 -72
  54. package/package.json +2 -6
  55. package/pi/prompts/test-audit.md +12 -0
  56. package/rules/README.md +6 -1
  57. package/rules/_index.md +3 -3
  58. package/rules/always-on/bash-harness-pitfalls.md +7 -1
  59. package/rules/opt-in-domain/prompt-caching.md +5 -1
  60. package/rules/opt-in-stack/backend-data.md +9 -1
  61. package/rules/opt-in-stack/backend.md +7 -1
  62. package/rules/opt-in-stack/frontend.md +6 -1
  63. package/rules/opt-in-stack/security-web.md +7 -1
  64. package/rules/opt-in-stack/swift.md +5 -1
  65. package/scripts/apply-session-learnings.mjs +513 -0
  66. package/scripts/archive-closed-prds.mjs +40 -12
  67. package/scripts/autopilot.mjs +57 -54
  68. package/scripts/backfill-abandoned-sessions.mjs +81 -6
  69. package/scripts/backfill-learnings-from-vault.mjs +49 -22
  70. package/scripts/baseline-archetypes.mjs +1 -1
  71. package/scripts/check-package-manager.mjs +62 -5
  72. package/scripts/codex-install.mjs +3 -0
  73. package/scripts/cursor-install.mjs +47 -6
  74. package/scripts/emit-session.mjs +298 -2
  75. package/scripts/export-hw-learnings.mjs +40 -10
  76. package/scripts/fleet-checkin.mjs +170 -0
  77. package/scripts/lib/agent-frontmatter.mjs +3 -3
  78. package/scripts/lib/auto-dream.mjs +7 -6
  79. package/scripts/lib/autopilot/flags.mjs +6 -1
  80. package/scripts/lib/autopilot/loop.mjs +32 -13
  81. package/scripts/lib/autopilot/stall-sampler.mjs +13 -9
  82. package/scripts/lib/baseline-archetypes.mjs +41 -22
  83. package/scripts/lib/blocked-commands-policy.mjs +40 -0
  84. package/scripts/lib/common.mjs +41 -0
  85. package/scripts/lib/config/host-paths.mjs +79 -0
  86. package/scripts/lib/config/persona-gate-wave.mjs +3 -3
  87. package/scripts/lib/config/reaper.mjs +7 -6
  88. package/scripts/lib/config/vault-integration.mjs +151 -16
  89. package/scripts/lib/config-schema.mjs +13 -0
  90. package/scripts/lib/config.mjs +75 -10
  91. package/scripts/lib/eval/engine.mjs +13 -0
  92. package/scripts/lib/events-retention-banner.mjs +393 -0
  93. package/scripts/lib/events-rotation.mjs +21 -6
  94. package/scripts/lib/events.mjs +72 -5
  95. package/scripts/lib/fleet-protocol.mjs +288 -0
  96. package/scripts/lib/gates/gate-baseline.mjs +13 -3
  97. package/scripts/lib/gates/gate-full.mjs +7 -2
  98. package/scripts/lib/gates/gate-helpers.mjs +93 -2
  99. package/scripts/lib/gates/gate-incremental.mjs +23 -22
  100. package/scripts/lib/gates/gate-per-file.mjs +16 -7
  101. package/scripts/lib/hardening.mjs +1 -0
  102. package/scripts/lib/harness-audit/categories/category4.mjs +8 -6
  103. package/scripts/lib/instruction-budget-guard.mjs +77 -11
  104. package/scripts/lib/learnings/expiry-sweep.mjs +420 -22
  105. package/scripts/lib/learnings/io.mjs +274 -37
  106. package/scripts/lib/learnings/schema.mjs +163 -15
  107. package/scripts/lib/learnings/sizing-subject.mjs +2 -2
  108. package/scripts/lib/lock-reaper.mjs +38 -14
  109. package/scripts/lib/maintenance-due-banner.mjs +23 -8
  110. package/scripts/lib/memory-proposals/schema.mjs +11 -5
  111. package/scripts/lib/named-baseline-resolver.mjs +53 -28
  112. package/scripts/lib/named-vault-resolver.mjs +147 -5
  113. package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
  114. package/scripts/lib/orphan-reaper/no-follow-append.mjs +87 -0
  115. package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
  116. package/scripts/lib/orphan-reaper/reaper-audit.mjs +323 -0
  117. package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
  118. package/scripts/lib/orphan-reaper/scan-throttle.mjs +144 -0
  119. package/scripts/lib/orphan-reaper/scan.mjs +510 -0
  120. package/scripts/lib/orphan-reaper/trigger.mjs +173 -0
  121. package/scripts/lib/orphan-reaper.mjs +47 -1315
  122. package/scripts/lib/owner-yaml.mjs +104 -6
  123. package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
  124. package/scripts/lib/platform.mjs +206 -2
  125. package/scripts/lib/plugin-update-banner.mjs +3 -1
  126. package/scripts/lib/process-group.mjs +134 -22
  127. package/scripts/lib/project-hygiene.mjs +21 -1
  128. package/scripts/lib/quality-gate.mjs +45 -5
  129. package/scripts/lib/resource-probe.mjs +1 -1
  130. package/scripts/lib/rule-loader.mjs +100 -62
  131. package/scripts/lib/rules-sync.mjs +27 -12
  132. package/scripts/lib/scope-baseline.mjs +119 -29
  133. package/scripts/lib/scope-echo.mjs +7 -0
  134. package/scripts/lib/scope-gate.mjs +194 -27
  135. package/scripts/lib/session-close-backfill.mjs +129 -4
  136. package/scripts/lib/session-end/tail-runner.mjs +10 -5
  137. package/scripts/lib/session-lock-shape.mjs +6 -3
  138. package/scripts/lib/session-lock.mjs +320 -67
  139. package/scripts/lib/session-record-repair.mjs +183 -26
  140. package/scripts/lib/session-registry.mjs +67 -17
  141. package/scripts/lib/session-schema/constants.mjs +30 -4
  142. package/scripts/lib/session-schema/validator.mjs +28 -1
  143. package/scripts/lib/session-start-probes.mjs +242 -68
  144. package/scripts/lib/session-token-rollup.mjs +252 -54
  145. package/scripts/lib/sessions-canonical.mjs +100 -9
  146. package/scripts/lib/sessions-staleness-banner.mjs +3 -1
  147. package/scripts/lib/sh-quote.mjs +29 -0
  148. package/scripts/lib/subagents-schema.mjs +30 -3
  149. package/scripts/lib/tail-window.mjs +27 -3
  150. package/scripts/lib/telemetry/pricing.mjs +121 -71
  151. package/scripts/lib/telemetry/sync.mjs +17 -0
  152. package/scripts/lib/tests-src-ratio.mjs +215 -23
  153. package/scripts/lib/tmux-layout/layouts.mjs +1 -11
  154. package/scripts/lib/tmux-layout/telemetry-stats.mjs +12 -0
  155. package/scripts/lib/tmux-layout/tmux-shell.mjs +2 -11
  156. package/scripts/lib/ux-grill/reconcile.mjs +1 -1
  157. package/scripts/lib/validate/check-agents.mjs +1 -1
  158. package/scripts/lib/validate/check-hooks-symmetry.mjs +5 -0
  159. package/scripts/lib/validate/check-learnings-shell-writes.mjs +214 -0
  160. package/scripts/lib/validate/check-test-value-bans.mjs +388 -27
  161. package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
  162. package/scripts/lib/validate/check-unwired-features.mjs +1 -1
  163. package/scripts/lib/validate/check-vcs-repo-flag.mjs +1 -1
  164. package/scripts/lib/validate-vendored-rules.mjs +206 -81
  165. package/scripts/lib/vault-mirror/auto-commit.mjs +5 -3
  166. package/scripts/lib/vault-mirror/namespace.mjs +47 -26
  167. package/scripts/lib/vault-mirror/process.mjs +350 -62
  168. package/scripts/lib/vault-mirror/pseudonym-map.mjs +14 -5
  169. package/scripts/lib/vault-mirror/render-learnings.mjs +66 -3
  170. package/scripts/lib/vault-mirror/render-sessions.mjs +28 -4
  171. package/scripts/lib/vault-mirror/telemetry.mjs +25 -11
  172. package/scripts/lib/vault-status/board-writer.mjs +53 -12
  173. package/scripts/lib/vault-status/narrative-mirror.mjs +40 -9
  174. package/scripts/lib/vcs-repo-spec.mjs +8 -0
  175. package/scripts/lib/wave-executor/dispatch-common.mjs +10 -18
  176. package/scripts/lib/wave-executor/foreign-dispatch.mjs +1 -1
  177. package/scripts/lib/wave-executor/remote-dispatch.mjs +1 -1
  178. package/scripts/lib/wave-resource-gate.mjs +34 -8
  179. package/scripts/materialize-wave-scope.mjs +7 -4
  180. package/scripts/memory-propose.mjs +18 -5
  181. package/scripts/release-session-lock.mjs +2 -1
  182. package/scripts/release.mjs +11 -3
  183. package/scripts/repair-invalid-sessions.mjs +37 -4
  184. package/scripts/run-migrate-v2-cross-repo.mjs +171 -31
  185. package/scripts/run-quality-gate.mjs +331 -307
  186. package/scripts/self-update.mjs +229 -0
  187. package/scripts/site-numbers.mjs +163 -5
  188. package/scripts/sweep-expired-learnings.mjs +603 -20
  189. package/scripts/sweep-expired-rules.mjs +2 -1
  190. package/scripts/validate-plugin.mjs +7 -0
  191. package/scripts/validate-wave-scope.mjs +21 -2
  192. package/scripts/vault-backfill.mjs +50 -37
  193. package/scripts/vault-integration-watcher.mjs +2 -1
  194. package/scripts/vault-mirror.mjs +238 -75
  195. package/scripts/wave-scope-binding.mjs +2 -1
  196. package/skills/_shared/config-reading.md +3 -3
  197. package/skills/_shared/fleet-protocol.md +134 -0
  198. package/skills/_shared/state-ownership.md +41 -12
  199. package/skills/autopilot/SKILL.md +25 -11
  200. package/skills/bootstrap/SKILL.md +6 -4
  201. package/skills/bootstrap/_shared-template.md +97 -2
  202. package/skills/bootstrap/fast-template.md +4 -1
  203. package/skills/bootstrap/private-contract.md +2 -1
  204. package/skills/bootstrap/public-fallback.md +6 -3
  205. package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +5 -3
  206. package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
  207. package/skills/bootstrap/standard-template.md +2 -0
  208. package/skills/claude-md-drift-check/SKILL.md +10 -10
  209. package/skills/claude-md-drift-check/checker.mjs +26 -18
  210. package/skills/discovery/probes/ssot-code-diff.mjs +12 -6
  211. package/skills/discovery/probes/vault-narrative-staleness.mjs +6 -2
  212. package/skills/discovery/probes/vault-staleness.mjs +9 -3
  213. package/skills/docs-orchestrator/SKILL.md +12 -5
  214. package/skills/docs-orchestrator/audience-mapping.md +5 -3
  215. package/skills/evolve/SKILL.md +13 -4
  216. package/skills/evolve/references/evolve-analyze-mode.md +42 -18
  217. package/skills/memory-cleanup/SKILL.md +1 -1
  218. package/skills/persona-panel/SKILL.md +2 -2
  219. package/skills/persona-panel/persona-format.md +2 -2
  220. package/skills/persona-panel/presets/designer-lens.md +1 -1
  221. package/skills/persona-panel/presets/engineer-lens.md +1 -1
  222. package/skills/persona-panel/presets/pm-lens.md +1 -1
  223. package/skills/plan/mode-retro.md +19 -4
  224. package/skills/quality-gates/SKILL.md +6 -4
  225. package/skills/remote-offload/SKILL.md +4 -2
  226. package/skills/session-end/learning-patterns.md +23 -13
  227. package/skills/session-end/metrics-collection.md +6 -7
  228. package/skills/session-end/phase-3-2-docs-verification.md +7 -6
  229. package/skills/session-end/plan-verification.md +25 -9
  230. package/skills/session-end/references/phase-3-documentation-updates.md +13 -3
  231. package/skills/session-end/references/phase-4a-worktree-cleanup.md +2 -0
  232. package/skills/session-end/references/session-summary-template.md +1 -1
  233. package/skills/session-end/session-metrics-write.md +65 -68
  234. package/skills/session-plan/SKILL.md +0 -3
  235. package/skills/session-start/SKILL.md +8 -2
  236. package/skills/session-start/references/operations-contract.md +6 -0
  237. package/skills/session-start/references/phase-1-2-session-lock.md +32 -7
  238. package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
  239. package/skills/session-start/references/phase-4-ssot-environment-check.md +10 -3
  240. package/skills/test-audit/SKILL.md +152 -0
  241. package/skills/test-audit/references/campaign.md +111 -0
  242. package/skills/test-audit/references/mutate.sh +50 -0
  243. package/skills/vault-mirror/SKILL.md +21 -11
  244. package/skills/wave-executor/SKILL.md +2 -2
  245. package/skills/wave-executor/circuit-breaker.md +1 -1
  246. package/skills/wave-executor/references/wave-executor-state-init.md +3 -0
  247. package/skills/wave-executor/references/wave-loop-dispatch.md +11 -1
  248. package/skills/wave-executor/references/wave-loop-review.md +4 -6
  249. package/skills/wave-executor/references/wave-loop-scope-manifest.md +13 -5
  250. package/templates/personas/README.md +5 -5
  251. package/templates/personas/accounting-compliance.v1.md +1 -1
  252. package/templates/personas/accounting-tax-advisor.v1.md +1 -1
  253. package/templates/personas/buyer-p1-cto.v1.md +1 -1
  254. package/templates/personas/buyer-p2-kanzlei.v1.md +1 -1
  255. package/templates/personas/buyer-p3-build.v1.md +1 -1
  256. package/templates/personas/buyer-p4-tech-veto.v1.md +1 -1
  257. package/templates/personas/buyer-p5-solo.v1.md +1 -1
  258. package/templates/personas/buyer-p6-ld.v1.md +1 -1
  259. package/templates/personas/klima-ai-expert.v1.md +1 -1
  260. package/templates/personas/klima-physicist.v1.md +1 -1
@@ -42,8 +42,16 @@
42
42
  * token_cache_creation integer | null — `usage.cache_creation_input_tokens` (v2+)
43
43
  * token_output integer | null — completion token count for this subagent
44
44
  * model string | null — model id from the transcript (v2+); null when
45
- * the transcript exposes none. Required to price
46
- * the record — see scripts/lib/telemetry/pricing.mjs.
45
+ * the transcript exposes none, or when its token-
46
+ * bearing turns used two or more models or one turn
47
+ * named none (#1470). Required to price the record —
48
+ * see scripts/lib/telemetry/pricing.mjs.
49
+ * models_usage array | absent — per-model breakdown of a record whose token-
50
+ * bearing turns used two or more models, every
51
+ * one named (#1470): [{ model, token_input_uncached,
52
+ * token_cache_read, token_cache_creation,
53
+ * token_output }], one entry per model. The session
54
+ * rollup prices each part; absent on every other record.
47
55
  * total_cost_usd number | null — native total cost in USD (#624, fractional,
48
56
  * best-effort: null when the harness does not
49
57
  * expose it; no rate table is applied)
@@ -115,7 +123,8 @@ export class ValidationError extends Error {
115
123
  *
116
124
  * Required fields: timestamp, event, agent_id, schema_version.
117
125
  * Additional requirement when event='stop': duration_ms (integer or null).
118
- * Optional: agent_type, parent_session_id, token_input, token_output.
126
+ * Optional: agent_type, parent_session_id, token_input, token_output, the cache
127
+ * buckets, model, models_usage, total_cost_usd and the OTel aliases.
119
128
  *
120
129
  * @param {object} entry
121
130
  * @param {object} [options]
@@ -233,6 +242,24 @@ export function validateSubagent(entry, options = {}) {
233
242
  throw new ValidationError('model must be a string or null', 'model');
234
243
  }
235
244
 
245
+ // models_usage (optional, #1470) — per-model parts of a mixed-model record.
246
+ // Each part is priced on its own, so each needs a model and four buckets.
247
+ if (entry.models_usage !== undefined && entry.models_usage !== null) {
248
+ if (!Array.isArray(entry.models_usage)) {
249
+ throw new ValidationError('models_usage must be an array or null', 'models_usage');
250
+ }
251
+ for (const part of entry.models_usage) {
252
+ const bucketsValid = ['token_input_uncached', 'token_cache_read', 'token_cache_creation', 'token_output']
253
+ .every((field) => Number.isInteger(part?.[field]) && part[field] >= 0);
254
+ if (typeof part?.model !== 'string' || part.model.length === 0 || !bucketsValid) {
255
+ throw new ValidationError(
256
+ 'models_usage entries must carry a model string and four non-negative integer token buckets',
257
+ 'models_usage'
258
+ );
259
+ }
260
+ }
261
+ }
262
+
236
263
  // total_cost_usd (optional, #624) — fractional number (not integer), best-effort.
237
264
  if (entry.total_cost_usd !== undefined && entry.total_cost_usd !== null) {
238
265
  if (typeof entry.total_cost_usd !== 'number' || !Number.isFinite(entry.total_cost_usd) || entry.total_cost_usd < 0) {
@@ -16,6 +16,18 @@
16
16
 
17
17
  import fs from 'node:fs';
18
18
 
19
+ /**
20
+ * `open(2)` flags of a `noFollow` read. `O_NOFOLLOW` refuses a symlink as the
21
+ * last path component (ELOOP) in the open itself; `O_NONBLOCK` makes the
22
+ * read-side open of a FIFO return at once instead of blocking until a writer
23
+ * appears (a plain `'r'` open of a planted FIFO blocked a detached scan child
24
+ * for good — reproduced 2026-10-02, #1487). On a regular file `O_NONBLOCK`
25
+ * changes nothing. Both are POSIX-only; Node leaves them undefined on Windows,
26
+ * where the read degrades to a plain open.
27
+ */
28
+ const NO_FOLLOW_READ_FLAGS = fs.constants.O_RDONLY
29
+ | (fs.constants.O_NOFOLLOW ?? 0) | (fs.constants.O_NONBLOCK ?? 0);
30
+
19
31
  /**
20
32
  * Read the last `maxBytes` of `file` as UTF-8.
21
33
  *
@@ -28,14 +40,26 @@ import fs from 'node:fs';
28
40
  * THROWS on any fs error (ENOENT, EACCES, …) — mapping the failure is the
29
41
  * caller's decision.
30
42
  *
43
+ * `opts.noFollow` is for a file a hostile local writer may have replaced: it
44
+ * opens with {@link NO_FOLLOW_READ_FLAGS} and then refuses anything the
45
+ * descriptor's `fstat` does not call a regular file (FIFO, directory, device),
46
+ * so a symlink throws ELOOP and every other non-regular file throws
47
+ * `ERR_NOT_REGULAR_FILE` — never a blocked open, never a read of a link
48
+ * target. The default (`false`) is the plain `'r'` open every other caller uses.
49
+ *
31
50
  * @param {string} file
32
51
  * @param {number} maxBytes window size in bytes (> 0)
52
+ * @param {{noFollow?: boolean}} [opts]
33
53
  * @returns {{ text: string, cut: boolean, size: number }} `size` is the file size at open time
34
54
  */
35
- export function readTailWindow(file, maxBytes) {
36
- const fd = fs.openSync(file, 'r');
55
+ export function readTailWindow(file, maxBytes, { noFollow = false } = {}) {
56
+ const fd = fs.openSync(file, noFollow ? NO_FOLLOW_READ_FLAGS : 'r');
37
57
  try {
38
- const size = fs.fstatSync(fd).size;
58
+ const stats = fs.fstatSync(fd);
59
+ if (noFollow && !stats.isFile()) {
60
+ throw Object.assign(new Error('not a regular file'), { code: 'ERR_NOT_REGULAR_FILE' });
61
+ }
62
+ const size = stats.size;
39
63
  const want = Math.min(size, maxBytes);
40
64
  const start = size - want;
41
65
  const buf = Buffer.allocUnsafe(want);
@@ -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,96 @@
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 agents no row can
126
+ // fix: a token-less record (transcript over the hook's size cap, without usage turns, or
127
+ // unreadable), a record with a token-bearing turn that names no model (#1475), a started agent
128
+ // whose stop found no transcript of its own, and an agent seen only in schema_version 1 records
129
+ // (#1487 Pkt 1). The one complete list is the `session-token-rollup.mjs` module doc — the code
130
+ // that decides; this comment and `emit-session.mjs` only point at it (#1498). Revisit when
131
+ // an unpriced record names a model id absent from this table: add that model as its own
132
+ // evidenced row, never as a family prefix or an alias to another generation.
133
+ 'claude-fable-5-1': usd({ input: 10.0, cache_read: 0.25, cache_creation: 12.5, output: 50.0 }),
134
+ 'claude-fable-5': usd({ input: 10.0, cache_read: 1.0, cache_creation: 12.5, output: 50.0 }),
135
+ 'claude-opus-5-5': usd({ input: 4.0, cache_read: 0.2, cache_creation: 5.0, output: 20.0 }),
136
+ 'claude-opus-5': usd({ input: 5.0, cache_read: 0.5, cache_creation: 6.25, output: 25.0 }),
137
+ 'claude-opus-4-8': usd({ input: 5.0, cache_read: 0.5, cache_creation: 6.25, output: 25.0 }),
138
+ 'claude-sonnet-5-5': usd({ input: 2.0, cache_read: 0.2, cache_creation: 2.5, output: 10.0 }),
139
+ 'claude-sonnet-5': usd({ input: 2.0, cache_read: 0.2, cache_creation: 2.5, output: 10.0 }),
140
+ 'claude-haiku-4-5': usd({ input: 1.0, cache_read: 0.1, cache_creation: 1.25, output: 5.0 }),
98
141
  });
99
142
 
100
143
  /**
101
144
  * 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.
145
+ * canonical table key — a dated snapshot of the SAME model and the `[1m]`
146
+ * context-window suffix the harness stamps on long-context sessions. An alias
147
+ * never points at another generation.
104
148
  * @type {Readonly<Record<string, string>>}
105
149
  */
106
150
  export const MODEL_ALIASES = Object.freeze({
@@ -113,10 +157,19 @@ export const MODEL_ALIASES = Object.freeze({
113
157
  /**
114
158
  * Look up the rate row for a model id.
115
159
  *
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`.
160
+ * Resolution: exact `PRICING_TABLE` key → explicit `MODEL_ALIASES` entry → `null`,
161
+ * after trimming surrounding whitespace.
162
+ * Nothing else resolves — no prefix, no pattern, no family fallback — so
163
+ * `claude-opus-5-20260401`, `claude-opus-5-99` and `claude-haiku-4-5-20251002` are
164
+ * all UNKNOWN even though each starts with a table key.
165
+ *
166
+ * Why no prefix: from the 4.6 generation on, Anthropic ships dateless pinned
167
+ * model ids (https://platform.claude.com/docs/en/about-claude/models/model-ids-and-versions,
168
+ * retrieved 2026-09-29). The ids that extend a current table key are therefore
169
+ * other generations, not snapshots of it — `claude-opus-5-5` extends
170
+ * `claude-opus-5` — and a prefix match priced them at another generation's
171
+ * rates, marked verified (#1470). A future generation gets its own evidenced
172
+ * row, never a family prefix.
120
173
  *
121
174
  * @param {string|null|undefined} modelId
122
175
  * @returns {PricingRow|null} the row, or null when the model is UNKNOWN
@@ -129,14 +182,10 @@ export function priceFor(modelId) {
129
182
 
130
183
  if (Object.hasOwn(PRICING_TABLE, id)) return PRICING_TABLE[id];
131
184
 
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;
185
+ if (Object.hasOwn(MODEL_ALIASES, id) && Object.hasOwn(PRICING_TABLE, MODEL_ALIASES[id])) {
186
+ return PRICING_TABLE[MODEL_ALIASES[id]];
138
187
  }
139
- return best === null ? null : PRICING_TABLE[best];
188
+ return null;
140
189
  }
141
190
 
142
191
  /**
@@ -161,9 +210,10 @@ function num(n) {
161
210
  /**
162
211
  * Estimate the USD cost of one record's four token buckets.
163
212
  *
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.
213
+ * Each bucket is multiplied by ITS OWN rate — depending on the row, a cache
214
+ * read costs a tenth to a fortieth of an uncached prompt token and a 5-minute
215
+ * cache write 1.25× as much, so a blended rate is not an approximation of
216
+ * this, it is a different number.
167
217
  *
168
218
  * @param {object} opts
169
219
  * @param {string|null|undefined} opts.model - model id from the transcript
@@ -312,6 +312,23 @@ export function readSessionProfileForMetricsDir(metricsDir) {
312
312
  }
313
313
  }
314
314
 
315
+ /**
316
+ * Events window this reader needs retained (#1401 part 3), in days — a CHOSEN
317
+ * ceiling, not a derived value. `deriveSessionFromEvents` loads the whole
318
+ * rotated ledger (no time filter) and keeps the LATEST
319
+ * `orchestrator.session.started`, so what it needs retained is that one
320
+ * record; 1 day is the assumed maximum age of that record when a flush reads
321
+ * it. Nothing in this module bounds that age: `buildBatch`'s
322
+ * `[now - DAILY_FLUSH_MS, now]` cutoff filters `skill-invocations.jsonl`, not
323
+ * the events ledger. CEILING (BV-004): a latest `session.started` older than a
324
+ * day at flush time (a session running longer than that) is found only if
325
+ * retention happens to reach it; revisit if that becomes common. Read by
326
+ * `scripts/lib/events-retention-banner.mjs`.
327
+ *
328
+ * @type {number}
329
+ */
330
+ export const REQUIRED_EVENTS_WINDOW_DAYS = 1;
331
+
315
332
  /**
316
333
  * Reconstruct the session facts a ping needs (`session_type`, `started_at`,
317
334
  * `completed_at`) from `<metricsDir>/events.jsonl` when `sessions.jsonl` has no