session-orchestrator 5.1.0 → 5.3.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 (484) hide show
  1. package/.agents/skills/architecture/SKILL.md +3 -1
  2. package/.agents/skills/autopilot/SKILL.md +6 -1
  3. package/.agents/skills/autopilot/agents/openai.yaml +5 -0
  4. package/.agents/skills/bootstrap/SKILL.md +7 -1
  5. package/.agents/skills/bootstrap/agents/openai.yaml +5 -0
  6. package/.agents/skills/brainstorm/SKILL.md +8 -1
  7. package/.agents/skills/brainstorm/agents/openai.yaml +5 -0
  8. package/.agents/skills/claude-md-drift-check/SKILL.md +3 -1
  9. package/.agents/skills/close/SKILL.md +21 -0
  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 +7 -1
  13. package/.agents/skills/debug/agents/openai.yaml +5 -0
  14. package/.agents/skills/discovery/SKILL.md +7 -2
  15. package/.agents/skills/discovery/agents/openai.yaml +5 -0
  16. package/.agents/skills/dispatcher/SKILL.md +7 -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 +7 -1
  21. package/.agents/skills/eli5/agents/openai.yaml +5 -0
  22. package/.agents/skills/eval/SKILL.md +7 -2
  23. package/.agents/skills/eval/agents/openai.yaml +5 -0
  24. package/.agents/skills/evolve/SKILL.md +8 -3
  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 +22 -0
  30. package/.agents/skills/go/agents/openai.yaml +5 -0
  31. package/.agents/skills/grill/SKILL.md +7 -1
  32. package/.agents/skills/grill/agents/openai.yaml +5 -0
  33. package/.agents/skills/harness-audit/SKILL.md +20 -0
  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 +6 -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/npm-publish/SKILL.md +4 -2
  41. package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
  42. package/.agents/skills/persona-panel/SKILL.md +6 -1
  43. package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
  44. package/.agents/skills/plan/SKILL.md +8 -2
  45. package/.agents/skills/plan/agents/openai.yaml +5 -0
  46. package/.agents/skills/playwright-driver/SKILL.md +3 -1
  47. package/.agents/skills/portfolio/SKILL.md +21 -0
  48. package/.agents/skills/portfolio/agents/openai.yaml +5 -0
  49. package/.agents/skills/quality-gates/SKILL.md +3 -1
  50. package/.agents/skills/reconcile/SKILL.md +6 -1
  51. package/.agents/skills/reconcile/agents/openai.yaml +5 -0
  52. package/.agents/skills/release/SKILL.md +22 -0
  53. package/.agents/skills/release/agents/openai.yaml +5 -0
  54. package/.agents/skills/remote-offload/SKILL.md +3 -1
  55. package/.agents/skills/repo-audit/SKILL.md +6 -1
  56. package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
  57. package/.agents/skills/session/SKILL.md +21 -0
  58. package/.agents/skills/session/agents/openai.yaml +5 -0
  59. package/.agents/skills/session-end/SKILL.md +3 -1
  60. package/.agents/skills/session-plan/SKILL.md +3 -1
  61. package/.agents/skills/session-start/SKILL.md +3 -1
  62. package/.agents/skills/spinout/SKILL.md +6 -1
  63. package/.agents/skills/spinout/agents/openai.yaml +5 -0
  64. package/.agents/skills/sunset-review/SKILL.md +7 -1
  65. package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
  66. package/.agents/skills/templates-ack/SKILL.md +21 -0
  67. package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
  68. package/.agents/skills/test/SKILL.md +21 -0
  69. package/.agents/skills/test/agents/openai.yaml +5 -0
  70. package/.agents/skills/test-runner/SKILL.md +3 -1
  71. package/.agents/skills/tmux-layout/SKILL.md +3 -1
  72. package/.agents/skills/using-orchestrator/SKILL.md +3 -1
  73. package/.agents/skills/ux-grill/SKILL.md +7 -1
  74. package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
  75. package/.agents/skills/vault-mirror/SKILL.md +3 -1
  76. package/.agents/skills/vault-sync/SKILL.md +3 -1
  77. package/.agents/skills/wave-executor/SKILL.md +3 -1
  78. package/.agents/skills/write-executable-plan/SKILL.md +3 -1
  79. package/.claude-plugin/marketplace.json +1 -1
  80. package/.claude-plugin/plugin.json +1 -1
  81. package/.codex-plugin/plugin.json +4 -4
  82. package/.codex-plugin/skills/autopilot/SKILL.md +5 -4
  83. package/.codex-plugin/skills/bootstrap/SKILL.md +8 -4
  84. package/.codex-plugin/skills/brainstorm/SKILL.md +11 -4
  85. package/.codex-plugin/skills/close/SKILL.md +3 -3
  86. package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -1
  87. package/.codex-plugin/skills/debug/SKILL.md +11 -4
  88. package/.codex-plugin/skills/discovery/SKILL.md +8 -4
  89. package/.codex-plugin/skills/dispatcher/SKILL.md +4 -4
  90. package/.codex-plugin/skills/eli5/SKILL.md +9 -4
  91. package/.codex-plugin/skills/eval/SKILL.md +9 -4
  92. package/.codex-plugin/skills/evolve/SKILL.md +9 -4
  93. package/.codex-plugin/skills/go/SKILL.md +3 -3
  94. package/.codex-plugin/skills/grill/SKILL.md +11 -4
  95. package/.codex-plugin/skills/harness-audit/SKILL.md +4 -3
  96. package/.codex-plugin/skills/memory-cleanup/SKILL.md +9 -4
  97. package/.codex-plugin/skills/npm-publish/SKILL.md +1 -1
  98. package/.codex-plugin/skills/persona-panel/SKILL.md +5 -5
  99. package/.codex-plugin/skills/plan/SKILL.md +8 -4
  100. package/.codex-plugin/skills/portfolio/SKILL.md +3 -3
  101. package/.codex-plugin/skills/reconcile/SKILL.md +9 -4
  102. package/.codex-plugin/skills/release/SKILL.md +3 -3
  103. package/.codex-plugin/skills/repo-audit/SKILL.md +6 -4
  104. package/.codex-plugin/skills/session/SKILL.md +1 -1
  105. package/.codex-plugin/skills/spinout/SKILL.md +4 -4
  106. package/.codex-plugin/skills/sunset-review/SKILL.md +5 -4
  107. package/.codex-plugin/skills/test/SKILL.md +3 -3
  108. package/.codex-plugin/skills/ux-grill/SKILL.md +11 -4
  109. package/.cursor/commands/autopilot.md +4 -4
  110. package/.cursor/commands/bootstrap.md +5 -4
  111. package/.cursor/commands/brainstorm.md +5 -4
  112. package/.cursor/commands/close.md +4 -3
  113. package/.cursor/commands/debug.md +4 -4
  114. package/.cursor/commands/discovery.md +4 -4
  115. package/.cursor/commands/dispatcher.md +4 -4
  116. package/.cursor/commands/eli5.md +4 -4
  117. package/.cursor/commands/eval.md +4 -4
  118. package/.cursor/commands/evolve.md +4 -4
  119. package/.cursor/commands/go.md +4 -3
  120. package/.cursor/commands/grill.md +4 -4
  121. package/.cursor/commands/harness-audit.md +3 -3
  122. package/.cursor/commands/memory-cleanup.md +4 -4
  123. package/.cursor/commands/persona-panel.md +4 -4
  124. package/.cursor/commands/plan.md +5 -4
  125. package/.cursor/commands/portfolio.md +3 -3
  126. package/.cursor/commands/reconcile.md +4 -4
  127. package/.cursor/commands/release.md +4 -3
  128. package/.cursor/commands/repo-audit.md +4 -4
  129. package/.cursor/commands/session.md +1 -1
  130. package/.cursor/commands/spinout.md +4 -4
  131. package/.cursor/commands/sunset-review.md +4 -4
  132. package/.cursor/commands/test.md +3 -3
  133. package/.cursor/commands/ux-grill.md +4 -4
  134. package/.cursor/rules/000-session-orchestrator.mdc +0 -2
  135. package/.cursor/rules/010-session-workflow.mdc +2 -2
  136. package/.cursor/rules/050-plan.mdc +1 -1
  137. package/.cursor/skills/bootstrap/SKILL.md +1 -0
  138. package/.cursor/skills/close/SKILL.md +13 -0
  139. package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
  140. package/.cursor/skills/debug/SKILL.md +0 -1
  141. package/.cursor/skills/discovery/SKILL.md +0 -1
  142. package/.cursor/skills/dispatcher/SKILL.md +0 -1
  143. package/.cursor/skills/eli5/SKILL.md +0 -1
  144. package/.cursor/skills/eval/SKILL.md +1 -1
  145. package/.cursor/skills/evolve/SKILL.md +0 -1
  146. package/.cursor/skills/go/SKILL.md +13 -0
  147. package/.cursor/skills/grill/SKILL.md +0 -1
  148. package/.cursor/skills/harness-audit/SKILL.md +12 -0
  149. package/.cursor/skills/npm-publish/SKILL.md +1 -0
  150. package/.cursor/skills/portfolio/SKILL.md +12 -0
  151. package/.cursor/skills/release/SKILL.md +13 -0
  152. package/.cursor/skills/repo-audit/SKILL.md +0 -1
  153. package/.cursor/skills/sunset-review/SKILL.md +0 -1
  154. package/.cursor/skills/test/SKILL.md +12 -0
  155. package/.cursor/skills/ux-grill/SKILL.md +0 -1
  156. package/.cursor-plugin/plugin.json +1 -1
  157. package/.orchestrator/policy/blocked-commands.json +13 -4
  158. package/AGENTS.md +3 -2
  159. package/CHANGELOG.md +197 -0
  160. package/README.md +11 -9
  161. package/SECURITY.md +12 -0
  162. package/agents/dialectic-deriver.md +13 -10
  163. package/agents/eval-judge.md +67 -45
  164. package/agents/skill-applied-judge.md +34 -19
  165. package/commands/session.md +17 -3
  166. package/docs/baseline.md +12 -6
  167. package/docs/ci-setup.md +53 -0
  168. package/docs/codex-setup.md +15 -3
  169. package/docs/components.md +13 -6
  170. package/docs/events-schema.md +59 -9
  171. package/docs/install.md +16 -0
  172. package/docs/persona-panel.md +1 -1
  173. package/docs/pi-setup.md +1 -1
  174. package/docs/rule-authoring.md +135 -14
  175. package/docs/scope-collision-guard.md +2 -0
  176. package/docs/session-config-reference.md +106 -11
  177. package/docs/session-config-template.md +31 -2
  178. package/docs/telemetry.md +2 -0
  179. package/hooks/_lib/hook-import-set.json +125 -8
  180. package/hooks/_lib/subagent-paths.mjs +15 -0
  181. package/hooks/_lib/subagent-transcript.mjs +582 -31
  182. package/hooks/_lib/vcs-create-matcher.mjs +217 -62
  183. package/hooks/config-protection.mjs +11 -3
  184. package/hooks/cwd-change-restore.mjs +11 -3
  185. package/hooks/enforce-commands.mjs +70 -23
  186. package/hooks/enforce-scope.mjs +143 -33
  187. package/hooks/hooks-codex.json +1 -1
  188. package/hooks/hooks.json +1 -1
  189. package/hooks/loop-guard.mjs +11 -3
  190. package/hooks/on-session-end.mjs +72 -25
  191. package/hooks/on-session-start.mjs +48 -11
  192. package/hooks/on-stop.mjs +211 -23
  193. package/hooks/operator-steer.mjs +11 -3
  194. package/hooks/post-bash-issue-budget-refund.mjs +18 -8
  195. package/hooks/post-bash-write-verify.mjs +6 -2
  196. package/hooks/post-edit-import-probe.mjs +17 -9
  197. package/hooks/post-edit-validate.mjs +13 -5
  198. package/hooks/post-subagent-discovery-validator.mjs +98 -13
  199. package/hooks/post-tool-batch-wave-signal.mjs +200 -38
  200. package/hooks/post-tool-failure-corrective-context.mjs +11 -5
  201. package/hooks/post-tooluse-frontend-slop.mjs +10 -4
  202. package/hooks/pre-auq-clarity.mjs +18 -2
  203. package/hooks/pre-bash-destructive-guard.mjs +80 -9
  204. package/hooks/pre-bash-issue-budget.mjs +119 -28
  205. package/hooks/pre-bash-memory-propose-audit.mjs +86 -54
  206. package/hooks/pre-bash-sessions-ledger-guard.mjs +391 -20
  207. package/hooks/pre-bash-staging-fence.mjs +335 -31
  208. package/hooks/pre-bash-templates-first.mjs +19 -14
  209. package/hooks/pre-task-scope-disjoint.mjs +385 -5
  210. package/hooks/skill-invocation-telemetry.mjs +2 -1
  211. package/hooks/subagent-telemetry.mjs +15 -19
  212. package/hooks/wave-scope-commit-guard.mjs +197 -100
  213. package/monitors/monitors.json +1 -1
  214. package/output-styles/wave-summary.md +1 -1
  215. package/package.json +2 -1
  216. package/pi/prompts/autopilot.md +3 -3
  217. package/pi/prompts/bootstrap.md +3 -3
  218. package/pi/prompts/brainstorm.md +3 -3
  219. package/pi/prompts/close.md +2 -2
  220. package/pi/prompts/debug.md +3 -3
  221. package/pi/prompts/discovery.md +3 -3
  222. package/pi/prompts/dispatcher.md +3 -3
  223. package/pi/prompts/eli5.md +3 -3
  224. package/pi/prompts/eval.md +3 -3
  225. package/pi/prompts/evolve.md +3 -3
  226. package/pi/prompts/go.md +2 -2
  227. package/pi/prompts/grill.md +3 -3
  228. package/pi/prompts/harness-audit.md +2 -3
  229. package/pi/prompts/memory-cleanup.md +3 -3
  230. package/pi/prompts/persona-panel.md +3 -3
  231. package/pi/prompts/plan.md +3 -3
  232. package/pi/prompts/portfolio.md +2 -2
  233. package/pi/prompts/reconcile.md +3 -3
  234. package/pi/prompts/release.md +3 -3
  235. package/pi/prompts/repo-audit.md +3 -4
  236. package/pi/prompts/session.md +2 -2
  237. package/pi/prompts/spinout.md +3 -3
  238. package/pi/prompts/sunset-review.md +3 -3
  239. package/pi/prompts/templates-ack.md +1 -1
  240. package/pi/prompts/test.md +3 -3
  241. package/pi/prompts/ux-grill.md +3 -3
  242. package/rules/README.md +1 -1
  243. package/rules/opt-in-domain/prompt-caching.md +1 -1
  244. package/rules/opt-in-stack/backend-data.md +1 -1
  245. package/rules/opt-in-stack/backend.md +3 -3
  246. package/rules/opt-in-stack/frontend.md +1 -1
  247. package/rules/opt-in-stack/security-web.md +3 -3
  248. package/rules/opt-in-stack/swift.md +1 -1
  249. package/scripts/archive-closed-prds.mjs +2 -2
  250. package/scripts/auq-audit.mjs +2 -3
  251. package/scripts/autopilot.mjs +23 -2
  252. package/scripts/backfill-abandoned-sessions.mjs +171 -15
  253. package/scripts/backfill-evidence-digest.mjs +2 -1
  254. package/scripts/backfill-learnings-from-vault.mjs +2 -2
  255. package/scripts/check-package-manager.mjs +2 -2
  256. package/scripts/check-sessions-integrity.mjs +300 -0
  257. package/scripts/ci/assert-vitest-green.mjs +2 -1
  258. package/scripts/dialectic-deriver.mjs +50 -13
  259. package/scripts/emit-session.mjs +77 -32
  260. package/scripts/eval-session.mjs +65 -3
  261. package/scripts/export-hw-learnings.mjs +2 -1
  262. package/scripts/express-path.mjs +1 -1
  263. package/scripts/gc-stale-worktrees.mjs +2 -1
  264. package/scripts/generate-agents-skills.mjs +102 -29
  265. package/scripts/generate-codex-skills.mjs +48 -4
  266. package/scripts/generate-cursor-adapter.mjs +220 -11
  267. package/scripts/generate-hook-import-set.mjs +12 -27
  268. package/scripts/generate-pi-prompts.mjs +183 -13
  269. package/scripts/github-protection-audit.mjs +2 -3
  270. package/scripts/lib/agent-frontmatter.mjs +23 -1
  271. package/scripts/lib/agent-status.mjs +2 -31
  272. package/scripts/lib/auq/clarity.mjs +10 -2
  273. package/scripts/lib/auq/parse.mjs +12 -31
  274. package/scripts/lib/auq/schema.mjs +56 -41
  275. package/scripts/lib/auto-dialectic.mjs +304 -15
  276. package/scripts/lib/autopilot/flags.mjs +12 -1
  277. package/scripts/lib/autopilot/kill-switches.mjs +6 -3
  278. package/scripts/lib/autopilot/loop.mjs +14 -1
  279. package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
  280. package/scripts/lib/ci-status-banner.mjs +376 -16
  281. package/scripts/lib/claude-md-budget-lint.mjs +2 -5
  282. package/scripts/lib/command-blocker.mjs +408 -33
  283. package/scripts/lib/config/dialectic.mjs +12 -3
  284. package/scripts/lib/config/drift-check.mjs +19 -0
  285. package/scripts/lib/config/gate.mjs +74 -0
  286. package/scripts/lib/config/reaper.mjs +162 -0
  287. package/scripts/lib/config.mjs +14 -0
  288. package/scripts/lib/convergence-monitor.mjs +76 -13
  289. package/scripts/lib/cursor-hook-bridge.mjs +2 -2
  290. package/scripts/lib/description-surface.mjs +2 -5
  291. package/scripts/lib/dispatcher/cli.mjs +2 -1
  292. package/scripts/lib/ecosystem-health.mjs +11 -0
  293. package/scripts/lib/ecosystem-wizard.mjs +2 -1
  294. package/scripts/lib/eval/engine.mjs +421 -53
  295. package/scripts/lib/eval/judge.mjs +463 -40
  296. package/scripts/lib/eval/schema.mjs +10 -1
  297. package/scripts/lib/events-rotation.mjs +221 -25
  298. package/scripts/lib/events-schema.mjs +114 -0
  299. package/scripts/lib/events.mjs +524 -5
  300. package/scripts/lib/fetch-baseline.mjs +3 -8
  301. package/scripts/lib/frontmatter-guard.mjs +21 -10
  302. package/scripts/lib/gates/gate-baseline.mjs +27 -2
  303. package/scripts/lib/gates/gate-full.mjs +28 -3
  304. package/scripts/lib/gates/gate-helpers.mjs +243 -21
  305. package/scripts/lib/gates/gate-incremental.mjs +28 -3
  306. package/scripts/lib/gates/gate-per-file.mjs +27 -2
  307. package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +2 -1
  308. package/scripts/lib/gitlab-portfolio/cli.mjs +2 -1
  309. package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
  310. package/scripts/lib/instruction-budget-guard.mjs +332 -50
  311. package/scripts/lib/io.mjs +42 -8
  312. package/scripts/lib/is-main-module.mjs +82 -0
  313. package/scripts/lib/issue-close-strip-labels.mjs +207 -49
  314. package/scripts/lib/js-mask.mjs +197 -0
  315. package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
  316. package/scripts/lib/locks/index.mjs +32 -25
  317. package/scripts/lib/maintenance-due-banner.mjs +122 -91
  318. package/scripts/lib/orphan-reaper.mjs +1588 -0
  319. package/scripts/lib/peer-cards/merger.mjs +48 -10
  320. package/scripts/lib/peer-cards/reader.mjs +78 -2
  321. package/scripts/lib/peer-discovery.mjs +2 -5
  322. package/scripts/lib/playwright-driver/runner.mjs +2 -1
  323. package/scripts/lib/process-group.mjs +899 -0
  324. package/scripts/lib/quality-gate.mjs +107 -28
  325. package/scripts/lib/reconcile/backlog.mjs +368 -0
  326. package/scripts/lib/reconcile/engine.mjs +55 -188
  327. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +884 -0
  328. package/scripts/lib/reconcile/sanitize.mjs +69 -3
  329. package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
  330. package/scripts/lib/resource-probe/parsers.mjs +31 -0
  331. package/scripts/lib/rule-loader.mjs +41 -12
  332. package/scripts/lib/rules-sync.mjs +2 -5
  333. package/scripts/lib/scope-echo.mjs +429 -7
  334. package/scripts/lib/scope-gate.mjs +605 -1
  335. package/scripts/lib/session-close-backfill.mjs +91 -12
  336. package/scripts/lib/session-id.mjs +9 -20
  337. package/scripts/lib/session-invocation.mjs +20 -0
  338. package/scripts/lib/session-schema/constants.mjs +30 -2
  339. package/scripts/lib/session-schema/normalizer.mjs +56 -4
  340. package/scripts/lib/session-schema.mjs +8 -3
  341. package/scripts/lib/session-start-probes.mjs +95 -10
  342. package/scripts/lib/sessions-canonical.mjs +23 -0
  343. package/scripts/lib/sessions-integrity-banner.mjs +7 -1
  344. package/scripts/lib/sessions-staleness-banner.mjs +193 -51
  345. package/scripts/lib/skill-evidence-window.mjs +891 -0
  346. package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
  347. package/scripts/lib/skill-evolution/engine.mjs +18 -9
  348. package/scripts/lib/skill-judge.mjs +45 -3
  349. package/scripts/lib/state-md.mjs +84 -3
  350. package/scripts/lib/sunset/walker.mjs +31 -4
  351. package/scripts/lib/tail-window.mjs +56 -0
  352. package/scripts/lib/telemetry/schema.mjs +30 -0
  353. package/scripts/lib/telemetry/sync.mjs +61 -6
  354. package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
  355. package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
  356. package/scripts/lib/tests-src-ratio.mjs +2 -6
  357. package/scripts/lib/tmux-layout/telemetry-stats.mjs +74 -14
  358. package/scripts/lib/user-invocable-skills.mjs +205 -0
  359. package/scripts/lib/ux-grill/reconcile.mjs +48 -22
  360. package/scripts/lib/validate/check-agents-skills.mjs +26 -15
  361. package/scripts/lib/validate/check-banner-parity.mjs +2 -2
  362. package/scripts/lib/validate/check-cursor-adapter.mjs +3 -2
  363. package/scripts/lib/validate/check-dead-bridge.mjs +2 -2
  364. package/scripts/lib/validate/check-doc-cli-commands.mjs +2 -2
  365. package/scripts/lib/validate/check-entry-guard.mjs +329 -0
  366. package/scripts/lib/validate/check-guard-requires-parity.mjs +2 -2
  367. package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
  368. package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +2 -2
  369. package/scripts/lib/validate/check-learning-provenance.mjs +2 -2
  370. package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
  371. package/scripts/lib/validate/check-rules.mjs +7 -5
  372. package/scripts/lib/validate/check-skill-links.mjs +35 -6
  373. package/scripts/lib/validate/check-skill-script-paths.mjs +241 -29
  374. package/scripts/lib/validate/check-test-git-config-target.mjs +26 -36
  375. package/scripts/lib/validate/check-unicode-safety.mjs +2 -2
  376. package/scripts/lib/validate/check-untracked-test-deps.mjs +9 -104
  377. package/scripts/lib/validate/check-unwired-features.mjs +220 -33
  378. package/scripts/lib/validate/check-validator-registration.mjs +36 -12
  379. package/scripts/lib/validate/check-vcs-repo-flag.mjs +2 -2
  380. package/scripts/lib/validate/confidential-names.mjs +10 -0
  381. package/scripts/lib/validate-vendored-rules.mjs +39 -12
  382. package/scripts/lib/vault-mirror/namespace.mjs +46 -8
  383. package/scripts/lib/vault-mirror/process.mjs +10 -3
  384. package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
  385. package/scripts/lib/vault-status/narrative-mirror.mjs +31 -7
  386. package/scripts/lib/vault-yaml.mjs +118 -0
  387. package/scripts/lib/wave-transcript-tail.mjs +2 -2
  388. package/scripts/lib/worktree/lifecycle.mjs +153 -1
  389. package/scripts/lock-reaper.mjs +2 -1
  390. package/scripts/materialize-wave-scope.mjs +87 -4
  391. package/scripts/migrate-sessions-jsonl.mjs +2 -1
  392. package/scripts/migrate-vault-paths.mjs +2 -3
  393. package/scripts/release-session-lock.mjs +305 -0
  394. package/scripts/release.mjs +109 -39
  395. package/scripts/relocate-vault-corpus.mjs +2 -3
  396. package/scripts/repair-invalid-sessions.mjs +2 -2
  397. package/scripts/resolve-session-invocation.mjs +59 -0
  398. package/scripts/run-quality-gate.mjs +156 -17
  399. package/scripts/session-shape.mjs +2 -2
  400. package/scripts/site-numbers.mjs +35 -11
  401. package/scripts/sweep-expired-rules.mjs +227 -0
  402. package/scripts/validate-plugin.mjs +21 -0
  403. package/scripts/validate-wave-scope.mjs +32 -105
  404. package/scripts/vault-consolidate.mjs +2 -2
  405. package/scripts/vault-mirror.mjs +11 -4
  406. package/scripts/wave-scope-binding.mjs +2 -3
  407. package/skills/_shared/bootstrap-gate.md +1 -1
  408. package/skills/_shared/monitor-patterns.md +1 -1
  409. package/skills/_shared/platform-tools.md +23 -11
  410. package/skills/_shared/research-evidence.md +53 -0
  411. package/skills/_shared/state-ownership.md +3 -0
  412. package/skills/autopilot/SKILL.md +80 -11
  413. package/skills/bootstrap/SKILL.md +51 -1
  414. package/skills/brainstorm/SKILL.md +16 -0
  415. package/skills/claude-md-drift-check/SKILL.md +1 -1
  416. package/skills/claude-md-drift-check/checker.mjs +49 -11
  417. package/{commands/close.md → skills/close/SKILL.md} +9 -3
  418. package/skills/convergence-monitoring/README.md +8 -1
  419. package/skills/convergence-monitoring/SIGNALS.md +50 -6
  420. package/skills/convergence-monitoring/SKILL.md +15 -6
  421. package/skills/debug/SKILL.md +10 -0
  422. package/skills/discovery/SKILL.md +24 -1
  423. package/skills/discovery/probes-session.md +2 -2
  424. package/skills/dispatcher/SKILL.md +38 -7
  425. package/skills/eli5/SKILL.md +11 -0
  426. package/skills/eval/SKILL.md +52 -23
  427. package/skills/eval/rubric-v1.md +1 -0
  428. package/skills/eval/rubric-v2.md +457 -0
  429. package/skills/evolve/SKILL.md +9 -2
  430. package/skills/evolve/references/evolve-dialectic-mode.md +46 -25
  431. package/skills/gitlab-ops/SKILL.md +3 -2
  432. package/{commands/go.md → skills/go/SKILL.md} +9 -1
  433. package/skills/grill/SKILL.md +19 -0
  434. package/{commands/harness-audit.md → skills/harness-audit/SKILL.md} +7 -2
  435. package/skills/hook-development/SKILL.md +46 -41
  436. package/skills/memory-cleanup/SKILL.md +7 -0
  437. package/skills/npm-publish/SKILL.md +2 -2
  438. package/skills/persona-panel/SKILL.md +56 -1
  439. package/skills/persona-panel/persona-format.md +1 -1
  440. package/skills/plan/SKILL.md +28 -1
  441. package/{commands/portfolio.md → skills/portfolio/SKILL.md} +8 -2
  442. package/skills/reconcile/SKILL.md +21 -0
  443. package/{commands/release.md → skills/release/SKILL.md} +16 -2
  444. package/skills/repo-audit/SKILL.md +7 -0
  445. package/skills/session-end/SKILL.md +13 -16
  446. package/skills/session-end/discovery-scan.md +1 -1
  447. package/skills/session-end/phase-3-6-tail.md +55 -9
  448. package/skills/session-end/plan-verification.md +2 -2
  449. package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
  450. package/skills/session-end/session-metrics-write.md +10 -0
  451. package/skills/session-plan/SKILL.md +18 -6
  452. package/skills/session-plan/references/session-plan-task-classification.md +2 -2
  453. package/skills/session-start/SKILL.md +5 -4
  454. package/skills/session-start/phase-8-5-express-path.md +6 -6
  455. package/skills/session-start/references/phase-1-5-session-continuity.md +1 -1
  456. package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +1 -1
  457. package/skills/session-start/references/phase-4-ssot-environment-check.md +6 -4
  458. package/skills/spinout/SKILL.md +12 -1
  459. package/skills/sunset-review/SKILL.md +13 -0
  460. package/{commands/test.md → skills/test/SKILL.md} +10 -4
  461. package/skills/ux-grill/SKILL.md +20 -2
  462. package/skills/wave-executor/SKILL.md +14 -7
  463. package/skills/wave-executor/circuit-breaker.md +2 -0
  464. package/skills/wave-executor/references/wave-executor-state-init.md +18 -4
  465. package/skills/wave-executor/references/wave-loop-dispatch.md +5 -2
  466. package/skills/wave-executor/references/wave-loop-review.md +17 -1
  467. package/commands/autopilot.md +0 -80
  468. package/commands/bootstrap.md +0 -56
  469. package/commands/brainstorm.md +0 -48
  470. package/commands/debug.md +0 -36
  471. package/commands/discovery.md +0 -32
  472. package/commands/dispatcher.md +0 -59
  473. package/commands/eli5.md +0 -33
  474. package/commands/eval.md +0 -28
  475. package/commands/evolve.md +0 -10
  476. package/commands/grill.md +0 -45
  477. package/commands/memory-cleanup.md +0 -26
  478. package/commands/persona-panel.md +0 -121
  479. package/commands/plan.md +0 -15
  480. package/commands/reconcile.md +0 -23
  481. package/commands/repo-audit.md +0 -24
  482. package/commands/spinout.md +0 -15
  483. package/commands/sunset-review.md +0 -27
  484. package/commands/ux-grill.md +0 -51
