session-orchestrator 5.2.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 (427) 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 +4 -2
  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/navigator/SKILL.md +22 -0
  41. package/.agents/skills/navigator/agents/openai.yaml +5 -0
  42. package/.agents/skills/npm-publish/SKILL.md +4 -2
  43. package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
  44. package/.agents/skills/persona-panel/SKILL.md +5 -1
  45. package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
  46. package/.agents/skills/plan/SKILL.md +5 -1
  47. package/.agents/skills/plan/agents/openai.yaml +5 -0
  48. package/.agents/skills/playwright-driver/SKILL.md +3 -1
  49. package/.agents/skills/portfolio/SKILL.md +5 -1
  50. package/.agents/skills/portfolio/agents/openai.yaml +5 -0
  51. package/.agents/skills/quality-gates/SKILL.md +3 -1
  52. package/.agents/skills/reconcile/SKILL.md +5 -1
  53. package/.agents/skills/reconcile/agents/openai.yaml +5 -0
  54. package/.agents/skills/release/SKILL.md +5 -1
  55. package/.agents/skills/release/agents/openai.yaml +5 -0
  56. package/.agents/skills/remote-offload/SKILL.md +3 -1
  57. package/.agents/skills/repo-audit/SKILL.md +5 -1
  58. package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
  59. package/.agents/skills/session/SKILL.md +21 -0
  60. package/.agents/skills/session/agents/openai.yaml +5 -0
  61. package/.agents/skills/session-end/SKILL.md +3 -1
  62. package/.agents/skills/session-plan/SKILL.md +3 -1
  63. package/.agents/skills/session-start/SKILL.md +3 -1
  64. package/.agents/skills/spinout/SKILL.md +5 -1
  65. package/.agents/skills/spinout/agents/openai.yaml +5 -0
  66. package/.agents/skills/sunset-review/SKILL.md +5 -1
  67. package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
  68. package/.agents/skills/templates-ack/SKILL.md +21 -0
  69. package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
  70. package/.agents/skills/test/SKILL.md +5 -1
  71. package/.agents/skills/test/agents/openai.yaml +5 -0
  72. package/.agents/skills/test-audit/SKILL.md +21 -0
  73. package/.agents/skills/test-audit/agents/openai.yaml +5 -0
  74. package/.agents/skills/test-runner/SKILL.md +3 -1
  75. package/.agents/skills/tmux-layout/SKILL.md +3 -1
  76. package/.agents/skills/using-orchestrator/SKILL.md +3 -1
  77. package/.agents/skills/ux-grill/SKILL.md +5 -1
  78. package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
  79. package/.agents/skills/vault-mirror/SKILL.md +3 -1
  80. package/.agents/skills/vault-sync/SKILL.md +3 -1
  81. package/.agents/skills/wave-executor/SKILL.md +3 -1
  82. package/.agents/skills/write-executable-plan/SKILL.md +3 -1
  83. package/.claude-plugin/marketplace.json +1 -1
  84. package/.claude-plugin/plugin.json +1 -1
  85. package/.codex-plugin/plugin.json +4 -4
  86. package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -1
  87. package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -3
  88. package/.codex-plugin/skills/eval/SKILL.md +1 -1
  89. package/.codex-plugin/skills/evolve/SKILL.md +1 -1
  90. package/.codex-plugin/skills/navigator/SKILL.md +22 -0
  91. package/.codex-plugin/skills/{convergence-monitoring → navigator}/agents/openai.yaml +2 -2
  92. package/.codex-plugin/skills/npm-publish/SKILL.md +1 -3
  93. package/.codex-plugin/skills/session/SKILL.md +1 -1
  94. package/.codex-plugin/skills/test-audit/SKILL.md +21 -0
  95. package/.codex-plugin/skills/{npm-publish → test-audit}/agents/openai.yaml +1 -1
  96. package/.cursor/commands/eval.md +1 -1
  97. package/.cursor/commands/navigator.md +15 -0
  98. package/.cursor/commands/session.md +1 -1
  99. package/.cursor/commands/test-audit.md +14 -0
  100. package/.cursor/rules/000-session-orchestrator.mdc +0 -2
  101. package/.cursor/rules/050-plan.mdc +1 -1
  102. package/.cursor/rules/060-evolve.mdc +27 -12
  103. package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
  104. package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
  105. package/.cursor/skills/eval/SKILL.md +1 -1
  106. package/.cursor/skills/navigator/SKILL.md +13 -0
  107. package/.cursor/skills/npm-publish/SKILL.md +1 -0
  108. package/.cursor/skills/test-audit/SKILL.md +12 -0
  109. package/.cursor-plugin/plugin.json +1 -1
  110. package/.orchestrator/policy/blocked-commands.json +12 -3
  111. package/AGENTS.md +9 -30
  112. package/CHANGELOG.md +206 -0
  113. package/README.md +13 -9
  114. package/SECURITY.md +12 -0
  115. package/agents/dialectic-deriver.md +13 -10
  116. package/agents/eval-judge.md +67 -45
  117. package/agents/skill-applied-judge.md +34 -19
  118. package/commands/session.md +7 -3
  119. package/docs/agent-authoring.md +2 -2
  120. package/docs/baseline.md +22 -7
  121. package/docs/ci-setup.md +57 -4
  122. package/docs/codex-setup.md +14 -2
  123. package/docs/components.md +12 -10
  124. package/docs/events-schema.md +65 -15
  125. package/docs/persona-panel.md +5 -4
  126. package/docs/rule-authoring.md +84 -17
  127. package/docs/scope-collision-guard.md +26 -4
  128. package/docs/session-config-reference.md +154 -12
  129. package/docs/session-config-template.md +41 -3
  130. package/docs/telemetry.md +2 -0
  131. package/hooks/_lib/hook-import-set.json +87 -6
  132. package/hooks/_lib/subagent-transcript.mjs +582 -31
  133. package/hooks/config-protection.mjs +11 -3
  134. package/hooks/cwd-change-restore.mjs +11 -3
  135. package/hooks/enforce-commands.mjs +70 -23
  136. package/hooks/enforce-scope.mjs +143 -33
  137. package/hooks/hooks-codex.json +1 -1
  138. package/hooks/hooks-cursor.json +5 -0
  139. package/hooks/hooks-pi.json +5 -0
  140. package/hooks/hooks.json +6 -1
  141. package/hooks/loop-guard.mjs +11 -3
  142. package/hooks/on-session-end.mjs +58 -23
  143. package/hooks/on-session-start.mjs +101 -11
  144. package/hooks/on-stop.mjs +170 -22
  145. package/hooks/operator-steer.mjs +11 -3
  146. package/hooks/post-bash-issue-budget-refund.mjs +18 -8
  147. package/hooks/post-bash-write-verify.mjs +3 -2
  148. package/hooks/post-edit-import-probe.mjs +17 -9
  149. package/hooks/post-edit-validate.mjs +13 -5
  150. package/hooks/post-subagent-discovery-validator.mjs +98 -13
  151. package/hooks/post-tool-batch-wave-signal.mjs +202 -38
  152. package/hooks/post-tool-failure-corrective-context.mjs +11 -5
  153. package/hooks/post-tooluse-frontend-slop.mjs +10 -4
  154. package/hooks/pre-auq-clarity.mjs +15 -2
  155. package/hooks/pre-bash-destructive-guard.mjs +80 -9
  156. package/hooks/pre-bash-issue-budget.mjs +16 -11
  157. package/hooks/pre-bash-memory-propose-audit.mjs +137 -56
  158. package/hooks/pre-bash-sessions-ledger-guard.mjs +419 -22
  159. package/hooks/pre-bash-staging-fence.mjs +335 -31
  160. package/hooks/pre-bash-templates-first.mjs +19 -14
  161. package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
  162. package/hooks/pre-task-scope-disjoint.mjs +792 -74
  163. package/hooks/subagent-telemetry.mjs +111 -38
  164. package/hooks/wave-scope-commit-guard.mjs +197 -100
  165. package/monitors/monitors.json +1 -1
  166. package/output-styles/wave-summary.md +1 -1
  167. package/package.json +3 -3
  168. package/pi/prompts/eval.md +1 -1
  169. package/pi/prompts/navigator.md +12 -0
  170. package/pi/prompts/session.md +1 -1
  171. package/pi/prompts/test-audit.md +12 -0
  172. package/rules/README.md +7 -2
  173. package/rules/_index.md +3 -3
  174. package/rules/always-on/bash-harness-pitfalls.md +7 -1
  175. package/rules/opt-in-domain/prompt-caching.md +6 -2
  176. package/rules/opt-in-stack/backend-data.md +10 -2
  177. package/rules/opt-in-stack/backend.md +10 -4
  178. package/rules/opt-in-stack/frontend.md +7 -2
  179. package/rules/opt-in-stack/security-web.md +10 -4
  180. package/rules/opt-in-stack/swift.md +6 -2
  181. package/scripts/apply-session-learnings.mjs +492 -0
  182. package/scripts/archive-closed-prds.mjs +2 -1
  183. package/scripts/autopilot.mjs +70 -53
  184. package/scripts/backfill-abandoned-sessions.mjs +118 -16
  185. package/scripts/baseline-archetypes.mjs +1 -1
  186. package/scripts/check-package-manager.mjs +62 -5
  187. package/scripts/check-sessions-integrity.mjs +300 -0
  188. package/scripts/cursor-install.mjs +1 -4
  189. package/scripts/dialectic-deriver.mjs +50 -13
  190. package/scripts/emit-session.mjs +371 -31
  191. package/scripts/eval-session.mjs +65 -3
  192. package/scripts/export-hw-learnings.mjs +22 -3
  193. package/scripts/fleet-checkin.mjs +147 -0
  194. package/scripts/generate-agents-skills.mjs +102 -29
  195. package/scripts/generate-cursor-adapter.mjs +61 -16
  196. package/scripts/lib/agent-frontmatter.mjs +3 -3
  197. package/scripts/lib/agent-status.mjs +2 -31
  198. package/scripts/lib/auq/clarity.mjs +10 -2
  199. package/scripts/lib/auq/parse.mjs +12 -31
  200. package/scripts/lib/auq/schema.mjs +56 -41
  201. package/scripts/lib/auto-dialectic.mjs +304 -15
  202. package/scripts/lib/auto-dream.mjs +7 -6
  203. package/scripts/lib/autopilot/flags.mjs +18 -2
  204. package/scripts/lib/autopilot/kill-switches.mjs +6 -3
  205. package/scripts/lib/autopilot/loop.mjs +19 -2
  206. package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
  207. package/scripts/lib/baseline-archetypes.mjs +41 -22
  208. package/scripts/lib/ci-status-banner.mjs +376 -16
  209. package/scripts/lib/command-blocker.mjs +275 -28
  210. package/scripts/lib/config/dialectic.mjs +12 -3
  211. package/scripts/lib/config/gate.mjs +74 -0
  212. package/scripts/lib/config/host-paths.mjs +79 -0
  213. package/scripts/lib/config/persona-gate-wave.mjs +3 -3
  214. package/scripts/lib/config/reaper.mjs +163 -0
  215. package/scripts/lib/config-schema.mjs +13 -0
  216. package/scripts/lib/config.mjs +85 -10
  217. package/scripts/lib/convergence-monitor.mjs +74 -11
  218. package/scripts/lib/ecosystem-health.mjs +11 -0
  219. package/scripts/lib/eval/engine.mjs +421 -53
  220. package/scripts/lib/eval/judge.mjs +463 -40
  221. package/scripts/lib/eval/schema.mjs +10 -1
  222. package/scripts/lib/events-rotation.mjs +221 -25
  223. package/scripts/lib/events-schema.mjs +114 -0
  224. package/scripts/lib/events.mjs +545 -7
  225. package/scripts/lib/fleet-protocol.mjs +270 -0
  226. package/scripts/lib/frontmatter-guard.mjs +21 -10
  227. package/scripts/lib/gates/gate-baseline.mjs +40 -5
  228. package/scripts/lib/gates/gate-full.mjs +35 -5
  229. package/scripts/lib/gates/gate-helpers.mjs +336 -23
  230. package/scripts/lib/gates/gate-incremental.mjs +49 -23
  231. package/scripts/lib/gates/gate-per-file.mjs +42 -8
  232. package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
  233. package/scripts/lib/instruction-budget-guard.mjs +208 -8
  234. package/scripts/lib/io.mjs +42 -8
  235. package/scripts/lib/issue-close-strip-labels.mjs +207 -49
  236. package/scripts/lib/js-mask.mjs +197 -0
  237. package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
  238. package/scripts/lib/learnings/expiry-sweep.mjs +34 -3
  239. package/scripts/lib/learnings/io.mjs +154 -26
  240. package/scripts/lib/learnings/schema.mjs +163 -15
  241. package/scripts/lib/learnings/sizing-subject.mjs +2 -2
  242. package/scripts/lib/maintenance-due-banner.mjs +53 -88
  243. package/scripts/lib/memory-proposals/schema.mjs +11 -5
  244. package/scripts/lib/named-baseline-resolver.mjs +53 -28
  245. package/scripts/lib/named-vault-resolver.mjs +104 -5
  246. package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
  247. package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
  248. package/scripts/lib/orphan-reaper/reaper-audit.mjs +291 -0
  249. package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
  250. package/scripts/lib/orphan-reaper/scan-throttle.mjs +92 -0
  251. package/scripts/lib/orphan-reaper/scan.mjs +506 -0
  252. package/scripts/lib/orphan-reaper.mjs +316 -0
  253. package/scripts/lib/owner-yaml.mjs +104 -6
  254. package/scripts/lib/peer-cards/merger.mjs +48 -10
  255. package/scripts/lib/peer-cards/reader.mjs +78 -2
  256. package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
  257. package/scripts/lib/process-group.mjs +950 -0
  258. package/scripts/lib/project-hygiene.mjs +21 -1
  259. package/scripts/lib/quality-gate.mjs +152 -33
  260. package/scripts/lib/reconcile/backlog.mjs +368 -0
  261. package/scripts/lib/reconcile/engine.mjs +55 -188
  262. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +302 -60
  263. package/scripts/lib/reconcile/sanitize.mjs +69 -3
  264. package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
  265. package/scripts/lib/resource-probe/parsers.mjs +31 -0
  266. package/scripts/lib/resource-probe.mjs +1 -1
  267. package/scripts/lib/rule-loader.mjs +141 -74
  268. package/scripts/lib/rules-sync.mjs +27 -12
  269. package/scripts/lib/scope-baseline.mjs +3 -4
  270. package/scripts/lib/scope-echo.mjs +39 -2
  271. package/scripts/lib/scope-gate.mjs +605 -1
  272. package/scripts/lib/session-close-backfill.mjs +33 -6
  273. package/scripts/lib/session-id.mjs +9 -20
  274. package/scripts/lib/session-invocation.mjs +20 -0
  275. package/scripts/lib/session-record-repair.mjs +183 -26
  276. package/scripts/lib/session-registry.mjs +27 -2
  277. package/scripts/lib/session-schema/constants.mjs +56 -2
  278. package/scripts/lib/session-schema/normalizer.mjs +56 -4
  279. package/scripts/lib/session-schema/validator.mjs +28 -1
  280. package/scripts/lib/session-schema.mjs +8 -3
  281. package/scripts/lib/session-start-probes.mjs +95 -10
  282. package/scripts/lib/session-token-rollup.mjs +245 -52
  283. package/scripts/lib/sessions-canonical.mjs +118 -4
  284. package/scripts/lib/sessions-integrity-banner.mjs +7 -1
  285. package/scripts/lib/sessions-staleness-banner.mjs +193 -51
  286. package/scripts/lib/sh-quote.mjs +29 -0
  287. package/scripts/lib/skill-evidence-window.mjs +891 -0
  288. package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
  289. package/scripts/lib/skill-evolution/engine.mjs +18 -9
  290. package/scripts/lib/skill-judge.mjs +45 -3
  291. package/scripts/lib/subagents-schema.mjs +30 -3
  292. package/scripts/lib/tail-window.mjs +56 -0
  293. package/scripts/lib/telemetry/pricing.mjs +118 -71
  294. package/scripts/lib/telemetry/schema.mjs +30 -0
  295. package/scripts/lib/telemetry/sync.mjs +61 -6
  296. package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
  297. package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
  298. package/scripts/lib/tests-src-ratio.mjs +140 -23
  299. package/scripts/lib/tmux-layout/layouts.mjs +1 -11
  300. package/scripts/lib/tmux-layout/telemetry-stats.mjs +72 -13
  301. package/scripts/lib/tmux-layout/tmux-shell.mjs +2 -11
  302. package/scripts/lib/user-invocable-skills.mjs +23 -3
  303. package/scripts/lib/ux-grill/reconcile.mjs +48 -22
  304. package/scripts/lib/validate/check-agents-skills.mjs +26 -15
  305. package/scripts/lib/validate/check-agents.mjs +1 -1
  306. package/scripts/lib/validate/check-cursor-adapter.mjs +1 -0
  307. package/scripts/lib/validate/check-entry-guard.mjs +13 -50
  308. package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
  309. package/scripts/lib/validate/check-hooks-symmetry.mjs +5 -0
  310. package/scripts/lib/validate/check-learnings-shell-writes.mjs +214 -0
  311. package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
  312. package/scripts/lib/validate/check-rules.mjs +7 -5
  313. package/scripts/lib/validate/check-skill-links.mjs +9 -1
  314. package/scripts/lib/validate/check-skill-script-paths.mjs +239 -27
  315. package/scripts/lib/validate/check-test-git-config-target.mjs +24 -34
  316. package/scripts/lib/validate/check-test-value-bans.mjs +87 -14
  317. package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
  318. package/scripts/lib/validate/check-untracked-test-deps.mjs +7 -102
  319. package/scripts/lib/validate/check-unwired-features.mjs +130 -27
  320. package/scripts/lib/validate/check-validator-registration.mjs +34 -10
  321. package/scripts/lib/validate/confidential-names.mjs +10 -0
  322. package/scripts/lib/validate-vendored-rules.mjs +210 -84
  323. package/scripts/lib/vault-mirror/namespace.mjs +46 -8
  324. package/scripts/lib/vault-mirror/process.mjs +10 -3
  325. package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
  326. package/scripts/lib/vault-mirror/telemetry.mjs +8 -1
  327. package/scripts/lib/vault-status/board-writer.mjs +48 -11
  328. package/scripts/lib/vault-status/narrative-mirror.mjs +71 -16
  329. package/scripts/lib/vault-yaml.mjs +118 -0
  330. package/scripts/lib/wave-executor/dispatch-common.mjs +10 -18
  331. package/scripts/lib/wave-executor/foreign-dispatch.mjs +1 -1
  332. package/scripts/lib/wave-executor/remote-dispatch.mjs +1 -1
  333. package/scripts/lib/wave-resource-gate.mjs +34 -8
  334. package/scripts/lib/worktree/lifecycle.mjs +153 -1
  335. package/scripts/memory-propose.mjs +18 -5
  336. package/scripts/release-session-lock.mjs +306 -0
  337. package/scripts/release.mjs +40 -8
  338. package/scripts/repair-invalid-sessions.mjs +37 -4
  339. package/scripts/resolve-session-invocation.mjs +59 -0
  340. package/scripts/run-migrate-v2-cross-repo.mjs +158 -30
  341. package/scripts/run-quality-gate.mjs +357 -197
  342. package/scripts/site-numbers.mjs +163 -5
  343. package/scripts/sweep-expired-learnings.mjs +56 -5
  344. package/scripts/sweep-expired-rules.mjs +16 -4
  345. package/scripts/validate-plugin.mjs +19 -0
  346. package/scripts/validate-wave-scope.mjs +32 -105
  347. package/scripts/vault-integration-watcher.mjs +2 -1
  348. package/scripts/vault-mirror.mjs +81 -47
  349. package/scripts/wave-scope-binding.mjs +2 -1
  350. package/skills/_shared/config-reading.md +3 -3
  351. package/skills/_shared/fleet-protocol.md +131 -0
  352. package/skills/_shared/platform-tools.md +23 -11
  353. package/skills/_shared/state-ownership.md +5 -0
  354. package/skills/autopilot/SKILL.md +47 -18
  355. package/skills/bootstrap/SKILL.md +1 -1
  356. package/skills/bootstrap/_shared-template.md +1 -1
  357. package/skills/bootstrap/fast-template.md +1 -1
  358. package/skills/bootstrap/private-contract.md +2 -1
  359. package/skills/bootstrap/public-fallback.md +6 -3
  360. package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
  361. package/skills/claude-md-drift-check/SKILL.md +10 -10
  362. package/skills/claude-md-drift-check/checker.mjs +26 -18
  363. package/skills/convergence-monitoring/README.md +8 -1
  364. package/skills/convergence-monitoring/SIGNALS.md +50 -6
  365. package/skills/convergence-monitoring/SKILL.md +15 -6
  366. package/skills/eval/SKILL.md +39 -24
  367. package/skills/eval/rubric-v1.md +1 -0
  368. package/skills/eval/rubric-v2.md +457 -0
  369. package/skills/evolve/SKILL.md +3 -3
  370. package/skills/evolve/references/evolve-analyze-mode.md +10 -6
  371. package/skills/evolve/references/evolve-dialectic-mode.md +42 -25
  372. package/skills/gitlab-ops/SKILL.md +3 -2
  373. package/skills/memory-cleanup/SKILL.md +1 -1
  374. package/skills/navigator/SKILL.md +193 -0
  375. package/skills/navigator/references/account-check.sh +87 -0
  376. package/skills/navigator/references/fleet-decisions.mjs +231 -0
  377. package/skills/navigator/references/slot-check.sh +114 -0
  378. package/skills/npm-publish/SKILL.md +1 -1
  379. package/skills/persona-panel/SKILL.md +2 -2
  380. package/skills/persona-panel/persona-format.md +2 -2
  381. package/skills/persona-panel/presets/designer-lens.md +1 -1
  382. package/skills/persona-panel/presets/engineer-lens.md +1 -1
  383. package/skills/persona-panel/presets/pm-lens.md +1 -1
  384. package/skills/plan/mode-retro.md +19 -4
  385. package/skills/quality-gates/SKILL.md +6 -4
  386. package/skills/reconcile/SKILL.md +11 -0
  387. package/skills/remote-offload/SKILL.md +4 -2
  388. package/skills/session-end/SKILL.md +13 -16
  389. package/skills/session-end/discovery-scan.md +1 -1
  390. package/skills/session-end/learning-patterns.md +22 -12
  391. package/skills/session-end/metrics-collection.md +5 -6
  392. package/skills/session-end/phase-3-6-tail.md +55 -9
  393. package/skills/session-end/references/phase-3-documentation-updates.md +2 -2
  394. package/skills/session-end/references/phase-4a-worktree-cleanup.md +2 -0
  395. package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
  396. package/skills/session-end/references/session-summary-template.md +1 -1
  397. package/skills/session-end/session-metrics-write.md +32 -60
  398. package/skills/session-plan/SKILL.md +17 -8
  399. package/skills/session-plan/references/session-plan-task-classification.md +2 -2
  400. package/skills/session-start/SKILL.md +6 -0
  401. package/skills/session-start/references/operations-contract.md +4 -0
  402. package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
  403. package/skills/session-start/references/phase-4-ssot-environment-check.md +4 -3
  404. package/skills/test-audit/SKILL.md +152 -0
  405. package/skills/test-audit/references/campaign.md +111 -0
  406. package/skills/test-audit/references/mutate.sh +50 -0
  407. package/skills/ux-grill/SKILL.md +1 -1
  408. package/skills/wave-executor/SKILL.md +9 -5
  409. package/skills/wave-executor/circuit-breaker.md +2 -0
  410. package/skills/wave-executor/references/wave-executor-state-init.md +8 -3
  411. package/skills/wave-executor/references/wave-loop-dispatch.md +5 -2
  412. package/skills/wave-executor/references/wave-loop-review.md +3 -5
  413. package/templates/personas/README.md +5 -5
  414. package/templates/personas/accounting-compliance.v1.md +1 -1
  415. package/templates/personas/accounting-tax-advisor.v1.md +1 -1
  416. package/templates/personas/buyer-p1-cto.v1.md +1 -1
  417. package/templates/personas/buyer-p2-kanzlei.v1.md +1 -1
  418. package/templates/personas/buyer-p3-build.v1.md +1 -1
  419. package/templates/personas/buyer-p4-tech-veto.v1.md +1 -1
  420. package/templates/personas/buyer-p5-solo.v1.md +1 -1
  421. package/templates/personas/buyer-p6-ld.v1.md +1 -1
  422. package/templates/personas/klima-ai-expert.v1.md +1 -1
  423. package/templates/personas/klima-physicist.v1.md +1 -1
  424. package/.cursor/commands/convergence-monitoring.md +0 -13
  425. package/.cursor/commands/npm-publish.md +0 -13
  426. package/pi/prompts/convergence-monitoring.md +0 -11
  427. package/pi/prompts/npm-publish.md +0 -11
