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
@@ -6,12 +6,15 @@
6
6
  * a single session record to `.orchestrator/metrics/sessions.jsonl` (or any
7
7
  * target path). Replaces the raw shell `>>` append with a validated path:
8
8
  *
9
- * node scripts/emit-session.mjs [--file PATH] [--entry JSON]
9
+ * node scripts/emit-session.mjs [--file PATH] [--entry JSON] [--session-uuid ID]
10
10
  *
11
11
  * Input modes:
12
12
  * --entry '<json>' pass the entry JSON literally (for shell pipelines)
13
13
  * (stdin) read the entry JSON from stdin (default when no --entry)
14
14
  *
15
+ * --session-uuid ID this session's raw harness UUID (overrides the
16
+ * current-session.json lookup; the test seam)
17
+ *
15
18
  * Defaults:
16
19
  * --file .orchestrator/metrics/sessions.jsonl
17
20
  *
@@ -33,6 +36,8 @@ import {
33
36
  stampMemoryCleanup,
34
37
  } from './lib/memory-cleanup-stamp.mjs';
35
38
  import { parseStateMd, readSessionProfile, resolveStateMdPath } from './lib/state-md.mjs';
39
+ import { readProcessLocalSessionIds } from './lib/session-identity/own-session.mjs';
40
+ import { rollupSessionTokens } from './lib/session-token-rollup.mjs';
36
41
  import { serializeSessionLineChecked } from './lib/session-schema/serializer.mjs';