@@ -230,6 +230,7 @@ import { shouldRunHook } from './_lib/profile-gate.mjs';
230
230
  /** @type {typeof import('../scripts/lib/io.mjs').emitWarn} */ let emitWarn;
231
231
  /** @type {typeof import('../scripts/lib/io.mjs').writeJsonAtomicSync} */ let writeJsonAtomicSync;
232
232
  /** @type {typeof import('../scripts/lib/file-lock.mjs').withFileLock} */ let withFileLock;
233
+ /** @type {typeof import('../scripts/lib/scope-echo.mjs').scopeDigest} */ let scopeDigest;
233
234
  let findScopeCollisions;
234
235
 
235
236
  const PLUGIN_ROOT = path.resolve(import.meta.dirname, '..');
@@ -287,6 +288,48 @@ const SHAPE_NONE = 'none';
287
288
  /** The per-dispatch observability record (#1092) — see § Observability. */
288
289
  const SCOPE_EVENT = 'orchestrator.wave_dispatch.scope_checked';
289
290
 
291
+ /**
292
+ * The worktree-base observability record (#1413).
293
+ *
294
+ * Same `wave_dispatch` domain and same `_checked` verb as `SCOPE_EVENT`, and
295
+ * written for EVERY `isolation: "worktree"` dispatch rather than only the
296
+ * alarming one. That is HR-105 applied at the source: a numerator-only stream
297
+ * cannot tell "genuinely rare" from "silently broken", and the firing rate this
298
+ * warning must stay under is then unfalsifiable. With every outcome in one
299
+ * stream the rate is `stale:true / all records of this name`.
300
+ *
301
+ * #1424 widened "every outcome" from two to three: the `stale: true|false`
302
+ * split was already covered, but each NON-measurement (peer STATE.md, no
303
+ * STATE.md, no `session-start-ref`, a git failure) returned early and emitted
304
+ * nothing — so the gate in front of the split reproduced exactly the
305
+ * zero-records ambiguity the split had removed. Those now carry
306
+ * `stale: null` plus a `skipped` reason, and only `isolation !== "worktree"`
307
+ * stays silent (HR-101: it is ~90% of all dispatches).
308
+ */
309
+ const WORKTREE_BASE_EVENT = 'orchestrator.wave_dispatch.worktree_base_checked';
310
+
311
+ /** State-dir candidates, in the same order `waveKeyOf()` probes them. */
312
+ const STATE_DIR_CANDIDATES = ['.pi', '.cursor', '.codex', '.claude'];
313
+
314
+ /**
315
+ * The receive-side instruction line `renderScopeEchoInstruction()` renders and
316
+ * the coordinator appends immediately after the fenced block
317
+ * (`wave-loop-dispatch.md` § Pre-Dispatch: File-Scope Injection).
318
+ *
319
+ * Matching it HERE, in the same prompt, is what makes the agent-A-block-with-
320
+ * agent-B-line mix-up catchable without a single filesystem read: the block and
321
+ * the line must come from the same `$AGENT_FILESCOPE_JSON`, so their digests
322
+ * must agree (`digest_consistent`). The marker half stays case-sensitive for the
323
+ * same reason `scope-echo.mjs` keeps it so — a lowercase lookalike is not the
324
+ * line; the HEX half is read case-insensitively and normalised to lowercase.
325
+ * Non-global on purpose: a `g` regex carries `lastIndex` between calls.
326
+ */
327
+ const ECHO_INSTRUCTION_RE =
328
+ /End your final report with the line:[ \t]*`{0,3}SCOPE-DIGEST:[ \t]*`{0,3}([0-9a-fA-F]{8})(?![0-9a-fA-F])/;
329
+
330
+ /** Shape of a well-formed digest, used to reject anything a broken digest fn returns. */
331
+ const DIGEST_RE = /^[0-9a-f]{8}$/;
332
+
290
333
  /**
291
334
  * Clamp for the one free-form string the event carries (`agent_id`, built from
292
335
  * the coordinator's own `description`). A dispatch description is a label, but
@@ -358,6 +401,14 @@ async function bootstrap() {
358
401
  io: { specifier: lib('io.mjs') },
359
402
  scopeGate: { specifier: lib('scope-gate.mjs') },
360
403
  fileLock: { specifier: lib('file-lock.mjs') },
404
+ // ONE normalization for the digest, shared with the receive side (#1092):
405
+ // `scope-echo.mjs` is pure (stdlib + `crypto-digest-utils.mjs`) and its
406
+ // `scopeDigest` is what `--verify` joins the two halves on. A second
407
+ // implementation here would let the halves disagree while both looked
408
+ // right — the class `guard-design.md` § "zero-import predicate module"
409
+ // names. Late-bound like every other repo module so a load failure
410
+ // banners instead of disarming the guard silently (#993).
411
+ scopeEcho: { specifier: lib('scope-echo.mjs') },
361
412
  },
362
413
  {
363
414
  hookName: HOOK_NAME,
@@ -370,6 +421,7 @@ async function bootstrap() {
370
421
  ({ readStdin, emitAllow, emitDeny, emitWarn, writeJsonAtomicSync } = modules.io);
371
422
  ({ findScopeCollisions } = modules.scopeGate);
372
423
  ({ withFileLock } = modules.fileLock);
424
+ ({ scopeDigest } = modules.scopeEcho);
373
425
  }
374
426
 
375
427
  // ---------------------------------------------------------------------------
@@ -407,8 +459,51 @@ const SCOPE_TERMS = 'DATEI[- ]SCOPE|FILE[- ]SCOPE|FILE SCOPE|DEIN SCOPE|SCOPE \\
407
459
  * hit in a region as one is the recorded failure of `parseEpicRef` (#1112).
408
460
  *
409
461
  * The real miss class is a different SHAPE, handled by {@link INLINE_SCOPE_DECL}.
462
+ *
463
+ * ## The marker vocabulary is CASE-SENSITIVE, and that is a measurement (#1092)
464
+ *
465
+ * Until 2026-09-16 both shapes carried the `i` flag, so the term `FILE SCOPE`
466
+ * also matched ordinary lowercase prose. The Learnings-Index header this repo
467
+ * injects into every agent prompt —
468
+ * `## Learnings Index (selected for your file scope) — …` — therefore matched
469
+ * the marker at column ~38 of a line-leading window, and the FIRST fenced block
470
+ * anywhere after it (a verification command, a report template) decided the
471
+ * class: no path survived, so the dispatch was recorded `unparseable`, i.e.
472
+ * matrix row 6 — "a declaration is present but the parser gave up".
473
+ *
474
+ * MEASURED in this session's own wave 1 (`.orchestrator/wave-dispatch-scopes.json`,
475
+ * waveKey `5de6560c-…|w1|Discovery`): `unparseable: 5, extracted: 0` for FIVE
476
+ * Discovery dispatches that carried no `FILE-SCOPE` block at all. Host-wide the
477
+ * same confusion accounts for the 52 historical `unparseable` records.
478
+ *
479
+ * Every DOCUMENTED marker is upper-case (`wave-loop-dispatch.md` § Pre-Dispatch:
480
+ * File-Scope Injection writes `FILE-SCOPE — exactly these:`), while every
481
+ * measured false positive is prose in sentence case — so dropping `i` separates
482
+ * them without narrowing the search window, which the § above showed recovers
483
+ * nothing.
484
+ *
485
+ * ## What dropping `i` costs, stated honestly (corrected 2026-09-16)
486
+ *
487
+ * The first cut of this note claimed the change "moves a CLASSIFICATION and
488
+ * never a decision", on the grounds that `marker-absent` and `unparseable` both
489
+ * resolve to ALLOW. That is only true for a declaration the parser would have
490
+ * given up on anyway. A MIXED-CASE declaration that WOULD have yielded paths
491
+ * (`File-Scope:` followed by a fenced path list) now matches nothing, so
492
+ * `extractScopeFromPrompt` returns `[]`, `findScopeCollisions` never runs, and
493
+ * the dispatch is ALLOWED unconditionally — a lost DENY capability for that
494
+ * spelling, i.e. a decision change, not a reclassification.
495
+ *
496
+ * It is safe today because every LIVE injector writes the upper-case canonical
497
+ * marker: `skills/wave-executor/references/wave-loop-dispatch.md`
498
+ * § Pre-Dispatch: File-Scope Injection documents `FILE-SCOPE — exactly these:`,
499
+ * and the repo-wide census (2026-09-16) finds the mixed-case spellings only in
500
+ * prose, never in an injected block. That is a property of the injectors, not of
501
+ * this regex, so it is PINNED rather than assumed:
502
+ * `tests/skills/wave-loop-scope-marker.test.mjs` asserts the documented marker
503
+ * line is upper-case and matches {@link SCOPE_MARKER}, so a future template edit
504
+ * to `File-Scope:` goes red instead of silently disarming the guard.
410
505
  */
