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
@@ -1,5 +1,5 @@
1
1
  /**
2
- * pricing.mjs — USD price table for token-bucket cost estimation (#1244).
2
+ * pricing.mjs — USD price table for token-bucket cost estimation (#1244, #1470).
3
3
  *
4
4
  * The subagent ledger records four token buckets per stop record
5
5
  * (`token_input_uncached`, `token_cache_read`, `token_cache_creation`,
@@ -8,23 +8,39 @@
8
8
  * an order of magnitude on a cache-heavy run — which is the whole reason this
9
9
  * table exists rather than a single `$/token` constant.
10
10
  *
11
- * ## What is measured and what is derived
11
+ * ## Every rate is a documented figure
12
12
  *
13
- * `input` and `output` are the first-party Anthropic API list prices as
14
- * published in the bundled model table, sourced on the date `PRICING_TABLE_DATE`
15
- * carries (2026-09-09) — that constant is the SSOT, never a second date repeated
16
- * in this prose. `cache_read` and `cache_creation` are DERIVED from the
17
- * documented ephemeral-cache multipliers (read ≈ 0.1×, 5-minute write ≈ 1.25×
18
- * the input rate) except where a row carries a documented figure — Claude Fable
19
- * 5.1 publishes $0.25/MTok cache reads directly, which is NOT 0.1× its $10
20
- * input rate. Every row therefore declares its own `cache_source` so a reader
21
- * never has to guess which numbers were read off a price list and which were
22
- * multiplied out here.
13
+ * All four rates of every row are first-party Anthropic API list prices, read
14
+ * off the price page each row names in `source_url` on the date
15
+ * `PRICING_TABLE_DATE` carries — that constant is the SSOT, never a second date
16
+ * repeated in this prose. Nothing here is multiplied out. `cache_read` in
17
+ * particular is never derived from `input`: the ratio is not a constant (Opus
18
+ * 5.5 reads at 0.05× its input rate, Fable 5.1 at 0.025×, Fable 5 at 0.1×), so
19
+ * a multiplier would misprice exactly the cache-heavy runs this table exists
20
+ * for. Each row carries its own provenance fields, so a consumer quoting a
21
+ * dollar figure can cite where it came from without reading this file.
23
22
  *
24
- * A row with `verified: false` carries a PLACEHOLDER: its numbers are a
25
- * best-effort stand-in, never a quoted price. `costUsd()` still prices such a
26
- * row (an estimate is more useful than a null when it is labelled), but any
27
- * consumer publishing a dollar figure must surface the flag.
23
+ * `verified: false` would mark a PLACEHOLDER row — a best-effort stand-in,
24
+ * never a quoted price. No current row carries it; a consumer publishing a
25
+ * dollar figure must still surface the flag.
26
+ *
27
+ * ## Only exact ids resolve
28
+ *
29
+ * `priceFor()` accepts an exact table key or an explicit `MODEL_ALIASES` entry
30
+ * and nothing else. Until #1470 it fell back to the longest table key an id
31
+ * started with, which handed an unlisted generation the rates of its nearest
32
+ * neighbour, marked verified.
33
+ *
34
+ * ## Series break (#1470, 2026-09-29)
35
+ *
36
+ * Session reports written before this fix priced records through that
37
+ * fallback: `claude-opus-5-5` at `claude-opus-5` rates, `claude-sonnet-5-5` at
38
+ * `claude-sonnet-5` rates (identical numbers, by coincidence rather than
39
+ * evidence), and `claude-fable-5` / `claude-opus-4-8` not at all. Nothing
40
+ * rewrites that history — the metrics ledgers are append-only. Measured
41
+ * 2026-09-29: 0 of the 9 priced sessions.jsonl reports in this repo contained
42
+ * such records (the first one reached subagents.jsonl after the last priced
43
+ * report was written).
28
44
  *
29
45
  * ## Null means UNKNOWN MODEL, never zero
30
46
  *
@@ -39,68 +55,93 @@
39
55
  */
40
56
 
41
57
  /**
42
- * The date the `input`/`output` rates in PRICING_TABLE were sourced.
43
- * Any consumer quoting a dollar figure should quote this date beside it — a
44
- * price without its measurement date ages silently.
58
+ * The date the rates in PRICING_TABLE were read off the price page. Every row's
59
+ * `checked_at` equals it. Any consumer quoting a dollar figure should quote this
60
+ * date beside it — a price without its measurement date ages silently.
45
61
  * @type {string}
46
62
  */
