session-orchestrator 5.0.0 → 5.2.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 (298) hide show
  1. package/.agents/skills/autopilot/SKILL.md +1 -0
  2. package/.agents/skills/bootstrap/SKILL.md +2 -0
  3. package/.agents/skills/brainstorm/SKILL.md +3 -0
  4. package/.agents/skills/close/SKILL.md +17 -0
  5. package/.agents/skills/debug/SKILL.md +2 -0
  6. package/.agents/skills/discovery/SKILL.md +2 -1
  7. package/.agents/skills/dispatcher/SKILL.md +2 -0
  8. package/.agents/skills/eli5/SKILL.md +2 -0
  9. package/.agents/skills/eval/SKILL.md +1 -0
  10. package/.agents/skills/evolve/SKILL.md +2 -1
  11. package/.agents/skills/go/SKILL.md +18 -0
  12. package/.agents/skills/grill/SKILL.md +2 -0
  13. package/.agents/skills/harness-audit/SKILL.md +16 -0
  14. package/.agents/skills/memory-cleanup/SKILL.md +1 -0
  15. package/.agents/skills/persona-panel/SKILL.md +1 -0
  16. package/.agents/skills/plan/SKILL.md +3 -1
  17. package/.agents/skills/portfolio/SKILL.md +17 -0
  18. package/.agents/skills/reconcile/SKILL.md +1 -0
  19. package/.agents/skills/release/SKILL.md +18 -0
  20. package/.agents/skills/repo-audit/SKILL.md +1 -0
  21. package/.agents/skills/spinout/SKILL.md +1 -0
  22. package/.agents/skills/sunset-review/SKILL.md +2 -0
  23. package/.agents/skills/test/SKILL.md +17 -0
  24. package/.agents/skills/ux-grill/SKILL.md +2 -0
  25. package/.claude-plugin/marketplace.json +3 -3
  26. package/.claude-plugin/plugin.json +2 -2
  27. package/.codex-plugin/plugin.json +2 -2
  28. package/.codex-plugin/skills/autopilot/SKILL.md +5 -4
  29. package/.codex-plugin/skills/bootstrap/SKILL.md +8 -4
  30. package/.codex-plugin/skills/brainstorm/SKILL.md +11 -4
  31. package/.codex-plugin/skills/close/SKILL.md +3 -3
  32. package/.codex-plugin/skills/convergence-monitoring/SKILL.md +2 -0
  33. package/.codex-plugin/skills/convergence-monitoring/agents/openai.yaml +5 -0
  34. package/.codex-plugin/skills/debug/SKILL.md +11 -4
  35. package/.codex-plugin/skills/discovery/SKILL.md +8 -4
  36. package/.codex-plugin/skills/dispatcher/SKILL.md +4 -4
  37. package/.codex-plugin/skills/eli5/SKILL.md +9 -4
  38. package/.codex-plugin/skills/eval/SKILL.md +9 -4
  39. package/.codex-plugin/skills/evolve/SKILL.md +9 -4
  40. package/.codex-plugin/skills/go/SKILL.md +3 -3
  41. package/.codex-plugin/skills/grill/SKILL.md +11 -4
  42. package/.codex-plugin/skills/harness-audit/SKILL.md +4 -3
  43. package/.codex-plugin/skills/memory-cleanup/SKILL.md +9 -4
  44. package/.codex-plugin/skills/npm-publish/SKILL.md +2 -0
  45. package/.codex-plugin/skills/npm-publish/agents/openai.yaml +5 -0
  46. package/.codex-plugin/skills/persona-panel/SKILL.md +5 -5
  47. package/.codex-plugin/skills/plan/SKILL.md +8 -4
  48. package/.codex-plugin/skills/portfolio/SKILL.md +3 -3
  49. package/.codex-plugin/skills/reconcile/SKILL.md +9 -4
  50. package/.codex-plugin/skills/release/SKILL.md +3 -3
  51. package/.codex-plugin/skills/repo-audit/SKILL.md +6 -4
  52. package/.codex-plugin/skills/spinout/SKILL.md +4 -4
  53. package/.codex-plugin/skills/sunset-review/SKILL.md +5 -4
  54. package/.codex-plugin/skills/test/SKILL.md +3 -3
  55. package/.codex-plugin/skills/ux-grill/SKILL.md +11 -4
  56. package/.cursor/commands/autopilot.md +4 -4
  57. package/.cursor/commands/bootstrap.md +5 -4
  58. package/.cursor/commands/brainstorm.md +5 -4
  59. package/.cursor/commands/close.md +4 -3
  60. package/.cursor/commands/convergence-monitoring.md +13 -0
  61. package/.cursor/commands/debug.md +4 -4
  62. package/.cursor/commands/discovery.md +4 -4
  63. package/.cursor/commands/dispatcher.md +4 -4
  64. package/.cursor/commands/eli5.md +4 -4
  65. package/.cursor/commands/eval.md +4 -4
  66. package/.cursor/commands/evolve.md +4 -4
  67. package/.cursor/commands/go.md +4 -3
  68. package/.cursor/commands/grill.md +4 -4
  69. package/.cursor/commands/harness-audit.md +3 -3
  70. package/.cursor/commands/memory-cleanup.md +4 -4
  71. package/.cursor/commands/npm-publish.md +13 -0
  72. package/.cursor/commands/persona-panel.md +4 -4
  73. package/.cursor/commands/plan.md +5 -4
  74. package/.cursor/commands/portfolio.md +3 -3
  75. package/.cursor/commands/reconcile.md +4 -4
  76. package/.cursor/commands/release.md +4 -3
  77. package/.cursor/commands/repo-audit.md +4 -4
  78. package/.cursor/commands/spinout.md +4 -4
  79. package/.cursor/commands/sunset-review.md +4 -4
  80. package/.cursor/commands/test.md +3 -3
  81. package/.cursor/commands/ux-grill.md +4 -4
  82. package/.cursor/rules/010-session-workflow.mdc +2 -2
  83. package/.cursor/skills/bootstrap/SKILL.md +1 -0
  84. package/.cursor/skills/close/SKILL.md +13 -0
  85. package/.cursor/skills/debug/SKILL.md +0 -1
  86. package/.cursor/skills/discovery/SKILL.md +0 -1
  87. package/.cursor/skills/dispatcher/SKILL.md +0 -1
  88. package/.cursor/skills/eli5/SKILL.md +0 -1
  89. package/.cursor/skills/evolve/SKILL.md +0 -1
  90. package/.cursor/skills/go/SKILL.md +13 -0
  91. package/.cursor/skills/grill/SKILL.md +0 -1
  92. package/.cursor/skills/harness-audit/SKILL.md +12 -0
  93. package/.cursor/skills/portfolio/SKILL.md +12 -0
  94. package/.cursor/skills/release/SKILL.md +13 -0
  95. package/.cursor/skills/repo-audit/SKILL.md +0 -1
  96. package/.cursor/skills/sunset-review/SKILL.md +0 -1
  97. package/.cursor/skills/test/SKILL.md +12 -0
  98. package/.cursor/skills/ux-grill/SKILL.md +0 -1
  99. package/.cursor-plugin/plugin.json +2 -2
  100. package/.orchestrator/policy/blocked-commands.json +10 -0
  101. package/AGENTS.md +1 -1
  102. package/CHANGELOG.md +80 -0
  103. package/README.md +74 -235
  104. package/commands/session.md +10 -0
  105. package/docs/USER-GUIDE.md +24 -0
  106. package/docs/ci-setup.md +53 -0
  107. package/docs/codex-setup.md +1 -1
  108. package/docs/components.md +12 -5
  109. package/docs/events-schema.md +5 -1
  110. package/docs/install.md +128 -0
  111. package/docs/persona-panel.md +1 -1
  112. package/docs/pi-setup.md +1 -1
  113. package/docs/rule-authoring.md +83 -14
  114. package/docs/scope-collision-guard.md +2 -0
  115. package/docs/session-config-reference.md +6 -4
  116. package/docs/session-config-template.md +38 -0
  117. package/docs/telemetry.md +15 -0
  118. package/hooks/_lib/hook-import-set.json +46 -6
  119. package/hooks/_lib/subagent-paths.mjs +15 -0
  120. package/hooks/_lib/vcs-create-matcher.mjs +217 -62
  121. package/hooks/enforce-scope.mjs +42 -1
  122. package/hooks/hooks-codex.json +1 -1
  123. package/hooks/hooks.json +1 -1
  124. package/hooks/on-session-end.mjs +14 -2
  125. package/hooks/on-stop.mjs +43 -1
  126. package/hooks/post-bash-write-verify.mjs +3 -0
  127. package/hooks/pre-auq-clarity.mjs +3 -0
  128. package/hooks/pre-bash-issue-budget.mjs +103 -17
  129. package/hooks/pre-task-scope-disjoint.mjs +152 -3
  130. package/hooks/skill-invocation-telemetry.mjs +2 -1
  131. package/package.json +3 -2
  132. package/pi/prompts/autopilot.md +3 -3
  133. package/pi/prompts/bootstrap.md +3 -3
  134. package/pi/prompts/brainstorm.md +3 -3
  135. package/pi/prompts/close.md +2 -2
  136. package/pi/prompts/convergence-monitoring.md +11 -0
  137. package/pi/prompts/debug.md +3 -3
  138. package/pi/prompts/discovery.md +3 -3
  139. package/pi/prompts/dispatcher.md +3 -3
  140. package/pi/prompts/eli5.md +3 -3
  141. package/pi/prompts/eval.md +3 -3
  142. package/pi/prompts/evolve.md +3 -3
  143. package/pi/prompts/go.md +2 -2
  144. package/pi/prompts/grill.md +3 -3
  145. package/pi/prompts/harness-audit.md +2 -3
  146. package/pi/prompts/memory-cleanup.md +3 -3
  147. package/pi/prompts/npm-publish.md +11 -0
  148. package/pi/prompts/persona-panel.md +3 -3
  149. package/pi/prompts/plan.md +3 -3
  150. package/pi/prompts/portfolio.md +2 -2
  151. package/pi/prompts/reconcile.md +3 -3
  152. package/pi/prompts/release.md +3 -3
  153. package/pi/prompts/repo-audit.md +3 -4
  154. package/pi/prompts/session.md +1 -1
  155. package/pi/prompts/spinout.md +3 -3
  156. package/pi/prompts/sunset-review.md +3 -3
  157. package/pi/prompts/templates-ack.md +1 -1
  158. package/pi/prompts/test.md +3 -3
  159. package/pi/prompts/ux-grill.md +3 -3
  160. package/scripts/archive-closed-prds.mjs +2 -2
  161. package/scripts/auq-audit.mjs +2 -3
  162. package/scripts/backfill-abandoned-sessions.mjs +57 -3
  163. package/scripts/backfill-evidence-digest.mjs +2 -1
  164. package/scripts/backfill-learnings-from-vault.mjs +2 -2
  165. package/scripts/check-package-manager.mjs +2 -2
  166. package/scripts/ci/assert-vitest-green.mjs +2 -1
  167. package/scripts/emit-session.mjs +2 -3
  168. package/scripts/export-hw-learnings.mjs +2 -1
  169. package/scripts/express-path.mjs +1 -1
  170. package/scripts/gc-stale-worktrees.mjs +2 -1
  171. package/scripts/generate-codex-skills.mjs +48 -4
  172. package/scripts/generate-cursor-adapter.mjs +173 -9
  173. package/scripts/generate-hook-import-set.mjs +12 -27
  174. package/scripts/generate-pi-prompts.mjs +183 -13
  175. package/scripts/github-protection-audit.mjs +2 -3
  176. package/scripts/lib/agent-frontmatter.mjs +23 -1
  177. package/scripts/lib/claude-md-budget-lint.mjs +2 -5
  178. package/scripts/lib/command-blocker.mjs +209 -9
  179. package/scripts/lib/config/drift-check.mjs +19 -0
  180. package/scripts/lib/convergence-monitor.mjs +2 -2
  181. package/scripts/lib/cursor-hook-bridge.mjs +2 -2
  182. package/scripts/lib/description-surface.mjs +2 -5
  183. package/scripts/lib/dispatcher/cli.mjs +2 -1
  184. package/scripts/lib/ecosystem-wizard.mjs +2 -1
  185. package/scripts/lib/fetch-baseline.mjs +3 -8
  186. package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +2 -1
  187. package/scripts/lib/gitlab-portfolio/cli.mjs +2 -1
  188. package/scripts/lib/instruction-budget-guard.mjs +186 -46
  189. package/scripts/lib/is-main-module.mjs +82 -0
  190. package/scripts/lib/locks/index.mjs +32 -25
  191. package/scripts/lib/maintenance-due-banner.mjs +69 -3
  192. package/scripts/lib/peer-discovery.mjs +2 -5
  193. package/scripts/lib/playwright-driver/runner.mjs +63 -2
  194. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +642 -0
  195. package/scripts/lib/rules-sync.mjs +2 -5
  196. package/scripts/lib/scope-echo.mjs +392 -7
  197. package/scripts/lib/session-close-backfill.mjs +58 -6
  198. package/scripts/lib/state-md.mjs +84 -3
  199. package/scripts/lib/sunset/walker.mjs +31 -4
  200. package/scripts/lib/tests-src-ratio.mjs +2 -6
  201. package/scripts/lib/tmux-layout/telemetry-stats.mjs +2 -1
  202. package/scripts/lib/user-invocable-skills.mjs +185 -0
  203. package/scripts/lib/validate/check-banner-parity.mjs +2 -2
  204. package/scripts/lib/validate/check-cursor-adapter.mjs +2 -2
  205. package/scripts/lib/validate/check-dead-bridge.mjs +2 -2
  206. package/scripts/lib/validate/check-doc-cli-commands.mjs +2 -2
  207. package/scripts/lib/validate/check-entry-guard.mjs +366 -0
  208. package/scripts/lib/validate/check-guard-requires-parity.mjs +2 -2
  209. package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +2 -2
  210. package/scripts/lib/validate/check-learning-provenance.mjs +2 -2
  211. package/scripts/lib/validate/check-skill-links.mjs +27 -6
  212. package/scripts/lib/validate/check-skill-script-paths.mjs +2 -2
  213. package/scripts/lib/validate/check-test-git-config-target.mjs +2 -2
  214. package/scripts/lib/validate/check-unicode-safety.mjs +2 -2
  215. package/scripts/lib/validate/check-untracked-test-deps.mjs +2 -2
  216. package/scripts/lib/validate/check-unwired-features.mjs +266 -11
  217. package/scripts/lib/validate/check-validator-registration.mjs +2 -2
  218. package/scripts/lib/validate/check-vcs-repo-flag.mjs +2 -2
  219. package/scripts/lib/validate-vendored-rules.mjs +35 -9
  220. package/scripts/lib/wave-transcript-tail.mjs +2 -2
  221. package/scripts/lock-reaper.mjs +2 -1
  222. package/scripts/materialize-wave-scope.mjs +87 -4
  223. package/scripts/migrate-sessions-jsonl.mjs +2 -1
  224. package/scripts/migrate-vault-paths.mjs +2 -3
  225. package/scripts/release.mjs +124 -35
  226. package/scripts/relocate-vault-corpus.mjs +2 -3
  227. package/scripts/repair-invalid-sessions.mjs +2 -2
  228. package/scripts/session-shape.mjs +2 -2
  229. package/scripts/site-numbers.mjs +35 -11
  230. package/scripts/sweep-expired-rules.mjs +216 -0
  231. package/scripts/validate-plugin.mjs +9 -0
  232. package/scripts/vault-consolidate.mjs +2 -2
  233. package/scripts/vault-mirror.mjs +2 -3
  234. package/scripts/wave-scope-binding.mjs +2 -3
  235. package/skills/_shared/bootstrap-gate.md +1 -1
  236. package/skills/_shared/monitor-patterns.md +1 -1
  237. package/skills/_shared/research-evidence.md +53 -0
  238. package/skills/_shared/state-ownership.md +3 -0
  239. package/skills/autopilot/SKILL.md +58 -4
  240. package/skills/bootstrap/SKILL.md +51 -1
  241. package/skills/brainstorm/SKILL.md +16 -0
  242. package/skills/claude-md-drift-check/checker.mjs +49 -11
  243. package/{commands/close.md → skills/close/SKILL.md} +9 -3
  244. package/skills/debug/SKILL.md +10 -0
  245. package/skills/discovery/SKILL.md +24 -1
  246. package/skills/discovery/probes-session.md +2 -2
  247. package/skills/dispatcher/SKILL.md +38 -7
  248. package/skills/eli5/SKILL.md +11 -0
  249. package/skills/eval/SKILL.md +14 -0
  250. package/skills/evolve/SKILL.md +8 -1
  251. package/skills/evolve/references/evolve-dialectic-mode.md +6 -2
  252. package/{commands/go.md → skills/go/SKILL.md} +9 -1
  253. package/skills/grill/SKILL.md +19 -0
  254. package/{commands/harness-audit.md → skills/harness-audit/SKILL.md} +7 -2
  255. package/skills/hook-development/SKILL.md +46 -41
  256. package/skills/memory-cleanup/SKILL.md +7 -0
  257. package/skills/npm-publish/SKILL.md +1 -1
  258. package/skills/persona-panel/SKILL.md +56 -1
  259. package/skills/persona-panel/persona-format.md +1 -1
  260. package/skills/plan/SKILL.md +28 -1
  261. package/skills/playwright-driver/SKILL.md +7 -10
  262. package/{commands/portfolio.md → skills/portfolio/SKILL.md} +8 -2
  263. package/skills/reconcile/SKILL.md +10 -0
  264. package/{commands/release.md → skills/release/SKILL.md} +16 -2
  265. package/skills/repo-audit/SKILL.md +7 -0
  266. package/skills/session-end/plan-verification.md +2 -2
  267. package/skills/session-plan/SKILL.md +1 -1
  268. package/skills/session-start/SKILL.md +5 -4
  269. package/skills/session-start/phase-8-5-express-path.md +6 -6
  270. package/skills/session-start/references/phase-1-5-session-continuity.md +1 -1
  271. package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +1 -1
  272. package/skills/session-start/references/phase-4-ssot-environment-check.md +4 -3
  273. package/skills/spinout/SKILL.md +12 -1
  274. package/skills/sunset-review/SKILL.md +13 -0
  275. package/{commands/test.md → skills/test/SKILL.md} +10 -4
  276. package/skills/ux-grill/SKILL.md +19 -1
  277. package/skills/wave-executor/SKILL.md +7 -4
  278. package/skills/wave-executor/references/wave-executor-state-init.md +13 -1
  279. package/skills/wave-executor/references/wave-loop-dispatch.md +3 -1
  280. package/skills/wave-executor/references/wave-loop-review.md +17 -1
  281. package/commands/autopilot.md +0 -80
  282. package/commands/bootstrap.md +0 -56
  283. package/commands/brainstorm.md +0 -48
  284. package/commands/debug.md +0 -36
  285. package/commands/discovery.md +0 -32
  286. package/commands/dispatcher.md +0 -59
  287. package/commands/eli5.md +0 -33
  288. package/commands/eval.md +0 -28
  289. package/commands/evolve.md +0 -10
  290. package/commands/grill.md +0 -45
  291. package/commands/memory-cleanup.md +0 -26
  292. package/commands/persona-panel.md +0 -121
  293. package/commands/plan.md +0 -15
  294. package/commands/reconcile.md +0 -23
  295. package/commands/repo-audit.md +0 -24
  296. package/commands/spinout.md +0 -15
  297. package/commands/sunset-review.md +0 -27
  298. package/commands/ux-grill.md +0 -51