411
- const SCOPE_MARKER = new RegExp(`^.{0,80}(${SCOPE_TERMS})`, 'im');
506
+ export const SCOPE_MARKER = new RegExp(`^.{0,80}(${SCOPE_TERMS})`, 'm');
412
507
 
413
508
  /**
414
509
  * SHAPE 2 (lower precedence) — the measured miss class: a declaration written
@@ -436,7 +531,7 @@ const SCOPE_MARKER = new RegExp(`^.{0,80}(${SCOPE_TERMS})`, 'im');
436
531
  * one false extraction (`[".filter"]`) is a prose fragment, which is why the
437
532
  * fenced shape keeps precedence and this one runs only when that found nothing.
438
533
  */
439
- const INLINE_SCOPE_DECL = new RegExp(`(${SCOPE_TERMS})`, 'ig');
534
+ const INLINE_SCOPE_DECL = new RegExp(`(${SCOPE_TERMS})`, 'g');
440
535
  const INLINE_DECL_OPERATOR = /^[^\n(:—–]{0,2}(\([^)\n]{0,80}\))?\s*(?:[—–][^\n:]{0,30})?\s*(:|—|–)/;
441
536
 
442
537
  /** Separators a coordinator uses between paths in an inline declaration. */
@@ -909,6 +1004,163 @@ export function listTrackedFiles(cwd) {
909
1004
  }