47
- export const PRICING_TABLE_DATE = '2026-09-09';
63
+ export const PRICING_TABLE_DATE = '2026-09-29';
48
64
 
49
65
  /**
50
66
  * @typedef {Object} PricingRow
51
67
  * @property {number} input - USD per 1M uncached prompt tokens.
52
68
  * @property {number} cache_read - USD per 1M tokens served from the prompt cache.
53
- * @property {number} cache_creation - USD per 1M tokens written to the prompt cache.
69
+ * @property {number} cache_creation - USD per 1M tokens written to the prompt cache
70
+ * (the `cache_write_ttl` rate).
54
71
  * @property {number} output - USD per 1M completion tokens.
55
72
  * @property {boolean} verified - false ⇒ the row is a labelled placeholder, not a quoted price.
56
73
  * @property {'documented'|'derived-multiplier'} cache_source - provenance of the two cache rates.
74
+ * Every current row is 'documented'; 'derived-multiplier' is kept for a future row whose cache
75
+ * rates are multiplied out and must say so.
76
+ * @property {string} source_url - the page all four rates were read from.
77
+ * @property {string} checked_at - ISO date the rates were read; equals PRICING_TABLE_DATE.
78
+ * @property {'anthropic'} provider
79
+ * @property {'5m'} cache_write_ttl - which cache-write rate `cache_creation` is.
80
+ * @property {string} unit - 'USD per 1M tokens'.
81
+ * @property {string} surface - the billing surface and speed the rates apply to.
82
+ * @property {'standard'} tier - the service tier the rates apply to.
83
+ */
84
+
85
+ /** Provenance shared by every current row — all read off one page on one date. */
86
+ const PROVENANCE = Object.freeze({
87
+ verified: true,
88
+ cache_source: 'documented',
89
+ source_url: 'https://platform.claude.com/docs/en/about-claude/pricing',
90
+ checked_at: PRICING_TABLE_DATE,
91
+ provider: 'anthropic',
92
+ cache_write_ttl: '5m',
93
+ unit: 'USD per 1M tokens',
94
+ surface: 'Claude API (first-party), standard speed',
95
+ tier: 'standard',
96
+ });
97
+
98
+ /**
99
+ * One frozen documented row: four USD rates plus the shared provenance. The
100
+ * rates are spread last, so provenance can never overwrite a price.
101
+ * @param {{input: number, cache_read: number, cache_creation: number, output: number}} rates
102
+ * @returns {PricingRow}
57
103
  */
104
+ function usd(rates) {
105
+ return Object.freeze({ ...PROVENANCE, ...rates });
106
+ }
58
107
 
59
108
  /**
60
109
  * Per-model USD-per-million-token rates.
61
110
  * @type {Readonly<Record<string, PricingRow>>}
62
111
  */
