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
@@ -531,15 +531,23 @@ function synthesizeRecord({ recordId, synthetic, gathered, nowMs, status = 'aban
531
531
  const BACKFILL_STUB_STATUSES = new Set(['abandoned']);
532
532
 
533
533
  /**
534
- * True when a sessions.jsonl record is a backfilled STUB — reconstructed
535
- * provenance (`_backfill_source`) AND a stub status. Both are required: a
536
- * hand-written `abandoned` record with no backfill provenance is somebody's
537
- * deliberate statement and is never superseded on our own initiative.
534
+ * True when a sessions.jsonl record is a backfilled stub this module may
535
+ * SUPERSEDE — reconstructed provenance (`_backfill_source`) AND a stub status.
536
+ * Both are required: a hand-written `abandoned` record with no backfill
537
+ * provenance is somebody's deliberate statement and is never superseded on our
538
+ * own initiative.
539
+ *
540
+ * Named for the decision it makes, not for the record class, because
541
+ * `sessions-staleness-banner.mjs` asks a DIFFERENT question about the same
542
+ * records — `isNonAnchorStub()` there accepts EITHER marker, since a record
543
+ * only needs to be doubtful to be disqualified as a time anchor, where this
544
+ * one must be proven reconstructed before it may be overwritten. Both are
545
+ * module-local on purpose; they must not be shared.
538
546
  *
539
547
  * @param {unknown} record
540
548
  * @returns {boolean}
541
549
  */
542
- function isBackfillStub(record) {
550
+ function isSupersedableStub(record) {
543
551
  if (!record || typeof record !== 'object' || Array.isArray(record)) return false;
544
552
  if (typeof record._backfill_source !== 'string') return false;
545
553
  return BACKFILL_STUB_STATUSES.has(record.status);
@@ -636,7 +644,7 @@ function classifyExisting(readFileSync, sessionsPath, { recordId, sessionId, sem
636
644
  const sessionRecords = readJsonlSafe(readFileSync, sessionsPath);
637
645
  const newest = findRecordedSession(sessionRecords, { sessionId, semanticSessionId, startedAt });
638
646
  if (!newest) return { kind: 'absent' };
639
- if (isBackfillStub(newest)) {
647
+ if (isSupersedableStub(newest)) {
640
648
  return { kind: 'stub', matchedId: newest.session_id, stubId: newest.session_id };
641
649
  }
642
650
  return { kind: 'canonical', matchedId: newest.session_id };
@@ -1034,6 +1042,9 @@ export async function backfillAbandonedSession({
1034
1042
  * { action: 'skipped-not-completed', status } — STATE.md status isn't 'completed'
1035
1043
  * { action: 'skipped-no-session-id' } — completed but no `session:` field
1036
1044
  * { action: 'skipped-already-recorded', sessionId } — sessions.jsonl already has it
1045
+ * { action: 'skipped-key-occupied', sessionId } — identity absent, but a canonical
1046
+ * non-stub record of a DIFFERENT session
1047
+ * already holds this key (#1388 P8)
1037
1048
  * { action: 'skipped-marker-exists', sessionId } — lost the TOCTOU claim
1038
1049
  * { action: 'error', error, sessionId? } — any failure, swallowed
1039
1050
  *
@@ -1118,6 +1129,22 @@ export async function backfillCompletedFromStateMd({
1118
1129
  if (existing.kind === 'canonical') {
1119
1130
  return { action: 'skipped-already-recorded', sessionId: existing.matchedId };
1120
1131
  }
1132
+ // Key-occupancy guard (#1380 P6) — the counterpart of the abandoned
1133
+ // path's occupied-key check. Identity can say `absent` (label drift
1134
+ // veto, conflicting native join) while a canonical, authoritative record
1135
+ // already holds `recordId`; canonical readers collapse by session_id
1136
+ // newest-wins, so appending here would displace that record with this
1137
+ // zero-counter one. `recordId` is already the native id whenever STATE.md
1138
+ // carries one, so there is no alternate key to fall back to: skip.
1139
+ if (existing.kind === 'absent'
1140
+ && canonicalizeSessions(readJsonlSafe(readFileSync, sessionsPath))
1141
+ .some((record) => record?.session_id === recordId && !isSupersedableStub(record))) {
1142
+ // Distinct action (#1388 P8): `skipped-already-recorded` above means
1143
+ // "this very session is already on file"; here identity said ABSENT and
1144
+ // a DIFFERENT session's record occupies the key. Sharing one string made
1145
+ // the two causes indistinguishable in the backfill_completed telemetry.
1146
+ return { action: 'skipped-key-occupied', sessionId: recordId };
1147
+ }
1121
1148
  const supersedes = existing.kind === 'stub' ? existing.stubId : null;
1122
1149
  // Preserve a matched stub's key for its append-only replacement. Without
1123
1150
  // event timestamps or a raw join on a legacy stub, a new UUID key would
@@ -39,10 +39,11 @@
39
39
  * - Production code is silent: no console.log, no console.warn.
40
40
  */
41
41
 
42
- import { open, readFile } from 'node:fs/promises';
42
+ import { readFile } from 'node:fs/promises';
43
43
  import path from 'node:path';
44
44
 
45
45
  import { readCanonicalSessions } from './sessions-canonical.mjs';
46
+ import { readTailWindow } from './tail-window.mjs';
46
47
  import { withStateMdLock } from './session-lock.mjs';
47
48
  import { resolveStateMdPath } from './state-md/frontmatter-mutators.mjs';
48
49
  import { parseStateMd } from './state-md/yaml-parser.mjs';
@@ -208,31 +209,19 @@ const EVENTS_MAX_AGE_MS = 2 * 24 * 60 * 60 * 1000;
208
209
  async function readSessionIdsFromEvents(repoRoot) {
209
210
  const filePath = path.join(repoRoot, '.orchestrator', 'metrics', 'events.jsonl');
210
211
 
211
- let text = '';
212
- // Assigned in the try below; every path that reaches its read site has
213
- // passed through that assignment (the catch returns early).
214
- let windowIsPartial;
215
- let handle = null;
212
+ let window;
216
213
  try {
217
- handle = await open(filePath, 'r');
218
- const { size } = await handle.stat();
219
- const start = size > EVENTS_TAIL_BYTES ? size - EVENTS_TAIL_BYTES : 0;
220
- windowIsPartial = start > 0;
221
- const length = size - start;
222
- if (length > 0) {
223
- const buf = Buffer.alloc(length);
224
- const { bytesRead } = await handle.read(buf, 0, length, start);
225
- text = buf.subarray(0, bytesRead).toString('utf8');
226
- }
214
+ // Sync read inside an async function: 1 MiB ceiling, one call per n-claim —
215
+ // the shared primitive (tail-window.mjs) owns the byte window, this reader
216
+ // keeps its own fail-open mapping.
217
+ window = readTailWindow(filePath, EVENTS_TAIL_BYTES);
227
218
  } catch {
228
219
  return [];
229
- } finally {
230
- if (handle !== null) await handle.close().catch(() => {});
231
220
  }
232
221
 
233
- const lines = text.split(/\r?\n/);
222
+ const lines = window.text.split(/\r?\n/);
234
223
  // The window started mid-file, so line 0 may be a truncated record.
235
- if (windowIsPartial) lines.shift();
224
+ if (window.cut) lines.shift();
236
225
 
237
226
  const cutoff = Date.now() - EVENTS_MAX_AGE_MS;
238
227
  const ids = [];
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Resolve the leading /session mode independently of the user's task context.
3
+ * This is a text boundary, not a shell/option parser: quotes and command syntax
4
+ * in the context remain data. `unknown` is a ledger value, never a live mode.
5
+ *
6
+ * @param {string} [argumentsText] Complete text following /session.
7
+ * @returns {{sessionType: string, context: string, profile?: string, invalidMode?: string}}
8
+ */
9
+ export function resolveSessionInvocation(argumentsText = '') {
10
+ if (typeof argumentsText !== 'string') {
11
+ throw new TypeError('session invocation arguments must be a string');
12
+ }
13
+ const [_, mode, context = ''] = argumentsText.trimStart().match(/^(\S+)(?:\s+([\s\S]*))?$/u) ?? [];
14
+ if (mode === undefined) return { sessionType: 'deep', context: '' };
15
+ if (mode === 'ultradeep') return { sessionType: 'deep', profile: 'ultradeep', context };
16
+ if (['housekeeping', 'feature', 'deep'].includes(mode)) {
17
+ return { sessionType: mode, context };
18
+ }
19
+ return { sessionType: 'deep', context, invalidMode: mode };
20
+ }
@@ -36,11 +36,17 @@
36
36
  * no key-order churn.
37
37
  *
38
38
  * ── PROVENANCE ───────────────────────────────────────────────────────────────
39
- * Every repaired record carries `_backfill_source` + `_backfill_incomplete_fields`
40
- * (both already schema-accepted by `_validateOptionalFields`, so no schema bump
41
- * is needed). The incomplete-fields list is the EXACT set of fields this record
42
- * had defaulted, which is what lets a downstream consumer tell a measured zero
43
- * apart from a repaired-to-zero.
39
+ * Every repaired record carries `_repair_source` (`REPAIR_SOURCE`) +
40
+ * `_backfill_incomplete_fields`; `_backfill_source` is set to `REPAIR_SOURCE`
41
+ * only when the record had no origin of its own. An existing origin
42
+ * (`state-md-completed`, `events-jsonl`, `abandoned`) is KEPT, because
43
+ * `sessions-canonical.mjs`'s stub guard reads it: overwriting it promoted a
44
+ * repaired zero-work stub to a real record that shadowed the real one (#1443
45
+ * F2). `_backfill_*` is schema-accepted by `_validateOptionalFields`, and
46
+ * `validateSession` ignores undeclared keys (as it does the `_<field>_raw`
47
+ * sidecars), so no schema bump is needed. The incomplete-fields list is the
48
+ * EXACT set of fields this record had defaulted, which is what lets a
49
+ * downstream consumer tell a measured zero apart from a repaired-to-zero.
44
50
  *
45
51
  * It also never DISCARDS what it replaces: any field whose present value is
46
52
  * overwritten by a default is first copied to a `_<field>_raw` sidecar
@@ -70,7 +76,10 @@ import { checkSessionsIntegrity as defaultCheckIntegrity } from './sessions-inte
70
76
  // Constants
71
77
  // ---------------------------------------------------------------------------
72
78
 
73
- /** Value written to `_backfill_source` on every record this module repairs. */
79
+ /**
80
+ * Value written to `_repair_source` on every record this module repairs, and to
81
+ * `_backfill_source` only on a record that had no origin there (#1443 F2).
82
+ */
74
83
  export const REPAIR_SOURCE = 'repair-invalid-sessions/1004';
75
84
 
76
85
  /** Ledger path (relative to repoRoot) that `checkSessionsIntegrity` inspects. */
@@ -98,6 +107,28 @@ const INCOMPLETE_FIELD_ORDER = Object.freeze([
98
107
  'total_agents',
99
108
  'total_files_changed',
100
109
  ]);
110
+ // `timestamp_precision` (#1442) deliberately adds NOTHING to the list above: a
111
+ // truncated timestamp is still a measurement (exact to the millisecond), not a
112
+ // default. Listing it would make consumers discard a real value —
113
+ // `sessions-staleness-banner.mjs` returns null for any timestamp named in
114
+ // `_backfill_incomplete_fields`, `session-close-backfill.mjs` keys on
115
+ // `includes('started_at')`. The sub-millisecond original survives in the
116
+ // `_<field>_raw` sidecar instead.
117
+
118
+ /**
119
+ * Timestamp fields `validateSession` holds to `ISO_8601_UTC_MS_RE`
120
+ * (`session-schema/validator.mjs`, not exported): exactly three fractional
121
+ * digits or none.
122
+ */
123
+ const MS_PRECISION_FIELDS = Object.freeze(['started_at', 'completed_at', 'lease_acquired_at']);
124
+
125
+ /**
126
+ * ISO-8601 UTC with MORE than three fractional digits (`.994600Z`) — the one
127
+ * rejected shape that has an unambiguous canonical form. Group 1 is the value
128
+ * cut at the millisecond. Offsets, `.3Z` and non-ISO strings do not match and
129
+ * stay for the validator to reject: there is no defensible default for them.
130
+ */
131
+ const ISO_UTC_SUB_MS_RE = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3})\d+Z$/;
101
132
 
102
133
  // ---------------------------------------------------------------------------
103
134
  // Helpers
@@ -112,6 +143,20 @@ function isCount(v) {
112
143
  return typeof v === 'number' && Number.isFinite(v) && v >= 0;
113
144
  }
114
145
 
146
+ /**
147
+ * Cut a sub-millisecond ISO-8601 UTC timestamp to milliseconds. TRUNCATES,
148
+ * never rounds: `.9999Z` must not roll into the next second, and truncation is
149
+ * monotone, so `completed_at >= started_at` survives on every pair it held on.
150
+ *
151
+ * @param {unknown} value
152
+ * @returns {string|null} the truncated value, or null when there is nothing to cut
153
+ */
154
+ function truncateToMs(value) {
155
+ if (typeof value !== 'string') return null;
156
+ const m = ISO_UTC_SUB_MS_RE.exec(value);
157
+ return m ? `${m[1]}Z` : null;
158
+ }
159
+
115
160
  function orderIncompleteFields(fields) {
116
161
  const rank = (f) => {
117
162
  const i = INCOMPLETE_FIELD_ORDER.indexOf(f);
@@ -321,6 +366,19 @@ export function repairRecord(record) {
321
366
  incomplete.add('total_files_changed');
322
367
  }
323
368
 
369
+ // -- timestamp precision (#1442) -------------------------------------------
370
+ // Runs BEFORE the completed_at default below, because that default copies
371
+ // `started_at` — a microsecond `started_at` would otherwise be copied into
372
+ // `completed_at` and spread the defect instead of fixing it.
373
+ let precisionCut = false;
374
+ for (const key of MS_PRECISION_FIELDS) {
375
+ const truncated = truncateToMs(out[key]);
376
+ if (truncated === null) continue;
377
+ preserveRaw(out, key, record[key], rescued);
378
+ out[key] = truncated;
379
+ precisionCut = true;
380
+ }
381
+
324
382
  // -- completed_at ---------------------------------------------------------
325
383
  // Legacy `ended_at` is the same fact under its old name (emit-session.mjs
326
384
  // aliasLegacyEndedAt precedent) — prefer it when parseable and monotonic,
@@ -332,13 +390,19 @@ export function repairRecord(record) {
332
390
  preserveRaw(out, 'completed_at', record.completed_at, rescued);
333
391
  const endedMs = typeof out.ended_at === 'string' ? Date.parse(out.ended_at) : NaN;
334
392
  const startedMs = Date.parse(out.started_at);
335
- out.completed_at =
336
- Number.isFinite(endedMs) && Number.isFinite(startedMs) && endedMs >= startedMs
337
- ? out.ended_at
338
- : out.started_at;
393
+ if (Number.isFinite(endedMs) && Number.isFinite(startedMs) && endedMs >= startedMs) {
394
+ // The COPY is cut to milliseconds; `ended_at` itself is not a validated
395
+ // field and keeps its original bytes, so nothing needs a sidecar here.
396
+ const endedTruncated = truncateToMs(out.ended_at);
397
+ if (endedTruncated !== null) precisionCut = true;
398
+ out.completed_at = endedTruncated ?? out.ended_at;
399
+ } else {
400
+ out.completed_at = out.started_at;
401
+ }
339
402
  defects.push('completed_at_missing');
340
403
  incomplete.add('completed_at');
341
404
  }
405
+ if (precisionCut) defects.push('timestamp_precision');
342
406
 
343
407
  if (defects.length === 0) {
344
408
  return { record, defects: [], incompleteFields: [], changed: false };
@@ -349,7 +413,14 @@ export function repairRecord(record) {
349
413
  ? record._backfill_incomplete_fields.filter((f) => typeof f === 'string')
350
414
  : [];
351
415
  const merged = orderIncompleteFields(incomplete);
352
- out._backfill_source = REPAIR_SOURCE;
416
+ // The repair is recorded in its own field; `_backfill_source` keeps the
417
+ // record's ORIGIN. Overwriting a synthesised origin ('state-md-completed',
418
+ // 'events-jsonl', 'abandoned') made a repaired zero-work stub read as a real
419
+ // record, and it shadowed the real one for its id (#1443 F2).
420
+ out._repair_source = REPAIR_SOURCE;
421
+ if (typeof record._backfill_source !== 'string' || record._backfill_source.length === 0) {
422
+ out._backfill_source = REPAIR_SOURCE;
423
+ }
353
424
  out._backfill_incomplete_fields = [...new Set([...prior, ...merged])];
354
425
 
355
426
  return { record: out, defects, incompleteFields: merged, changed: true };
@@ -487,6 +558,11 @@ export function repairText(raw, deps = {}) {
487
558
  repaired,
488
559
  // Projected: every line that was invalid and did NOT get repaired.
489
560
  invalid_after: invalidBefore - repaired,
561
+ // Lines with no defensible repair (`repairLine` status `error`). They are
562
+ // emitted byte-identically and are EXPECTED to stay invalid after a
563
+ // write — `verifyWritten` tolerates exactly these line numbers (#1442).
564
+ // Same population as `errors`, which carries the per-line detail.
565
+ unrepairable: errors.length,
490
566
  duplicate_ids_observed: duplicates,
491
567
  defects_by_class: defectsByClass,
492
568
  errors,
@@ -507,9 +583,32 @@ export function repairText(raw, deps = {}) {
507
583
  * any other target it is reported as skipped rather than quietly measuring the
508
584
  * wrong file.
509
585
  *
510
- * @returns {{ok: boolean, invalid_after: number, invalid_lines: object[], integrity: string|object}}
586
+ * ── WHAT "OK" MEANS (#1442) ──────────────────────────────────────────────────
587
+ * Not "zero invalid lines". A ledger that carries lines this module has no
588
+ * defensible repair for could never pass that bar, so one unrepairable record
589
+ * rolled back every repair beside it (all-or-nothing). `ok` now means NO
590
+ * WORSENING and ALL REPAIRABLE VALID:
591
+ * - every invalid line in the written file is one of `expected.unrepairableLines`
592
+ * (so every repaired AND every already-valid line validates), and
593
+ * - `invalid_after <= expected.invalidBefore`, and
594
+ * - the integrity probe flags no line it did not already flag in
595
+ * `expected.integrityBefore` (compared by line number — the repair never
596
+ * adds or removes lines, so positions are stable across the write).
597
+ * With no `expected`, the defaults reproduce the old strict bar: no invalid
598
+ * line tolerated, and a clean pre-write integrity baseline.
599
+ *
600
+ * NAMED CEILING (BV-004): integrity is compared per line, not per reason — a
601
+ * line flagged before AND after for a DIFFERENT reason is not "worse". The
602
+ * per-line `validateSession` pass above still catches it for the schema half.
603
+ *
604
+ * @param {object} args
605
+ * @param {object} [args.expected]
606
+ * @param {number[]} [args.expected.unrepairableLines] 1-based lines allowed to stay invalid
607
+ * @param {number} [args.expected.invalidBefore] invalid count before the write
608
+ * @param {object|null} [args.expected.integrityBefore] pre-write `checkIntegrity` banner (null = clean)
609
+ * @returns {{ok: boolean, invalid_after: number, invalid_lines: object[], unexpected_invalid: object[], integrity: string|object, integrity_worsened: boolean}}
511
610
  */
512
- export function verifyWritten({ file, repoRoot, deps = {} }) {
611
+ export function verifyWritten({ file, repoRoot, deps = {}, expected = {} }) {
513
612
  const {
514
613
  readFileSync = fs.readFileSync,
515
614
  validateSession = defaultValidateSession,
@@ -539,17 +638,50 @@ export function verifyWritten({ file, repoRoot, deps = {} }) {
539
638
  }
540
639
  }
541
640
 
641
+ const { unrepairableLines = [], invalidBefore = 0, integrityBefore = null } = expected;
642
+ const tolerated = new Set(unrepairableLines);
643
+ const unexpectedInvalid = invalidLines.filter((l) => !tolerated.has(l.line));
644
+
542
645
  let integrity = 'skipped-not-canonical-path';
543
- if (typeof repoRoot === 'string' && repoRoot.length > 0) {
544
- const canonical = path.join(repoRoot, CANONICAL_LEDGER_REL);
545
- if (path.resolve(file) === path.resolve(canonical)) {
546
- const banner = checkIntegrity({ repoRoot });
547
- integrity = banner === null ? 'clean' : banner;
548
- }
646
+ let worsened = false;
647
+ if (isCanonicalLedger(file, repoRoot)) {
648
+ const banner = checkIntegrity({ repoRoot });
649
+ integrity = banner === null ? 'clean' : banner;
650
+ worsened = integrityWorsened(integrityBefore, banner);
549
651
  }
550
652
 
551
- const ok = invalidLines.length === 0 && (integrity === 'clean' || integrity === 'skipped-not-canonical-path');
552
- return { ok, invalid_after: invalidLines.length, invalid_lines: invalidLines, integrity };
653
+ const ok = unexpectedInvalid.length === 0 && invalidLines.length <= invalidBefore && !worsened;
654
+ return {
655
+ ok,
656
+ invalid_after: invalidLines.length,
657
+ invalid_lines: invalidLines,
658
+ unexpected_invalid: unexpectedInvalid,
659
+ integrity,
660
+ integrity_worsened: worsened,
661
+ };
662
+ }
663
+
664
+ /** True when `file` IS `<repoRoot>/.orchestrator/metrics/sessions.jsonl` — the only file `checkIntegrity` reads. */
665
+ function isCanonicalLedger(file, repoRoot) {
666
+ if (typeof repoRoot !== 'string' || repoRoot.length === 0) return false;
667
+ return path.resolve(file) === path.resolve(path.join(repoRoot, CANONICAL_LEDGER_REL));
668
+ }
669
+
670
+ /**
671
+ * Did the integrity probe flag a line after the write that it did not flag
672
+ * before? Fail-safe on an unreadable banner shape: a non-null banner without
673
+ * its `schemaInvalid` / `mirrorSkipped` arrays counts as worse.
674
+ */
675
+ function integrityWorsened(before, after) {
676
+ if (after === null || after === undefined) return false;
677
+ for (const key of ['schemaInvalid', 'mirrorSkipped']) {
678
+ const now = after && Array.isArray(after[key]) ? after[key] : null;
679
+ if (now === null) return true;
680
+ const prior = before && Array.isArray(before[key]) ? before[key] : [];
681
+ const priorLines = new Set(prior.map((e) => e?.line));
682
+ if (now.some((e) => !priorLines.has(e?.line))) return true;
683
+ }
684
+ return false;
553
685
  }
554
686
 
555
687
  /**
@@ -560,7 +692,10 @@ export function verifyWritten({ file, repoRoot, deps = {} }) {
560
692
  * `<file>.tmp-<pid>` in the same directory and renames it over the target
561
693
  * (atomic within a filesystem), then re-verifies the written file. A failed
562
694
  * verification restores the backup byte-identically and reports `ok: false` —
563
- * the caller maps that to exit 3.
695
+ * the caller maps that to exit 3. Lines with no defensible repair are kept
696
+ * byte-identical and counted in `summary.unrepairable`; they are NOT a
697
+ * verification failure (see `verifyWritten` § WHAT "OK" MEANS) — the caller
698
+ * maps an ok run with an unrepairable remainder to exit 4.
564
699
  *
565
700
  * Never throws for a defective RECORD (those land in `summary.errors`); a
566
701
  * genuine I/O failure DOES throw and is the caller's exit-2 case.
@@ -584,6 +719,8 @@ export function repairLedger({ file, repoRoot = null, apply = false, backup = tr
584
719
  existsSync = fs.existsSync,
585
720
  } = deps;
586
721
 
722
+ const { checkIntegrity = defaultCheckIntegrity } = deps;
723
+
587
724
  const raw = readFileSync(file, 'utf8');
588
725
  const { text, summary: base } = repairText(raw, deps);
589
726
 
@@ -594,10 +731,17 @@ export function repairLedger({ file, repoRoot = null, apply = false, backup = tr
594
731
  backup_path: null,
595
732
  post_verify: null,
596
733
  ok: true,
734
+ restored: false,
597
735
  };
598
736
 
599
737
  if (!apply) return summary;
600
738
 
739
+ // -- integrity baseline (#1442) --------------------------------------------
740
+ // Measured on the ORIGINAL bytes, before anything is written, so the
741
+ // post-write probe can be judged "not worse" instead of "perfectly clean" —
742
+ // unrepairable lines keep the banner non-null on both sides by construction.
743
+ const integrityBefore = isCanonicalLedger(file, repoRoot) ? checkIntegrity({ repoRoot }) : null;
744
+
601
745
  // -- backup FIRST (unconditional unless explicitly opted out) --------------
602
746
  let backupPath = null;
603
747
  if (backup) {
@@ -621,14 +765,27 @@ export function repairLedger({ file, repoRoot = null, apply = false, backup = tr
621
765
  }
622
766
 
623
767
  // -- post-verification ----------------------------------------------------
624
- const verdict = verifyWritten({ file, repoRoot, deps });
768
+ const verdict = verifyWritten({
769
+ file,
770
+ repoRoot,
771
+ deps,
772
+ expected: {
773
+ unrepairableLines: base.errors.map((e) => e.line),
774
+ invalidBefore: base.invalid_before,
775
+ integrityBefore,
776
+ },
777
+ });
625
778
  summary.post_verify = verdict;
626
779
  summary.invalid_after = verdict.invalid_after;
627
780
 
628
781
  if (!verdict.ok) {
629
- // Restore byte-identically. The backup is preferred (it is the on-disk
630
- // artefact an operator can inspect); `raw` is the in-memory fallback for
631
- // `--no-backup`, and both are the same bytes.
782
+ // Restore byte-identically — ONLY for a repaired (or previously valid) line
783
+ // that fails verification, or an integrity probe that got worse. An
784
+ // unrepairable remainder alone is not a failure (#1442): those lines were
785
+ // emitted byte-identically and keep the ledger exactly as broken as it was.
786
+ // The backup is preferred (it is the on-disk artefact an operator can
787
+ // inspect); `raw` is the in-memory fallback for `--no-backup`, and both are
788
+ // the same bytes.
632
789
  if (backupPath) {
633
790
  copyFileSync(backupPath, file);
634
791
  } else {
@@ -17,7 +17,9 @@
17
17
  * {
18
18
  * session_id, pid, platform, repo_path_hash, repo_name, branch,
19
19
  * started_at, last_heartbeat, status ('active'|'wave'|'idle'),
20
- * current_wave, host_class, mode (Epic #583 W2-I3)
20
+ * current_wave, host_class, mode (Epic #583 W2-I3),
21
+ * role ('navigator'|null, optional, #1462 — display only, never ownership;
22
+ * set by heartbeat(patch.role), NOT by registerSelf)
21
23
  * }
22
24
  *
23
25
  * Schema v2 (Epic #583, W2-I3): `mode` field added so the exclusivity-matrix
@@ -142,6 +144,14 @@ function _validEntry(obj) {
142
144
  if ('mode' in obj && obj.mode !== null && typeof obj.mode !== 'string') {
143
145
  return false;
144
146
  }
147
+ // #1462: `role` is optional and a DISPLAY field only — the registry shows who
148
+ // claims to be navigator, it never decides it. The authoritative claim is the
149
+ // navigator lease under `~/.config/navigator/leases/navigator.json`, and
150
+ // classifyMode ignores `role`. When present it MUST be null or a string (no
151
+ // number / object smuggling); absent is accepted like a v1 `mode`.
152
+ if ('role' in obj && obj.role !== null && typeof obj.role !== 'string') {
153
+ return false;
154
+ }
145
155
  return true;
146
156
  }
147
157
 
@@ -236,11 +246,25 @@ export async function registerSelf(opts) {
236
246
  return entry;
237
247
  }
238
248
 
249
+ /** Roles `heartbeat(id, { role })` accepts besides `null` (#1462). */
250
+ export const REGISTRY_ROLES = Object.freeze(['navigator']);
251
+
239
252
  /**
240
253
  * Refresh last_heartbeat on the existing entry (and optionally status /
241
- * current_wave). No-op if the entry is missing.
254
+ * current_wave / role). No-op if the entry is missing.
255
+ *
256
+ * `patch.role`: `undefined` keeps the stored role, `null` clears it, a member
257
+ * of `REGISTRY_ROLES` sets it. Anything else throws before the file is touched.
258
+ *
259
+ * @param {string} sessionId
260
+ * @param {{ status?: string, currentWave?: number, role?: string|null }} [patch]
261
+ * @returns {Promise<object|null>} the updated entry, or null if none exists.
262
+ * @throws {TypeError} when `patch.role` is neither undefined, null, nor a REGISTRY_ROLES member.
242
263
  */
243
264
  export async function heartbeat(sessionId, patch = {}) {
265
+ if (patch.role !== undefined && patch.role !== null && !REGISTRY_ROLES.includes(patch.role)) {
266
+ throw new TypeError('heartbeat: role must be "navigator" or null');
267
+ }
244
268
  const file = entryPath(sessionId);
245
269
  const existing = await _readJsonSafe(file);
246
270
  if (!_validEntry(existing)) return null;
@@ -249,6 +273,7 @@ export async function heartbeat(sessionId, patch = {}) {
249
273
  last_heartbeat: utcTimestamp(),
250
274
  ...(patch.status !== undefined ? { status: patch.status } : {}),
251
275
  ...(patch.currentWave !== undefined ? { current_wave: patch.currentWave } : {}),
276
+ ...(patch.role !== undefined ? { role: patch.role } : {}),
252
277
  };
253
278
  await _writeJsonAtomic(file, updated);
254
279
  return updated;
@@ -5,8 +5,8 @@
5
5
  * Leaf module — no imports from siblings or parent.
6
6
  *
7
7
  * Exports: CURRENT_SESSION_SCHEMA_VERSION, SESSION_KEY_ALIASES,
8
- * VALID_SESSION_TYPES, VALID_SESSION_PROFILES, REQUIRED_FIELDS,
9
- * AGENT_SUMMARY_FIELDS
8
+ * WAVE_KEY_ALIASES, VALID_SESSION_TYPES, VALID_SESSION_PROFILES,
9
+ * REQUIRED_FIELDS, AGENT_SUMMARY_FIELDS, OPTIONAL_FIELDS
10
10
  */
11
11
 
12
12
  // ---------------------------------------------------------------------------
@@ -53,6 +53,24 @@ export const SESSION_KEY_ALIASES = Object.freeze({
53
53
  isolation_override: 'isolation',
54
54
  });
55
55
 
56
+ /**
57
+ * Safe per-wave key aliases (legacy -> canonical) — the `waves[]` counterpart of
58
+ * SESSION_KEY_ALIASES, same-shape renames only. Applied by normalizeWaveKeys
59
+ * (normalizer.mjs) on read AND by scripts/emit-session.mjs on write (#1390 P1).
60
+ *
61
+ * The canonical spelling is `agent_count_*` (skills/session-end/
62
+ * metrics-collection.md). The ledger carried 23 sessions in that spelling and
63
+ * exactly one, `main-2026-09-18-session-1` (5 waves), in the de-underscored
64
+ * `agents_*` spelling (measured 2026-09-19 @ 8f6ac022 over
65
+ * .orchestrator/metrics/sessions.jsonl) — a consumer reading
66
+ * `w.agent_count_completed` got `undefined` for every one of its waves.
67
+ */
68
+ export const WAVE_KEY_ALIASES = Object.freeze({
69
+ agents_planned: 'agent_count_planned',
70
+ agents_started: 'agent_count_started',
71
+ agents_completed: 'agent_count_completed',
72
+ });
73
+
56
74
  // ---------------------------------------------------------------------------
57
75
  // Enums / required field lists
58
76
  // ---------------------------------------------------------------------------
@@ -198,4 +216,40 @@ export const OPTIONAL_FIELDS = Object.freeze([
198
216
  // lacking the field validates unchanged. Value set + the full rationale for
199
217
  // why it is NOT a VALID_SESSION_TYPES member: VALID_SESSION_PROFILES above.
200
218
  'session_profile',
219
+ // #1339 P8 — HEAD sha at session start. Gives `/evolve analyze` an
220
+ // attributable commit range instead of a time window that also catches
221
+ // parallel sessions' commits. Absent = not measured. NOT guaranteed to be a
222
+ // full sha: scripts/emit-session.mjs now writes only a full 40/64-hex sha or
223
+ // an explicit `null` (a derived ref is adopted only in full form, and an
224
+ // explicit short ref on the record is dropped with a WARN), but every record
225
+ // before that derivation was hand-composed — 19 with a 40-char and 9 with a 7-char
226
+ // value (measured 2026-09-19 @ 8f6ac022, jq over sessions.jsonl).
227
+ // Consumers must accept a short sha or `null`, never assume a full one.
228
+ 'session_start_ref',
229
+ // #1457 point 7 — four fields that were written and (two of them) shape-
230
+ // checked while appearing in NEITHER list, the same "status only inferable
231
+ // from an `if`" class as `effectiveness` above (#964). Each one has a live
232
+ // writer:
233
+ // `total_tokens`, `matched_records` — merged from the #1436 subagents.jsonl
234
+ // rollup by scripts/emit-session.mjs (ROLLUP_KEYS) and produced by
235
+ // scripts/lib/session-token-rollup.mjs; validator.mjs checks both as
236
+ // non-negative integers.
237
+ // `raw_session_id` — the native harness uuid beside the semantic
238
+ // `session_id`, written by scripts/emit-session.mjs and by the SessionEnd
239
+ // backfill in scripts/lib/session-close-backfill.mjs.
240
+ // `_repair_source` — provenance marker every record repaired by
241
+ // scripts/lib/session-record-repair.mjs carries (#1443 F2), distinct from
242
+ // `_backfill_source`, which keeps the record's origin.
243
+ 'total_tokens',
244
+ 'matched_records',
245
+ 'raw_session_id',
246
+ '_repair_source',
247
+ // #1475 — how many of the session's cost-relevant subagent records were
248
+ // priced, out of how many. Merged from the rollup by scripts/emit-session.mjs;
249
+ // non-negative integers, priced ≤ total. `total_cost_usd` is omitted when
250
+ // priced < total (some agent's cost is unknown) AND when total is 0 (nothing
251
+ // to price — 1223 of 2093 ledger×session pairs, 31 fleet ledgers,
252
+ // 2026-10-01), so the counters say which of the two it is.
253
+ 'cost_records_priced',
254
+ 'cost_records_total',
201
255
  ]);