910
1005
  }
911
1006
 
1007
+ // ---------------------------------------------------------------------------
1008
+ // Stale worktree base (#1413) — a WARNING, never a decision
1009
+ //
1010
+ // `isolation: "worktree"` makes the HARNESS create `<repo>/.claude/worktrees/
1011
+ // agent-<hex>`; no code in this repo creates it and the `Agent` payload carries
1012
+ // no base-ref field (the 147-dispatch key census in the module docblock). The
1013
+ // base is the SESSION-START commit, and it does not follow a mid-session commit:
1014
+ // measured 2026-09-19 (session s18), worktrees created 25 minutes AFTER commit
1015
+ // `240efda6` still stood on its parent `8f15f77b`. A fix agent then silently
1016
+ // edits the OLD code, and its test run is structurally red on top — `validate-
1017
+ // plugin` is vitest's globalSetup and `check-guard-requires-parity.mjs` compares
1018
+ // against `git show HEAD:` inside the stale worktree.
1019
+ //
1020
+ // We have no lever on the base, so the only available fix is to be LOUD. This
1021
+ // never touches the verdict: the two functions below are TOTAL (every failure
1022
+ // resolves to silence), and the caller ignores their result for the decision.
1023
+ // ---------------------------------------------------------------------------
1024
+
1025
+ /**
1026
+ * Facts for the worktree-base check, as a DISCRIMINATED record with three
1027
+ * outcomes rather than two (#1424):
1028
+ *
1029
+ * - `{stale: true|false, head, session_start_ref, subagent_type?}` — measured.
1030
+ * - `{stale: null, skipped: <reason>}` — the dispatch WAS on the `worktree`
1031
+ * branch, but the question could not be answered honestly. Still emitted,
1032
+ * because HR-105 needs the denominator: while every such case returned bare
1033
+ * `null` and the caller emitted nothing, "the check never had anything to
1034
+ * say" and "the check was silently disarmed" both read as zero records —
1035
+ * precisely the shape this event exists to close.
1036
+ * - `null` — `isolation !== 'worktree'`. SILENT by construction, and the one
1037
+ * outcome that must stay so: `isolation` appeared in 14 of 147 measured
1038
+ * dispatches, so recording the other ~90% would put the denominator on the
1039
+ * whole hot dispatch path (HR-101).
1040
+ *
1041
+ * IDENTITY GATE (`.claude/rules/identity-and-locks.md`): `session-start-ref`
1042
+ * lives in STATE.md, a SHARED working-copy artefact that routinely belongs to a
1043
+ * peer session. It is read only when STATE.md's own `session-id` equals this
1044
+ * process's raw id (hook payload `session_id`, else `CLAUDE_CODE_SESSION_ID`) —
1045
+ * a process-local witness REPLACES the shared one, never unions with it. On
1046
+ * mismatch, absence or any read failure this reports `stale: null`: a confident
1047
+ * warning about somebody else's session is worse than no warning at all. The
1048
+ * test runs PER CANDIDATE (`continue`, not `break`) — see the loop below.
1049
+ *
1050
+ * `parseStateMd` is imported DYNAMICALLY and only on the rare `worktree` branch
1051
+ * (14 of 147 measured dispatches carried `isolation` at all): it is a leaf module
1052
+ * with zero imports and already inside the committed hook-import closure, so
1053
+ * reuse costs no new edge, and binding it late keeps a load failure catchable
1054
+ * instead of disarming the guard at ESM link time (§ #993 late-bound deps).
1055
+ *
1056
+ * @param {{tool_input?: unknown}} input — the raw hook payload
1057
+ * @param {string} projectDir
1058
+ * @param {string} sessionId — `input.session_id`, or `'no-session'`
1059
+ * @returns {Promise<{head: string, session_start_ref: string, stale: boolean,
1060
+ * subagent_type?: string}|{stale: null, skipped: string}|null>}
1061
+ */
1062
+ async function worktreeBaseFacts(input, projectDir, sessionId) {
1063
+ // The applicability gate sits OUTSIDE the try on purpose: everything below it
1064
+ // resolves to a `skipped` RECORD, so a throw here must not be able to mint one
1065
+ // for a dispatch that was never on the `worktree` branch at all.
1066
+ const toolInput = input?.tool_input;
1067
+ if (toolInput === null || typeof toolInput !== 'object') return null;
1068
+ if (toolInput.isolation !== 'worktree') return null;
1069
+
1070
+ try {
1071
+ const ownId = sessionId && sessionId !== 'no-session'
1072
+ ? sessionId
1073
+ : (process.env.CLAUDE_CODE_SESSION_ID || '');
1074
+ if (!ownId) return { stale: null, skipped: 'no-own-id' };
1075
+
1076
+ const { parseStateMd } = await import(
1077
+ pathToFileURL(path.join(PLUGIN_ROOT, 'scripts', 'lib', 'state-md', 'yaml-parser.mjs')).href
1078
+ );
1079
+
1080
+ // #1424: the identity test lives INSIDE the loop. Breaking at the first
1081
+ // PARSEABLE STATE.md and testing `session-id` afterwards meant a single
1082
+ // left-over `.pi/STATE.md` — `.pi` sorts before `.claude` in
1083
+ // STATE_DIR_CANDIDATES — disarmed the check for a session whose own
1084
+ // `.claude/STATE.md` sat right there, readable and matching. `sawStateMd`
1085
+ // keeps the two silences apart afterwards: "nothing to read" is a different
1086
+ // fact from "read, and none of it was mine".
1087
+ let frontmatter = null;
1088
+ let sawStateMd = false;
1089
+ for (const dir of STATE_DIR_CANDIDATES) {
1090
+ try {
1091
+ const parsed = parseStateMd(readFileSync(path.join(projectDir, dir, 'STATE.md'), 'utf8'));
1092
+ if (!parsed?.frontmatter) continue;
1093
+ sawStateMd = true;
1094
+ if (parsed.frontmatter['session-id'] !== ownId) continue;
1095
+ frontmatter = parsed.frontmatter;
1096
+ break;
1097
+ } catch { /* try the next state dir */ }
1098
+ }
1099
+ if (frontmatter === null) {
1100
+ return { stale: null, skipped: sawStateMd ? 'identity-mismatch' : 'no-state-md' };
1101
+ }
1102
+
1103
+ const startRef = typeof frontmatter['session-start-ref'] === 'string'
1104
+ ? frontmatter['session-start-ref'].trim()
1105
+ : '';
1106
+ if (startRef === '') return { stale: null, skipped: 'no-start-ref' };
1107
+
1108
+ let head = '';
1109
+ try {
1110
+ head = execFileSync('git', ['rev-parse', 'HEAD'], {
1111
+ cwd: projectDir,
1112
+ encoding: 'utf8',
1113
+ stdio: ['ignore', 'pipe', 'ignore'],
1114
+ }).trim();
1115
+ } catch {
1116
+ return { stale: null, skipped: 'git-error' };
1117
+ }
1118
+ if (head === '') return { stale: null, skipped: 'git-error' };
1119
+
1120
+ const facts = { head, session_start_ref: startRef, stale: head !== startRef };
1121
+ if (typeof toolInput.subagent_type === 'string' && toolInput.subagent_type !== '') {
1122
+ facts.subagent_type = toolInput.subagent_type;
1123
+ }
1124
+ return facts;
1125
+ } catch {
1126
+ // Total by construction: no parser, an unreadable state dir, anything else.
1127
+ // Still a RECORD rather than silence — the dispatch WAS on the `worktree`
1128
+ // branch, and a broken probe that emits nothing is indistinguishable from a
1129
+ // probe that had nothing to report (#1424 / HR-105).
1130
+ return { stale: null, skipped: 'probe-error' };
1131
+ }
1132
+ }
1133
+
1134
+ /**
1135
+ * The operator-facing warning. It names the ACTION, not just the condition —
1136
+ * a warning the coordinator cannot act on is noise (HR-106).
1137
+ *
1138
+ * NO `⚠ ` PREFIX HERE: this text is handed to `emitWarn`, which prefixes it on
1139
+ * both channels it writes (stderr and the `systemMessage` payload). Prefixing
1140
+ * here too produced `⚠ ⚠ …`.
1141
+ *
1142
+ * @param {{head: string, session_start_ref: string}} facts
1143
+ * @returns {string}
1144
+ */
1145
+ function staleWorktreeWarning(facts) {
1146
+ const head = facts.head.slice(0, 12);
1147
+ const base = facts.session_start_ref.slice(0, 12);
1148
+ return [
1149
+ `${HOOK_NAME}: STALE WORKTREE BASE (#1413) — this dispatch uses `
1150
+ + `isolation: "worktree", but HEAD (${head}) has moved past this session's `
1151
+ + `session-start-ref (${base}).`,
1152
+ ' The harness bases a new agent worktree on the SESSION-START commit and offers',
1153
+ ' no base-ref field, so the agent will silently get the OLDER code — and its test',
1154
+ ' run is structurally red on top (check-guard-requires-parity compares against',
1155
+ ' `git show HEAD:`, and validate-plugin is vitest\'s globalSetup).',
1156
+ ' DO ONE OF: dispatch this wave IN-PLACE (omit `isolation`) — or verify the base',
1157
+ ' before trusting the agent\'s result:',
1158
+ ' git worktree list --porcelain | awk -v h="$(git rev-parse HEAD)" '
1159
+ + '\'/^worktree /{w=$2} /^HEAD /{if (w ~ /\\.claude\\/worktrees\\/agent-/ && $2 != h) '
1160
+ + 'print "STALE " substr($2,1,12) " " w}\'',
1161
+ ].join('\n');
1162
+ }
1163
+
912
1164
  /**
913
1165
  * Clip a path for the deny reason without losing the discriminating tail.
914
1166
  *
@@ -980,6 +1232,69 @@ export function bumpSignalCounter(ledger, waveKey, status) {
980
1232
  return next;
981
1233
  }
982
1234
 
1235
+ /**
1236
+ * The digest half of the per-dispatch record (#1092) — the field set that makes
1237
+ * the SEND side joinable to the RECEIVE side.
1238
+ *
1239
+ * Four facts, all derived from the PROMPT alone, none of them a path:
1240
+ *
1241
+ * `scope_digest` — `scopeDigest()` over the paths extracted FROM THE
1242
+ * PROMPT. This is the join key `scope-echo --verify`
1243
+ * uses; `agent_id` cannot be one, because the two
1244
+ * halves spell it differently (measured 2026-09-16:
1245
+ * 609 send-side vs 51 receive-side records, agent-id
1246
+ * overlap ZERO — send writes
1247
+ * `"i-3 #1353 … (session-orchestrator:code-implementer)"`,
1248
+ * receive writes `"i-3"`).
1249
+ * `echo_instruction_present`— the coordinator appended the echo line at all.
1250
+ * `instructed_digest` — the digest THAT LINE names.
1251
+ * `digest_consistent` — the two agree. `false` is agent A's fenced block
1252
+ * beside agent B's echo line, caught at dispatch
1253
+ * time with no filesystem read.
1254
+ *
1255
+ * ABSENT IS NOT ZERO, in both directions (`docs/events-schema.md`):
1256
+ * `scope_digest` is OMITTED for an empty scope — never the digest of the empty
1257
+ * string, which is a real 8-hex value and would join marker-absent Discovery
1258
+ * dispatches to each other. `instructed_digest` is omitted when no line was
1259
+ * found, and `digest_consistent` unless BOTH are present.
1260
+ *
1261
+ * TOTAL by construction: every branch is wrapped, because this runs on the
1262
+ * decision path of a deny-capable hook. A throwing digest function must cost the
1263
+ * FIELD, never the verdict — same discipline as the awaited-and-caught emit.
1264
+ *
1265
+ * @param {string[]} files paths extracted from the prompt
1266
+ * @param {unknown} prompt the dispatch prompt
1267
+ * @param {((paths: string[]) => string)} [digestFn] injectable for tests; defaults
1268
+ * to the late-bound `scopeDigest` (undefined when `bootstrap()` has not run,
1269
+ * which this function treats exactly like a throwing one — the field is omitted)
1270
+ * @returns {{scope_digest?: string, echo_instruction_present: boolean,
1271
+ * instructed_digest?: string, digest_consistent?: boolean}}
1272
+ */
1273
+ export function scopeDigestFields(files, prompt, digestFn) {
1274
+ /** @type {Record<string, unknown>} */
1275
+ const out = {};
1276
+ try {
1277
+ const fn = typeof digestFn === 'function' ? digestFn : scopeDigest;
1278
+ if (typeof fn === 'function' && Array.isArray(files) && files.length > 0) {
1279
+ const digest = fn(files);
1280
+ if (typeof digest === 'string' && DIGEST_RE.test(digest)) out.scope_digest = digest;
1281
+ }
1282
+ } catch { /* a broken digest costs the field, never the decision */ }
1283
+
1284
+ let instructed = null;
1285
+ try {
1286
+ const match = typeof prompt === 'string' ? ECHO_INSTRUCTION_RE.exec(prompt) : null;
1287
+ if (match !== null) instructed = match[1].toLowerCase();
1288
+ } catch { instructed = null; }
1289
+
1290
+ out.echo_instruction_present = instructed !== null;
1291
+ if (instructed !== null) out.instructed_digest = instructed;
1292
+ if (typeof out.scope_digest === 'string' && instructed !== null) {
1293
+ out.digest_consistent = out.scope_digest === instructed;
1294
+ }
1295
+ return /** @type {any} */ (out);
1296
+ }
1297
+
983
1298
  /**
984
1299
  * @typedef {{action: 'allow'|'deny'|'warn', reason?: string, suggestion?: string,
985
1300
  * ledger?: object|null, note?: string, telemetry?: object}} Verdict
@@ -997,9 +1312,11 @@ export function bumpSignalCounter(ledger, waveKey, status) {
997
1312
  * @param {Function} params.collide `findScopeCollisions` (injected for testability)
998
1313
  * @param {(entry: object) => boolean} [params.isFinished] liveness probe (§ Liveness)
999
1314
  * @param {string} [params.nowIso] dispatch timestamp recorded on the entry
1315
+ * @param {(paths: string[]) => string} [params.digestFn] scope-digest function
1316
+ * (injected for testability; defaults to the late-bound `scopeDigest`)
1000
1317
  * @returns {Verdict}
1001
1318
  */