63
112
  export const PRICING_TABLE = Object.freeze({
64
- // Claude Fable 5.1 — $10 / $50 list; cache reads documented at $0.25/MTok
65
- // (NOT the usual 0.1× multiplier), cache writes derived at 1.25× input.
66
- 'claude-fable-5-1': Object.freeze({
67
- input: 10.0,
68
- cache_read: 0.25,
69
- cache_creation: 12.5,
70
- output: 50.0,
71
- verified: true,
72
- cache_source: 'documented',
73
- }),
74
- 'claude-opus-5': Object.freeze({
75
- input: 5.0,
76
- cache_read: 0.5,
77
- cache_creation: 6.25,
78
- output: 25.0,
79
- verified: true,
80
- cache_source: 'derived-multiplier',
81
- }),
82
- 'claude-sonnet-5': Object.freeze({
83
- input: 2.0,
84
- cache_read: 0.2,
85
- cache_creation: 2.5,
86
- output: 10.0,
87
- verified: true,
88
- cache_source: 'derived-multiplier',
89
- }),
90
- 'claude-haiku-4-5': Object.freeze({
91
- input: 1.0,
92
- cache_read: 0.1,
93
- cache_creation: 1.25,
94
- output: 5.0,
95
- verified: true,
96
- cache_source: 'derived-multiplier',
97
- }),
113
+ // Ceiling: only the 5-minute cache-write rate is priced; a 1-hour write costs 2× input
114
+ // (Opus 5.5: $8/MTok) and would be under-priced here. Measured 2026-09-29 over 803,788
115
+ // assistant usage blocks in 10,745 subagent transcripts: `usage.cache_creation
116
+ // .ephemeral_1h_input_tokens > 0` on 0 blocks. Revisit when the first such block appears.
117
+ //
118
+ // Ceiling: list price only — fast mode, `inference_geo: "us"` (1.1×) and batch discounts are
119
+ // not modelled. Measured 2026-09-29: `usage.speed` = "fast" on 0 of the 374,331 blocks that
120
+ // carry the field; `inference_geo` = "not_available" on 803,479 and no other value seen.
121
+ // Revisit when either field shows another value.
122
+ //
123
+ // Ceiling: a model absent from this table prices as null, never at a neighbour's rate, so the
124
+ // rollup reports `total_cost_usd: null` and `cost_records_priced < cost_records_total` — the
125
+ // intended way missing evidence shows up. That ratio also drops below 1 for records no row can
126
+ // fix: a token-less record (transcript over the hook's size cap, without usage turns, or
127
+ // unreadable) and a record with a token-bearing turn that names no model (#1475). Revisit when
128
+ // an unpriced record names a model id absent from this table: add that model as its own
129
+ // evidenced row, never as a family prefix or an alias to another generation.
130
+ 'claude-fable-5-1': usd({ input: 10.0, cache_read: 0.25, cache_creation: 12.5, output: 50.0 }),
131
+ 'claude-fable-5': usd({ input: 10.0, cache_read: 1.0, cache_creation: 12.5, output: 50.0 }),
132
+ 'claude-opus-5-5': usd({ input: 4.0, cache_read: 0.2, cache_creation: 5.0, output: 20.0 }),
133
+ 'claude-opus-5': usd({ input: 5.0, cache_read: 0.5, cache_creation: 6.25, output: 25.0 }),
134
+ 'claude-opus-4-8': usd({ input: 5.0, cache_read: 0.5, cache_creation: 6.25, output: 25.0 }),
135
+ 'claude-sonnet-5-5': usd({ input: 2.0, cache_read: 0.2, cache_creation: 2.5, output: 10.0 }),
136
+ 'claude-sonnet-5': usd({ input: 2.0, cache_read: 0.2, cache_creation: 2.5, output: 10.0 }),
137
+ 'claude-haiku-4-5': usd({ input: 1.0, cache_read: 0.1, cache_creation: 1.25, output: 5.0 }),
98
138
  });
99
139
 
100
140
  /**
101
141
  * Explicit aliases for model ids that appear in transcripts but are not the
102
- * canonical table key — dated snapshots and the `[1m]` context-window suffix
103
- * the harness stamps on long-context sessions.
142
+ * canonical table key — a dated snapshot of the SAME model and the `[1m]`
143
+ * context-window suffix the harness stamps on long-context sessions. An alias
144
+ * never points at another generation.
104
145
  * @type {Readonly<Record<string, string>>}
105
146
  */