@@ -12,9 +12,12 @@
12
12
  * - File-absent or all-null-token sessions return a sentinel shape with null
13
13
  * totals (not 0) so callers can distinguish "session had no token data" from
14
14
  * "session was genuinely free / cost $0".
15
+ * - `match_status` and `ledger_records` distinguish why totals are absent
16
+ * without interpreting the session key's shape (#1027 Nachtrag 7).
15
17
  * - Malformed JSONL lines are silently skipped (resilience over strictness).
16
18
  * - `subagents_with_tokens` counts distinct agent_ids that have at least one
17
- * TOKEN-BEARING record (coverage metric).
19
+ * TOKEN-BEARING record (coverage metric); every total counts each agent once
20
+ * (§ One record per agent).
18
21
  *
19
22
  * ## Token provenance — why a bare Σ over token_input is wrong (#949)
20
23
  *
@@ -64,10 +67,48 @@
64
67
  * `total_token_*` and reports the excluded ones as `legacy_v1_records`: the
65
68
  * boundary is DECLARED, never silent.
66
69
  *
67
- * `total_cost_usd` is computed per record via `costUsd()` and is null when ANY
68
- * priced record carries a model the price table does not know — a partial cost
69
- * is worse than no cost, because it reads as a complete one. `cost_records_priced`
70
- * / `cost_records_total` say how much of the session the estimate covers.
70
+ * ## One record per agent — a stop record carries a RUNNING total
71
+ *
72
+ * `hooks/subagent-telemetry.mjs` re-reads the agent's WHOLE transcript at every
73
+ * stop, so an agent that stops twice (resumed via SendMessage, for one) writes
74
+ * its running total twice, and summing both counts the first part again. Every
75
+ * total below therefore takes ONE v2 record per `agent_id`: its tokens come from
76
+ * the agent's last stop that found its transcript and carried tokens, its cost
77
+ * candidacy from its last stop that found its transcript ("last" = latest
78
+ * `timestamp`, ties and unparseable timestamps by file order). Those two differ
79
+ * only when the last read yielded no tokens — an oversized transcript: the
80
+ * earlier running total is then a lower bound for the tokens and the cost is
81
+ * unknown. Measured 2026-10-01 over the 31 local fleet ledgers (1,002 distinct
82
+ * session/agent pairs with two or more found stops, 407 sessions): no v2 token
83
+ * bucket ever decreased from one found stop to the next, so the last record is
84
+ * also the largest; 2 pairs end in a token-less stop, both transcripts now past
85
+ * the hook's 50 MiB read limit. FORWARD-ONLY like the #949 cut: totals already
86
+ * in `sessions.jsonl` are not recomputed.
87
+ *
88
+ * `total_cost_usd` is computed per agent via `costUsd()` and is null whenever
89
+ * `cost_records_priced < cost_records_total` (#1475) — a partial cost is worse
90
+ * than no cost, because it reads as a complete one. An agent is unpriced when the
91
+ * model of its last found record is unknown to the price table, when it has no model (a token-bearing turn
92
+ * named none), when it has no tokens at all (transcript found but oversized,
93
+ * without usage turns, or unreadable, #1474), or when the agent has a start
94
+ * record but its stop found no transcript of its own and no other record of the
95
+ * same agent did (`start_record_found: true`, `subagent_transcript_found: false`
96
+ * — mostly `workflow-subagent`). A phantom stop (#939, no start record) is no
97
+ * agent and no candidate. A record whose four token buckets
98
+ * are all 0 is priced at $0 whatever its model, and never turns a null total into
99
+ * a number on its own. A record whose turns span several models carries
100
+ * `models_usage` (#1470) and is priced part by part. `cost_records_priced` /
101
+ * `cost_records_total` say how much of the session the estimate covers; with
102
+ * nothing to price (total 0) the cost is null as well.
103
+ *
104
+ * Blind spots the ledger cannot close: an agent whose start record lies outside
105
+ * the hook's tail window reads `start_record_found: false` and is
106
+ * indistinguishable from a phantom, and an agent that left neither a start nor a
107
+ * stop record is invisible. One blind spot is a rule of this module, not the
108
+ * ledger: a session spanning the 2026-09-09 schema boundary is priced over its v2
109
+ * records only — its v1 records are `legacy_v1_records`, never cost candidates
110
+ * (4 distinct fleet sessions carry such a numeric cost, 31 ledgers, 2026-10-01).
111
+ * "Never a partial sum" holds only over the v2 agents the ledger can see.
71
112
  *
72
113
  * @module session-token-rollup
73
114
  */
@@ -102,6 +143,26 @@ function isTokenBearing(record) {
102
143
  return record?.subagent_transcript_found === true;
103
144
  }
104
145
 
146
+ /**
147
+ * Did a REAL agent run whose transcript the hook could not find? (#1475 follow-up)
148
+ *
149
+ * A stop record with a start record (`start_record_found: true`) but no own
150
+ * transcript (`subagent_transcript_found: false`) is not a phantom: the agent
151
+ * existed, it just left nothing to price — 813 of the 877 such records in the
152
+ * 33 fleet ledgers are `workflow-subagent` (2026-10-01). Its cost is unknown, so it must reach
153
+ * `cost_records_total`. A phantom stop (#939) has `start_record_found` false and
154
+ * stays out; a record written before either flag existed matches neither.
155
+ * @param {object} record
156
+ * @returns {boolean}
157
+ */
158
+ function isStartedWithoutTranscript(record) {
159
+ return (
160
+ record?.event === 'stop' &&
161
+ record.start_record_found === true &&
162
+ record.subagent_transcript_found === false
163
+ );
164
+ }
165
+
105
166
  /**
106
167
  * Is this record inside the schema_version 2 token contract? (#1244)
107
168
  * @param {object} record
@@ -111,18 +172,75 @@ function isV2(record) {
111
172
  return typeof record?.schema_version === 'number' && record.schema_version >= 2;
112
173
  }
113
174
 
175
+ /**
176
+ * Does this record carry a token count on either side?
177
+ * @param {object} record
178
+ * @returns {boolean}
179
+ */
180
+ function hasTokens(record) {
181
+ const { token_input: inp, token_output: out } = record;
182
+ return (typeof inp === 'number' && inp >= 0) || (typeof out === 'number' && out >= 0);
183
+ }
184
+
185
+ /**
186
+ * Was `candidate` written at a later stop than `current`? Ordered by
187
+ * `timestamp`; a tie or an unparseable timestamp falls back to file order, and
188
+ * the caller reads in file order, so the candidate then wins.
189
+ * @param {object} candidate — read after `current` in the ledger
190
+ * @param {object} current
191
+ * @returns {boolean}
192
+ */
193
+ function isLaterStop(candidate, current) {
194
+ const a = Date.parse(candidate.timestamp);
195
+ const b = Date.parse(current.timestamp);
196
+ if (Number.isNaN(a) || Number.isNaN(b) || a === b) return true;
197
+ return a > b;
198
+ }
199
+
200
+ /**
201
+ * USD cost of one v2 record, or null when it cannot be priced. A record whose
202
+ * token-bearing turns span several models carries `models_usage` (#1470): each
203
+ * part is priced at its own model's rates, and one unknown part leaves the whole
204
+ * record unpriced. Without that field the record's single `model` prices all
205
+ * four buckets.
206
+ * @param {object} record
207
+ * @returns {number|null}
208
+ */
209
+ function recordCostUsd(record) {
210
+ const parts =
211
+ Array.isArray(record.models_usage) && record.models_usage.length > 0
212
+ ? record.models_usage
213
+ : [record];
214
+ let total = 0;
215
+ for (const part of parts) {
216
+ const cost = costUsd({
217
+ model: part?.model,
218
+ tokenInputUncached: part?.token_input_uncached,
219
+ tokenCacheRead: part?.token_cache_read,
220
+ tokenCacheCreation: part?.token_cache_creation,
221
+ tokenOutput: part?.token_output,
222
+ });
223
+ if (cost === null) return null;
224
+ total += cost;
225
+ }
226
+ return total;
227
+ }
228
+
114
229
  /**
115
230
  * @typedef {Object} TokenRollupResult
116
- * @property {number|null} total_token_input - Sum of token_input across TOKEN-BEARING matched records; null when none had a non-null value.
117
- * @property {number|null} total_token_output - Sum of token_output across TOKEN-BEARING matched records; null when none had a non-null value.
231
+ * @property {'invalid-key'|'ledger-absent'|'ledger-empty'|'unmatched'|'matched'} match_status - Distinguishes absent telemetry from an unmatched key (#1027 Nachtrag 7).
232
+ * @property {number|null} ledger_records - Count of parsed non-null, non-array objects; null when the ledger was not read (#1027 Nachtrag 7).
233
+ * @property {number|null} total_token_input - Sum of token_input over one v2 token-bearing record per agent (its last stop that carried tokens — § One record per agent); null when none had a non-null value.
234
+ * @property {number|null} total_token_output - Sum of token_output over the same one record per agent; null when none had a non-null value.
235
+ * @property {number|null} total_tokens - total_token_input + total_token_output (#1436); a null side counts as absent, null only when BOTH are null.
118
236
  * @property {number} subagents_with_tokens - Count of distinct agent_ids with at least one token-bearing record. This is the numerator of the honest coverage ratio.
119
237
  * @property {number} matched_records - Total count of JSONL records matched by parentSessionId. Counts start records, phantom stops and pre-#949 records alike, so it is NOT the denominator for a token-coverage ratio — dividing by it is what made healthy sessions read as 12% covered.
120
- * @property {number|null} total_token_input_uncached - Sum of token_input_uncached across v2 token-bearing records.
121
- * @property {number|null} total_token_cache_read - Sum of token_cache_read across v2 token-bearing records.
122
- * @property {number|null} total_token_cache_creation - Sum of token_cache_creation across v2 token-bearing records.
123
- * @property {number|null} total_cost_usd - Σ costUsd() over v2 token-bearing records; null when ANY of them carries an unknown model (never 0 — see telemetry/pricing.mjs).
124
- * @property {number} cost_records_priced - How many token-bearing records the price table could price.
125
- * @property {number} cost_records_total - How many token-bearing records were candidates for pricing.
238
+ * @property {number|null} total_token_input_uncached - Sum of token_input_uncached over the same one record per agent.
239
+ * @property {number|null} total_token_cache_read - Sum of token_cache_read over the same one record per agent.
240
+ * @property {number|null} total_token_cache_creation - Sum of token_cache_creation over the same one record per agent.
241
+ * @property {number|null} total_cost_usd - Σ cost over the agents counted in cost_records_priced; null when cost_records_priced < cost_records_total (#1475 — any unpriced candidate: unknown model, no model, no tokens, or a started agent without a transcript), null when there is nothing to price (cost_records_total 0), and null when no record with a non-zero bucket was priced (all-zero records alone never yield a fabricated $0). Never 0 for unknown — see telemetry/pricing.mjs.
242
+ * @property {number} cost_records_priced - How many agents of cost_records_total carry a known cost on their last found v2 record: priced by the table (per model part when the record carries models_usage, #1470), or all four token buckets 0 (priced at $0 whatever the model, #1474).
243
+ * @property {number} cost_records_total - How many agents were candidates for pricing, each counted once however often it stopped: every agent with a v2 record whose own transcript was found — priced on its LAST such record, so one whose last read yielded no tokens (transcript oversized, without usage turns, or unreadable, #1474) is unpriced — plus every started agent whose v2 stops found no transcript of its own (`start_record_found: true`, `subagent_transcript_found: false`) and that has no token-bearing record in the session. Both latter kinds have an unknown cost. Phantom stops (no start record, #939) are never candidates. priced < total nulls total_cost_usd.
126
244
  * @property {number} legacy_v1_records - Token-bearing records EXCLUDED from every total above because their schema_version < 2 (their token_input is a different quantity).
127
245
  * @property {2} _token_schema - The token contract these totals were computed under.
128
246
  */
@@ -142,8 +260,11 @@ export function rollupSessionTokens({
142
260
  }) {
143
261
  /** @type {TokenRollupResult} */
144
262
  const ZERO = {
263
+ match_status: 'invalid-key',
264
+ ledger_records: null,
145
265
  total_token_input: null,
146
266
  total_token_output: null,
267
+ total_tokens: null,
147
268
  subagents_with_tokens: 0,
148
269
  matched_records: 0,
149
270
  total_token_input_uncached: null,
@@ -169,7 +290,7 @@ export function rollupSessionTokens({
169
290
  raw = readFileSync(resolvedPath, 'utf8');
170
291
  } catch (err) {
171
292
  if (err.code === 'ENOENT') {
172
- return { ...ZERO };
293
+ return { ...ZERO, match_status: 'ledger-absent' };
173
294
  }
174
295
  throw err;
175
296
  }
@@ -177,6 +298,7 @@ export function rollupSessionTokens({
177
298
  // Parse JSONL — skip malformed lines, filter to parentSessionId.
178
299
  const lines = raw.split('\n');
179
300
  const matched = [];
301
+ let ledgerRecords = 0;
180
302
  for (const line of lines) {
181
303
  const trimmed = line.trim();
182
304
  if (!trimmed) continue;
@@ -187,13 +309,19 @@ export function rollupSessionTokens({
187
309
  // Malformed line — skip silently.
188
310
  continue;
189
311
  }
190
- if (record && record.parent_session_id === parentSessionId) {
312
+ if (record === null || typeof record !== 'object' || Array.isArray(record)) continue;
313
+ ledgerRecords += 1;
314
+ if (record.parent_session_id === parentSessionId) {
191
315
  matched.push(record);
192
316
  }
193
317
  }
194
318
 
195
319
  if (matched.length === 0) {
196
- return { ...ZERO };
320
+ return {
321
+ ...ZERO,
322
+ match_status: ledgerRecords === 0 ? 'ledger-empty' : 'unmatched',
323
+ ledger_records: ledgerRecords,
324
+ };
197
325
  }
198
326
 
199
327
  // Aggregate — skip null/undefined token values.
@@ -205,7 +333,6 @@ export function rollupSessionTokens({
205
333
  let sumCost = null;
206
334
  let costPriced = 0;
207
335
  let costTotal = 0;
208
- let costUnknownModel = false;
209
336
  let legacyV1 = 0;
210
337
 
211
338
  // Track distinct agent_ids that contributed at least one non-null token.
@@ -214,13 +341,50 @@ export function rollupSessionTokens({
214
341
  const addNonNegative = (acc, value) =>
215
342
  typeof value === 'number' && value >= 0 ? (acc ?? 0) + value : acc;
216
343
 
344
+ // Agents with at least one record whose own transcript WAS read. A later stop
345
+ // of the same agent can find a transcript an earlier one missed (measured
346
+ // 2026-10-01: 4 agents in one fleet session, `workflow-subagent` found:false
347
+ // first, then found:true) — its cost is then known and must not null the total.
348
+ const agentsWithTranscript = new Set(
349
+ matched.filter(isTokenBearing).map((r) => r.agent_id).filter((id) => typeof id === 'string'),
350
+ );
351
+
352
+ // One agent, one count (see module header § One record per agent). Per agent:
353
+ // `last` is its last stop that found its transcript — the cost candidate —
354
+ // and `lastWithTokens` its last such stop that carried tokens — the token
355
+ // contribution. They differ only when the last read yielded no tokens (an
356
+ // oversized transcript): the earlier running total is then a known lower bound
357
+ // for the tokens, while the cost stays unknown. A record without an agent_id
358
+ // cannot be joined to another and stands alone.
359
+ /** @type {Map<unknown, {last: object, lastWithTokens: object|null}>} */
360
+ const agents = new Map();
361
+ /** Started agents whose stops found no transcript — each counts once. */
362
+ const startedWithoutTranscript = new Set();
363
+ let unkeyedStartedWithoutTranscript = 0;
364
+
217
365
  for (const record of matched) {
218
366
  // Provenance gate (#949) — a record whose tokens describe the PARENT
219
- // transcript, or no transcript at all, contributes nothing. Skipping it
367
+ // transcript, or no transcript at all, contributes no TOKENS. Skipping it
220
368
  // entirely (rather than treating its values as 0) preserves the null
221
369
  // sentinel: a session of only untrustworthy records reports "no data",
222
370
  // which is true, instead of a fabricated 0.
223
- if (!isTokenBearing(record)) continue;
371
+ if (!isTokenBearing(record)) {
372
+ // …but a v2 agent that really ran without a readable transcript has an
373
+ // unknown cost: an unpriced candidate (total, not priced), so priced <
374
+ // total nulls `total_cost_usd` (#1475). Phantoms never reach this branch.
375
+ if (
376
+ isV2(record) &&
377
+ isStartedWithoutTranscript(record) &&
378
+ !agentsWithTranscript.has(record.agent_id)
379
+ ) {
380
+ if (record.agent_id === undefined || record.agent_id === null) {
381
+ unkeyedStartedWithoutTranscript += 1;
382
+ } else {
383
+ startedWithoutTranscript.add(record.agent_id);
384
+ }
385
+ }
386
+ continue;
387
+ }
224
388
 
225
389
  // Schema gate (#1244) — a v1 record's token_input is raw uncached input,
226
390
  // a different quantity from a v2 record's billable prompt volume. Count it
@@ -230,52 +394,81 @@ export function rollupSessionTokens({
230
394
  continue;
231
395
  }
232
396
 
233
- const inp = record.token_input;
234
- const out = record.token_output;
235
-
236
- sumInput = addNonNegative(sumInput, inp);
237
- sumOutput = addNonNegative(sumOutput, out);
238
- sumUncached = addNonNegative(sumUncached, record.token_input_uncached);
239
- sumCacheRead = addNonNegative(sumCacheRead, record.token_cache_read);
240
- sumCacheCreation = addNonNegative(sumCacheCreation, record.token_cache_creation);
397
+ const withTokens = hasTokens(record) ? record : null;
398
+ const key = record.agent_id ?? Symbol('unkeyed');
399
+ const seen = agents.get(key);
400
+ if (!seen) {
401
+ agents.set(key, { last: record, lastWithTokens: withTokens });
402
+ continue;
403
+ }
404
+ // A token-bearing `last` is always `lastWithTokens` too — set together, so
405
+ // an unparseable timestamp (no transitive order) cannot split them.
406
+ if (isLaterStop(record, seen.last)) {
407
+ seen.last = record;
408
+ if (withTokens) seen.lastWithTokens = withTokens;
409
+ } else if (withTokens && (!seen.lastWithTokens || isLaterStop(withTokens, seen.lastWithTokens))) {
410
+ seen.lastWithTokens = withTokens;
411
+ }
412
+ }
241
413
 
242
- // Count this agent as having tokens if either field is a non-null number.
243
- const hasTokens =
244
- (typeof inp === 'number' && inp >= 0) || (typeof out === 'number' && out >= 0);
245
- if (hasTokens) {
246
- if (record.agent_id !== undefined && record.agent_id !== null) {
247
- agentsWithTokens.add(record.agent_id);
248
- }
414
+ for (const [key, { last, lastWithTokens }] of agents) {
415
+ if (lastWithTokens) {
416
+ sumInput = addNonNegative(sumInput, lastWithTokens.token_input);
417
+ sumOutput = addNonNegative(sumOutput, lastWithTokens.token_output);
418
+ sumUncached = addNonNegative(sumUncached, lastWithTokens.token_input_uncached);
419
+ sumCacheRead = addNonNegative(sumCacheRead, lastWithTokens.token_cache_read);
420
+ sumCacheCreation = addNonNegative(sumCacheCreation, lastWithTokens.token_cache_creation);
421
+ if (typeof key !== 'symbol') agentsWithTokens.add(key);
422
+ }
249
423
 
250
- // Cost: every token-bearing v2 record is a pricing candidate. One unknown
251
- // model poisons the SESSION total — a cost covering some of the agents
252
- // reads as covering all of them.
253
- costTotal += 1;
254
- const cost = costUsd({
255
- model: record.model,
256
- tokenInputUncached: record.token_input_uncached,
257
- tokenCacheRead: record.token_cache_read,
258
- tokenCacheCreation: record.token_cache_creation,
259
- tokenOutput: record.token_output,
260
- });
261
- if (cost === null) {
262
- costUnknownModel = true;
263
- } else {
264
- costPriced += 1;
265
- sumCost = (sumCost ?? 0) + cost;
266
- }
424
+ // Cost: every agent with a found transcript is ONE pricing candidate. One
425
+ // unpriced agent poisons the SESSION total — a cost covering some of the
426
+ // agents reads as covering all of them.
427
+ costTotal += 1;
428
+ if (last !== lastWithTokens) {
429
+ // #1474 — the agent's last read of its own transcript yielded no tokens:
430
+ // it exceeded the hook's MAX_TRANSCRIPT_BYTES and was not read, it held no
431
+ // usage turns, or it was unreadable. Either way the agent ran and its cost
432
+ // is unknown, so it counts as an unpriced candidate (total, not priced),
433
+ // and priced < total nulls `total_cost_usd` (#1475).
434
+ continue;
435
+ }
436
+ // #1474 — all four buckets 0 costs $0 whatever the model says. The hook
437
+ // names such a record by its last turn, typically `<synthetic>`, which
438
+ // the price table does not know; pricing it would null the session total
439
+ // for a record that added nothing to it.
440
+ const allZero = [
441
+ last.token_input_uncached,
442
+ last.token_cache_read,
443
+ last.token_cache_creation,
444
+ last.token_output,
445
+ ].every((v) => v === 0);
446
+ const cost = allZero ? 0 : recordCostUsd(last);
447
+ if (cost !== null) {
448
+ costPriced += 1;
449
+ // An all-zero record adds nothing, so it must not be what turns
450
+ // `sumCost` from null into a number: next to a token-less record it
451
+ // would persist a fabricated $0 for a session whose real cost is unknown.
452
+ if (!allZero) sumCost = (sumCost ?? 0) + cost;
267
453
  }
268
454
  }
455
+ costTotal += startedWithoutTranscript.size + unkeyedStartedWithoutTranscript;
269
456
 
270
457
  return {
458
+ match_status: 'matched',
459
+ ledger_records: ledgerRecords,
271
460
  total_token_input: sumInput,
272
461
  total_token_output: sumOutput,
462
+ // #1436 — the one figure a session-level budget compares against. Null only
463
+ // when neither side has data; never a fabricated 0.
464
+ total_tokens: sumInput === null && sumOutput === null ? null : (sumInput ?? 0) + (sumOutput ?? 0),
273
465
  subagents_with_tokens: agentsWithTokens.size,
274
466
  matched_records: matched.length,
275
467
  total_token_input_uncached: sumUncached,
276
468
  total_token_cache_read: sumCacheRead,
277
469
  total_token_cache_creation: sumCacheCreation,
278
- total_cost_usd: costUnknownModel ? null : sumCost,
470
+ // #1475 — a cost that leaves out any candidate record is not persisted.
471
+ total_cost_usd: costPriced < costTotal ? null : sumCost,
279
472
  cost_records_priced: costPriced,
280
473
  cost_records_total: costTotal,
281
474
  legacy_v1_records: legacyV1,
@@ -20,6 +20,18 @@
20
20
  * (2026-05-10) carries a byte-identical duplicate LINE — an older
21
21
  * collision class than (3), and rule (1) alone resolves it.
22
22
  *
23
+ * EXCEPT: a backfill stub never shadows a record that is not one (#1443).
24
+ * A stub (a synthesised `_backfill_source` — repair provenance is not one,
25
+ * see `isBackfillStub()` — and no wave, agent or changed file, whatever the
26
+ * `status` or `_repair_source`) appended AFTER the real record for the
27
+ * same id hid that record from every reader — the writers were closed by
28
+ * #1368 and the key-occupancy guard, this reader was the remaining gap.
29
+ * Among non-stubs, and among stubs, the newest still wins. The same guard
30
+ * joins ACROSS ids: a stub sharing a non-empty `raw_session_id` with a
31
+ * non-stub under a DIFFERENT `session_id` is dropped for the non-stub. That
32
+ * join runs LAST, so it can neither orphan a synthetic twin of (2) nor
33
+ * change what (3) reports.
34
+ *
23
35
  * (2) NARROW COLLAPSE OF THE SYSTEMIC DOUBLE-STUB CLASS.
24
36
  * Two `abandoned` records with an EXACT `started_at` + `completed_at`
25
37
  * tuple match are one physical session recorded twice by the two backfill
@@ -70,8 +82,8 @@
70
82
  * refused marker keeps both records and is reported (never logged)
71
83
  * via `canonicalizeSessionsDetailed().ignoredSupersedes`.
72
84
  *
73
- * RULE ORDER: (1) → (2) → (3). The double-stub collapse must run BEFORE
74
- * supersede removal: with the reverse order a `supersedes` append deleted the
85
+ * RULE ORDER: (1) → (2) → (3) → (1)'s raw-id join. The double-stub collapse
86
+ * must run BEFORE supersede removal: with the reverse order a `supersedes` append deleted the
75
87
  * authentic stub first, shrank the tuple group to a single member, and the
76
88
  * synthetic phantom then survived the very session that refuted it.
77
89
  *
@@ -158,6 +170,80 @@ function supersedeRejectReason(superseder, target) {
158
170
  return 'no-shared-join-key';
159
171
  }
160
172
 
173
+ /**
174
+ * `_backfill_source` prefix that `session-record-repair.mjs` stamps
175
+ * (`REPAIR_SOURCE`, `'repair-invalid-sessions/1004'`) on a record it repaired
176
+ * in place that had NO origin of its own — i.e. a REAL record. A record that
177
+ * already carried a synthesised origin keeps it, and the repair goes to the
178
+ * separate `_repair_source` field, which this module deliberately ignores
179
+ * (#1443 F2). Kept as a literal so this module stays a stdlib-only leaf.
180
+ */
181
+ const REPAIR_PROVENANCE_PREFIX = 'repair-invalid-sessions/';
182
+
183
+ /**
184
+ * True when a record is a BACKFILL STUB for rule (1)'s #1443 guard: it carries
185
+ * SYNTHESISED provenance (`_backfill_source`, but not repair provenance) AND
186
+ * measured no work (0 waves, agents and files) — whatever its `status`. A
187
+ * missing `status` is the norm on real records, so status cannot decide it.
188
+ *
189
+ * Repair provenance (`repair-invalid-sessions/…`) in `_backfill_source` is
190
+ * never a stub: the repair tool puts it there only on a record without an
191
+ * origin, i.e. a real record it fixed in place, not one it invented (#1443 F1 —
192
+ * a repaired 4-wave record lost to an older 2-wave one). A repaired STUB keeps
193
+ * its synthesised `_backfill_source` (`state-md-completed`, `events-jsonl`,
194
+ * `abandoned`) and is judged on that and on its work, whatever its
195
+ * `_repair_source` says (#1443 F2 — a repaired 0-wave stub shadowed a 4-wave
196
+ * record once the repair overwrote its origin). Legacy records repaired before
197
+ * F2 lost that origin irrecoverably and read as real.
198
+ *
199
+ * Exported for `scripts/autopilot.mjs`, which must not read a stub as a
200
+ * healthy 0-token iteration (#1457 F2). Not shared with
201
+ * `session-close-backfill.mjs::isSupersedableStub()` on purpose: that asks a
202
+ * narrower WRITER question ("may this be overwritten?" — `abandoned` only), and
203
+ * that module imports this one, so reusing it here would be a cycle.
204
+ *
205
+ * @param {object} rec
206
+ * @returns {boolean}
207
+ */
208
+ export function isBackfillStub(rec) {
209
+ if (!isNonEmptyString(rec._backfill_source)) return false;
210
+ if (rec._backfill_source.startsWith(REPAIR_PROVENANCE_PREFIX)) return false;
211
+ const hasWork = (Array.isArray(rec.waves) && rec.waves.length > 0)
212
+ || rec.total_waves > 0 || rec.total_agents > 0 || rec.total_files_changed > 0;
213
+ return !hasWork;
214
+ }
215
+
216
+ /**
217
+ * Rule (1), cross-id half (#1443) — drop every backfill stub that shares a
218
+ * non-empty `raw_session_id` with at least one non-stub. A group of stubs only
219
+ * (or of non-stubs only) is left intact: with no non-stub to prefer there is no
220
+ * evidence which record is the artefact. Mutates `byId`.
221
+ *
222
+ * LIMIT (#1457 point 9): the join assumes one raw_session_id = one physical
223
+ * session. A raw id can also span a CLOSED session and a later, separate
224
+ * abandoned tail (measured: a stub starting 4 minutes after its partner's
225
+ * `completed_at`, LeadPipeDACH `main-2026-09-03-session-11`) — that stub is
226
+ * dropped too, so the tail is invisible to every reader. No time-window check
227
+ * guards against it; adding one is a separate decision.
228
+ * @param {Map<string, object>} byId
229
+ * @returns {void}
230
+ */
231
+ function collapseStubsByRawSessionId(byId) {
232
+ const byRaw = new Map();
233
+ for (const rec of byId.values()) {
234
+ if (!isNonEmptyString(rec.raw_session_id)) continue;
235
+ const group = byRaw.get(rec.raw_session_id);
236
+ if (group) group.push(rec);
237
+ else byRaw.set(rec.raw_session_id, [rec]);
238
+ }
239
+ for (const group of byRaw.values()) {
240
+ if (group.length < 2 || group.every(isBackfillStub)) continue;
241
+ for (const rec of group) {
242
+ if (isBackfillStub(rec)) byId.delete(rec.session_id);
243
+ }
244
+ }
245
+ }
246
+
161
247
  /**
162
248
  * Rule (3) — collapse the systemic two-writer double stub. Mutates `byId` by
163
249
  * deleting the synthetic twin of each qualifying pair.
@@ -285,10 +371,13 @@ function resolveSupersedes(byId, ignored) {
285
371
  *
286
372
  * Rules, applied in this order (see the module header for the measured
287
373
  * justification of each):
288
- * 1. newest-wins per `session_id` (file order is chronological);
374
+ * 1. newest-wins per `session_id` (file order is chronological), except
375
+ * that a later backfill stub never shadows an earlier non-stub (#1443);
289
376
  * 2. two `abandoned` records with an exact, both-present
290
377
  * `started_at` + `completed_at` tuple collapse to the non-synthetic one;
291
- * 3. a surviving record's ATTESTABLE `supersedes: X` removes record `X`.
378
+ * 3. a surviving record's ATTESTABLE `supersedes: X` removes record `X`;
379
+ * 4. a backfill stub sharing a non-empty `raw_session_id` with a surviving
380
+ * non-stub under another `session_id` is dropped (#1443).
292
381
  *
293
382
  * The double-stub collapse runs BEFORE supersede removal so that a stub which
294
383
  * is itself about to be superseded still shadows its synthetic twin — with the
@@ -325,6 +414,9 @@ export function canonicalizeSessionsDetailed(records, { keepUnidentified = false
325
414
  if (keepUnidentified) unidentified.push(rec);
326
415
  continue;
327
416
  }
417
+ // #1443 — a later backfill stub never shadows an earlier non-stub.
418
+ const prev = byId.get(rec.session_id);
419
+ if (prev && isBackfillStub(rec) && !isBackfillStub(prev)) continue;
328
420
  byId.set(rec.session_id, rec);
329
421
  }
330
422
 
@@ -335,6 +427,9 @@ export function canonicalizeSessionsDetailed(records, { keepUnidentified = false
335
427
  const ignoredSupersedes = [];
336
428
  resolveSupersedes(byId, ignoredSupersedes);
337
429
 
430
+ // -- (1b) stub vs non-stub across ids, joined by raw_session_id (#1443) ----
431
+ collapseStubsByRawSessionId(byId);
432
+
338
433
  return { records: [...byId.values(), ...unidentified], ignoredSupersedes };
339
434
  }
340
435
 
@@ -399,6 +494,25 @@ export function countSessionsInJsonl(raw) {
399
494
  * is skipped rather than aborting the whole read (same posture as the readers
400
495
  * in `session-close-backfill.mjs` and `backfill-abandoned-sessions.mjs`).
401
496
  *
497
+ * NOT EVERY RAW READ OF `sessions.jsonl` IS A GAP TO CLOSE (#1221 P2). Four
498
+ * more readers bypass this module's collapse on purpose — verified via
499
+ * `rg -n "sessions.jsonl|SESSIONS"` against each file — and none of them
500
+ * should be "migrated" to `readCanonicalSessions`:
501
+ * - `scripts/lib/sessions-integrity-banner.mjs` reads every raw line
502
+ * (`readJsonlLines`) because it EXISTS to count malformed/mirror-invalid
503
+ * records; canonicalizing first would hide the corruption it reports on.
504
+ * - `scripts/lib/session-record-repair.mjs` is the repair tool itself — it
505
+ * must see every raw record, malformed and duplicate alike, to fix them;
506
+ * a canonical read would collapse or drop the very rows it needs to repair.
507
+ * - `scripts/migrate-sessions-jsonl.mjs` rewrites legacy-shaped records one
508
+ * raw line at a time; a canonicalized read could silently skip a
509
+ * superseded or duplicate legacy record before the migration ever sees it.
510
+ * - `scripts/gc-stale-worktrees.mjs` (`loadRecentSessionRefs`) scans every
511
+ * raw line's string content for worktree references within a 7-day
512
+ * window; a canonical read could drop a duplicate/superseded stub that
513
+ * still names a worktree, making a live worktree look unreferenced and
514
+ * eligible for garbage collection.
515
+ *
402
516
  * @param {object} [args]
403
517
  * @param {string} [args.repoRoot] project root; the ledger is resolved as
404
518
  * `<repoRoot>/.orchestrator/metrics/sessions.jsonl`. Defaults to
@@ -103,7 +103,13 @@ import {
103
103
  } from './vault-mirror/render-sessions.mjs';
104
104
 
105
105
  /** Repo-relative path to the session ledger (one record per closed session). */
106
- const SESSIONS_PATH = '.orchestrator/metrics/sessions.jsonl';
106
+ /**
107
+ * Ledger path, relative to the repo root. EXPORTED (#1417) so the CLI front-end
108
+ * `scripts/check-sessions-integrity.mjs` addresses the same file this banner
109
+ * judges instead of re-typing the literal — two spellings of one path is how a
110
+ * guard ends up measuring a file nobody writes.
111
+ */
112
+ export const SESSIONS_PATH = '.orchestrator/metrics/sessions.jsonl';
107
113
 
108
114
  /** Max session_ids listed inline before the message collapses to "+N more". */
109
115
  export const MAX_LISTED_IDS = 5;