37
42
  import {
38
43
  validateSession,
@@ -40,22 +45,168 @@ import {
40
45
  CURRENT_SESSION_SCHEMA_VERSION,
41
46
  clampTimestampsMonotonic,
42
47
  aliasLegacyEndedAt,
48
+ normalizeWaveKeys,
43
49
  } from './lib/session-schema.mjs';
44
50
  import { isMainModule } from './lib/is-main-module.mjs';
45
51
 
46
52
  export { serializeSessionLineChecked };
47
53
 
54
+ // A FULL object name only (SHA-1 or SHA-256 repo). A short sha or a symbolic
55
+ // ref (`HEAD`, a branch) would resolve to a DIFFERENT commit when read later.
56
+ const FULL_SHA_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
57
+
58
+ // Rollup fields merged into the record (#1436) — the totals plus the #1244
59
+ // cache buckets and contract marker the former prose recipe merged, and the
60
+ // #1475 cost-coverage counters, which say why `total_cost_usd` is absent when
61
+ // priced < total. `legacy_v1_records` stays in the rollup's own return value.
62
+ const ROLLUP_KEYS = Object.freeze([
63
+ 'total_tokens',
64
+ 'total_token_input',
65
+ 'total_token_output',
66
+ 'total_token_input_uncached',
67
+ 'total_token_cache_read',
68
+ 'total_token_cache_creation',
69
+ 'total_cost_usd',
70
+ 'subagents_with_tokens',
71
+ 'matched_records',
72
+ 'cost_records_priced',
73
+ 'cost_records_total',
74
+ '_token_schema',
75
+ ]);
76
+
77
+ // Rollup keys validateSession() requires to be non-negative integers.
78
+ const INTEGER_ROLLUP_KEYS = new Set([
79
+ 'total_tokens',
80
+ 'matched_records',
81
+ 'cost_records_priced',
82
+ 'cost_records_total',
83
+ ]);
84
+
85
+ /**
86
+ * Resolve THIS session's raw harness UUID — the join key `subagents.jsonl`
87
+ * (`parent_session_id`) and `events.jsonl` (`session_id`) carry, which the
88
+ * record itself does not (its `session_id` is the semantic slug).
89
+ *
90
+ * `--session-uuid` wins (an explicit assertion, and the test seam). Otherwise
91
+ * `.orchestrator/current-session.json` is read. That file is SHARED by the
92
+ * working copy — the last SessionStart hook wrote it — so its UUID is adopted
93
+ * ONLY when its `semantic_session_id` (or, for a legacy raw-id record, its
94
+ * `session_id`) equals this record's `session_id`: the same "named owner
95
+ * equals the record" gate as the STATE.md derivation in main(). When this
96
+ * process carries its own session id (`readProcessLocalSessionIds()` — the
97
+ * harness env, which a dispatched subagent inherits from the coordinator), that
98
+ * witness OUTRANKS the shared marker: a marker UUID the process does not carry
99
+ * was written by a peer and is refused even when the labels agree (two sessions
100
+ * can mint the same semantic label, #1066). Ranked, never unioned
101
+ * (.claude/rules/identity-and-locks.md). A foreign marker is WARNed and yields
102
+ * null; an absent marker yields null silently.
103
+ *
104
+ * CEILING: a resumed session that received a NEW raw id keeps its semantic id,
105
+ * so only the latest raw id resolves and subagents dispatched under the earlier
106
+ * one are not rolled up. Revisit if current-session.json gains a raw-id history.
107
+ *
108
+ * Never throws.
109
+ *
110
+ * @param {{ override: string|null, recordSessionId: unknown }} opts
111
+ * @returns {string|null}
112
+ */
113
+ function resolveOwnSessionUuid({ override, recordSessionId }) {
114
+ const trimmed = (v) => (typeof v === 'string' ? v.trim() : '');
115
+ const explicit = trimmed(override);
116
+ if (explicit) return explicit;
117
+ const markerPath = join(process.cwd(), '.orchestrator', 'current-session.json');
118
+ let marker;
119
+ try {
120
+ if (!existsSync(markerPath)) return null;
121
+ marker = JSON.parse(readFileSync(markerPath, 'utf8'));
122
+ } catch (err) {
123
+ process.stderr.write(
124
+ `emit-session: WARN could not read ${markerPath} (${err?.message ?? err}); ` +
125
+ `omitting raw_session_id and the token rollup\n`
126
+ );
127
+ return null;
128
+ }
129
+ const uuid = trimmed(marker?.session_id);
130
+ if (!uuid) return null;
131
+ const semantic = trimmed(marker?.semantic_session_id);
132
+ const namesRecord = (semantic !== '' && semantic === recordSessionId) || uuid === recordSessionId;
133
+ const processIds = readProcessLocalSessionIds();
134
+ const processAgrees = processIds.length === 0 || processIds.includes(uuid);
135
+ if (namesRecord && processAgrees) return uuid;
136
+ // The record's session_id is the semantic label (STATE.md `session`); name
137
+ // both sides of whichever comparison failed, so the operator sees which drifted.
138
+ const why = namesRecord
139
+ ? `session label ${recordSessionId} matches, but this process runs as ` +
140
+ `${processIds.join('/')}, not the marker's uuid ${uuid}`
141
+ : `session label mismatch: marker semantic_session_id=${semantic || '<absent>'} ` +
142
+ `vs record session_id=${recordSessionId ?? '<unknown>'}`;
143
+ process.stderr.write(
144
+ `emit-session: WARN current-session.json ${why}; omitting raw_session_id and the token rollup\n`
145
+ );
146
+ return null;
147
+ }
148
+
149
+ /**
150
+ * The `head_sha` of THIS session's own `orchestrator.session.started` event —
151
+ * matched on the raw UUID, so a parallel session's start event never supplies
152
+ * it. The FIRST own event decides: a resume or compact that re-emits the event
153
+ * under the same raw id is not the session's start, so its sha is never taken —
154
+ * not even when the first own event carries none. Returns null when there is no
155
+ * own event, or the first one's head_sha is absent or not a full sha.
156
+ *
157
+ * LIMIT — rotation (#1457 point 3): events.jsonl rotates at SessionStart once
158
+ * it exceeds `events-rotation.max-size-mb` (default 10), and when that start is
159
+ * a compact/resume of THIS session the first SURVIVING own event can be a
160
+ * compact/resume re-emit whose head_sha is later than the real start. When the
161
+ * first own event carries `native_source` 'compact' or 'resume' it is provably
162
+ * not the start, so this returns null (unknown) rather than a wrong ref. An
163
+ * event without `native_source` (Codex/Cursor, or a pre-#1091 writer) is still
164
+ * taken — so a rotation that leaves an unlabelled re-emit first stays wrong.
165
+ *
166
+ * Whole-file read, like deriveMemoryCleanupSignal() on the same file — fine at
167
+ * today's events.jsonl size; revisit if the file outgrows a single read.
168
+ *
169
+ * Never throws.
170
+ *
171
+ * @param {string} eventsFile
172
+ * @param {string} uuid
173
+ * @returns {string|null}
174
+ */
175
+ function readOwnStartHeadSha(eventsFile, uuid) {
176
+ let raw;
177
+ try {
178
+ raw = readFileSync(eventsFile, 'utf8');
179
+ } catch {
180
+ return null;
181
+ }
182
+ for (const line of raw.split('\n')) {
183
+ if (!line.includes(uuid)) continue;
184
+ let ev;
185
+ try {
186
+ ev = JSON.parse(line);
187
+ } catch {
188
+ continue;
189
+ }
190
+ if (ev?.event !== 'orchestrator.session.started' || ev.session_id !== uuid) continue;
191
+ if (ev.native_source === 'compact' || ev.native_source === 'resume') return null;
192
+ return typeof ev.head_sha === 'string' && FULL_SHA_RE.test(ev.head_sha) ? ev.head_sha : null;
193
+ }
194
+ return null;
195
+ }
196
+
48
197
  function parseArgs(argv) {
49
- const args = { file: '.orchestrator/metrics/sessions.jsonl', entry: null };
198
+ const args = { file: '.orchestrator/metrics/sessions.jsonl', entry: null, sessionUuid: null };
50
199
  for (let i = 0; i < argv.length; i++) {
51
200
  const a = argv[i];
52
201
  if (a === '--file') args.file = argv[++i];
53
202
  else if (a === '--entry') args.entry = argv[++i];
203
+ else if (a === '--session-uuid') args.sessionUuid = argv[++i];
54
204
  else if (a === '--help' || a === '-h') {
55
205
  process.stdout.write(
56
206
  'Usage: node scripts/emit-session.mjs [--file PATH] [--entry JSON]\n' +
57
207
  ' --file target JSONL file (default: .orchestrator/metrics/sessions.jsonl)\n' +
58
208
  ' --entry entry JSON (if omitted, read from stdin)\n' +
209
+ ' --session-uuid raw harness UUID (default: owned .orchestrator/current-session.json)\n' +
59
210
  'Exit codes: 0 append ok, 1 validation error, 2 I/O error\n'
60
211
  );
61
212
  process.exit(0);
@@ -118,6 +269,37 @@ async function main() {
118
269
  );
119
270
  }
120
271
 
272
+ // 3. Alias legacy per-wave agent-count keys (`agents_completed`, …) onto
273
+ // their canonical `agent_count_*` spelling (#1390 P1), non-clobbering.
274
+ // ONLY this pass of normalizeSession runs on the write path: the rest
275
+ // would tag a missing `schema_version` as 0 (validateSession stamps the
276
+ // current version) and apply top-level aliases that validateSession's
277
+ // contract deliberately does not.
278
+ repaired = normalizeWaveKeys(repaired);
279
+
280
+ // Own raw UUID (#1436), resolved once for the three derivations further down:
281
+ // raw_session_id, the session_start_ref events fallback, the token rollup.
282
+ const ownUuid = resolveOwnSessionUuid({
283
+ override: args.sessionUuid,
284
+ recordSessionId: repaired.session_id,
285
+ });
286
+
287
+ // An explicit session_start_ref that is not a full sha (#1443) names a
288
+ // DIFFERENT commit once the short form becomes ambiguous. Dropped with a WARN
289
+ // rather than rejected: the record is the load-bearing artefact, and the
290
+ // derivations below may still supply the full sha. Explicit `null` stays.
291
+ // Enforced HERE, not in validateSession(): the ledger already holds records
292
+ // with 7-char refs, and the session-start integrity banner runs every one of
293
+ // them through validateSession().
294
+ if (typeof repaired.session_start_ref === 'string' && !FULL_SHA_RE.test(repaired.session_start_ref)) {
295
+ process.stderr.write(
296
+ `emit-session: WARN session_start_ref=${repaired.session_start_ref} is not a full hex sha; ` +
297
+ `dropping it\n`
298
+ );
299
+ const { session_start_ref: _dropped, ...rest } = repaired;
300
+ repaired = rest;
301
+ }
302
+
121
303
  // `memory_cleanup_at` derivation (#699 follow-up — Disziplin statt Mechanik).
122
304
  // The flag used to be a boolean the coordinator-LLM remembered to pass at
123
305
  // session-end; it was forgotten on 2026-08-14 and the cadence marker stalled
@@ -154,73 +336,231 @@ async function main() {
154
336
  }
155
337
  }
156
338
 
157
- // `session_profile` derivation (#1247 — GitLab issue "key the effective-sizing
158
- // row on waves x profile"). The mandatory write path is the coordinator
159
- // composing `$METRICS_ENTRY` in prose (session-metrics-write.md), which never
160
- // reliably set this field — so it is read here instead, from this repo's own
161
- // STATE.md frontmatter via `readSessionProfile()` (scripts/lib/state-md.mjs).
339
+ // STATE.md-derived enrichment — ONE read, ONE parse, ONE ownership check for
340
+ // both fields below.
341
+ //
342
+ // `session_profile` (#1247 — GitLab issue "key the effective-sizing row on
343
+ // waves x profile"). The mandatory write path is the coordinator composing
344
+ // `$METRICS_ENTRY` in prose (session-metrics-write.md), which never reliably
345
+ // set this field — so it is read here instead, from this repo's own STATE.md
346
+ // frontmatter via `readSessionProfile()` (scripts/lib/state-md.mjs).
347
+ //
348
+ // `session_start_ref` (#1339 P8) — frontmatter `session-start-ref`, the HEAD
349
+ // sha the wave-executor pinned at session start. `/evolve analyze` needs it
350
+ // for an attributable `<start>..<end>` commit range; without it, it falls
351
+ // back to a time window that also attributes a PARALLEL session's commits to
352
+ // this one (skills/evolve/references/evolve-analyze-mode.md). Only a full
353
+ // hex sha (FULL_SHA_RE) is adopted; anything else is omitted with a WARN.
354
+ // Ceiling: the frontmatter parser coerces an all-decimal-digit value to a
355
+ // number, so such a sha (p ~ 7e-9) is omitted, never adopted as a number.
356
+ // NO END REF, deliberately: this runs at session-end Phase 3.7, BEFORE the
357
+ // Phase 4 close commit exists, so HEAD here would cut the session's own
358
+ // commit out of its range. Consumers use HEAD + `--until=<completed_at>`.
359
+ // Revisit when a post-commit writer exists that can stamp `session_end_ref`.
162
360
  //
163
- // Precedence: an EXPLICIT `session_profile` KEY on the incoming record (even
164
- // `null`) wins and is never overwritten — same "explicit assertion beats
165
- // derivation" convention as `memory_cleanup_at` above. Absence of a profile in
166
- // STATE.md (readSessionProfile returns null) leaves the field OMITTED —
167
- // never coerced to `''`/`'none'`/a literal `"null"` string.
361
+ // Precedence: an EXPLICIT key on the incoming record (even `null`) wins and
362
+ // is never overwritten — same "explicit assertion beats derivation"
363
+ // convention as `memory_cleanup_at` above. Absence in STATE.md leaves the
364
+ // field OMITTED — never coerced to `''`/`'none'`/a literal `"null"` string.
168
365
  //
169
366
  // OWNERSHIP: STATE.md belongs to the session named in its OWN frontmatter
170
367
  // (`session:`), which in a shared working copy need not be the session this
171
368
  // record describes — two parallel sessions, or a `/close` run after a
172
369
  // foreign `/plan` session left its STATE.md behind (see
173
370
  // `.claude/rules/parallel-sessions.md`). Filing session B's waves under
174
- // session A's profile is exactly the cross-contamination #1247 removes, only
175
- // inverted. So the profile is adopted ONLY when the frontmatter `session`
176
- // equals this record's `session_id`; on any mismatch (or an unprovable
177
- // ownership, i.e. no `session` key) the field is OMITTED — never guessed.
371
+ // session A's profile — or A's start ref — is exactly the cross-contamination
372
+ // both fields exist to remove. So they are adopted ONLY when the frontmatter
373
+ // `session` equals this record's `session_id`; on any mismatch (or an
374
+ // unprovable ownership, i.e. no `session` key) they are OMITTED — never guessed.
178
375
  //
179
376
  // FAIL-SAFE: the read is enrichment; the session record is the load-bearing
180
377
  // artefact. Any failure reading or parsing STATE.md (EISDIR when the path is
181
- // a directory, EACCES/EPERM, a truncated file) omits the field and lets the
378
+ // a directory, EACCES/EPERM, a truncated file) omits the fields and lets the
182
379
  // record through — it must never be the reason `sessions.jsonl` gains no
183
380
  // line at all.
184
- if (!Object.prototype.hasOwnProperty.call(repaired, 'session_profile')) {
381
+ const hasOwn = (key) => Object.prototype.hasOwnProperty.call(repaired, key);
382
+ const needsProfile = !hasOwn('session_profile');
383
+ const needsStartRef = !hasOwn('session_start_ref');
384
+ // What STATE.md contributed to session_start_ref, for the WARN below when no
385
+ // source supplies one (#1457 point 2): none, or a ref that was discarded.
386
+ let stateMdStartRefNote = 'STATE.md carried none';
387
+ if (needsProfile || needsStartRef) {
185
388
  let stateMdContents = '';
186
389
  try {
187
390
  const stateMdPath = resolveStateMdPath(process.cwd());
188
391
  if (existsSync(stateMdPath)) stateMdContents = readFileSync(stateMdPath, 'utf8');
189
392
  } catch (err) {
190
393
  process.stderr.write(
191
- `emit-session: WARN could not read STATE.md for session_profile derivation ` +
192
- `(${err?.message ?? err}); omitting session_profile\n`
394
+ `emit-session: WARN could not read STATE.md for session_profile/session_start_ref ` +
395
+ `derivation (${err?.message ?? err}); omitting both\n`
193
396
  );
194
397
  stateMdContents = '';
398
+ stateMdStartRefNote = 'STATE.md unreadable';
195
399
  }
196
400
  // No initialiser: both the try and the catch below assign `profile`.
197
401
  let profile;
402
+ let startRef = null;
198
403
  let owner = null;
199
404
  try {
200
405
  profile = readSessionProfile(stateMdContents);
201
- const parsed = parseStateMd(stateMdContents);
202
- const rawOwner = parsed?.frontmatter?.session;
406
+ const fm = parseStateMd(stateMdContents)?.frontmatter;
407
+ const rawOwner = fm?.session;
203
408
  owner = typeof rawOwner === 'string' && rawOwner.trim().length > 0 ? rawOwner.trim() : null;
409
+ const rawRef = fm?.['session-start-ref'];
410
+ if (typeof rawRef === 'string' && FULL_SHA_RE.test(rawRef.trim())) {
411
+ startRef = rawRef.trim();
412
+ } else if (needsStartRef && rawRef !== undefined && rawRef !== null && rawRef !== '') {
413
+ process.stderr.write(
414
+ `emit-session: WARN STATE.md session-start-ref=${rawRef} is not a full hex sha; ` +
415
+ `omitting session_start_ref\n`
416
+ );
417
+ stateMdStartRefNote = 'STATE.md ref discarded (not a full hex sha)';
418
+ }
204
419
  } catch (err) {
205
420
  process.stderr.write(
206
- `emit-session: WARN could not parse STATE.md for session_profile derivation ` +
207
- `(${err?.message ?? err}); omitting session_profile\n`
421
+ `emit-session: WARN could not parse STATE.md for session_profile/session_start_ref ` +
422
+ `derivation (${err?.message ?? err}); omitting both\n`
208
423
  );
209
424
  profile = null;
425
+ startRef = null;
426
+ stateMdStartRefNote = 'STATE.md unparseable';
210
427
  }
211
- if (profile !== null) {
428
+ const derived = {};
429
+ if (needsProfile && profile !== null) derived.session_profile = profile;
430
+ if (needsStartRef && startRef !== null) derived.session_start_ref = startRef;
431
+ const derivedKeys = Object.keys(derived);
432
+ if (derivedKeys.length > 0) {
212
433
  if (owner !== null && owner === repaired.session_id) {
213
- repaired = { ...repaired, session_profile: profile };
434
+ repaired = { ...repaired, ...derived };
214
435
  } else {
436
+ if (derived.session_start_ref !== undefined) {
437
+ stateMdStartRefNote = `STATE.md ref discarded (belongs to session=${owner ?? '<absent>'})`;
438
+ }
215
439
  // A visible omission: silence here is indistinguishable from "STATE.md
216
- // carries no profile", and a foreign STATE.md in this working copy is
217
- // precisely what the operator wants to know about.
440
+ // carries no such field", and a foreign STATE.md in this working copy
441
+ // is precisely what the operator wants to know about.
442
+ process.stderr.write(
443
+ `emit-session: WARN STATE.md ${derivedKeys.map((k) => `${k}=${derived[k]}`).join(' ')} ` +
444
+ `belongs to session=${owner ?? '<absent>'}, not session_id=${repaired.session_id ?? '<unknown>'}; ` +
445
+ `omitting ${derivedKeys.join(', ')}\n`
446
+ );
447
+ }
448
+ }
449
+ }
450
+
451
+ // session_start_ref fallback (#1443): STATE.md supplied none (absent key,
452
+ // foreign owner, unparseable) — take the head_sha the SessionStart hook
453
+ // recorded on this session's own start event. Absent there too → omitted,
454
+ // with a WARN whether or not an own UUID was available (#1457 point 2) —
455
+ // without one the fallback cannot run, which is exactly when silence hid it.
456
+ if (!hasOwn('session_start_ref')) {
457
+ const headSha = ownUuid !== null
458
+ ? readOwnStartHeadSha(join(dirname(args.file), 'events.jsonl'), ownUuid)
459
+ : null;
460
+ if (headSha !== null) {
461
+ repaired = { ...repaired, session_start_ref: headSha };
462
+ process.stderr.write(
463
+ `emit-session: derived session_start_ref=${headSha} from the own orchestrator.session.started event\n`
464
+ );
465
+ } else {
466
+ process.stderr.write(
467
+ `emit-session: WARN no session_start_ref — ${stateMdStartRefNote}, ` +
468
+ 'no own session.started head_sha available\n'
469
+ );
470
+ }
471
+ }
472
+
473
+ if (ownUuid !== null && !hasOwn('raw_session_id')) {
474
+ repaired = { ...repaired, raw_session_id: ownUuid };
475
+ }
476
+
477
+ // Token rollup (#1436) — mechanical, replacing the session-metrics-write.md
478
+ // prose step that had no production caller. Joins the sibling subagents.jsonl
479
+ // on the own raw UUID and fills ONLY keys the entry does not carry; an
480
+ // explicit `total_tokens` means the caller already rolled up, so nothing is
481
+ // merged. No matching records or a failed read OMITS the fields with a WARN —
482
+ // never a fabricated 0; a null total is omitted rather than written — for
483
+ // `total_cost_usd` that is any session with an unpriced subagent record
484
+ // (#1475: unknown model, no model, or no tokens), which the persisted
485
+ // `cost_records_priced < cost_records_total` then explains.
486
+ // match_status explains the omission; diagnostic fields stay out of the record.
487
+ if (ownUuid !== null && !hasOwn('total_tokens')) {
488
+ let rollup = null;
489
+ try {
490
+ rollup = rollupSessionTokens({
491
+ parentSessionId: ownUuid,
492
+ subagentsPath: join(dirname(args.file), 'subagents.jsonl'),
493
+ });
494
+ } catch (err) {
495
+ process.stderr.write(
496
+ `emit-session: WARN token rollup failed (${err?.message ?? err}); omitting token fields\n`
497
+ );
498
+ }
499
+ if (rollup !== null && rollup.matched_records > 0) {
500
+ const merged = {};
501
+ for (const key of ROLLUP_KEYS) {
502
+ if (hasOwn(key) || !Number.isFinite(rollup[key])) continue;
503
+ // Mirrors validateSession(): these two are non-negative INTEGERS, while
504
+ // total_token_input/output may be fractional — one fractional token
505
+ // value upstream must cost the field, not the record.
506
+ if (INTEGER_ROLLUP_KEYS.has(key) && !Number.isInteger(rollup[key])) {
507
+ process.stderr.write(
508
+ `emit-session: WARN token rollup produced an invalid field (${key} must be a ` +
509
+ `non-negative integer, got: ${rollup[key]}); omitting ${key}\n`
510
+ );
511
+ continue;
512
+ }
513
+ merged[key] = rollup[key];
514
+ }
515
+ // Enrichment must never be the reason the ledger gains no line: validate
516
+ // the enriched record first and fall back to the un-enriched one. Blamed
517
+ // on the rollup only when the un-enriched record validates on its own —
518
+ // otherwise the validation below reports the record's own defect.
519
+ try {
520
+ validateSession({ ...repaired, ...merged });
521
+ repaired = { ...repaired, ...merged };
522
+ } catch (err) {
523
+ if (!(err instanceof ValidationError)) throw err;
524
+ let baseValid = true;
525
+ try {
526
+ validateSession(repaired);
527
+ } catch {
528
+ baseValid = false;
529
+ }
530
+ if (baseValid) {
531
+ process.stderr.write(
532
+ `emit-session: WARN token rollup produced an invalid field (${err.message}); ` +
533
+ `omitting token fields\n`
534
+ );
535
+ }
536
+ }
537
+ if (rollup.total_tokens === null) {
218
538
  process.stderr.write(
219
- `emit-session: WARN STATE.md session_profile=${profile} belongs to ` +
220
- `session=${owner ?? '<absent>'}, not session_id=${repaired.session_id ?? '<unknown>'}; ` +
221
- `omitting session_profile\n`
539
+ `emit-session: WARN token rollup matched ${rollup.matched_records} subagents.jsonl record(s) ` +
540
+ `for ${ownUuid}, none token-bearing; total_tokens omitted\n`
222
541
  );
223
542
  }
543
+ } else if (rollup !== null) {
544
+ let warning = `token rollup found no subagents.jsonl records for ${ownUuid}`;
545
+ switch (rollup.match_status) {
546
+ case 'ledger-absent':
547
+ warning += ' (ledger absent)';
548
+ break;
549
+ case 'ledger-empty':
550
+ warning += ' (ledger holds no readable records)';
551
+ break;
552
+ case 'unmatched':
553
+ warning = `token rollup found ${rollup.ledger_records} subagents.jsonl record(s) present, ` +
554
+ `none with parent_session_id=${ownUuid} — not attributable via this UUID, not zero cost`;
555
+ break;
556
+ case 'invalid-key':
557
+ warning = `token rollup rejected invalid parent_session_id=${ownUuid}`;
558
+ break;
559
+ default:
560
+ // Unknown or missing diagnostics retain the generic omission warning.
561
+ break;
562
+ }
563
+ process.stderr.write(`emit-session: WARN ${warning}; omitting token fields\n`);
224
564
  }
225
565
  }
226
566
 
@@ -8,7 +8,8 @@
8
8
  * Follows .claude/rules/cli-design.md:
9
9
  * - `--json` for machine output; human-readable by default.
10
10
  * - Data → stdout, diagnostics → stderr.
11
- * - Exit codes: 0 success/match · 1 user-error/drift · 2 system error.
11
+ * - Exit codes: 0 success/match · 1 user-error/drift · 2 system error ·
12
+ * 3 verify version-mismatch (non-standard, documented in --help).
12
13
  *
13
14
  * ## Append failures are NOT exit 0 (GitLab #969)
14
15
  *
@@ -40,6 +41,17 @@
40
41
  * Session-end Phase 3.7d remains advisory and MUST NOT gate on this exit code —
41
42
  * it catches a non-zero exit and logs a WARN (skills/session-end/SKILL.md).
42
43
  *
44
+ * ## A rubric change is not scoring drift (GitLab #1400)
45
+ *
46
+ * `--verify` re-scores a stored record with the CURRENT engine. When the stored
47
+ * `rubric_version` differs from `RUBRIC_VERSION`, the two sides answer different
48
+ * pre-registered check sets, so neither MATCH nor DRIFT is true: the run exits
49
+ * `3` with `verdict: "version-mismatch"` and re-scores nothing. Exit `0` would
50
+ * sell a cross-rubric comparison as a reproducibility proof; exit `1` would put
51
+ * a planned rubric bump in the same bucket as a forged record. Measured
52
+ * 2026-09-19: all 40 records in this repo's journal are `rubric-v1` and every
53
+ * one of them exited 1 as DRIFT under the rubric-v2 engine.
54
+ *
43
55
  * Usage:
44
56
  * eval-session.mjs [--session <id>] [--json] [--no-write]
45
57
  * [--metrics-dir <path>] [--rubric <path>]
@@ -68,6 +80,8 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
68
80
  const EXIT_OK = 0;
69
81
  const EXIT_USER = 1;
70
82
  const EXIT_SYSTEM = 2;
83
+ /** Non-standard, `--verify` only: the stored record predates the current rubric (#1400). */
84
+ const EXIT_VERSION_MISMATCH = 3;
71
85
 
72
86
  function readPkgVersion() {
73
87
  try {
@@ -103,6 +117,9 @@ EXIT CODES
103
117
  1 user error (session not found, unknown run-id, record failed validation)
104
118
  / verify drift
105
119
  2 system error (could not append the record to the eval journal)
120
+ 3 verify version-mismatch: the stored record was scored under a different
121
+ rubric than ${RUBRIC_VERSION}, so it is not re-verifiable with this engine
122
+ (non-standard code — neither a match nor drift)
106
123
  `;
107
124
 
108
125
  function fail(exitCode, message) {
@@ -211,6 +228,49 @@ function runVerify(runId, values) {
211
228
  fail(EXIT_USER, `--verify: run-id not found in eval journal: ${runId}`);
212
229
  }
213
230
 
231
+ // #1400 — check the rubric BEFORE re-scoring. A record scored under another
232
+ // rubric answers a different pre-registered check set, so re-running the
233
+ // current engine over it produces a comparison nobody asked for. A record
234
+ // with no `rubric_version` at all is unattributable and counts as a mismatch.
235
+ const storedRubric = typeof stored.rubric_version === 'string' && stored.rubric_version
236
+ ? stored.rubric_version
237
+ : null;
238
+ if (storedRubric !== RUBRIC_VERSION) {
239
+ // Deliberately NO diff of the dimensions the two rubrics share. Such a diff
240
+ // is technically possible but would carry almost no signal here. Measured
241
+ // 2026-09-19 over this repo's 40-record journal, replaying every record
242
+ // through `evaluateSession` + `diffDimensions`: all 40 differ on
243
+ // `process-safety` (its formula IS the v2 change) and all 40 gain a
244
+ // `present-in-fresh-only` `guard-friction`; restricted to the
245
+ // formula-unchanged dimensions, only 4 of 40 are byte-identical and the
246
+ // other 36 differ on `verification-evidence` / `gate-health` because the
247
+ // events they were scored from predate this journal's `events.jsonl`. So a
248
+ // "common dimensions" diff would re-report 90% of the journal as drift under
249
+ // a new name — the same misdirection in another shape (HR-101: a class that
250
+ // fires on ~90% of samples is a broken instrument, not a signal).
251
+ // Ceiling: holds while a rubric bump coincides with truncated evidence.
252
+ // Revisit-Trigger: the next rubric bump where every stored record's source
253
+ // events are still present — then a common-dimension diff carries signal.
254
+ if (values.json) {
255
+ writeStdoutLineSync(
256
+ JSON.stringify({
257
+ run_id: runId,
258
+ match: false,
259
+ verdict: 'version-mismatch',
260
+ stored_rubric_version: storedRubric,
261
+ engine_rubric_version: RUBRIC_VERSION,
262
+ }),
263
+ );
264
+ } else {
265
+ writeStdoutLineSync(
266
+ `VERSION-MISMATCH: ${runId} was scored under ${storedRubric ?? '(no rubric_version)'}; ` +
267
+ `this engine scores ${RUBRIC_VERSION} — not re-verifiable with this engine ` +
268
+ '(not drift, not a match).',
269
+ );
270
+ }
271
+ process.exit(EXIT_VERSION_MISMATCH);
272
+ }
273
+
214
274
  let fresh;
215
275
  try {
216
276
  fresh = evaluateSession({
@@ -236,7 +296,9 @@ function runVerify(runId, values) {
236
296
 
237
297
  if (diffs.length === 0) {
238
298
  if (values.json) {
239
- writeStdoutLineSync(JSON.stringify({ run_id: runId, match: true, dimensions: fresh.dimensions.length }));
299
+ writeStdoutLineSync(
300
+ JSON.stringify({ run_id: runId, match: true, verdict: 'match', dimensions: fresh.dimensions.length }),
301
+ );
240
302
  } else {
241
303
  writeStdoutLineSync(`MATCH: ${runId} re-evaluates identically across ${fresh.dimensions.length} dimension(s).`);
242
304
  }
@@ -244,7 +306,7 @@ function runVerify(runId, values) {
244
306
  }
245
307
 
246
308
  if (values.json) {
247
- writeStdoutLineSync(JSON.stringify({ run_id: runId, match: false, diffs }));
309
+ writeStdoutLineSync(JSON.stringify({ run_id: runId, match: false, verdict: 'drift', diffs }));
248
310
  } else {
249
311
  const out = [`DRIFT: ${runId} re-evaluation differs from the stored record:`];
250
312
  for (const d of diffs) {
@@ -14,8 +14,9 @@
14
14
  * research/hardware-patterns.md`), via the same `vault-integration.vault-dir`
15
15
  * resolution `scripts/archive-closed-prds.mjs` uses (findProjectRoot →
16
16
  * CLAUDE.md/AGENTS.md → parseSessionConfig; host-resolved: SO_VAULT_DIR env >
17
- * owner.yaml paths.vault-dir > committed default). Pass `--output <path>` to
18
- * override.
17
+ * owner.yaml vault-dirs: cwd match > owner.yaml paths.vault-dir > committed
18
+ * default, see `resolveVaultDir` in scripts/lib/config.mjs). Pass `--output <path>`
19
+ * to override.
19
20
  *
20
21
  * Idempotent: running without new data rewrites the same file byte-for-byte
21
22
  * modulo the generated-at line.
@@ -58,6 +59,7 @@ import {
58
59
  rewriteLearnings,
59
60
  CURRENT_ANONYMIZATION_VERSION,
60
61
  } from './lib/learnings.mjs';
62
+ import { withLearningsLock } from './lib/learnings/io.mjs';
61
63
  import { findProjectRoot, resolveInstructionFile, expandTilde } from './lib/common.mjs';
62
64
  import { parseSessionConfig } from './lib/config.mjs';
63
65
  import { createSecretValueMasker } from './lib/secret-masker.mjs';
@@ -74,7 +76,8 @@ const DEFAULT_VAULT_SUBPATH = path.join('01-projects', 'session-orchestrator', '
74
76
  * explicitly. Mirrors the vault-dir resolution `scripts/archive-closed-prds.mjs`
75
77
  * uses: findProjectRoot → CLAUDE.md/AGENTS.md → parseSessionConfig →
76
78
  * vault-integration.vault-dir (host-resolved: SO_VAULT_DIR env > owner.yaml
77
- * paths.vault-dir > committed default).
79
+ * vault-dirs: cwd match > owner.yaml paths.vault-dir > committed default, see
80
+ * `resolveVaultDir` in scripts/lib/config.mjs).
78
81
  *
79
82
  * @param {object} [opts]
80
83
  * @param {string} [opts.repoRoot] — override for findProjectRoot() (tests).
@@ -634,6 +637,22 @@ function parseArgs(argv) {
634
637
  * @returns {Promise<{promoted: number, skipped: number, flags: string[]}>}
635
638
  */
636
639
  export async function promoteHwLearnings(opts) {
640
+ // The read → rewrite below runs under the store lock (#1447 point 8): an
641
+ // append landing between them would vanish in the rename. A dry run writes
642
+ // nothing and takes no lock. A lock not acquired throws LearningsLockError
643
+ // (nothing written), which the CLI reports as `export failed: …`, exit 1.
644
+ if (opts.dryRun) return promoteHwLearningsUnlocked(opts);
645
+ return withLearningsLock(opts.input, () => promoteHwLearningsUnlocked(opts));
646
+ }
647
+
648
+ /**
649
+ * Body of {@link promoteHwLearnings}; the caller holds the store lock or runs
650
+ * a dry run.
651
+ *
652
+ * @param {Parameters<typeof promoteHwLearnings>[0]} opts
653
+ * @returns {Promise<{promoted: number, skipped: number, flags: string[]}>}
654
+ */
655
+ async function promoteHwLearningsUnlocked(opts) {
637
656
  const repoRoot = opts.repoRoot;
638
657
  const { entries, malformed } = await readLearnings(opts.input);
639
658