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,10 +6,18 @@
6
6
  * 1. `/evolve` learnings (`.orchestrator/metrics/learnings.jsonl` records)
7
7
  * 2. `claude-md-drift-check` output (`driftResult.errors[]`)
8
8
  *
9
- * This module is a PURE transform: it performs NO I/O. It never reads or writes
10
- * any file. Persistence and the `processed_at` / `superseded_by` lifecycle are
11
- * OWNED BY the sibling `idempotency.mjs` module — this module only emits the
12
- * raw candidates with those fields nulled out.
9
+ * This module WRITES nothing. Its only read is target resolution for the
10
+ * learnings feeder: at most one `git ls-files` per `extractCandidates` call
11
+ * (lazy — only when a learning survives the cheaper filters), plus a
12
+ * `realpathSync` containment check that applies with or without git. A
13
+ * learning becomes a candidate only when its extracted path resolves — symlinks
14
+ * followed — to a regular file inside the realpath of `repoRoot` that is also
15
+ * the ONE tracked file it names (tracking is skipped when git is unavailable);
16
+ * the kept `target_path` is the repo-relative form of that resolved file, so a
17
+ * bare basename becomes its full path and `scripts/../x.mjs` becomes `x.mjs`. Persistence and
18
+ * the `processed_at` / `superseded_by` lifecycle are OWNED BY the sibling
19
+ * `idempotency.mjs` module — this module only emits the raw candidates with
20
+ * those fields nulled out.
13
21
  *
14
22
  * The `id` field is a deterministic short hash of (source, target_path,
15
23
  * fingerprint), so the same input always yields the same id. That determinism
@@ -18,7 +26,10 @@
18
26
  * Part of Epic #643 → issue #647 (C2 auto-repair engine).
19
27
  */
20
28
 
29
+ import { execFileSync } from 'node:child_process';
21
30
  import { createHash } from 'node:crypto';
31
+ import { realpathSync, statSync } from 'node:fs';
32
+ import path from 'node:path';
22
33
 