1002
- export function decide({ input, ledger, ledgerCorrupt, waveKey, knownFiles, collide, isFinished, nowIso }) {
1319
+ export function decide({ input, ledger, ledgerCorrupt, waveKey, knownFiles, collide, isFinished, nowIso, digestFn }) {
1003
1320
  const toolName = input?.tool_name;
1004
1321
  // Row 4: not our tool.
1005
1322
  if (toolName !== DISPATCH_TOOL) return { action: 'allow' };
@@ -1023,13 +1340,19 @@ export function decide({ input, ledger, ledgerCorrupt, waveKey, knownFiles, coll
1023
1340
  // agent id, never a path, never a byte of the prompt (issue #1092 acceptance
1024
1341
  // criterion 3). `wave` is omitted rather than zeroed when unknown.
1025
1342
  const wave = waveNumberOf(waveKey);
1343
+ const digestFields = scopeDigestFields(files, toolInput.prompt, digestFn);
1026
1344
  const telemetryFor = (ledgerResult, collisionCount = 0) => ({
1027
1345
  ...(wave === null ? {} : { wave }),
1028
1346
  agent_id: id.slice(0, MAX_AGENT_ID_CHARS),
1029
1347
  declared_path_count: files.length,
1030
1348
  injected: files.length > 0,
1349
+ // `marker_found` is NOT a second spelling of `injected`: a fenced block whose
1350
+ // lines are prose is `marker_found: true, injected: false` (matrix row 6),
1351
+ // which is the row-5-vs-row-6 split expressed as a boolean a query can group by.
1352
+ marker_found: signal.status !== SIGNAL_MARKER_ABSENT,
1031
1353
  shape: signal.shape,
1032
1354
  signal: signal.status,
1355
+ ...digestFields,
1033
1356
  ledger_result: ledgerResult,
1034
1357
  collision_count: collisionCount,
1035
1358
  });
@@ -1297,8 +1620,59 @@ async function main() {
1297
1620
  } catch { /* observability is best-effort — it never blocks the decision */ }
1298
1621
  }
1299
1622
 
1300
- if (verdict.action === 'deny') return emitDeny(verdict.reason, verdict.suggestion);
1301
- if (verdict.action === 'warn') return emitWarn(verdict.note);
1623
+ // #1413 the stale-worktree-base warning, also awaited BEFORE the terminal
1624
+ // emit for the same process.exit() reason as the block above. It reads
1625
+ // `verdict` not at all: it can never turn an allow into a deny, and a git or
1626
+ // STATE.md failure resolves to silence rather than to a wrong accusation.
1627
+ //
1628
+ // ROUTED TO THE OPERATOR, not to stderr alone (w3-5): under the exit-0
1629
+ // PreToolUse protocol stderr goes nowhere — `io.mjs:545` calls it the "Debug
1630
+ // channel … Invisible under exit 0" — so the first cut of this check announced
1631
+ // the s18 incident class to a log and to nobody. The visible channel is the
1632
+ // top-level `systemMessage`, i.e. `emitWarn`, which carries NO
1633
+ // `permissionDecision` and therefore still means ALLOW (io.mjs § "which is
1634
+ // precisely what keeps warn non-blocking").
1635
+ //
1636
+ // The note is CARRIED to the single terminal emit below rather than emitted
1637
+ // here, because `emitWarn` calls `process.exit(0)` and never returns — warning
1638
+ // inline would terminate the process before a collision DENY could be emitted,
1639
+ // flipping a block into an allow (§ stdout discipline, the same reason
1640
+ // `decide()` is pure).
1641
+ //
1642
+ // `null` here means "not a worktree dispatch" and stays silent; ANY other
1643
+ // shape is emitted, including the `{stale: null, skipped: …}` non-measurements
1644
+ // (#1424). The warning still fires on `stale === true` alone — a skipped probe
1645
+ // accuses nobody.
1646
+ const worktreeBase = await worktreeBaseFacts(input, projectDir, sessionId);
1647
+ let staleNote = null;
1648
+ if (worktreeBase !== null) {
1649
+ if (worktreeBase.stale === true) staleNote = staleWorktreeWarning(worktreeBase);
1650
+ try {
1651
+ const { emitEvent, sessionAttribution } = await import(
1652
+ pathToFileURL(path.join(PLUGIN_ROOT, 'scripts', 'lib', 'events.mjs')).href
1653
+ );
1654
+ await emitEvent(
1655
+ WORKTREE_BASE_EVENT,
1656
+ { hook: HOOK_NAME, ...worktreeBase, ...sessionAttribution(projectDir) },
1657
+ { repoRoot: projectDir }
1658
+ );
1659
+ } catch { /* observability is best-effort — it never blocks the decision */ }
1660
+ }
1661
+
1662
+ // ONE terminal emit, with the #1413 note folded in where a visible channel is
1663
+ // free. On DENY the dispatch does not happen and `systemMessage` already
1664
+ // carries the ⛔ headline, so the note stays on the debug channel — a stale
1665
+ // base is moot for a dispatch that was just blocked.
1666
+ if (verdict.action === 'deny') {
1667
+ if (staleNote !== null) {
1668
+ try { console.error(`⚠ ${staleNote}`); } catch { /* stderr may be closed */ }
1669
+ }
1670
+ return emitDeny(verdict.reason, verdict.suggestion);
1671
+ }
1672
+ if (verdict.action === 'warn') {
1673
+ return emitWarn(staleNote === null ? verdict.note : `${verdict.note}\n\n${staleNote}`);
1674
+ }
1675
+ if (staleNote !== null) return emitWarn(staleNote);
1302
1676
  return emitAllow();
1303
1677
  }
1304
1678
 
@@ -1309,6 +1683,12 @@ async function main() {
1309
1683
  // symlinked plugin install) while `import.meta.url` is realpath-resolved by
1310
1684
  // node's default loader, so BOTH sides are realpath'd — the same comparison
1311
1685
  // `hooks/post-bash-write-verify.mjs` documents (#938 MED-2).
1686
+ //
1687
+ // NAMED CEILING (BV-004): kept inline rather than imported — hooks avoid
1688
+ // importing `scripts/lib` on the hot path, where every added module is paid on
1689
+ // every dispatch. The canonical predicate is `scripts/lib/is-main-module.mjs`;
1690
+ // revisit if this hook ever imports that tree for another reason, at which
1691
+ // point the copy costs more than the import saves.
1312
1692
  // ---------------------------------------------------------------------------
1313
1693
  function invokedAsScript() {
1314
1694
  const entry = process.argv[1];
@@ -37,6 +37,7 @@ import { getProjectDir } from '../scripts/lib/platform.mjs';
37
37
  import { shouldDailyFlush } from '../scripts/lib/telemetry/sync.mjs';
38
38
  import { resolveConsent, readTelemetryState } from '../scripts/lib/telemetry/consent.mjs';
39
39
  import { loadOwnerConfig } from '../scripts/lib/owner-yaml.mjs';
40
+ import { isMainModule } from '../scripts/lib/is-main-module.mjs';
40
41
 
41
42
  // ---------------------------------------------------------------------------
42
43
  // Constants
@@ -209,7 +210,7 @@ async function main() {
209
210
  // Self-execution guard — run only when invoked directly (not when imported).
210
211
  // ---------------------------------------------------------------------------
211
212
 
212
- const isMain = process.argv[1] === fileURLToPath(import.meta.url);
213
+ const isMain =isMainModule(import.meta.url);
213
214
  if (isMain) {
214
215
  // Exit 0 immediately when disabled via SO_HOOK_PROFILE / SO_DISABLED_HOOKS.
215
216
  if (!shouldRunHook('skill-invocation-telemetry')) process.exit(0);
@@ -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
@@ -593,25 +593,18 @@ function resolveSubagentTranscriptPath(parentTranscriptPath, agentId) {
593
593
  * @returns {number|null} epoch-ms of the matching start, or null
594
594
  */
595
595
  function findStartTimestampMs(filePath, agentId) {
596
- let fd;
597
596
  try {
598
597
  if (typeof agentId !== 'string' || !agentId.trim()) return null;
599
598
  if (!fs.existsSync(filePath)) return null;
600
599
 
601
- const { size } = fs.statSync(filePath);
602
- if (size === 0) return null;
600
+ const window = readTailWindow(filePath, START_JOIN_TAIL_BYTES);
601
+ if (window.size === 0) return null;
603
602
 
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');
603
+ const lines = window.text.split('\n');
611
604
  // When the window does not cover the whole file, the first element is a
612
605
  // record sliced mid-line (possibly mid-UTF-8-sequence). Drop it rather than
613
606
  // feed a corrupt fragment to JSON.parse.
614
- if (from > 0) lines.shift();
607
+ if (window.cut) lines.shift();
615
608
 
616
609
  for (let i = lines.length - 1; i >= 0; i--) {
617
610
  const line = lines[i].trim();
@@ -631,10 +624,6 @@ function findStartTimestampMs(filePath, agentId) {
631
624
  return null;
632
625
  } catch {
633
626
  return null;
634
- } finally {
635
- if (fd !== undefined) {
636
- try { fs.closeSync(fd); } catch { /* ignore */ }
637
- }
638
627
  }
639
628
  }
640
629
 
@@ -815,5 +804,12 @@ async function main() {
815
804
  await appendSubagent(jsonlPath(), record);
816
805
  }
817
806
 
818
- // Exit 0 always informational hook must never block Claude.
819
- main().catch(() => {}).finally(() => process.exit(0));
807
+ // Entry guard (#1393): run only as the node script the harness execs — a bare
808
+ // `import()` must run no handler and must not exit the importing process.
809
+ if (isMainModule(import.meta.url)) {
810
+ // Exit 0 immediately when disabled via SO_HOOK_PROFILE / SO_DISABLED_HOOKS.
811
+ if (!shouldRunHook('subagent-telemetry')) process.exit(0);
812
+
813
+ // Exit 0 always — informational hook must never block Claude.
814
+ main().catch(() => {}).finally(() => process.exit(0));
815
+ }