package/hooks/on-stop.mjs CHANGED
@@ -551,6 +551,39 @@ async function handleSubagentStop(input) {
551
551
  } catch { /* probe failed — omit rather than assert `false` */ }
552
552
 
553
553
  try {
554
+ // `sidecar_missing` — the DENOMINATOR for every meta-derived field below
555
+ // (#1289 Befund 2). `agent_type_meta` and `tool_use_id` come from the same
556
+ // read, so they co-occur perfectly (measured 2026-09-16 over
557
+ // `.orchestrator/metrics/events.jsonl`: 15.291 `orchestrator.agent.stopped`
558
+ // records, 542 with `agent_type_meta`, 542 with `tool_use_id`,
559
+ // both/only-meta/only-tool = 542/0/0). That made the low presence rate look
560
+ // like a producer defect in the KEY, when 1000/1000 sidecars on this host
561
+ // carry `agentType`. This flag makes "no sidecar found" visible in the
562
+ // record instead of indistinguishable from "sidecar found, field absent".
563
+ //
564
+ // WHAT THIS KEY IS NOT: a sidecar-LOOKUP failure rate. Re-measured
565
+ // 2026-09-16 over 15.457 `orchestrator.agent.stopped` records (5086
566
+ // `transcript_found:false` vs 553 `true`), the false mass is the #939/#949
567
+ // PHANTOM-STOP class — no subagent ever existed, so there is nothing for
568
+ // the lookup to find:
569
+ // - 0 of 5077 distinct false `agent_id`s have a sidecar ANYWHERE on this
570
+ // host (`find ~/.claude/projects -path '*/subagents/agent-*.jsonl'` →
571
+ // 8626 ids), and 0 have a `SubagentStart` record (2176 starts in
572
+ // `.orchestrator/metrics/subagents.jsonl`);
573
+ // - 0 records carrying a valid `agent_id` got a null resolver result;
574
+ // - the two classes are perfectly bimodal on the type field:
575
+ // `false` + no `agent` 5086, `true` + typed 553, off-diagonal 0.
576
+ // `agent` (the harness `agent_type`) is therefore the ready-made
577
+ // discriminator — no further key is needed. Do NOT "repair" the derivation
578
+ // in hooks/_lib/subagent-paths.mjs to chase this rate: it already resolves
579
+ // 553/553 of the real stops, and a widened one would only start resolving
580
+ // onto a FOREIGN agent's sidecar. This misreading has now cost three
581
+ // investigations (#939, the 2026-08-11 ledger re-run, #1289 Befund 2).
582
+ //
583
+ // Present ONLY when true, like every other optional key in this payload;
584
+ // its own probe is inside this try, so a failed `existsSync` omits it and
585
+ // nothing else (a telemetry fault must never change a decision).
586
+ if (!existsSync(metaPath)) payload.sidecar_missing = true;
554
587
  // One small read, one parse, two fields. `description` is operator prose
555
588
  // and is deliberately NOT carried: this payload also travels over the
556
589
  // optional Clank webhook unredacted.
@@ -565,7 +598,16 @@ async function handleSubagentStop(input) {
565
598
  payload.tool_use_id = meta.toolUseId.trim();
566
599
  }
567
600
  // A SECOND witness for the type — never merged into `agent`, so the
568
- // empty-`agent_type` rate stays measurable.
601
+ // empty-`agent_type` rate stays measurable. NO FALLBACK MERGE, deliberately:
602
+ // `payload.agent` already carries the harness's `input.agent_type`, and
603
+ // filling one from the other would erase exactly the signal this pair
604
+ // exists to produce.
605
+ //
606
+ // REPORTING RULE: the presence rate of this key is only meaningful against
607
+ // the events whose SIDECAR WAS FOUND — "present on N of M events without
608
+ // `sidecar_missing`", never N of all `orchestrator.agent.stopped` records.
609
+ // Measured against all events the rate reads as a producer defect in this
610
+ // block; against its real denominator it is a lookup-failure rate.
569
611
  if (typeof meta?.agentType === 'string' && AGENT_TYPE_META_RE.test(meta.agentType.trim())) {
570
612
  payload.agent_type_meta = meta.agentType.trim();
571
613
  }
@@ -1120,6 +1120,9 @@ async function main() {
1120
1120
  // string compare then reads false and silently no-ops the whole scope-detector
1121
1121
  // under a symlinked `$CLAUDE_PLUGIN_ROOT`. realpath'ing both sides also survives
1122
1122
  // `--preserve-symlinks` (where import.meta.url stays symlinked instead).
1123
+ // BV-004 ceiling: kept inline — hooks avoid importing scripts/lib on the hot
1124
+ // path; the canonical predicate is scripts/lib/is-main-module.mjs. Revisit if the
1125
+ // hook-import-set (hooks/_lib/hook-import-set.json) ever admits scripts/lib here.
1123
1126
  function invokedAsScript() {
1124
1127
  const entry = process.argv[1];
1125
1128
  if (!entry) return false;
@@ -778,6 +778,9 @@ async function main() {
778
778
  // under a symlinked plugin install) while `import.meta.url` is realpath-resolved
779
779
  // by node's default loader, so BOTH sides are realpath'd.
780
780
  // ---------------------------------------------------------------------------
781
+ // BV-004 ceiling: kept inline — hooks avoid importing scripts/lib on the hot
782
+ // path; the canonical predicate is scripts/lib/is-main-module.mjs. Revisit if the
783
+ // hook-import-set (hooks/_lib/hook-import-set.json) ever admits scripts/lib here.
781
784
  function invokedAsScript() {
782
785
  const entry = process.argv[1];
783
786
  if (!entry) return false;
@@ -53,7 +53,7 @@
53
53
  import { readStdin, emitAllow, emitDeny, emitWarn } from '../scripts/lib/io.mjs';
54
54
  import { resolveProjectDir } from '../scripts/lib/platform.mjs';
55
55
  import { readJson } from '../scripts/lib/common.mjs';
56
- import { findIssueCreateStatements, isLoopedIssueCreate } from './_lib/vcs-create-matcher.mjs';
56
+ import { findIssueCreateStatements, findLoopedIssueCreates } from './_lib/vcs-create-matcher.mjs';
57
57
  import {
58
58
  loadIssueBudgetConfig,
59
59
  resolveIssueBudgetSessionId,
@@ -187,15 +187,39 @@ function resolveToolCallId(input) {
187
187
  * entry #N … nothing is lost", which would be false here — an uncountable bulk
188
188
  * request is not parked, it is handed back whole.
189
189
  *
190
+ * ## Why the lane is a parameter (#1379)
191
+ *
192
+ * Until 2026-09-17 this text said "sits inside a shell loop body (`do … done`)"
193
+ * on BOTH lanes, so the `xargs` deny (`echo b | xargs -I% glab issue create
194
+ * --title junk%` — no loop anywhere) sent the operator looking for a loop the
195
+ * command does not have. Only the lane SENTENCE varies; every other line is
196
+ * byte-identical across lanes, and tests pin them.
197
+ *
190
198
  * @param {{ "max-per-session": number }} config
199
+ * @param {{ lane?: "loop"|"xargs"|"mixed" }} [opts]
191
200
  * @returns {string}
192
201
  */
193
- function formatLoopDenyReason(config) {
202
+ function formatLoopDenyReason(config, { lane = 'loop' } = {}) {
203
+ const LANE_SENTENCES = {
204
+ loop: [
205
+ 'The `issue create` call sits inside a shell loop body (`do … done`), so the cap cannot',
206
+ 'charge it honestly: the word list is expanded by the shell AFTER this hook runs, so',
207
+ '`for i in $(seq 1 50)` would file 50 issues against a count of 1.',
208
+ ],
209
+ xargs: [
210
+ 'The `issue create` call is driven by `xargs`, so the cap cannot charge it honestly: the',
211
+ 'word list arrives on stdin AFTER this hook runs, so `seq 1 50 | xargs` would file 50',
212
+ 'issues against a count of 1.',
213
+ ],
214
+ mixed: [
215
+ 'The `issue create` calls are driven by a shell loop body (`do … done`) AND by `xargs`,',
216
+ 'so the cap cannot charge them honestly: both word lists are produced AFTER this hook',
217
+ 'runs, so `seq 1 50 | xargs` would file 50 issues against a count of 1.',
218
+ ],
219
+ };
194
220
  return [
195
221
  'issue-budget: this command creates an UNKNOWN number of issues — refusing to guess.',
196
- 'The `issue create` call sits inside a shell loop body (`do … done`), so the cap cannot',
197
- 'charge it honestly: the word list is expanded by the shell AFTER this hook runs, so',
198
- '`for i in $(seq 1 50)` would file 50 issues against a count of 1.',
222
+ ...(LANE_SENTENCES[lane] ?? LANE_SENTENCES.loop),
199
223
  '',
200
224
  'Nothing was parked as overflow, because nothing is lost: re-issue the create calls as',
201
225
  'SEPARATE commands and each one is counted normally against the cap',
@@ -302,18 +326,59 @@ async function main() {
302
326
  const config = loadIssueBudgetConfig(projectDir);
303
327
  if (config.mode === 'off') return emitAllow();
304
328
 
305
- // G3b — bulk creation whose multiplicity is not computable (#1145). The
306
- // exemption is asked FIRST, through the same classifier chargeIssueBudget
307
- // uses, so a looped carryover sweep keeps its unconditional pass. It is asked
308
- // on the FIRST issue-create statement's text, which is the very statement
309
- // `isLoopedIssueCreate` judges classifying it on the whole command would
310
- // let an exempt NEIGHBOUR statement lift the loop deny.
311
- const uncountableBulk =
312
- isLoopedIssueCreate(command) && !classifyExemption(statements[0].text).exempt;
329
+ // G3b — bulk creation whose multiplicity is not computable (#1145). Two
330
+ // sources, one policy: a LOOP BODY (#1145) and an `xargs`-driven create
331
+ // (#1289), where the word list arrives on stdin. Both file N issues for one
332
+ // statement, so both are denied rather than charged 1 — the fix had to land
333
+ // here and not only in `isLoopedIssueCreate`, because G3 above short-circuits
334
+ // on `statements.length === 0` and an xargs create used to produce zero
335
+ // statements, so no loop-side fix could ever run.
336
+ //
337
+ // THE INVARIANT (rewritten 2026-09-16): the exemption is classified on the
338
+ // statement that CAUSED the bulk classification — never on `statements[0]`,
339
+ // and never on the whole command. A `[Carryover]` create standing NEXT TO an
340
+ // uncountable one is an unrelated neighbour and must not lift the deny; a
341
+ // `[Carryover]` create that IS the bulk statement keeps its documented
342
+ // unconditional pass (session-end's "those are never deferred" promise, which
343
+ // holds inside a loop too). Measured 2026-09-16 against the previous
344
+ // `statements[0]` binding, both lanes ALLOW where they must DENY:
345
+ // glab issue create --title "[Carryover] real"; echo X | xargs -I% glab issue create --title %
346
+ // glab issue create --title "[Carryover] real"; for i in 1 2 3; do glab issue create --title junk$i; done
347
+ // Same class as the bypass-scoping regression on `matchesBypass` — a matcher
348
+ // widened per statement while its exemption stayed whole-command
349
+ // (`.claude/rules/guard-design.md` § "Widening a matcher without narrowing
350
+ // its bypass", #1106).
351
+ //
352
+ // Fail-CLOSED when a command carries SEVERAL bulk statements and only some are
353
+ // exempt: `some()` over the non-exempt ones denies, because the command as a
354
+ // whole still files an uncountable number of untemplated issues.
355
+ //
356
+ // EVERY loop, not the first (#1379): the loop lane used to contribute at most
357
+ // ONE entry here, so an exempt FIRST loop was the only loop classified and
358
+ // every later loop went unjudged. Reproduced 2026-09-17 @ `9e8146b4`:
359
+ // for i in 1 2 3; do glab issue create --label carryover --title x$i; done;
360
+ // for j in 1 2 3; do glab issue create --title junk$j; done → ALLOW (count=1)
361
+ const bulkEntries = [
362
+ ...findLoopedIssueCreates(command).map((tokens) => ({
363
+ lane: 'loop',
364
+ text: tokens.map((t) => t.text).join(' '),
365
+ })),
366
+ ...statements.filter((s) => s.bulk).map((s) => ({ lane: 'xargs', text: s.text })),
367
+ ];
368
+ const uncountableEntries = bulkEntries.filter((e) => !classifyExemption(e.text).exempt);
369
+ const uncountableBulk = uncountableEntries.length > 0;
370
+ // ONE lane resolution for BOTH reports (#1379 follow-up). The deny already
371
+ // named the lane it fired on; the `warn` undercount notice at the bottom of
372
+ // main() said "inside a loop body" unconditionally, so the xargs lane — where
373
+ // no loop exists anywhere in the command — sent the operator looking for one.
374
+ // Resolved here rather than twice, so the two reports can never disagree.
375
+ const uncountableLanes = new Set(uncountableEntries.map((e) => e.lane));
376
+ const uncountableLane =
377
+ uncountableLanes.size > 1 ? 'mixed' : ([...uncountableLanes][0] ?? 'loop');
313
378
  if (uncountableBulk && config.mode === 'strict') {
314
379
  // Nothing is charged and nothing is parked — the command is handed back
315
380
  // whole, which is what makes unrolling it the correct next action.
316
- return emitDeny(formatLoopDenyReason(config));
381
+ return emitDeny(formatLoopDenyReason(config, { lane: uncountableLane }));
317
382
  }
318
383
 
319
384
  const sessionId = await resolveSessionId(input, projectDir);
@@ -401,11 +466,32 @@ async function main() {
401
466
  // to name the undercount out loud — a silent 1-for-N is the exact failure the
402
467
  // deny above exists to prevent, and `warn` must not reintroduce it quietly.
403
468
  // emitWarn, not stderr: under exit 0 stderr reaches only the debug log (#916).
404
- if (uncountableBulk && verdict.decision === 'allow') {
469
+ //
470
+ // GATED ON THE BULK STATEMENTS, NEVER ON THE LAST VERDICT (2026-09-17). The
471
+ // condition used to read `verdict.decision === 'allow'`, i.e. the verdict of
472
+ // the LAST statement of the chain — so one exempt statement written AFTER an
473
+ // uncountable one silenced the notice the two paragraphs above declare
474
+ // mandatory. Reproduced through this hook binary in `mode: warn`:
475
+ // for i in 1 2; do glab issue create --title j$i; done; \
476
+ // glab issue create --title "[Carryover] z"
477
+ // → stdout EMPTY, ledger count=1 exempt=1 (the trailing exempt statement is
478
+ // an unrelated neighbour, exactly as in G3b's own invariant); without it,
479
+ // the identical loop reported the UNDERCOUNT.
480
+ // Reaching this point already means no statement blocked and none warned
481
+ // (both branches above return), so the uncountable bulk statement — non-exempt
482
+ // by construction of `uncountableEntries` — was charged as 1. The two negated
483
+ // conditions are kept explicit so a future reordering of those branches cannot
484
+ // turn this back into a report about the wrong statement.
485
+ if (uncountableBulk && !blocked && !verdicts.some((v) => v.decision === 'warn')) {
486
+ const LANE_PHRASES = {
487
+ loop: 'inside a loop body',
488
+ xargs: 'driven by `xargs`',
489
+ mixed: 'inside a loop body and by `xargs`',
490
+ };
405
491
  return emitWarn(
406
- `pre-bash-issue-budget: bulk create inside a loop body charged as 1 ` +
492
+ `pre-bash-issue-budget: bulk create ${LANE_PHRASES[uncountableLane]} charged as 1 ` +
407
493
  `(${verdict.count}/${verdict.max}) — the real number of issues this files is not ` +
408
- `knowable before the shell expands the word list, so the count is an UNDERCOUNT. ` +
494
+ `knowable before the word list is expanded, so the count is an UNDERCOUNT. ` +
409
495
  `Set \`issue-budget.mode: strict\` to deny this shape instead.`,
410
496
  );
411
497
  }
@@ -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,25 @@ 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 receive-side instruction line `renderScopeEchoInstruction()` renders and
293
+ * the coordinator appends immediately after the fenced block
294
+ * (`wave-loop-dispatch.md` § Pre-Dispatch: File-Scope Injection).
295
+ *
296
+ * Matching it HERE, in the same prompt, is what makes the agent-A-block-with-
297
+ * agent-B-line mix-up catchable without a single filesystem read: the block and
298
+ * the line must come from the same `$AGENT_FILESCOPE_JSON`, so their digests
299
+ * must agree (`digest_consistent`). The marker half stays case-sensitive for the
300
+ * same reason `scope-echo.mjs` keeps it so — a lowercase lookalike is not the
301
+ * line; the HEX half is read case-insensitively and normalised to lowercase.
302
+ * Non-global on purpose: a `g` regex carries `lastIndex` between calls.
303
+ */
304
+ const ECHO_INSTRUCTION_RE =
305
+ /End your final report with the line:[ \t]*`{0,3}SCOPE-DIGEST:[ \t]*`{0,3}([0-9a-fA-F]{8})(?![0-9a-fA-F])/;
306
+
307
+ /** Shape of a well-formed digest, used to reject anything a broken digest fn returns. */
308
+ const DIGEST_RE = /^[0-9a-f]{8}$/;
309
+
290
310
  /**
291
311
  * Clamp for the one free-form string the event carries (`agent_id`, built from
292
312
  * the coordinator's own `description`). A dispatch description is a label, but
@@ -358,6 +378,14 @@ async function bootstrap() {
358
378
  io: { specifier: lib('io.mjs') },
359
379
  scopeGate: { specifier: lib('scope-gate.mjs') },
360
380
  fileLock: { specifier: lib('file-lock.mjs') },
381
+ // ONE normalization for the digest, shared with the receive side (#1092):
382
+ // `scope-echo.mjs` is pure (stdlib + `crypto-digest-utils.mjs`) and its
383
+ // `scopeDigest` is what `--verify` joins the two halves on. A second
384
+ // implementation here would let the halves disagree while both looked
385
+ // right — the class `guard-design.md` § "zero-import predicate module"
386
+ // names. Late-bound like every other repo module so a load failure
387
+ // banners instead of disarming the guard silently (#993).
388
+ scopeEcho: { specifier: lib('scope-echo.mjs') },
361
389
  },
362
390
  {
363
391
  hookName: HOOK_NAME,
@@ -370,6 +398,7 @@ async function bootstrap() {
370
398
  ({ readStdin, emitAllow, emitDeny, emitWarn, writeJsonAtomicSync } = modules.io);
371
399
  ({ findScopeCollisions } = modules.scopeGate);
372
400
  ({ withFileLock } = modules.fileLock);
401
+ ({ scopeDigest } = modules.scopeEcho);
373
402
  }
374
403
 
375
404
  // ---------------------------------------------------------------------------
@@ -407,8 +436,51 @@ const SCOPE_TERMS = 'DATEI[- ]SCOPE|FILE[- ]SCOPE|FILE SCOPE|DEIN SCOPE|SCOPE \\
407
436
  * hit in a region as one is the recorded failure of `parseEpicRef` (#1112).
408
437
  *
409
438
  * The real miss class is a different SHAPE, handled by {@link INLINE_SCOPE_DECL}.
439
+ *
440
+ * ## The marker vocabulary is CASE-SENSITIVE, and that is a measurement (#1092)
441
+ *
442
+ * Until 2026-09-16 both shapes carried the `i` flag, so the term `FILE SCOPE`
443
+ * also matched ordinary lowercase prose. The Learnings-Index header this repo
444
+ * injects into every agent prompt —
445
+ * `## Learnings Index (selected for your file scope) — …` — therefore matched
446
+ * the marker at column ~38 of a line-leading window, and the FIRST fenced block
447
+ * anywhere after it (a verification command, a report template) decided the
448
+ * class: no path survived, so the dispatch was recorded `unparseable`, i.e.
449
+ * matrix row 6 — "a declaration is present but the parser gave up".
450
+ *
451
+ * MEASURED in this session's own wave 1 (`.orchestrator/wave-dispatch-scopes.json`,
452
+ * waveKey `5de6560c-…|w1|Discovery`): `unparseable: 5, extracted: 0` for FIVE
453
+ * Discovery dispatches that carried no `FILE-SCOPE` block at all. Host-wide the
454
+ * same confusion accounts for the 52 historical `unparseable` records.
455
+ *
456
+ * Every DOCUMENTED marker is upper-case (`wave-loop-dispatch.md` § Pre-Dispatch:
457
+ * File-Scope Injection writes `FILE-SCOPE — exactly these:`), while every
458
+ * measured false positive is prose in sentence case — so dropping `i` separates
459
+ * them without narrowing the search window, which the § above showed recovers
460
+ * nothing.
461
+ *
462
+ * ## What dropping `i` costs, stated honestly (corrected 2026-09-16)
463
+ *
464
+ * The first cut of this note claimed the change "moves a CLASSIFICATION and
465
+ * never a decision", on the grounds that `marker-absent` and `unparseable` both
466
+ * resolve to ALLOW. That is only true for a declaration the parser would have
467
+ * given up on anyway. A MIXED-CASE declaration that WOULD have yielded paths
468
+ * (`File-Scope:` followed by a fenced path list) now matches nothing, so
469
+ * `extractScopeFromPrompt` returns `[]`, `findScopeCollisions` never runs, and
470
+ * the dispatch is ALLOWED unconditionally — a lost DENY capability for that
471
+ * spelling, i.e. a decision change, not a reclassification.
472
+ *
473
+ * It is safe today because every LIVE injector writes the upper-case canonical
474
+ * marker: `skills/wave-executor/references/wave-loop-dispatch.md`
475
+ * § Pre-Dispatch: File-Scope Injection documents `FILE-SCOPE — exactly these:`,
476
+ * and the repo-wide census (2026-09-16) finds the mixed-case spellings only in
477
+ * prose, never in an injected block. That is a property of the injectors, not of
478
+ * this regex, so it is PINNED rather than assumed:
479
+ * `tests/skills/wave-loop-scope-marker.test.mjs` asserts the documented marker
480
+ * line is upper-case and matches {@link SCOPE_MARKER}, so a future template edit
481
+ * to `File-Scope:` goes red instead of silently disarming the guard.
410
482
  */
411
- const SCOPE_MARKER = new RegExp(`^.{0,80}(${SCOPE_TERMS})`, 'im');
483
+ export const SCOPE_MARKER = new RegExp(`^.{0,80}(${SCOPE_TERMS})`, 'm');
412
484
 
413
485
  /**
414
486
  * SHAPE 2 (lower precedence) — the measured miss class: a declaration written
@@ -436,7 +508,7 @@ const SCOPE_MARKER = new RegExp(`^.{0,80}(${SCOPE_TERMS})`, 'im');
436
508
  * one false extraction (`[".filter"]`) is a prose fragment, which is why the
437
509
  * fenced shape keeps precedence and this one runs only when that found nothing.
438
510
  */
439
- const INLINE_SCOPE_DECL = new RegExp(`(${SCOPE_TERMS})`, 'ig');
511
+ const INLINE_SCOPE_DECL = new RegExp(`(${SCOPE_TERMS})`, 'g');
440
512
  const INLINE_DECL_OPERATOR = /^[^\n(:—–]{0,2}(\([^)\n]{0,80}\))?\s*(?:[—–][^\n:]{0,30})?\s*(:|—|–)/;
441
513
 
442
514
  /** Separators a coordinator uses between paths in an inline declaration. */
@@ -980,6 +1052,69 @@ export function bumpSignalCounter(ledger, waveKey, status) {
980
1052
  return next;
981
1053
  }
982
1054
 
1055
+ /**
1056
+ * The digest half of the per-dispatch record (#1092) — the field set that makes
1057
+ * the SEND side joinable to the RECEIVE side.
1058
+ *
1059
+ * Four facts, all derived from the PROMPT alone, none of them a path:
1060
+ *
1061
+ * `scope_digest` — `scopeDigest()` over the paths extracted FROM THE
1062
+ * PROMPT. This is the join key `scope-echo --verify`
1063
+ * uses; `agent_id` cannot be one, because the two
1064
+ * halves spell it differently (measured 2026-09-16:
1065
+ * 609 send-side vs 51 receive-side records, agent-id
1066
+ * overlap ZERO — send writes
1067
+ * `"i-3 #1353 … (session-orchestrator:code-implementer)"`,
1068
+ * receive writes `"i-3"`).
1069
+ * `echo_instruction_present`— the coordinator appended the echo line at all.
1070
+ * `instructed_digest` — the digest THAT LINE names.
1071
+ * `digest_consistent` — the two agree. `false` is agent A's fenced block
1072
+ * beside agent B's echo line, caught at dispatch
1073
+ * time with no filesystem read.
1074
+ *
1075
+ * ABSENT IS NOT ZERO, in both directions (`docs/events-schema.md`):
1076
+ * `scope_digest` is OMITTED for an empty scope — never the digest of the empty
1077
+ * string, which is a real 8-hex value and would join marker-absent Discovery
1078
+ * dispatches to each other. `instructed_digest` is omitted when no line was
1079
+ * found, and `digest_consistent` unless BOTH are present.
1080
+ *
1081
+ * TOTAL by construction: every branch is wrapped, because this runs on the
1082
+ * decision path of a deny-capable hook. A throwing digest function must cost the
1083
+ * FIELD, never the verdict — same discipline as the awaited-and-caught emit.
1084
+ *
1085
+ * @param {string[]} files paths extracted from the prompt
1086
+ * @param {unknown} prompt the dispatch prompt
1087
+ * @param {((paths: string[]) => string)} [digestFn] injectable for tests; defaults
1088
+ * to the late-bound `scopeDigest` (undefined when `bootstrap()` has not run,
1089
+ * which this function treats exactly like a throwing one — the field is omitted)
1090
+ * @returns {{scope_digest?: string, echo_instruction_present: boolean,
1091
+ * instructed_digest?: string, digest_consistent?: boolean}}
1092
+ */
1093
+ export function scopeDigestFields(files, prompt, digestFn) {
1094
+ /** @type {Record<string, unknown>} */
1095
+ const out = {};
1096
+ try {
1097
+ const fn = typeof digestFn === 'function' ? digestFn : scopeDigest;
1098
+ if (typeof fn === 'function' && Array.isArray(files) && files.length > 0) {
1099
+ const digest = fn(files);
1100
+ if (typeof digest === 'string' && DIGEST_RE.test(digest)) out.scope_digest = digest;
1101
+ }
1102
+ } catch { /* a broken digest costs the field, never the decision */ }
1103
+
1104
+ let instructed = null;
1105
+ try {
1106
+ const match = typeof prompt === 'string' ? ECHO_INSTRUCTION_RE.exec(prompt) : null;
1107
+ if (match !== null) instructed = match[1].toLowerCase();
1108
+ } catch { instructed = null; }
1109
+
1110
+ out.echo_instruction_present = instructed !== null;
1111
+ if (instructed !== null) out.instructed_digest = instructed;
1112
+ if (typeof out.scope_digest === 'string' && instructed !== null) {
1113
+ out.digest_consistent = out.scope_digest === instructed;
1114
+ }
1115
+ return /** @type {any} */ (out);
1116
+ }
1117
+
983
1118
  /**
984
1119
  * @typedef {{action: 'allow'|'deny'|'warn', reason?: string, suggestion?: string,
985
1120
  * ledger?: object|null, note?: string, telemetry?: object}} Verdict
@@ -997,9 +1132,11 @@ export function bumpSignalCounter(ledger, waveKey, status) {
997
1132
  * @param {Function} params.collide `findScopeCollisions` (injected for testability)
998
1133
  * @param {(entry: object) => boolean} [params.isFinished] liveness probe (§ Liveness)
999
1134
  * @param {string} [params.nowIso] dispatch timestamp recorded on the entry
1135
+ * @param {(paths: string[]) => string} [params.digestFn] scope-digest function
1136
+ * (injected for testability; defaults to the late-bound `scopeDigest`)
1000
1137
  * @returns {Verdict}
1001
1138
  */
1002
- export function decide({ input, ledger, ledgerCorrupt, waveKey, knownFiles, collide, isFinished, nowIso }) {
1139
+ export function decide({ input, ledger, ledgerCorrupt, waveKey, knownFiles, collide, isFinished, nowIso, digestFn }) {
1003
1140
  const toolName = input?.tool_name;
1004
1141
  // Row 4: not our tool.
1005
1142
  if (toolName !== DISPATCH_TOOL) return { action: 'allow' };
@@ -1023,13 +1160,19 @@ export function decide({ input, ledger, ledgerCorrupt, waveKey, knownFiles, coll
1023
1160
  // agent id, never a path, never a byte of the prompt (issue #1092 acceptance
1024
1161
  // criterion 3). `wave` is omitted rather than zeroed when unknown.
1025
1162
  const wave = waveNumberOf(waveKey);
1163
+ const digestFields = scopeDigestFields(files, toolInput.prompt, digestFn);
1026
1164
  const telemetryFor = (ledgerResult, collisionCount = 0) => ({
1027
1165
  ...(wave === null ? {} : { wave }),
1028
1166
  agent_id: id.slice(0, MAX_AGENT_ID_CHARS),
1029
1167
  declared_path_count: files.length,
1030
1168
  injected: files.length > 0,
1169
+ // `marker_found` is NOT a second spelling of `injected`: a fenced block whose
1170
+ // lines are prose is `marker_found: true, injected: false` (matrix row 6),
1171
+ // which is the row-5-vs-row-6 split expressed as a boolean a query can group by.
1172
+ marker_found: signal.status !== SIGNAL_MARKER_ABSENT,
1031
1173
  shape: signal.shape,
1032
1174
  signal: signal.status,
1175
+ ...digestFields,
1033
1176
  ledger_result: ledgerResult,
1034
1177
  collision_count: collisionCount,
1035
1178
  });
@@ -1309,6 +1452,12 @@ async function main() {
1309
1452
  // symlinked plugin install) while `import.meta.url` is realpath-resolved by
1310
1453
  // node's default loader, so BOTH sides are realpath'd — the same comparison
1311
1454
  // `hooks/post-bash-write-verify.mjs` documents (#938 MED-2).
1455
+ //
1456
+ // NAMED CEILING (BV-004): kept inline rather than imported — hooks avoid
1457
+ // importing `scripts/lib` on the hot path, where every added module is paid on
1458
+ // every dispatch. The canonical predicate is `scripts/lib/is-main-module.mjs`;
1459
+ // revisit if this hook ever imports that tree for another reason, at which
1460
+ // point the copy costs more than the import saves.
1312
1461
  // ---------------------------------------------------------------------------
1313
1462
  function invokedAsScript() {
1314
1463
  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);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "session-orchestrator",
3
- "version": "5.0.0",
4
- "description": "Loop engineering for AI coding agents turn ad-hoc sessions into a repeatable research plan wave-execute close loop with verification gates. Runs on Claude Code, Codex CLI, Cursor, and Pi.",
3
+ "version": "5.2.0",
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",
7
7
  "keywords": [
@@ -57,6 +57,7 @@
57
57
  "test": "node scripts/check-package-manager.mjs && vitest --run",
58
58
  "test:coverage": "node scripts/check-package-manager.mjs && vitest --run --coverage",
59
59
  "test:watch": "node scripts/check-package-manager.mjs && vitest",
60
+ "test:pack": "node scripts/check-package-manager.mjs && SO_PACK_TEST=1 vitest --run tests/scripts/pack-install-lifecycle.test.mjs",
60
61
  "quality-gate": "node scripts/run-quality-gate.mjs --variant full-gate",
61
62
  "lint": "eslint .",
62
63
  "lint:fix": "eslint . --fix",
@@ -1,12 +1,12 @@
1
1
  ---
2
- description: Autonomous session-orchestration loop with kill-switches (Phase C-1.b all 10 kill-switches shipped)
2
+ description: "Use this skill when running an autonomous session-orchestration loop. Chains session-start → session-plan → wave-executor → session-end for N iterations with all 10 kill-switches (SPIRAL, FAILED wave, carryover > 50%, max-hours, max-sessions, resource-overload, token-budget, stall-timeout, sub-threshold confidence, user-abort). Reads Mode-Selector output (Phase B) to decide auto-execute vs. fallback. Writes one autopilot.jsonl record per loop run. Phase C scaffold (issue #277); implementation lives in scripts/lib/autopilot.mjs (Phase C-1 follow-up)."
3
3
  argument-hint: "[--headless] [--verbose] [--max-sessions=N] [--max-hours=H] [--confidence-threshold=0.X] [--dry-run]"
4
4
  ---
5
5
 
6
6
  # /autopilot
7
7
 
8
- Use the Session Orchestrator command definition at `commands/autopilot.md`.
8
+ Use the Session Orchestrator skill definition at `skills/autopilot/SKILL.md`.
9
9
 
10
10
  Arguments: $@
11
11
 
12
- Read that command file and follow it exactly. When it references `$ARGUMENTS`, substitute the arguments above. Keep all Session Orchestrator platform fallbacks intact.
12
+ Read that skill file and follow it exactly. When it references `$ARGUMENTS`, substitute the arguments above. Keep all Session Orchestrator platform fallbacks intact.
@@ -1,12 +1,12 @@
1
1
  ---
2
- description: Scaffold the minimum repo structure required by session-orchestrator
2
+ description: "Use this skill when scaffolding the minimum repository structure required by session-orchestrator. Invoked automatically by the Bootstrap Gate when CLAUDE.md, Session Config, or bootstrap.lock is missing. Also available as /bootstrap for manual invocation. Three intensity tiers: fast (demos/spikes), standard (MVPs), deep (production/team)."
3
3
  argument-hint: "[--upgrade <tier>]"
4
4
  ---
5
5
 
6
6
  # /bootstrap
7
7
 
8
- Use the Session Orchestrator command definition at `commands/bootstrap.md`.
8
+ Use the Session Orchestrator skill definition at `skills/bootstrap/SKILL.md`.
9
9
 
10
10
  Arguments: $@
11
11
 
12
- Read that command file and follow it exactly. When it references `$ARGUMENTS`, substitute the arguments above. Keep all Session Orchestrator platform fallbacks intact.
12
+ Read that skill file and follow it exactly. When it references `$ARGUMENTS`, substitute the arguments above. Keep all Session Orchestrator platform fallbacks intact.
@@ -1,12 +1,12 @@
1
1
  ---
2
- description: Run a lightweight Socratic design dialogue (3-5 AUQ rounds) and write a spec markdown file before any implementation work. Use BEFORE /plan feature when scope/UX is ambiguous.
2
+ description: "Use when you have a feature idea but the scope or UX is still ambiguous — runs a lightweight Socratic design dialogue (3-5 AUQ rounds) and writes a spec markdown file. Use BEFORE /plan feature when product intent needs validation; skip to /plan feature when scope is already clear. HARD-GATE prevents any code work until the design is user-approved."
3
3
  argument-hint: "[topic-or-feature-slug]"
4
4
  ---
5
5
 
6
6
  # /brainstorm
7
7
 
8
- Use the Session Orchestrator command definition at `commands/brainstorm.md`.
8
+ Use the Session Orchestrator skill definition at `skills/brainstorm/SKILL.md`.
9
9
 
10
10
  Arguments: $@
11
11
 
12
- Read that command file and follow it exactly. When it references `$ARGUMENTS`, substitute the arguments above. Keep all Session Orchestrator platform fallbacks intact.
12
+ Read that skill file and follow it exactly. When it references `$ARGUMENTS`, substitute the arguments above. Keep all Session Orchestrator platform fallbacks intact.
@@ -4,8 +4,8 @@ description: End session with verification, commits, and documentation
4
4
 
5
5
  # /close
6
6
 
7
- Use the Session Orchestrator command definition at `commands/close.md`.
7
+ Use the Session Orchestrator skill definition at `skills/close/SKILL.md`.
8
8
 
9
9
  Arguments: $@
10
10
 
11
- Read that command file and follow it exactly. When it references `$ARGUMENTS`, substitute the arguments above. Keep all Session Orchestrator platform fallbacks intact.
11
+ Read that skill file and follow it exactly. When it references `$ARGUMENTS`, substitute the arguments above. Keep all Session Orchestrator platform fallbacks intact.
@@ -0,0 +1,11 @@
1
+ ---
2
+ description: "Monitor iterative improvement loops for convergence. Three signals — shrinking diff, pass-rate plateau, velocity — drive a Stop/Continue/Investigate decision at each inter-wave checkpoint. Distinct from /evolve (retrospective) and session-reviewer (wave output review): convergence-monitoring answers \"are we making progress?\" not \"was the last wave correct?\". Primary consumer: /autoresearch loops and wave-executor inter-wave checkpoints."
3
+ ---
4
+
5
+ # /convergence-monitoring
6
+
7
+ Use the Session Orchestrator skill definition at `skills/convergence-monitoring/SKILL.md`.
8
+
9
+ Arguments: $@
10
+
11
+ Read that skill file and follow it exactly. When it references `$ARGUMENTS`, substitute the arguments above. Keep all Session Orchestrator platform fallbacks intact.
@@ -1,12 +1,12 @@
1
1
  ---
2
- description: Run a 4-phase systematic debugging investigation before proposing any fix. Iron Law NO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST. Produces a `.orchestrator/debug/` artifact the fixer agent must reference.
2
+ description: "Use when encountering any bug, test failure, build break, or unexpected behavior — runs a 4-phase systematic debugging process before proposing any fix. Iron Law: NO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST. Produces a `.orchestrator/debug/` artifact the fixer agent must reference."
3
3
  argument-hint: "[bug-description-or-issue-ref]"
4
4
  ---
5
5
 
6
6
  # /debug
7
7
 
8
- Use the Session Orchestrator command definition at `commands/debug.md`.
8
+ Use the Session Orchestrator skill definition at `skills/debug/SKILL.md`.
9
9
 
10
10
  Arguments: $@
11
11
 
12
- Read that command file and follow it exactly. When it references `$ARGUMENTS`, substitute the arguments above. Keep all Session Orchestrator platform fallbacks intact.
12
+ Read that skill file and follow it exactly. When it references `$ARGUMENTS`, substitute the arguments above. Keep all Session Orchestrator platform fallbacks intact.
@@ -1,12 +1,12 @@
1
1
  ---
2
- description: Systematic quality discovery and issue detection
2
+ description: "Use this skill when running systematic quality discovery and issue detection. Runs modular probes adapted to the project's tech stack, presents findings interactively for user triage, and creates VCS issues for confirmed problems. Invoked standalone via /discovery or embedded in session-end."
3
3
  argument-hint: "[all|code|infra|ui|arch|session|audit|vault|feature] [--since <git-ref>] [--full]"
4
4
  ---
5
5
 
6
6
  # /discovery
7
7
 
8
- Use the Session Orchestrator command definition at `commands/discovery.md`.
8
+ Use the Session Orchestrator skill definition at `skills/discovery/SKILL.md`.
9
9
 
10
10
  Arguments: $@
11
11
 
12
- Read that command file and follow it exactly. When it references `$ARGUMENTS`, substitute the arguments above. Keep all Session Orchestrator platform fallbacks intact.
12
+ Read that skill file and follow it exactly. When it references `$ARGUMENTS`, substitute the arguments above. Keep all Session Orchestrator platform fallbacks intact.