23
34
  /**
24
35
  * @typedef {Object} RepairCandidate
@@ -96,6 +107,104 @@ function extractPath(text) {
96
107
  return match ? match[0] : null;
97
108
  }
98
109
 
110
+ /**
111
+ * Build the target resolver for ONE extraction run. `PATH_RE` also admits bare
112
+ * basenames (`engine.mjs`, `mcp.json`) that exist nowhere at the repo root, so
113
+ * an extracted path is a claim, not a target, until it resolves here.
114
+ *
115
+ * Resolution is fail-closed — `null` drops the candidate. In BOTH modes the
116
+ * path is resolved with `realpathSync` (every symlink followed) and must land
117
+ * on a regular file inside `realpathSync(repoRoot)`; any resolution error
118
+ * drops it. The kept `target_path` is the repo-relative POSIX form of that
119
+ * RESOLVED path, so `scripts/../CLAUDE.md` and `CLAUDE.md` name one target
120
+ * (and mint one id). Containment follows `classifyTarget` in
121
+ * `blast-radius-classifier.mjs`, plus the realpath of the target itself.
122
+ *
123
+ * With git, the resolved path must additionally be a tracked file:
124
+ * - a path containing `/` is resolved as given;
125
+ * - a bare basename must match exactly ONE tracked file (zero = unresolvable,
126
+ * two or more = ambiguous) and is resolved from that file's full path.
127
+ * A tracked symlink whose target lies outside the repo is therefore dropped.
128
+ *
129
+ * The tracked-file index is read lazily and at most once per resolver
130
+ * (`git ls-files -z` in `repoRoot`), so a run without a qualifying learning
131
+ * never spawns git. Without git (binary missing, not a repository) only the
132
+ * realpath containment applies — a bare basename then resolves only at the
133
+ * root, since nothing enumerates the tree.
134
+ *
135
+ * @param {string} repoRoot
136
+ * @returns {(extracted: string) => string|null}
137
+ */
138
+ function makeTargetResolver(repoRoot) {
139
+ /** @type {{ tracked: Set<string>, byBasename: Map<string, string[]> }|null|undefined} */
140
+ let index; // undefined = not read yet, null = git unavailable
141
+
142
+ // Canonicalise the root once, as blast-radius-classifier does: a symlinked
143
+ // repoRoot (macOS /var → /private/var) must still anchor the escape check.
144
+ let root;
145
+ try {
146
+ root = realpathSync(path.resolve(repoRoot));
147
+ } catch {
148
+ root = path.resolve(repoRoot);
149
+ }
150
+
151
+ function readIndex() {
152
+ try {
153
+ const out = execFileSync('git', ['ls-files', '-z'], {
154
+ cwd: repoRoot,
155
+ encoding: 'utf8',
156
+ stdio: ['ignore', 'pipe', 'ignore'],
157
+ maxBuffer: 64 * 1024 * 1024,
158
+ });
159
+ const tracked = new Set(out.split('\0').filter(Boolean));
160
+ /** @type {Map<string, string[]>} */
161
+ const byBasename = new Map();
162
+ for (const file of tracked) {
163
+ const base = file.slice(file.lastIndexOf('/') + 1);
164
+ const list = byBasename.get(base);
165
+ if (list) list.push(file);
166
+ else byBasename.set(base, [file]);
167
+ }
168
+ return { tracked, byBasename };
169
+ } catch {
170
+ return null;
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Realpath-resolve `rel` against the canonical root.
176
+ * @param {string} rel
177
+ * @returns {string|null} repo-relative POSIX path of the regular file it
178
+ * resolves to, or null when it escapes the root, is not a regular file, or
179
+ * does not resolve at all.
180
+ */
181
+ function resolveInsideRoot(rel) {
182
+ let real;
183
+ try {
184
+ real = realpathSync(path.resolve(root, rel));
185
+ if (!statSync(real).isFile()) return null;
186
+ } catch {
187
+ return null;
188
+ }
189
+ const back = path.relative(root, real);
190
+ if (back === '' || back.startsWith('..') || path.isAbsolute(back)) return null;
191
+ return back.split(path.sep).join('/');
192
+ }
193
+
194
+ return (extracted) => {
195
+ if (index === undefined) index = readIndex();
196
+ if (index === null) return resolveInsideRoot(extracted);
197
+ let candidate = extracted;
198
+ if (!extracted.includes('/')) {
199
+ const matches = index.byBasename.get(extracted) ?? [];
200
+ if (matches.length !== 1) return null;
201
+ candidate = matches[0];
202
+ }
203
+ const resolved = resolveInsideRoot(candidate);
204
+ return resolved !== null && index.tracked.has(resolved) ? resolved : null;
205
+ };
206
+ }
207
+
99
208
  /**
100
209
  * Map a single learning record to a RepairCandidate, or null when it fails any
101
210
  * actionable filter. learnings.jsonl records are NOT uniform — only `confidence`
@@ -104,9 +213,10 @@ function extractPath(text) {
104
213
  * @param {Record<string, unknown>} learning
105
214
  * @param {number} evidenceFloor
106
215
  * @param {string} nowIso
216
+ * @param {(extracted: string) => string|null} resolveTarget
107
217
  * @returns {RepairCandidate|null}
108
218
  */
109
- function learningToCandidate(learning, evidenceFloor, nowIso) {
219
+ function learningToCandidate(learning, evidenceFloor, nowIso, resolveTarget) {
110
220
  if (!learning || typeof learning !== 'object') return null;
111
221
 
112
222
  // Filter 1: confidence gate.
@@ -123,13 +233,18 @@ function learningToCandidate(learning, evidenceFloor, nowIso) {
123
233
  const subject = typeof learning.subject === 'string' ? learning.subject : '';
124
234
  const insight = typeof learning.insight === 'string' ? learning.insight : '';
125
235
 
126
- // Filter 2: resolvable target path from subject OR insight.
127
- const targetPath = extractPath(subject) ?? extractPath(insight);
128
- if (!targetPath) return null;
236
+ // Filter 2: a path-shaped token in subject OR insight.
237
+ const extracted = extractPath(subject) ?? extractPath(insight);
238
+ if (!extracted) return null;
129
239
 
130
240
  // Filter 3: insight must be prescriptive.
131
241
  if (!PRESCRIPTIVE_RE.test(insight)) return null;
132
242
 
243
+ // Filter 5: the token resolves to exactly one tracked file (last — it is the
244
+ // only filter that may spawn git).
245
+ const targetPath = resolveTarget(extracted);
246
+ if (!targetPath) return null;
247
+
133
248
  const source = 'evolve-learning';
134
249
  const sourceRef = typeof learning.id === 'string' && learning.id.length > 0
135
250
  ? learning.id
@@ -218,7 +333,9 @@ function driftErrorToCandidate(err, nowIso) {
218
333
 
219
334
  /**
220
335
  * Ingest the two repair-candidate feeders and return a normalised
221
- * `RepairCandidate[]`. Pure transform — performs no I/O.
336
+ * `RepairCandidate[]`. Writes nothing; learnings are resolved against the files
337
+ * tracked in `repoRoot` (see `makeTargetResolver`). Drift errors pass through
338
+ * unresolved — the checker reported them from a file it read.
222
339
  *
223
340
  * @param {Object} params
224
341
  * @param {Array<Record<string, unknown>>} [params.learnings] - `/evolve` learning records.
@@ -226,7 +343,10 @@ function driftErrorToCandidate(err, nowIso) {
226
343
  * claude-md-drift-check output. Only `errors[]` are mapped; `warnings[]`
227
344
  * are skipped. A null result, or a status of `skipped`/`skipped-mode-off`/
228
345
  * undefined, emits zero drift candidates.
229
- * @param {string} [params.repoRoot] - repo root (accepted for caller symmetry; unused in the pure transform).
346
+ * @param {string} [params.repoRoot] - repo root learning targets must resolve in
347
+ * (default `process.cwd()`). A learning is dropped unless its extracted
348
+ * path realpath-resolves to a regular file inside this root that is
349
+ * exactly one tracked file there (tracking skipped without git).
230
350
  * @param {number} [params.evidenceFloor=0.5] - minimum learning confidence to qualify.
231
351
  * @param {string} [params.now] - ISO timestamp for `created_at` + expiry checks (test determinism).
232
352
  * @returns {RepairCandidate[]}
@@ -234,13 +354,14 @@ function driftErrorToCandidate(err, nowIso) {
234
354
  export function extractCandidates({
235
355
  learnings,
236
356
  driftResult,
237
- // eslint-disable-next-line no-unused-vars -- accepted for caller symmetry; pure transform does no path resolution
238
357
  repoRoot,
239
358
  evidenceFloor = 0.5,
240
359
  now,
241
360
  } = {}) {
242
361
  const nowIso = typeof now === 'string' && now.length > 0 ? now : new Date().toISOString();
243
362
  const floor = Number.isFinite(evidenceFloor) ? evidenceFloor : 0.5;
363
+ const root = typeof repoRoot === 'string' && repoRoot.length > 0 ? repoRoot : process.cwd();
364
+ const resolveTarget = makeTargetResolver(root);
244
365
 
245
366
  /** @type {RepairCandidate[]} */
246
367
  const candidates = [];
@@ -248,7 +369,7 @@ export function extractCandidates({
248
369
  // Feeder 1: /evolve learnings.
249
370
  if (Array.isArray(learnings)) {
250
371
  for (const learning of learnings) {
251
- const candidate = learningToCandidate(learning, floor, nowIso);
372
+ const candidate = learningToCandidate(learning, floor, nowIso, resolveTarget);
252
373
  if (candidate) candidates.push(candidate);
253
374
  }
254
375
  }
@@ -8,7 +8,7 @@
8
8
  * — the matrix is the load-bearing deliverable, not the file-write mechanics.
9
9
  *
10
10
  * COMPOSITION (the five siblings, all DI-injectable via `opts`):
11
- * - candidate-intake.mjs → extractCandidates (pure transform)
11
+ * - candidate-intake.mjs → extractCandidates (no writes; resolves targets against repoRoot)
12
12
  * - idempotency.mjs → mergeCandidates / markProcessed / isProcessed
13
13
  * / loadCandidates (store I/O + supersession)
14
14
  * - blast-radius-classifier.mjs→ classifyTarget (R5 posture/gate triple)
@@ -17,7 +17,7 @@
17
17
  *
18
18
  * PIPELINE:
19
19
  * extractCandidates(learnings, driftResult)
20
- * → mergeCandidates (persist + supersede)
20
+ * → mergeCandidates (persist + supersede; skipped under dryRun)
21
21
  * → for each NON-processed candidate:
22
22
  * classify → decide per matrix → act
23
23
  *
@@ -583,7 +583,8 @@ async function finishOpenMr({ candidate, candidateId, targetPath, targetType, re
583
583
  * @param {Array<Record<string, unknown>>} [params.learnings] — `/evolve` learning records.
584
584
  * @param {{ status?: string, errors?: Array<Record<string, unknown>> }|null} [params.driftResult]
585
585
  * claude-md-drift-check output.
586
- * @param {boolean} [params.dryRun=false] — when true, no mutation/MR/stamp; previews only.
586
+ * @param {boolean} [params.dryRun=false] — when true, no mutation/MR/stamp and no
587
+ * candidate-store write (the skip is logged via `opts.log`); previews only.
587
588
  * @param {Object} [opts] — DI seams (all default to the real sibling functions).
588
589
  * @param {typeof realExtractCandidates} [opts.extractCandidates]
589
590
  * @param {typeof realMergeCandidates} [opts.mergeCandidates]
@@ -629,7 +630,7 @@ export async function runRepairEngine(
629
630
  const outcomes = [];
630
631
  const summary = { autonomousApplied: 0, mrsOpened: 0, advisories: 0, blocked: 0, total: 0 };
631
632
 
632
- // --- Pipeline step 1 — intake (pure transform) ---------------------------
633
+ // --- Pipeline step 1 — intake (no writes; repoRoot anchors target resolution)
633
634
  let candidates;
634
635
  try {
635
636
  candidates = seams.extractCandidates({
@@ -650,11 +651,19 @@ export async function runRepairEngine(
650
651
  }
651
652
 
652
653
  // --- Pipeline step 2 — merge (persist + supersede) -----------------------
653
- try {
654
- seams.mergeCandidates({ candidates, repoRoot });
655
- } catch (err) {
656
- seams.log('error', `engine: mergeCandidates threw — ${err?.message ?? err}`);
657
- // Persistence failure is non-fatal: continue with the in-memory candidates.
654
+ // A dry run previews from the in-memory candidates and never writes the store.
655
+ if (dryRun === true) {
656
+ seams.log(
657
+ 'info',
658
+ `engine: dry-run — candidate store write skipped (${candidates.length} candidate(s) not persisted)`,
659
+ );
660
+ } else {
661
+ try {
662
+ seams.mergeCandidates({ candidates, repoRoot });
663
+ } catch (err) {
664
+ seams.log('error', `engine: mergeCandidates threw — ${err?.message ?? err}`);
665
+ // Persistence failure is non-fatal: continue with the in-memory candidates.
666
+ }
658
667
  }
659
668
 
660
669
  // --- Pipeline step 3 — per-candidate classify → decide → act -------------
@@ -31,6 +31,7 @@
31
31
  * - estimateInputTokens(str) — char-count/4 heuristic
32
32
  * - checkBudget(estimated, budget) — verdict for the budget gate
33
33
  * - buildJudgePrompt(skills, tail, nonce) — pure prompt assembly (untrusted-data fence)
34
+ * - evidenceBudgetChars(skills, budget) — characters left for evidence after the prompt frame
34
35
  * - parseJudgeResponse(text) — extract one fenced ```json block, validate, drop malformed
35
36
  */
36
37
 
@@ -54,6 +55,9 @@ export const DEFAULT_BUDGET = Object.freeze({ input: 8000, output: 4000 });
54
55
  /** Industry-standard heuristic: ~4 chars per token for English prose. */
55
56
  const CHARS_PER_TOKEN = 4;
56
57
 
58
+ /** Hex length of the per-call untrusted-data nonce (`randomBytes(16)` → 32 hex). */
59
+ const NONCE_HEX_LENGTH = 32;
60
+
57
61
  // ---------------------------------------------------------------------------
58
62
  // Pure-function gates
59
63
  // ---------------------------------------------------------------------------
@@ -182,6 +186,25 @@ export function buildJudgePrompt(selectedSkills, transcriptTail, nonce) {
182
186
  ].join('\n');
183
187
  }
184
188
 
189
+ /**
190
+ * How many characters of evidence fit inside `budget.input` for THIS judged set.
191
+ *
192
+ * The caller must not have to guess: the prompt frame is MEASURED by building
193
+ * the very prompt that will be sent with an empty fence, so the answer tracks
194
+ * every future edit to `buildJudgePrompt` automatically. The nonce is
195
+ * length-accurate (`NONCE_HEX_LENGTH`) rather than the real random value, since
196
+ * only its length affects the frame.
197
+ *
198
+ * @param {string[]} selectedSkills — the judged set (its JSON is part of the frame)
199
+ * @param {{input?: number}} [budget]
200
+ * @returns {number} characters available for `evidence.text` (never negative)
201
+ */
202
+ export function evidenceBudgetChars(selectedSkills = [], budget = DEFAULT_BUDGET) {
203
+ const maxInput = typeof budget?.input === 'number' ? budget.input : DEFAULT_BUDGET.input;
204
+ const frame = buildJudgePrompt(selectedSkills, '', 'f'.repeat(NONCE_HEX_LENGTH)).length;
205
+ return Math.max(0, maxInput * CHARS_PER_TOKEN - frame);
206
+ }
207
+
185
208
  /**
186
209
  * Parse the judge response into validated judgment records. Extracts the FIRST
187
210
  * fenced ```json block, JSON.parses it, and validates each entry against
@@ -253,7 +276,7 @@ export function parseJudgeResponse(text) {
253
276
 
254
277
  /**
255
278
  * @typedef {Object} JudgeResult
256
- * @property {'ok' | 'empty-input' | 'budget-exceeded'} status
279
+ * @property {'ok' | 'empty-input' | 'no-evidence' | 'budget-exceeded'} status
257
280
  * @property {Array<{skill: string, applied: string, completed: string, confidence: number}>} judgments
258
281
  * @property {{input_tokens?: number, output_tokens?: number, estimated_input?: number}} [usage]
259
282
  * @property {string} [skipped_reason]
@@ -269,6 +292,7 @@ export function parseJudgeResponse(text) {
269
292
  *
270
293
  * Control flow (matches the ratified #645 L3 contract):
271
294
  * - empty selectedSkills → {status:'empty-input', judgments:[]} (no dispatch).
295
+ * - empty evidence text → {status:'no-evidence', judgments:[]} (no dispatch, #1399).
272
296
  * - budget exceeded → {status:'budget-exceeded', judgments:[]} (no dispatch,
273
297
  * NOT truncated — mirrors dialectic-deriver).
274
298
  * - else → dispatch → parse → {status:'ok', judgments, usage}.
@@ -277,7 +301,8 @@ export function parseJudgeResponse(text) {
277
301
  * @param {(args: {model: string, prompt: string, maxTokens: number}) => Promise<{text: string, usage?: {input_tokens?: number, output_tokens?: number}}>} opts.dispatchAgent — REQUIRED, injected DI boundary
278
302
  * @param {string} [opts.repoRoot]
279
303
  * @param {string|null} [opts.sessionId]
280
- * @param {string} [opts.transcriptTail]
304
+ * @param {{text?: string}} [opts.evidence] — preferred input; `buildSkillEvidence()` output (#1399)
305
+ * @param {string} [opts.transcriptTail] — legacy raw-string input; used when `evidence` is absent
281
306
  * @param {string[]} [opts.selectedSkills]
282
307
  * @param {'haiku'|'sonnet'|'opus'} [opts.model='haiku']
283
308
  * @param {{input: number, output: number}} [opts.budget]
@@ -293,6 +318,7 @@ export async function runSkillJudge({
293
318
  // `_`-prefixed names so the contract keys stay documented without tripping no-unused-vars.
294
319
  repoRoot: _repoRoot,
295
320
  sessionId: _sessionId,
321
+ evidence,
296
322
  transcriptTail = '',
297
323
  selectedSkills = [],
298
324
  model = 'haiku',
@@ -313,8 +339,24 @@ export async function runSkillJudge({
313
339
  return { status: 'empty-input', judgments: [], skipped_reason: 'no-selected-skills' };
314
340
  }
315
341
 
342
+ // Gate 2b (#1399): empty evidence — do NOT dispatch. Measured 2026-09-19 at
343
+ // `8f15f77b`: a prompt whose fence is empty is 1387 chars = 346 estimated
344
+ // tokens, so the budget gate below waves it through and the judge is asked to
345
+ // rule on a transcript it was never shown. An advisory verdict from no
346
+ // evidence is worse than no verdict — it looks exactly like a real one in
347
+ // `skill-judgments.jsonl`.
348
+ const evidenceText =
349
+ typeof evidence?.text === 'string' && evidence.text.trim()
350
+ ? evidence.text
351
+ : typeof transcriptTail === 'string'
352
+ ? transcriptTail
353
+ : '';
354
+ if (!evidenceText.trim()) {
355
+ return { status: 'no-evidence', judgments: [], skipped_reason: 'empty-transcript-evidence' };
356
+ }
357
+
316
358
  const nonce = randomNonce();
317
- const prompt = buildJudgePrompt(skills, transcriptTail, nonce);
359
+ const prompt = buildJudgePrompt(skills, evidenceText, nonce);
318
360
 
319
361
  // Gate 3: budget — fail-fast BEFORE dispatch when the prompt would exceed it.
320
362
  const estimatedInput = estimateInputTokens(prompt);
@@ -42,8 +42,16 @@
42
42
  * token_cache_creation integer | null — `usage.cache_creation_input_tokens` (v2+)
43
43
  * token_output integer | null — completion token count for this subagent
44
44
  * model string | null — model id from the transcript (v2+); null when
45
- * the transcript exposes none. Required to price
46
- * the record — see scripts/lib/telemetry/pricing.mjs.
45
+ * the transcript exposes none, or when its token-
46
+ * bearing turns used two or more models or one turn
47
+ * named none (#1470). Required to price the record —
48
+ * see scripts/lib/telemetry/pricing.mjs.
49
+ * models_usage array | absent — per-model breakdown of a record whose token-
50
+ * bearing turns used two or more models, every
51
+ * one named (#1470): [{ model, token_input_uncached,
52
+ * token_cache_read, token_cache_creation,
53
+ * token_output }], one entry per model. The session
54
+ * rollup prices each part; absent on every other record.
47
55
  * total_cost_usd number | null — native total cost in USD (#624, fractional,
48
56
  * best-effort: null when the harness does not
49
57
  * expose it; no rate table is applied)
@@ -115,7 +123,8 @@ export class ValidationError extends Error {
115
123
  *
116
124
  * Required fields: timestamp, event, agent_id, schema_version.
117
125
  * Additional requirement when event='stop': duration_ms (integer or null).
118
- * Optional: agent_type, parent_session_id, token_input, token_output.
126
+ * Optional: agent_type, parent_session_id, token_input, token_output, the cache
127
+ * buckets, model, models_usage, total_cost_usd and the OTel aliases.
119
128
  *
120
129
  * @param {object} entry
121
130
  * @param {object} [options]
@@ -233,6 +242,24 @@ export function validateSubagent(entry, options = {}) {
233
242
  throw new ValidationError('model must be a string or null', 'model');
234
243
  }
235
244
 
245
+ // models_usage (optional, #1470) — per-model parts of a mixed-model record.
246
+ // Each part is priced on its own, so each needs a model and four buckets.
247
+ if (entry.models_usage !== undefined && entry.models_usage !== null) {
248
+ if (!Array.isArray(entry.models_usage)) {
249
+ throw new ValidationError('models_usage must be an array or null', 'models_usage');
250
+ }
251
+ for (const part of entry.models_usage) {
252
+ const bucketsValid = ['token_input_uncached', 'token_cache_read', 'token_cache_creation', 'token_output']
253
+ .every((field) => Number.isInteger(part?.[field]) && part[field] >= 0);
254
+ if (typeof part?.model !== 'string' || part.model.length === 0 || !bucketsValid) {
255
+ throw new ValidationError(
256
+ 'models_usage entries must carry a model string and four non-negative integer token buckets',
257
+ 'models_usage'
258
+ );
259
+ }
260
+ }
261
+ }
262
+
236
263
  // total_cost_usd (optional, #624) — fractional number (not integer), best-effort.
237
264
  if (entry.total_cost_usd !== undefined && entry.total_cost_usd !== null) {
238
265
  if (typeof entry.total_cost_usd !== 'number' || !Number.isFinite(entry.total_cost_usd) || entry.total_cost_usd < 0) {
@@ -0,0 +1,56 @@
1
+ /**
2
+ * tail-window.mjs — the ONE bounded tail-window reader for append-only JSONL
3
+ * ledgers and transcripts (#1272).
4
+ *
5
+ * Five call sites used to carry their own copy of this byte-window primitive,
6
+ * and one of them (hooks/subagent-telemetry.mjs) ignored the returned read
7
+ * length and decoded a whole `allocUnsafe` buffer — on a short read that is
8
+ * uninitialised memory handed to JSON.parse. This module owns ONLY the byte
9
+ * window; every caller keeps its own error mapping (null / [] / discriminated
10
+ * result / throw) and its own line logic.
11
+ *
12
+ * Ceiling: the whole window is held in memory at once and decoded in one pass
13
+ * — fine for the windows in use today (64 KiB .. 1 MiB). Revisit if a caller
14
+ * needs a window above ~16 MiB: stream backwards in chunks instead.
15
+ */
16
+
17
+ import fs from 'node:fs';
18
+
19
+ /**
20
+ * Read the last `maxBytes` of `file` as UTF-8.
21
+ *
22
+ * `cut` is true when the window did not start at byte 0 — the first line of
23
+ * `text` is then (or may be) a fragment of a record, possibly starting inside
24
+ * a multi-byte UTF-8 sequence, and callers that parse lines should drop it.
25
+ * Loops until the window is filled or the file reports EOF, so a short read
26
+ * never leaves undecoded buffer bytes in `text`.
27
+ *
28
+ * THROWS on any fs error (ENOENT, EACCES, …) — mapping the failure is the
29
+ * caller's decision.
30
+ *
31
+ * @param {string} file
32
+ * @param {number} maxBytes window size in bytes (> 0)
33
+ * @returns {{ text: string, cut: boolean, size: number }} `size` is the file size at open time
34
+ */
35
+ export function readTailWindow(file, maxBytes) {
36
+ const fd = fs.openSync(file, 'r');
37
+ try {
38
+ const size = fs.fstatSync(fd).size;
39
+ const want = Math.min(size, maxBytes);
40
+ const start = size - want;
41
+ const buf = Buffer.allocUnsafe(want);
42
+ let read = 0;
43
+ while (read < want) {
44
+ const n = fs.readSync(fd, buf, read, want - read, start + read);
45
+ if (n <= 0) break;
46
+ read += n;
47
+ }
48
+ return { text: buf.subarray(0, read).toString('utf8'), cut: start > 0, size };
49
+ } finally {
50
+ try {
51
+ fs.closeSync(fd);
52
+ } catch {
53
+ /* best-effort */
54
+ }
55
+ }
56
+ }