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
@@ -105,14 +105,14 @@
105
105
  */
106
106
 
107
107
  import { shouldRunHook } from './_lib/profile-gate.mjs';
108
- // Exit 0 immediately when disabled via SO_HOOK_PROFILE / SO_DISABLED_HOOKS.
109
- if (!shouldRunHook('subagent-telemetry')) process.exit(0);
108
+ import { isMainModule } from '../scripts/lib/is-main-module.mjs';
110
109
 
111
110
  import fs from 'node:fs';
112
111
  import path from 'node:path';
113
112
  import { appendSubagent } from '../scripts/lib/subagents-schema.mjs';
114
113
  import { getProjectDir } from '../scripts/lib/platform.mjs';
115
114
  import { resolveSubagentSidecar } from './_lib/subagent-paths.mjs';
115
+ import { readTailWindow } from '../scripts/lib/tail-window.mjs';
116
116
 
117
117
  // ---------------------------------------------------------------------------
118
118
  // Constants
@@ -133,7 +133,16 @@ function jsonlPath() {
133
133
  * Defense-in-depth byte ceiling for transcript reads (#624). The transcript path
134
134
  * is harness-trusted, so this is not an exploitable vector — but the repo follows
135
135
  * a bounded-read discipline: a pathologically large transcript is skipped (yields
136
- * the empty/zero-usage result) rather than read whole into memory.
136
+ * the null-usage result) rather than read whole into memory.
137
+ *
138
+ * CEILING: a skipped transcript yields a stop record with found=true but null
139
+ * tokens and model; the session rollup counts it as an unpriced cost candidate
140
+ * (#1474) instead of reading it. Frequency per issue #1474 (2026-09-29): 2 of
141
+ * 10,482 subagent transcripts exceeded this size. REVISIT when oversized
142
+ * transcripts exceed ~0.1% of found stop records — count them from transcript
143
+ * sizes, because the ledger writes the same token-less record for an oversized,
144
+ * a usage-free and an unreadable transcript — then read large transcripts
145
+ * streamed instead of skipping them.
137
146
  */
138
147
  const MAX_TRANSCRIPT_BYTES = 50 * 1024 * 1024; // ~50 MB
139
148
 
@@ -423,13 +432,23 @@ function readStdinJson() {
423
432
  * Four buckets, not two (#1244 / schema_version 2): `input_tokens`,
424
433
  * `cache_read_input_tokens`, `cache_creation_input_tokens` and `output_tokens`
425
434
  * are accumulated SEPARATELY per deduped turn, because they are billed at three
426
- * different rates. `model` is captured from `message.model` of the last kept
427
- * block that carries one (null when absent) — it is what makes a cost estimate
428
- * possible at all downstream.
435
+ * different rates. `model` is what makes a cost estimate possible at all
436
+ * downstream, so it names only a model that actually carried tokens (#1470): a
437
+ * kept turn is token-bearing when its four clamped buckets sum to > 0. Exactly
438
+ * one distinct `message.model` across the token-bearing turns → that model;
439
+ * zero-token turns of another model (a trailing `<synthetic>` turn) are ignored.
440
+ * Two or more, or a token-bearing turn that names no model → null, an honest
441
+ * unknown, with the token sums unchanged. No token-bearing turn at all → the
442
+ * last kept block that carries one (null when absent). Two or more models with
443
+ * every token-bearing turn named → `modelsUsage` carries the four buckets per
444
+ * model, so the session rollup can price each part; otherwise it is null.
429
445
  *
430
446
  * @param {string|undefined|null} transcriptPath — absolute path from stdin
431
447
  * @returns {{ tokenInputUncached: number|null, tokenCacheRead: number|null,
432
- * tokenCacheCreation: number|null, tokenOutput: number|null, model: string|null }}
448
+ * tokenCacheCreation: number|null, tokenOutput: number|null, model: string|null,
449
+ * modelsUsage: Array<{ model: string, token_input_uncached: number,
450
+ * token_cache_read: number, token_cache_creation: number,
451
+ * token_output: number }>|null }}
433
452
  */
434
453
  function extractTranscriptUsage(transcriptPath) {
435
454
  const nullResult = {
@@ -438,6 +457,7 @@ function extractTranscriptUsage(transcriptPath) {
438
457
  tokenCacheCreation: null,
439
458
  tokenOutput: null,
440
459
  model: null,
460
+ modelsUsage: null,
441
461
  };
442
462
  try {
443
463
  if (typeof transcriptPath !== 'string' || !transcriptPath.trim()) return nullResult;
@@ -523,22 +543,73 @@ function extractTranscriptUsage(transcriptPath) {
523
543
  let tokenCacheRead = 0;
524
544
  let tokenCacheCreation = 0;
525
545
  let tokenOutput = 0;
546
+ /** Last non-null model over ALL kept turns — the zero-token fallback. */
526
547
  let model = null;
548
+ /** Per-model bucket sums of the token-bearing kept turns (#1470). */
549
+ const billedModels = new Map();
550
+ /** A token-bearing turn that names no model: its price is unknowable. */
551
+ let unattributed = false;
552
+ // Per-turn clamp (#624): a turn's value counts ONLY when it is a non-negative
553
+ // integer. A poisoned value (negative, NaN, float like 10.5) contributes 0 so
554
+ // the good turns survive. An absent side contributes 0, not null.
555
+ const clamp = (value) => (Number.isInteger(value) && value >= 0 ? value : 0);
527
556
  for (const { usage, model: turnModel } of kept) {
528
- // Per-turn clamp (#624): add a turn's value ONLY when it is a non-negative
529
- // integer. A poisoned value (negative, NaN, float like 10.5) is skipped so
530
- // the good turns survive. An absent side contributes 0, not null.
531
- const inTok = usage.input_tokens;
532
- const cacheRead = usage.cache_read_input_tokens;
533
- const cacheCreation = usage.cache_creation_input_tokens;
534
- const outTok = usage.output_tokens;
535
- if (Number.isInteger(inTok) && inTok >= 0) tokenInputUncached += inTok;
536
- if (Number.isInteger(cacheRead) && cacheRead >= 0) tokenCacheRead += cacheRead;
537
- if (Number.isInteger(cacheCreation) && cacheCreation >= 0) tokenCacheCreation += cacheCreation;
538
- if (Number.isInteger(outTok) && outTok >= 0) tokenOutput += outTok;
539
- if (turnModel !== null) model = turnModel;
557
+ const inTok = clamp(usage.input_tokens);
558
+ const cacheRead = clamp(usage.cache_read_input_tokens);
559
+ const cacheCreation = clamp(usage.cache_creation_input_tokens);
560
+ const outTok = clamp(usage.output_tokens);
561
+ tokenInputUncached += inTok;
562
+ tokenCacheRead += cacheRead;
563
+ tokenCacheCreation += cacheCreation;
564
+ tokenOutput += outTok;
565
+ const billed = inTok + cacheRead + cacheCreation + outTok > 0;
566
+ if (turnModel === null) {
567
+ if (billed) unattributed = true;
568
+ continue;
569
+ }
570
+ model = turnModel;
571
+ if (!billed) continue;
572
+ let part = billedModels.get(turnModel);
573
+ if (part === undefined) {
574
+ part = {
575
+ model: turnModel,
576
+ token_input_uncached: 0,
577
+ token_cache_read: 0,
578
+ token_cache_creation: 0,
579
+ token_output: 0,
580
+ };
581
+ billedModels.set(turnModel, part);
582
+ }
583
+ part.token_input_uncached += inTok;
584
+ part.token_cache_read += cacheRead;
585
+ part.token_cache_creation += cacheCreation;
586
+ part.token_output += outTok;
540
587
  }
541
588
 
589
+ // Model attribution (#1470): only a token-bearing turn is an observation of
590
+ // its model; one distinct model → that model, two or more → null, none →
591
+ // the fallback above. A token-bearing turn that names no model also gives
592
+ // null: its tokens must not be priced at whichever other model was named.
593
+ // A mixed-model record carries its per-model sums as `modelsUsage`, which
594
+ // the session rollup prices part by part (#1470). Measured 2026-09-29 over
595
+ // 10,745 subagent transcripts under `~/.claude/projects`: 17 (0.16%)
596
+ // contained more than one model on token-bearing turns (0 of 917 in this
597
+ // repo), while 273 contained more than one model id only through zero-token
598
+ // `<synthetic>` turns, which this rule ignores. A model-less token-bearing
599
+ // turn gets no breakdown: its tokens belong to no part, so the record stays
600
+ // unpriced. The ledger now tells a mixed-model null (`models_usage`
601
+ // present) from a missing model (absent). The fallback also names the LAST
602
+ // model of a transcript whose turns all carry 0 tokens — measured 2026-09-29,
603
+ // 31 of 10,479 records, all `<synthetic>`; the session rollup prices a record
604
+ // whose four buckets are all 0 at $0 regardless of its model (#1474), so that
605
+ // name is harmless.
606
+ let modelsUsage = null;
607
+ if (unattributed) model = null;
608
+ else if (billedModels.size > 1) {
609
+ model = null;
610
+ modelsUsage = [...billedModels.values()];
611
+ } else if (billedModels.size === 1) [model] = billedModels.keys();
612
+
542
613
  // The aggregates are guaranteed non-negative integers by per-turn clamping
543
614
  // above (Σ of non-negative integers), so emit them directly.
544
615
  return {
@@ -547,6 +618,7 @@ function extractTranscriptUsage(transcriptPath) {
547
618
  tokenCacheCreation,
548
619
  tokenOutput,
549
620
  model,
621
+ modelsUsage,
550
622
  };
551
623
  } catch {
552
624
  return nullResult;
@@ -593,25 +665,18 @@ function resolveSubagentTranscriptPath(parentTranscriptPath, agentId) {
593
665
  * @returns {number|null} epoch-ms of the matching start, or null
594
666
  */
595
667
  function findStartTimestampMs(filePath, agentId) {
596
- let fd;
597
668
  try {
598
669
  if (typeof agentId !== 'string' || !agentId.trim()) return null;
599
670
  if (!fs.existsSync(filePath)) return null;
600
671
 
601
- const { size } = fs.statSync(filePath);
602
- if (size === 0) return null;
672
+ const window = readTailWindow(filePath, START_JOIN_TAIL_BYTES);
673
+ if (window.size === 0) return null;
603
674
 
604
- const readLen = Math.min(size, START_JOIN_TAIL_BYTES);
605
- const from = size - readLen;
606
- const buf = Buffer.allocUnsafe(readLen);
607
- fd = fs.openSync(filePath, 'r');
608
- fs.readSync(fd, buf, 0, readLen, from);
609
-
610
- const lines = buf.toString('utf8').split('\n');
675
+ const lines = window.text.split('\n');
611
676
  // When the window does not cover the whole file, the first element is a
612
677
  // record sliced mid-line (possibly mid-UTF-8-sequence). Drop it rather than
613
678
  // feed a corrupt fragment to JSON.parse.
614
- if (from > 0) lines.shift();
679
+ if (window.cut) lines.shift();
615
680
 
616
681
  for (let i = lines.length - 1; i >= 0; i--) {
617
682
  const line = lines[i].trim();
@@ -631,10 +696,6 @@ function findStartTimestampMs(filePath, agentId) {
631
696
  return null;
632
697
  } catch {
633
698
  return null;
634
- } finally {
635
- if (fd !== undefined) {
636
- try { fs.closeSync(fd); } catch { /* ignore */ }
637
- }
638
699
  }
639
700
  }
640
701
 
@@ -763,7 +824,7 @@ async function main() {
763
824
  // NO fallback to input.transcript_path: that path is the parent session
764
825
  // transcript, and reading it is the #949 defect (every stop inherited the
765
826
  // parent's running totals). A phantom stop gets null — the honest value.
766
- const { tokenInputUncached, tokenCacheRead, tokenCacheCreation, tokenOutput, model } =
827
+ const { tokenInputUncached, tokenCacheRead, tokenCacheCreation, tokenOutput, model, modelsUsage } =
767
828
  subagentTranscriptFound
768
829
  ? extractTranscriptUsage(subagentTranscriptPath)
769
830
  : {
@@ -772,6 +833,7 @@ async function main() {
772
833
  tokenCacheCreation: null,
773
834
  tokenOutput: null,
774
835
  model: null,
836
+ modelsUsage: null,
775
837
  };
776
838
 
777
839
  // schema_version 2 (#1244): `token_input` is now BILLABLE PROMPT VOLUME —
@@ -787,11 +849,15 @@ async function main() {
787
849
  }
788
850
  if (tokenOutput !== null) record.token_output = tokenOutput;
789
851
 
790
- // Model id (#1244) — null when the transcript exposes none. Cost is NOT
852
+ // Model id (#1244) — null when the transcript exposes none, or when its
853
+ // token-bearing turns are not attributable to one model (#1470). Cost is NOT
791
854
  // computed here: pricing lives in scripts/lib/telemetry/pricing.mjs and is
792
855
  // applied by the session rollup, so this hot-path hook keeps its import
793
856
  // graph unchanged.
794
857
  record.model = model;
858
+ // Per-model breakdown (#1470) — written only for a mixed-model record, so
859
+ // the rollup can price it part by part; absent on every other record.
860
+ if (modelsUsage !== null) record.models_usage = modelsUsage;
795
861
 
796
862
  // Cost is best-effort / forward-compat (#624): the native transcript does
797
863
  // NOT expose total_cost_usd today, so this is null in practice. No rate
@@ -815,5 +881,12 @@ async function main() {
815
881
  await appendSubagent(jsonlPath(), record);
816
882
  }
817
883
 
818
- // Exit 0 always — informational hook must never block Claude.
819
- main().catch(() => {}).finally(() => process.exit(0));
884
+ // Entry guard (#1393): run only as the node script the harness execs — a bare
885
+ // `import()` must run no handler and must not exit the importing process.
886
+ if (isMainModule(import.meta.url)) {
887
+ // Exit 0 immediately when disabled via SO_HOOK_PROFILE / SO_DISABLED_HOOKS.
888
+ if (!shouldRunHook('subagent-telemetry')) process.exit(0);
889
+
890
+ // Exit 0 always — informational hook must never block Claude.
891
+ main().catch(() => {}).finally(() => process.exit(0));
892
+ }
@@ -41,63 +41,7 @@ import { join } from 'node:path';
41
41
  // consumer repo or a test tmp-dir.
42
42
  import { pathMatchesPattern, findScopeFile } from '../scripts/lib/hardening.mjs';
43
43
  import { withStagingFenceLock } from '../scripts/lib/session-lock.mjs';
44
-
45
- const repoRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();
46
- // #801: resolve wave-scope.json via the same precedence every other reader
47
- // uses (.pi/.cursor/.codex/.claude — scope-gate.mjs findScopeFile). The prior
48
- // hardcoded `.orchestrator/wave-scope.json` path was DEAD — the coordinator
49
- // writes wave-scope.json to the state dir (.claude/ on Claude Code; see
50
- // skills/wave-executor/wave-loop.md), so this guard never fired. findScopeFile
51
- // returns null when no scope file exists at any precedence dir, preserving
52
- // the "no active wave → exit 0" semantics below.
53
- const scopePath = findScopeFile(repoRoot);
54
- const fenceDir = join(repoRoot, '.orchestrator', 'staging-fence');
55
-
56
- // ---------------------------------------------------------------------------
57
- // Sub-mode B — allowedPaths check
58
- // ---------------------------------------------------------------------------
59
-
60
- const stagedOutput = execSync('git diff --cached --name-only', { encoding: 'utf8' });
61
- const stagedFiles = stagedOutput.split('\n').filter(Boolean);
62
-
63
- if (scopePath) {
64
- let scope;
65
- try {
66
- scope = JSON.parse(readFileSync(scopePath, 'utf8'));
67
- } catch (err) {
68
- process.stderr.write(`wave-scope-commit-guard: failed to parse wave-scope.json: ${err.message}\n`);
69
- process.exit(1);
70
- }
71
-
72
- const allowedPaths = Array.isArray(scope.allowedPaths) ? scope.allowedPaths : [];
73
- if (allowedPaths.length > 0) {
74
- const violations = stagedFiles.filter(
75
- (f) => !allowedPaths.some((pattern) => pathMatchesPattern(f, pattern)),
76
- );
77
-
78
- if (violations.length > 0) {
79
- process.stderr.write('✗ wave-scope-commit-guard: staged paths outside wave-scope.allowedPaths:\n');
80
- for (const v of violations) process.stderr.write(` - ${v}\n`);
81
- process.stderr.write('\nThese files were likely added by lint-staged eslint --fix / prettier --write.\n');
82
- process.stderr.write('To proceed:\n');
83
- process.stderr.write(' 1) git restore --staged <path> # for each foreign path\n');
84
- process.stderr.write(' 2) git commit # retry\n');
85
- process.exit(1);
86
- }
87
- }
88
- }
89
-
90
- // ---------------------------------------------------------------------------
91
- // Sub-mode C — cross-agent staging-fence reconciliation (issue #552)
92
- // ---------------------------------------------------------------------------
93
- //
94
- // Skip entirely when no fence dir exists OR no staged files. The fence dir
95
- // only appears when a wave-agent invoked `git add` while SO_WAVE_AGENT=1.
96
- // Manual / coordinator commits never write a fence file, so this branch
97
- // short-circuits to exit 0 for them (AC5 safe-default).
98
- if (!existsSync(fenceDir) || stagedFiles.length === 0) {
99
- process.exit(0);
100
- }
44
+ import { isMainModule } from '../scripts/lib/is-main-module.mjs';
101
45
 
102
46
  // Resolve the "current" agent id from SO_WAVE_AGENT_ID (when set by the
103
47
  // caller) or fall back to a PID-derived marker. The fence files themselves
@@ -105,11 +49,20 @@ if (!existsSync(fenceDir) || stagedFiles.length === 0) {
105
49
  // fence entries (we cross-check against SIBLINGS, not ourselves).
106
50
  const ownAgentId = process.env.SO_WAVE_AGENT_ID ?? null;
107
51
 
52
+ /**
53
+ * Recorded by the writer when a staging command stages a set no path operand
54
+ * names (`git add -A`, `git add .`, `git add -u`). Overlaps everything.
55
+ * Mirrors ALL_PATHS_MARKER in hooks/pre-bash-staging-fence.mjs.
56
+ */
57
+ const ALL_PATHS_MARKER = '*';
58
+
108
59
  /**
109
60
  * Build a regex that finds a staged path inside a `git add` command string.
110
61
  * Word-boundary on both sides so `src/foo.ts` does not match `src/foo.ts.bak`.
111
62
  * The path is escaped so glob metacharacters (`*`, `?`) and shell metas
112
63
  * cannot be reinterpreted.
64
+ *
65
+ * LEGACY PATH ONLY since #1404 — see the fallback branch in findOverlaps.
113
66
  */
114
67
  function pathRegex(p) {
115
68
  const escaped = p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -117,8 +70,56 @@ function pathRegex(p) {
117
70
  }
118
71
 
119
72
  /**
120
- * Walk a single sibling fence file and return the staged paths whose path
121
- * pattern matches any of our staged files.
73
+ * Normalise a path token so reader and writer compare the same spelling.
74
+ *
75
+ * DELIBERATE DUPLICATE of the identically-named function in
76
+ * hooks/pre-bash-staging-fence.mjs: this guard is a husky pre-commit hook
77
+ * outside the Claude-Code hook import set, and the two sides must agree byte
78
+ * for byte. Change one, change both.
79
+ *
80
+ * @param {string} raw
81
+ * @returns {string}
82
+ */
83
+ function normalizeStagedPath(raw) {
84
+ let p = String(raw).trim();
85
+ while (p.startsWith('./')) p = p.slice(2);
86
+ p = p.replace(/\/{2,}/g, '/');
87
+ while (p.length > 1 && p.endsWith('/')) p = p.slice(0, -1);
88
+ return p;
89
+ }
90
+
91
+ /**
92
+ * Does a path a sibling recorded overlap one of OUR staged files? Both sides
93
+ * arrive already normalised. A DIRECTORY entry overlaps every file beneath it
94
+ * (`src` overlaps `src/foo.ts`); the marker overlaps everything.
95
+ *
96
+ * @param {string} fencePath
97
+ * @param {string} ourPath
98
+ * @returns {boolean}
99
+ */
100
+ function pathsOverlap(fencePath, ourPath) {
101
+ if (fencePath === ALL_PATHS_MARKER) return true;
102
+ if (fencePath === ourPath) return true;
103
+ return ourPath.startsWith(`${fencePath}/`);
104
+ }
105
+
106
+ /**
107
+ * Walk a single sibling fence file and return the staged paths a sibling
108
+ * agent also recorded an intent to stage.
109
+ *
110
+ * Two entry shapes are accepted:
111
+ * - #1404 shape `{ paths, command_hash, timestamp }` — path LISTS compared
112
+ * against our staged set.
113
+ * - LEGACY shape `{ command, timestamp }` — a fence file written by a
114
+ * pre-#1404 hook version. Live sessions on this host may hold such files
115
+ * RIGHT NOW, and dropping them would read as "no overlap" — silently
116
+ * weaker than before the change. So the old raw-command regex still runs
117
+ * for an entry that has `command` and no `paths`.
118
+ * NAMED CEILING (BV-004): this fallback exists only to span the sessions
119
+ * running across the upgrade. REVISIT TRIGGER — remove it one minor
120
+ * release after 5.3.0 (i.e. in 5.4.0), by which point no process started
121
+ * before the upgrade can still be writing fence files. The command text it
122
+ * reads is never PRINTED (issue #1404) — only the fact of the match is.
122
123
  */
123
124
  function findOverlaps(fenceJsonPath, ourStaged) {
124
125
  let body;
@@ -131,61 +132,157 @@ function findOverlaps(fenceJsonPath, ourStaged) {
131
132
  if (!Array.isArray(body.staged_paths)) return [];
132
133
  if (ownAgentId && body.agent_id === ownAgentId) return []; // skip self
133
134
 
135
+ const siblingAgent = body.agent_id ?? '<unknown>';
134
136
  const matches = [];
135
137
  for (const entry of body.staged_paths) {
138
+ if (Array.isArray(entry?.paths)) {
139
+ const hash = typeof entry.command_hash === 'string' ? entry.command_hash : '<no-hash>';
140
+ for (const raw of entry.paths) {
141
+ if (typeof raw !== 'string') continue;
142
+ const fencePath = normalizeStagedPath(raw);
143
+ for (const ours of ourStaged) {
144
+ if (pathsOverlap(fencePath, normalizeStagedPath(ours))) {
145
+ matches.push({ ourPath: ours, siblingAgent, fencePath, hash });
146
+ }
147
+ }
148
+ }
149
+ continue;
150
+ }
151
+
136
152
  const cmd = entry?.command;
137
153
  if (typeof cmd !== 'string') continue;
138
154
  for (const ours of ourStaged) {
139
155
  if (pathRegex(ours).test(cmd)) {
140
- matches.push({ ourPath: ours, siblingAgent: body.agent_id ?? '<unknown>', cmd });
156
+ matches.push({
157
+ ourPath: ours,
158
+ siblingAgent,
159
+ fencePath: '<legacy entry, pre-#1404>',
160
+ hash: '<legacy entry, pre-#1404>',
161
+ });
141
162
  }
142
163
  }
143
164
  }
144
165
  return matches;
145
166
  }
146
167
 
147
- let overlaps = [];
148
-
149
- try {
150
- await withStagingFenceLock(
151
- repoRoot,
152
- async () => {
153
- let entries;
154
- try {
155
- entries = readdirSync(fenceDir);
156
- } catch {
157
- return;
158
- }
159
- for (const name of entries) {
160
- if (!name.endsWith('.json')) continue;
161
- if (name.startsWith('.')) continue; // skip .commit.lock, tmp files
162
- const overlapsFromFile = findOverlaps(join(fenceDir, name), stagedFiles);
163
- overlaps = overlaps.concat(overlapsFromFile);
168
+ /**
169
+ * Both sub-modes, in the order the pre-commit hook needs them. Every statement
170
+ * here used to sit at module top level, which meant a bare `import()` of this
171
+ * file ran `git rev-parse` / `git diff --cached` and could exit the IMPORTING
172
+ * process with 1 (#1393). Moved into a function so the entry guard below can
173
+ * gate it; the logic and its exit codes are unchanged.
174
+ */
175
+ async function main() {
176
+ const repoRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();
177
+ // #801: resolve wave-scope.json via the same precedence every other reader
178
+ // uses (.pi/.cursor/.codex/.claude — scope-gate.mjs findScopeFile). The prior
179
+ // hardcoded `.orchestrator/wave-scope.json` path was DEAD — the coordinator
180
+ // writes wave-scope.json to the state dir (.claude/ on Claude Code; see
181
+ // skills/wave-executor/wave-loop.md), so this guard never fired. findScopeFile
182
+ // returns null when no scope file exists at any precedence dir, preserving
183
+ // the "no active wave → exit 0" semantics below.
184
+ const scopePath = findScopeFile(repoRoot);
185
+ const fenceDir = join(repoRoot, '.orchestrator', 'staging-fence');
186
+
187
+ // -------------------------------------------------------------------------
188
+ // Sub-mode B — allowedPaths check
189
+ // -------------------------------------------------------------------------
190
+
191
+ const stagedOutput = execSync('git diff --cached --name-only', { encoding: 'utf8' });
192
+ const stagedFiles = stagedOutput.split('\n').filter(Boolean);
193
+
194
+ if (scopePath) {
195
+ let scope;
196
+ try {
197
+ scope = JSON.parse(readFileSync(scopePath, 'utf8'));
198
+ } catch (err) {
199
+ process.stderr.write(`wave-scope-commit-guard: failed to parse wave-scope.json: ${err.message}\n`);
200
+ process.exit(1);
201
+ }
202
+
203
+ const allowedPaths = Array.isArray(scope.allowedPaths) ? scope.allowedPaths : [];
204
+ if (allowedPaths.length > 0) {
205
+ const violations = stagedFiles.filter(
206
+ (f) => !allowedPaths.some((pattern) => pathMatchesPattern(f, pattern)),
207
+ );
208
+
209
+ if (violations.length > 0) {
210
+ process.stderr.write('✗ wave-scope-commit-guard: staged paths outside wave-scope.allowedPaths:\n');
211
+ for (const v of violations) process.stderr.write(` - ${v}\n`);
212
+ process.stderr.write('\nThese files were likely added by lint-staged eslint --fix / prettier --write.\n');
213
+ process.stderr.write('To proceed:\n');
214
+ process.stderr.write(' 1) git restore --staged <path> # for each foreign path\n');
215
+ process.stderr.write(' 2) git commit # retry\n');
216
+ process.exit(1);
164
217
  }
165
- },
166
- { timeoutMs: 5000 },
167
- );
168
- } catch (err) {
169
- // Lock acquisition failed — emit a warning but do NOT block the commit.
170
- // The race-detection layer is opportunistic; a lock-acquire timeout is
171
- // strictly less severe than blocking a legitimate commit on flaky FS.
172
- process.stderr.write(
173
- `⚠ wave-scope-commit-guard: staging-fence lock failed — ${err?.message ?? err}\n`,
174
- );
175
- process.exit(0);
176
- }
218
+ }
219
+ }
177
220
 
178
- if (overlaps.length > 0) {
179
- process.stderr.write('✗ wave-scope-commit-guard: staging-fence: cross-agent overlap detected:\n');
180
- for (const o of overlaps) {
181
- process.stderr.write(` - ${o.ourPath} (also staged by sibling agent ${o.siblingAgent})\n`);
221
+ // -------------------------------------------------------------------------
222
+ // Sub-mode C — cross-agent staging-fence reconciliation (issue #552)
223
+ // -------------------------------------------------------------------------
224
+ //
225
+ // Skip entirely when no fence dir exists OR no staged files. The fence dir
226
+ // only appears when a wave-agent invoked `git add` while SO_WAVE_AGENT=1.
227
+ // Manual / coordinator commits never write a fence file, so this branch
228
+ // short-circuits to exit 0 for them (AC5 safe-default).
229
+ if (!existsSync(fenceDir) || stagedFiles.length === 0) {
230
+ process.exit(0);
182
231
  }
183
- process.stderr.write('\nAnother wave-agent recorded a `git add` for one or more of your staged paths.\n');
184
- process.stderr.write('To proceed:\n');
185
- process.stderr.write(' 1) Coordinate with the sibling agent OR\n');
186
- process.stderr.write(' 2) git restore --staged <path> # for each conflicting path, then retry\n');
187
- process.stderr.write(' 3) git commit --no-verify # bypass (PSA-001/PSA-003 risk — operator opt-out)\n');
188
- process.exit(1);
232
+
233
+ let overlaps = [];
234
+
235
+ try {
236
+ await withStagingFenceLock(
237
+ repoRoot,
238
+ async () => {
239
+ let entries;
240
+ try {
241
+ entries = readdirSync(fenceDir);
242
+ } catch {
243
+ return;
244
+ }
245
+ for (const name of entries) {
246
+ if (!name.endsWith('.json')) continue;
247
+ if (name.startsWith('.')) continue; // skip .commit.lock, tmp files
248
+ const overlapsFromFile = findOverlaps(join(fenceDir, name), stagedFiles);
249
+ overlaps = overlaps.concat(overlapsFromFile);
250
+ }
251
+ },
252
+ { timeoutMs: 5000 },
253
+ );
254
+ } catch (err) {
255
+ // Lock acquisition failed — emit a warning but do NOT block the commit.
256
+ // The race-detection layer is opportunistic; a lock-acquire timeout is
257
+ // strictly less severe than blocking a legitimate commit on flaky FS.
258
+ process.stderr.write(
259
+ `⚠ wave-scope-commit-guard: staging-fence lock failed — ${err?.message ?? err}\n`,
260
+ );
261
+ process.exit(0);
262
+ }
263
+
264
+ if (overlaps.length > 0) {
265
+ process.stderr.write('✗ wave-scope-commit-guard: staging-fence: cross-agent overlap detected:\n');
266
+ for (const o of overlaps) {
267
+ process.stderr.write(
268
+ ` - ${o.ourPath} (sibling agent ${o.siblingAgent} recorded ${o.fencePath}, command ${o.hash})\n`,
269
+ );
270
+ }
271
+ process.stderr.write('\nAnother wave-agent recorded a `git add` for one or more of your staged paths.\n');
272
+ process.stderr.write('To proceed:\n');
273
+ process.stderr.write(' 1) Coordinate with the sibling agent OR\n');
274
+ process.stderr.write(' 2) git restore --staged <path> # for each conflicting path, then retry\n');
275
+ process.stderr.write(' 3) git commit --no-verify # bypass (PSA-001/PSA-003 risk — operator opt-out)\n');
276
+ process.exit(1);
277
+ }
278
+
279
+ process.exit(0);
189
280
  }
190
281
 
191
- process.exit(0);
282
+ // Entry guard (#1393): run only as the node script `.husky/pre-commit` execs —
283
+ // a bare `import()` must run no git command and must not exit the importing
284
+ // process. No shouldRunHook gate here: this file is NOT registered in
285
+ // hooks/hooks.json, so the profile-gate never governed it.
286
+ if (isMainModule(import.meta.url)) {
287
+ await main();
288
+ }
@@ -2,7 +2,7 @@
2
2
  {
3
3
  "name": "ecosystem-health",
4
4
  "command": "node \"${CLAUDE_PLUGIN_ROOT}\"/scripts/lib/ecosystem-health.mjs --watch",
5
- "description": "Ecosystem repo health watcher (CI status, branch drift, stale baselines across vault-registered projects). Emits one NDJSON line per detected state change. Hung on session-start because the previous trigger (on-skill-invoke:ecosystem-health) was self-referential: it could only start when the ecosystem-health skill ran, and that skill has 0 recorded invocations fleet-wide, so the watcher never started once.",
5
+ "description": "Local state-file watcher: every 900 s it stats and reads ONE file, .orchestrator/metrics/ecosystem-health.jsonl under the plugin root, and emits one NDJSON line per change (appeared / disappeared / changed). No network, no CI or branch probing, no repo enumeration, no Session Config read. Nothing writes that file yet (0 writers, measured 2026-09-18), so every run emits `no-state-yet` and idles until SIGTERM — ceiling and revisit trigger at DEFAULT_INTERVAL_S in scripts/lib/ecosystem-health.mjs. Hung on session-start because the previous trigger (on-skill-invoke:ecosystem-health) was self-referential: it could only start when the ecosystem-health skill ran, and that skill has 0 recorded invocations fleet-wide, so the watcher never started once.",
6
6
  "when": "on-skill-invoke:session-start"
7
7
  },
8
8
  {
@@ -32,7 +32,7 @@ Only include if the plan changed:
32
32
 
33
33
  - One table row per agent — no nested details
34
34
  - Status is exactly one of: `done`, `partial`, `failed`
35
- - File lists use short relative paths (e.g., `hooks/on-stop.sh`)
35
+ - File lists use short relative paths (e.g., `hooks/on-stop.mjs`)
36
36
  - Quality gates are pass/fail — no ambiguity
37
37
  - Adaptations only when deviating from plan — omit if on track
38
38
  - Keep under 20 lines total
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "session-orchestrator",
3
- "version": "5.2.0",
3
+ "version": "5.4.0",
4
4
  "description": "A repeatable Plan, Go, Close workflow for AI coding sessions: /session reads your repo and agrees the scope, /go runs the work in waves with a quality gate between each, /close verifies and commits. Runs on Claude Code, Codex CLI, Cursor and Pi.",
5
5
  "type": "module",
6
6
  "homepage": "https://session-orchestrator.com",
@@ -94,8 +94,8 @@
94
94
  },
95
95
  "overrides": {
96
96
  "vite": "^8.0.16",
97
- "fast-uri": "^3.1.5",
98
- "brace-expansion": "^5.0.9"
97
+ "fast-uri": "^3.1.8",
98
+ "brace-expansion": "^5.0.12"
99
99
  },
100
100
  "license": "MIT",
101
101
  "repository": {
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: "Use this skill to run an honest session-process evaluation (Standard v1, aiat-llm-eval/1.0) — score the last completed orchestrator session against the pre-registered rubric-v1 dimensions, run /eval, evaluate this session, produce an eval report, or re-verify a stored eval run for reproducibility. Deterministic-first with an optional advisory LLM judge; never produces a global score."
2
+ description: "Use this skill to run an honest session-process evaluation (Standard v1, aiat-llm-eval/1.0) — score the last completed orchestrator session against the pre-registered rubric-v2 dimensions, run /eval, evaluate this session, produce an eval report, or re-verify a stored eval run for reproducibility. Deterministic-first with an optional advisory LLM judge; never produces a global score."
3
3
  argument-hint: "[--session <id>] [--no-write] [--verify <run-id>]"
4
4
  ---
5
5
 
@@ -0,0 +1,12 @@
1
+ ---
2
+ description: "Fleet coordinator persona for sessions running side by side on one host: it measures resources, pipelines and merge readiness itself, writes conditions (\"Auflagen\") as files, starts headless runs behind slot and busy gates, and grants no approvals. Operator-only, invoke as /session-orchestrator:navigator ticker|status|start|fenster|handover."
3
+ argument-hint: "[ticker|status|start|fenster|handover]"
4
+ ---
5
+
6
+ # /navigator
7
+
8
+ Use the Session Orchestrator skill definition at `skills/navigator/SKILL.md`.
9
+
10
+ Arguments: $@
11
+
12
+ Read that skill file and follow it exactly. When it references `$ARGUMENTS`, substitute the arguments above. Keep all Session Orchestrator platform fallbacks intact.