106
147
  export const MODEL_ALIASES = Object.freeze({
@@ -113,10 +154,19 @@ export const MODEL_ALIASES = Object.freeze({
113
154
  /**
114
155
  * Look up the rate row for a model id.
115
156
  *
116
- * Resolution order: exact key → explicit alias → longest matching table key
117
- * that the id starts with (so `claude-opus-5-20260401` and `claude-opus-5[1m]`
118
- * both resolve to `claude-opus-5`). Longest-prefix wins so a future
119
- * `claude-opus-5-1` row is never shadowed by `claude-opus-5`.
157
+ * Resolution: exact `PRICING_TABLE` key → explicit `MODEL_ALIASES` entry → `null`,
158
+ * after trimming surrounding whitespace.
159
+ * Nothing else resolves — no prefix, no pattern, no family fallback — so
160
+ * `claude-opus-5-20260401`, `claude-opus-5-99` and `claude-haiku-4-5-20251002` are
161
+ * all UNKNOWN even though each starts with a table key.
162
+ *
163
+ * Why no prefix: from the 4.6 generation on, Anthropic ships dateless pinned
164
+ * model ids (https://platform.claude.com/docs/en/about-claude/models/model-ids-and-versions,
165
+ * retrieved 2026-09-29). The ids that extend a current table key are therefore
166
+ * other generations, not snapshots of it — `claude-opus-5-5` extends
167
+ * `claude-opus-5` — and a prefix match priced them at another generation's
168
+ * rates, marked verified (#1470). A future generation gets its own evidenced
169
+ * row, never a family prefix.
120
170
  *
121
171
  * @param {string|null|undefined} modelId
122
172
  * @returns {PricingRow|null} the row, or null when the model is UNKNOWN
@@ -129,14 +179,10 @@ export function priceFor(modelId) {
129
179
 
130
180
  if (Object.hasOwn(PRICING_TABLE, id)) return PRICING_TABLE[id];
131
181
 
132
- const aliased = MODEL_ALIASES[id];
133
- if (aliased && Object.hasOwn(PRICING_TABLE, aliased)) return PRICING_TABLE[aliased];
134
-
135
- let best = null;
136
- for (const key of Object.keys(PRICING_TABLE)) {
137
- if (id.startsWith(key) && (best === null || key.length > best.length)) best = key;
182
+ if (Object.hasOwn(MODEL_ALIASES, id) && Object.hasOwn(PRICING_TABLE, MODEL_ALIASES[id])) {
183
+ return PRICING_TABLE[MODEL_ALIASES[id]];
138
184
  }
139
- return best === null ? null : PRICING_TABLE[best];
185
+ return null;
140
186
  }
141
187
 
142
188
  /**
@@ -161,9 +207,10 @@ function num(n) {
161
207
  /**
162
208
  * Estimate the USD cost of one record's four token buckets.
163
209
  *
164
- * Each bucket is multiplied by ITS OWN rate — a cache read is ~10× cheaper
165
- * than an uncached prompt token and a cache write ~1.25× more expensive, so a
166
- * blended rate is not an approximation of this, it is a different number.
210
+ * Each bucket is multiplied by ITS OWN rate — depending on the row, a cache
211
+ * read costs a tenth to a fortieth of an uncached prompt token and a 5-minute
212
+ * cache write 1.25× as much, so a blended rate is not an approximation of
213
+ * this, it is a different number.
167
214
  *
168
215
  * @param {object} opts
169
216
  * @param {string|null|undefined} opts.model - model id from the transcript
@@ -93,6 +93,7 @@ export const USAGE_PING_OPTIONAL_FIELDS = Object.freeze([
93
93
  'fleet_self_declared',
94
94
  'session_record',
95
95
  'session_profile',
96
+ 'ledger_complete',
96
97
  ]);
97
98
 
98
99
  /**
@@ -117,6 +118,29 @@ const USAGE_PING_PROJECTED_FIELDS = Object.freeze([...USAGE_PING_FIELDS, ...USAG
117
118
  */
118
119
  export const SESSION_RECORD_SOURCES = Object.freeze(['ledger', 'derived', 'absent']);
119
120
 
121
+ /*
122
+ * `ledger_complete` — a SECOND, ORTHOGONAL axis beside `session_record`
123
+ * (GitLab #1416), deliberately NOT a fourth provenance token.
124
+ *
125
+ * `deriveSessionFromEvents` (telemetry/sync.mjs) returns a three-state verdict on
126
+ * the events ledger it read FROM: `true` (every rotation archive present),
127
+ * `false` (at least one measured gap — a deleted/missing archive), and `null`
128
+ * (no verdict was produced at all: the read threw, or there was no source). That
129
+ * verdict decided nothing on the wire, so a derived record reconstructed from a
130
+ * holey ledger was indistinguishable from one reconstructed from a whole one.
131
+ *
132
+ * Why a boolean and not a `derived-partial` token: `SESSION_RECORD_SOURCES` is a
133
+ * frozen enum mirrored in prose and in the server's `raw_json` consumers, and
134
+ * completeness is a property of the SOURCE READ, not of WHICH source was used —
135
+ * widening the provenance enum would make the two facts inseparable and force
136
+ * every reader of `session_record === 'derived'` to learn a second spelling.
137
+ *
138
+ * THE THIRD STATE IS OMISSION: only a real boolean is written. `null` (nothing
139
+ * measured) omits the key, exactly like `session_profile` — "we did not look" is
140
+ * never sent as a measured `false`. On the `ledger` branch the field is not set
141
+ * at ALL: that path never reads the events ledger, so it has no verdict to give.
142
+ */
143
+
120
144
  /** Exact duration-bucket tokens (ASCII, stable wire values). */
121
145
  export const DURATION_BUCKETS = Object.freeze(['<15m', '15-60m', '1-3h', '>3h']);
122
146
 
@@ -541,6 +565,7 @@ export function buildUsagePing({
541
565
  consentState,
542
566
  sessionRecordSource,
543
567
  sessionProfile,
568
+ ledgerComplete,
544
569
  } = {}) {
545
570
  const session = isPlainObject(sessionRecord) ? sessionRecord : {};
546
571
  const invocations = Array.isArray(skillInvocations) ? skillInvocations : [];
@@ -637,6 +662,11 @@ export function buildUsagePing({
637
662
  session_record: SESSION_RECORD_SOURCES.includes(sessionRecordSource)
638
663
  ? sessionRecordSource
639
664
  : 'absent',
665
+ // ONLY a real boolean travels (#1416). `null`/`undefined` — the read never
666
+ // happened, or this path never read the events ledger at all — OMITS the
667
+ // key, the same absent-is-not-empty contract `session_profile` uses above.
668
+ // Writing `false` there would turn "we did not look" into a measured gap.
669
+ ...(typeof ledgerComplete === 'boolean' ? { ledger_complete: ledgerComplete } : {}),
640
670
  skills: filterRosterNames(skillNames, rosterSkills),
641
671
  commands: filterRosterNames(commandNames, rosterCommands),
642
672
  };
@@ -41,6 +41,7 @@ import { ensureAnonId } from './anon-id.mjs';
41
41
  import { peekAll, enqueue, clear, queueStats } from './queue.mjs';
42
42
  import { loadOwnerConfig } from '../owner-yaml.mjs';
43
43
  import { readJsonlFile } from '../io.mjs';
44
+ import { readEventsWithRotations } from '../events.mjs';
44
45
  import { readCanonicalSessions } from '../sessions-canonical.mjs';
45
46
  import { resolvePrivateConfigDir } from '../config/private-config-dir.mjs';
46
47
  import { readSessionProfile } from '../state-md.mjs';
@@ -338,12 +339,54 @@ export function readSessionProfileForMetricsDir(metricsDir) {
338
339
  * Never throws.
339
340
  *
340
341
  * @param {string} metricsDir
341
- * @returns {{ session: object, source: 'derived'|'absent' }}
342
+ * @returns {{ session: object, source: 'derived'|'absent',
343
+ * ledger_complete: boolean|null, ledger_gaps: object[] }}
344
+ * `ledger_complete` is `null` — never `false` — when the read produced no
345
+ * verdict at all: an undetermined ledger is not a proven gap. TWO paths reach
346
+ * that third state (#1423): this function throwing, and the reader finding NO
347
+ * source (no active file, no archive, no legacy ring) — in the latter case
348
+ * `events: []` is the absence of a ledger, not a measured quiet window.
349
+ * `ledger_complete === false` additionally writes ONE stderr line naming the
350
+ * gap kinds, because a verdict nothing surfaces is a verdict nobody acts on.
342
351
  */
343
352
  export function deriveSessionFromEvents(metricsDir) {
344
353
  try {
345
- const events = readJsonlFile(path.join(metricsDir, 'events.jsonl'), { skipInvalid: true });
346
- if (!Array.isArray(events) || events.length === 0) return { session: {}, source: 'absent' };
354
+ // #1407: read ACROSS rotation boundaries. This reconstruction is the ONLY
355
+ // source of session facts whenever `sessions.jsonl` has no usable record,
356
+ // and its sibling window is `DAILY_FLUSH_MS` (24 h) — so a rotation between
357
+ // a session's start and its flush silently truncated it.
358
+ //
359
+ // CEILING (BV-004): the reader loads the active file AND every archive
360
+ // fully into memory — up to ~60 MB transient at the default
361
+ // `max-size-mb: 10` / `max-backups: 5`. Measured 2026-09-20 A/B in ONE
362
+ // process on exactly that worst case (60 MB / 6 sources / 293,994 records,
363
+ // host loadavg 11.2): median 25.5 ms → 215.8 ms, i.e. +190 ms, which is
364
+ // 6.4 % of this path's `POST_TIMEOUT_MS` (3000). Acceptable for a
365
+ // once-per-flush cold read. Revisit if `max-size-mb` is raised past ~100,
366
+ // if `max-backups` grows, or if the flush budget drops below ~1 s.
367
+ const ledger = readEventsWithRotations(undefined, {
368
+ filePath: path.join(metricsDir, 'events.jsonl'),
369
+ });
370
+ const events = ledger.events;
371
+ // A missing archive is a GAP, never an empty window (#1407 AC-3). Carried
372
+ // on EVERY return below, including the empty ones — that is precisely where
373
+ // a deleted archive and a quiet period are otherwise indistinguishable.
374
+ const ledgerVerdict = { ledger_complete: ledger.complete, ledger_gaps: ledger.gaps };
375
+ // Visibility (#1423): the field had ZERO production consumers — a measured
376
+ // gap reached nobody. ONE stderr line, at the single point the verdict is
377
+ // formed, so every return below is covered by construction. Diagnostics on
378
+ // stderr, never stdout (cli-design.md); no new event, because the gap is a
379
+ // property of the LEDGER and an event about it would land in the same file.
380
+ if (ledger.complete === false) {
381
+ const kinds = [...new Set((ledger.gaps ?? []).map((g) => g?.kind ?? 'unknown'))].sort();
382
+ process.stderr.write(
383
+ `⚠ telemetry: events ledger INCOMPLETE — ${ledger.gaps.length} gap(s) ` +
384
+ `[${kinds.join(', ')}]; the session facts below are reconstructed from a PARTIAL read\n`,
385
+ );
386
+ }
387
+ if (!Array.isArray(events) || events.length === 0) {
388
+ return { session: {}, source: 'absent', ...ledgerVerdict };
389
+ }
347
390
 
348
391
  let startedAt = null;
349
392
  let sessionType = null;
@@ -412,7 +455,9 @@ export function deriveSessionFromEvents(metricsDir) {
412
455
  profile = undatedShapeProfile;
413
456
  }
414
457
 
415
- if (startedAt === null && sessionType === null && profile === null) return { session: {}, source: 'absent' };
458
+ if (startedAt === null && sessionType === null && profile === null) {
459
+ return { session: {}, source: 'absent', ...ledgerVerdict };
460
+ }
416
461
 
417
462
  const session = {};
418
463
  if (sessionType !== null) session.session_type = sessionType;
@@ -422,9 +467,11 @@ export function deriveSessionFromEvents(metricsDir) {
422
467
  // omit-never-fabricate contract session-close-backfill.mjs uses (#914 R1).
423
468
  if (lastTs !== null && startedAt !== null && lastTs >= startedAt) session.completed_at = lastTs;
424
469
 
425
- return { session, source: 'derived' };
470
+ return { session, source: 'derived', ...ledgerVerdict };
426
471
  } catch {
427
- return { session: {}, source: 'absent' };
472
+ // No verdict was ever produced — `null`, not `false`: a measurement that
473
+ // did not happen is not a measured zero.
474
+ return { session: {}, source: 'absent', ledger_complete: null, ledger_gaps: [] };
428
475
  }
429
476
  }
430
477
 
@@ -483,6 +530,12 @@ export function buildBatch({
483
530
  let sessionForPing;
484
531
  /** @type {'ledger'|'derived'|'absent'} */
485
532
  let sessionRecordSource = 'ledger';
533
+ // #1416: the events-ledger completeness verdict, carried to the ping as its
534
+ // own optional boolean. Stays `null` on the LEDGER branch by construction —
535
+ // that branch never reads events.jsonl, so it has nothing to report, and
536
+ // `buildUsagePing` omits the key for anything that is not a real boolean.
537
+ /** @type {boolean|null} */
538
+ let ledgerComplete = null;
486
539
  if (sessionRecord && typeof sessionRecord.started_at === 'string' && !Number.isNaN(Date.parse(sessionRecord.started_at))) {
487
540
  const startMs = Date.parse(sessionRecord.started_at);
488
541
  windowInvocations = invocations.filter((rec) => {
@@ -500,6 +553,7 @@ export function buildBatch({
500
553
  const derived = deriveSessionFromEvents(dir);
501
554
  sessionForPing = derived.session;
502
555
  sessionRecordSource = derived.source;
556
+ ledgerComplete = derived.ledger_complete;
503
557
 
504
558
  const derivedStartMs = Date.parse(sessionForPing.started_at);
505
559
  const cutoff = (Number.isNaN(Date.parse(nowIso)) ? Date.now() : Date.parse(nowIso)) - DAILY_FLUSH_MS;
@@ -522,6 +576,7 @@ export function buildBatch({
522
576
  consentState,
523
577
  sessionRecordSource,
524
578
  sessionProfile: readSessionProfileForMetricsDir(dir),
579
+ ledgerComplete,
525
580
  });
526
581
 
527
582
  const target = statePath || TELEMETRY_JSON_PATH;
@@ -31,9 +31,11 @@
31
31
  * @module scripts/lib/telemetry-flush-health-banner
32
32
  */
33
33
 
34
- import { existsSync, openSync, readSync, fstatSync, closeSync } from 'node:fs';
34
+ import { existsSync } from 'node:fs';
35
35
  import path from 'node:path';
36
36
 
37
+ import { readTailWindow } from './tail-window.mjs';
38
+
37
39
  /**
38
40
  * Bytes of `events.jsonl` read from the END of the file.
39
41
  *
@@ -82,31 +84,11 @@ function sanitizeReason(reason) {
82
84
  * @returns {{text: string} | {missing: true} | {error: string}}
83
85
  */
84
86
  function readTail(file) {
85
- let fd;
86
87
  try {
87
- fd = openSync(file, 'r');
88
- const { size } = fstatSync(fd);
89
- const length = Math.min(size, TAIL_BYTES);
90
- const start = size - length;
91
- const buf = Buffer.allocUnsafe(length);
92
- let read = 0;
93
- while (read < length) {
94
- const n = readSync(fd, buf, read, length - read, start + read);
95
- if (n <= 0) break;
96
- read += n;
97
- }
98
- return { text: buf.subarray(0, read).toString('utf8') };
88
+ return { text: readTailWindow(file, TAIL_BYTES).text };
99
89
  } catch (err) {
100
90
  if (err?.code === 'ENOENT') return { missing: true };
101
91
  return { error: typeof err?.code === 'string' ? err.code : 'EUNKNOWN' };
102
- } finally {
103
- if (fd !== undefined) {
104
- try {
105
- closeSync(fd);
106
- } catch {
107
- /* best effort */
108
- }
109
- }
110
92
  }
111
93
  }
112
94
 
@@ -22,10 +22,17 @@
22
22
  * triageDecision(finding, candidates)
23
23
  * → {action: 'create'|'update'|'ignore', target?, reason: string, confidence: number}
24
24
  *
25
+ * buildIssueBody(finding, fp) → string
26
+ *
27
+ * neutralizeFingerprintSentinel(text) → string
28
+ * fingerprintSentinelLine(fp) → string
29
+ * The one shared sentinel writer + neutraliser, also used by
30
+ * ux-grill/reconcile.mjs (#1339 P6).
31
+ *
25
32
  * ReconcileError
26
33
  *
27
34
  * Security:
28
- * - #388 (SEC-IR-MED-1): sentinel injection hardening — sanitizeRecommendation()
35
+ * - #388 (SEC-IR-MED-1): sentinel injection hardening — neutralizeFingerprintSentinel()
29
36
  * strips **Fingerprint:** literals from free-text fields before they are
30
37
  * embedded in the body (before the authoritative sentinel line is appended).
31
38
  * - #389 (SEC-IR-LOW-1): maxBuffer set to 4 MB on every execFile call;
@@ -126,39 +133,64 @@ function validateFinding(finding) {
126
133
  // ---------------------------------------------------------------------------
127
134
 
128
135
  /**
129
- * Sanitize free-text recommendation fields before embedding in the issue body.
130
- * Replaces any literal `**Fingerprint:**` with `__Fingerprint__` to prevent
131
- * a crafted recommendation from spoofing the authoritative fingerprint sentinel
132
- * line that is appended by buildIssueBody().
136
+ * Neutralise every `**Fingerprint:**` literal in untrusted text by rewriting it
137
+ * to `__Fingerprint__`. The ONE copy of this neutralisation: this module's
138
+ * {@link buildIssueBody} and `ux-grill/reconcile.mjs` both route free text
139
+ * through it (#1339 P6), so a hardening added here reaches both producers.
133
140
  *
134
- * Context: glab parses issue bodies as Markdown. If `recommendation` contained
141
+ * Context: glab parses issue bodies as Markdown. If a free-text field contained
135
142
  * `**Fingerprint:** <attacker-hash>`, a grep for the fingerprint sentinel in
136
143
  * listExistingFindings() would match the spoofed value instead of the real one,
137
144
  * bypassing dedup. Replacing the literal prevents the attack without altering
138
- * the semantic meaning of the recommendation text.
145
+ * the semantic meaning of the text.
146
+ *
147
+ * @param {unknown} text - coerced with `String(text ?? '')`
148
+ * @returns {string}
149
+ */
150
+ export function neutralizeFingerprintSentinel(text) {
151
+ // gi flag: case-insensitive match so **fingerprint:** and **FINGERPRINT:** variants
152
+ // are also neutralized. Sanitizer is intentionally broader than the case-sensitive
153
+ // extractor regex — this is the correct asymmetry (#388 SEC-IR-MED-1).
154
+ return String(text ?? '').replace(/\*\*Fingerprint:\*\*/gi, '__Fingerprint__');
155
+ }
156
+
157
+ /**
158
+ * The authoritative dedup sentinel line, in the exact shape
159
+ * {@link extractFingerprintFromBody} reads. The caller passes an
160
+ * already-trusted value; this function does not sanitise it.
161
+ *
162
+ * @param {string} fp - 16-char hex fingerprint
163
+ * @returns {string}
164
+ */
165
+ export function fingerprintSentinelLine(fp) {
166
+ return `**Fingerprint:** \`${fp}\``;
167
+ }
168
+
169
+ /**
170
+ * Sanitize free-text recommendation fields before embedding in the issue body
171
+ * (#388 SEC-IR-MED-1) — {@link neutralizeFingerprintSentinel} for a non-empty
172
+ * string, anything else returned unchanged (a `null` description is dropped by
173
+ * buildIssueBody's line filter rather than rendered as an empty line).
139
174
  *
140
175
  * @param {string|undefined|null} text
141
176
  * @returns {string|undefined|null}
142
177
  */
143
178
  function sanitizeRecommendation(text) {
144
179
  if (!text || typeof text !== 'string') return text;
145
- // gi flag: case-insensitive match so **fingerprint:** and **FINGERPRINT:** variants
146
- // are also neutralized. Sanitizer is intentionally broader than the case-sensitive
147
- // extractor regex — this is the correct asymmetry (#388 SEC-IR-MED-1).
148
- return text.replace(/\*\*Fingerprint:\*\*/gi, '__Fingerprint__');
180
+ return neutralizeFingerprintSentinel(text);
149
181
  }
150
182
 
151
183
  /**
152
184
  * Render untrusted text as the content of ONE inline Markdown code span:
153
- * sentinel literals neutralised (sanitizeRecommendation), line breaks and NUL
154
- * folded to a space, backticks turned into `'` — so a page-controlled locator
155
- * can neither close its code span nor start a new body line.
185
+ * sentinel literals neutralised, line breaks and NUL folded to a space,
186
+ * backticks turned into `'` — so a page-controlled locator can neither close
187
+ * its code span nor start a new body line.
156
188
  *
157
189
  * @param {unknown} text
158
190
  * @returns {string}
159
191
  */
160
192
  function codeSpanContent(text) {
161
- return sanitizeRecommendation(String(text ?? ''))
193
+ return neutralizeFingerprintSentinel(text)
162
194
  .replace(/[\r\n\0]+/g, ' ')
163
195
  .replace(/`/g, "'");
164
196
  }
@@ -218,7 +250,7 @@ export function buildIssueBody(finding, fp) {
218
250
  '',
219
251
  safeRecommendation ? `**Recommendation:** ${safeRecommendation}` : null,
220
252
  '',
221
- `**Fingerprint:** \`${fp}\``,
253
+ fingerprintSentinelLine(fp),
222
254
  `**Severity:** ${finding.severity}`,
223
255
  `**Check:** ${finding.checkId}`,
224
256
  `**Locator:** \`${codeSpanContent(finding.locator)}\``,