session-orchestrator 3.24.0 → 4.0.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 (350) hide show
  1. package/.agents/skills/architecture/SKILL.md +18 -0
  2. package/.agents/skills/autopilot/SKILL.md +17 -0
  3. package/.agents/skills/bootstrap/SKILL.md +20 -0
  4. package/.agents/skills/brainstorm/SKILL.md +22 -0
  5. package/.agents/skills/claude-md-drift-check/SKILL.md +15 -0
  6. package/.agents/skills/convergence-monitoring/SKILL.md +22 -0
  7. package/.agents/skills/debug/SKILL.md +22 -0
  8. package/.agents/skills/discovery/SKILL.md +20 -0
  9. package/.agents/skills/dispatcher/SKILL.md +15 -0
  10. package/.agents/skills/docs-orchestrator/SKILL.md +18 -0
  11. package/.agents/skills/ecosystem-health/SKILL.md +20 -0
  12. package/.agents/skills/eli5/SKILL.md +20 -0
  13. package/.agents/skills/eval/SKILL.md +21 -0
  14. package/.agents/skills/evolve/SKILL.md +21 -0
  15. package/.agents/skills/frontmatter-guard/SKILL.md +15 -0
  16. package/.agents/skills/gitlab-ops/SKILL.md +20 -0
  17. package/.agents/skills/gitlab-portfolio/SKILL.md +15 -0
  18. package/.agents/skills/grill/SKILL.md +22 -0
  19. package/.agents/skills/hook-development/SKILL.md +15 -0
  20. package/.agents/skills/mcp-builder/SKILL.md +15 -0
  21. package/.agents/skills/memory-cleanup/SKILL.md +21 -0
  22. package/.agents/skills/mode-selector/SKILL.md +17 -0
  23. package/.agents/skills/npm-publish/SKILL.md +16 -0
  24. package/.agents/skills/peekaboo-driver/SKILL.md +18 -0
  25. package/.agents/skills/persona-panel/SKILL.md +17 -0
  26. package/.agents/skills/plan/SKILL.md +20 -0
  27. package/.agents/skills/playwright-driver/SKILL.md +20 -0
  28. package/.agents/skills/quality-gates/SKILL.md +20 -0
  29. package/.agents/skills/reconcile/SKILL.md +21 -0
  30. package/.agents/skills/remote-offload/SKILL.md +20 -0
  31. package/.agents/skills/repo-audit/SKILL.md +16 -0
  32. package/.agents/skills/session-end/SKILL.md +20 -0
  33. package/.agents/skills/session-plan/SKILL.md +20 -0
  34. package/.agents/skills/session-start/SKILL.md +20 -0
  35. package/.agents/skills/spinout/SKILL.md +16 -0
  36. package/.agents/skills/sunset-review/SKILL.md +16 -0
  37. package/.agents/skills/test-runner/SKILL.md +20 -0
  38. package/.agents/skills/tmux-layout/SKILL.md +21 -0
  39. package/.agents/skills/using-orchestrator/SKILL.md +17 -0
  40. package/.agents/skills/vault-mirror/SKILL.md +15 -0
  41. package/.agents/skills/vault-sync/SKILL.md +15 -0
  42. package/.agents/skills/wave-executor/SKILL.md +20 -0
  43. package/.agents/skills/write-executable-plan/SKILL.md +22 -0
  44. package/.claude-plugin/marketplace.json +1 -1
  45. package/.claude-plugin/plugin.json +1 -1
  46. package/.codex-plugin/plugin.json +1 -1
  47. package/.cursor/commands/autopilot.md +2 -2
  48. package/.cursor/commands/bootstrap.md +1 -1
  49. package/.cursor/commands/brainstorm.md +1 -1
  50. package/.cursor/commands/debug.md +1 -1
  51. package/.cursor/commands/discovery.md +1 -1
  52. package/.cursor/commands/dispatcher.md +2 -2
  53. package/.cursor/commands/eli5.md +2 -2
  54. package/.cursor/commands/eval.md +2 -2
  55. package/.cursor/commands/evolve.md +1 -1
  56. package/.cursor/commands/go.md +1 -1
  57. package/.cursor/commands/grill.md +2 -2
  58. package/.cursor/commands/memory-cleanup.md +2 -2
  59. package/.cursor/commands/persona-panel.md +1 -1
  60. package/.cursor/commands/plan.md +1 -1
  61. package/.cursor/commands/portfolio.md +1 -1
  62. package/.cursor/commands/reconcile.md +2 -2
  63. package/.cursor/commands/release.md +2 -2
  64. package/.cursor/commands/session.md +2 -2
  65. package/.cursor/commands/spinout.md +2 -2
  66. package/.cursor/commands/sunset-review.md +2 -2
  67. package/.cursor/commands/templates-ack.md +2 -2
  68. package/.cursor/commands/test.md +2 -2
  69. package/.cursor/skills/brainstorm/SKILL.md +1 -1
  70. package/.cursor/skills/eval/SKILL.md +1 -1
  71. package/.cursor/skills/quality-gates/SKILL.md +1 -1
  72. package/.cursor/skills/remote-offload/SKILL.md +1 -1
  73. package/.orchestrator/policy/blocked-commands.json +121 -0
  74. package/.orchestrator/policy/ecosystem.schema.json +66 -0
  75. package/.orchestrator/policy/quality-gates.example.json +16 -0
  76. package/.orchestrator/policy/quality-gates.schema.json +38 -0
  77. package/.orchestrator/policy/templates-policy.json +27 -0
  78. package/.orchestrator/policy/test-profiles.json +47 -0
  79. package/AGENTS.md +225 -0
  80. package/CHANGELOG.md +1125 -2
  81. package/NOTICE +11 -6
  82. package/README.md +127 -94
  83. package/agents/eval-judge.md +1 -1
  84. package/agents/skill-applied-judge.md +1 -1
  85. package/assets/wave-lifecycle.svg +98 -0
  86. package/commands/release.md +6 -3
  87. package/commands/session.md +18 -3
  88. package/docs/README.md +4 -0
  89. package/{agents/AGENTS.md → docs/agent-authoring.md} +19 -26
  90. package/docs/baseline.md +67 -0
  91. package/docs/ci-setup.md +108 -62
  92. package/docs/codex-setup.md +65 -21
  93. package/docs/components.md +36 -15
  94. package/docs/cursor-setup.md +6 -2
  95. package/docs/events-schema.md +9 -6
  96. package/docs/instruction-delivery.md +62 -0
  97. package/{agents/memory-proposal-collector.md → docs/memory-proposal-flow.md} +1 -8
  98. package/docs/migration-v4.md +341 -0
  99. package/docs/pi-setup.md +6 -1
  100. package/docs/plugin-architecture-v3.md +1 -1
  101. package/docs/rule-authoring.md +85 -19
  102. package/docs/scope-collision-guard.md +5 -5
  103. package/docs/session-config-reference.md +57 -56
  104. package/docs/session-config-template.md +6 -29
  105. package/docs/telemetry.md +157 -3
  106. package/docs/vault-docs-architecture.md +50 -11
  107. package/hooks/_lib/hook-import-set.json +1487 -0
  108. package/hooks/_lib/subagent-transcript.mjs +562 -0
  109. package/hooks/config-protection.mjs +2 -2
  110. package/hooks/cwd-change-restore.mjs +2 -2
  111. package/hooks/enforce-commands.mjs +69 -0
  112. package/hooks/hooks-codex.json +1 -1
  113. package/hooks/hooks-cursor.json +10 -0
  114. package/hooks/hooks-pi.json +5 -0
  115. package/hooks/hooks.json +6 -1
  116. package/hooks/loop-guard.mjs +3 -3
  117. package/hooks/on-session-end.mjs +2 -2
  118. package/hooks/on-session-start.mjs +103 -2
  119. package/hooks/on-stop.mjs +36 -11
  120. package/hooks/operator-steer.mjs +2 -2
  121. package/hooks/post-bash-write-verify.mjs +85 -0
  122. package/hooks/post-edit-import-probe.mjs +344 -0
  123. package/hooks/post-subagent-discovery-validator.mjs +187 -431
  124. package/hooks/post-tool-batch-wave-signal.mjs +118 -4
  125. package/hooks/post-tool-failure-corrective-context.mjs +2 -2
  126. package/hooks/post-tooluse-frontend-slop.mjs +3 -3
  127. package/hooks/pre-bash-destructive-guard.mjs +39 -13
  128. package/hooks/skill-invocation-telemetry.mjs +17 -5
  129. package/hooks/subagent-telemetry.mjs +13 -4
  130. package/monitors/monitors.json +3 -3
  131. package/package.json +9 -1
  132. package/pi/prompts/session.md +2 -2
  133. package/plugin.json +27 -0
  134. package/scripts/backfill-abandoned-sessions.mjs +50 -4
  135. package/scripts/backfill-learnings-from-vault.mjs +9 -3
  136. package/scripts/dialectic-deriver.mjs +73 -8
  137. package/scripts/export-hw-learnings.mjs +113 -1
  138. package/scripts/generate-agents-skills.mjs +378 -0
  139. package/scripts/generate-cursor-adapter.mjs +45 -8
  140. package/scripts/generate-hook-import-set.mjs +249 -0
  141. package/scripts/lib/agent-status.mjs +13 -2
  142. package/scripts/lib/auto-dream.mjs +38 -36
  143. package/scripts/lib/autonomy/suitability.mjs +6 -0
  144. package/scripts/lib/autopilot/loop.mjs +2 -2
  145. package/scripts/lib/ci-status-banner.mjs +220 -75
  146. package/scripts/lib/codex/plugin-contract.mjs +82 -6
  147. package/scripts/lib/config/auto-dream.mjs +2 -1
  148. package/scripts/lib/config/block-header.mjs +8 -0
  149. package/scripts/lib/config/block-preprocess.mjs +177 -0
  150. package/scripts/lib/config/broken-window.mjs +2 -1
  151. package/scripts/lib/config/cold-start.mjs +2 -1
  152. package/scripts/lib/config/config-protection.mjs +22 -2
  153. package/scripts/lib/config/context-coverage.mjs +2 -1
  154. package/scripts/lib/config/cross-repo.mjs +2 -1
  155. package/scripts/lib/config/custom-phases.mjs +2 -1
  156. package/scripts/lib/config/dialectic.mjs +2 -1
  157. package/scripts/lib/config/discovery-validator.mjs +2 -1
  158. package/scripts/lib/config/dispatcher-autonomy-capture.mjs +24 -1
  159. package/scripts/lib/config/dispatcher-autonomy.mjs +2 -1
  160. package/scripts/lib/config/docs-orchestrator.mjs +2 -1
  161. package/scripts/lib/config/docs-staleness.mjs +2 -1
  162. package/scripts/lib/config/drift-check.mjs +2 -1
  163. package/scripts/lib/config/eval.mjs +2 -1
  164. package/scripts/lib/config/events-rotation.mjs +2 -1
  165. package/scripts/lib/config/evolve.mjs +8 -2
  166. package/scripts/lib/config/frontend-slop-hook.mjs +7 -3
  167. package/scripts/lib/config/gitlab-portfolio.mjs +2 -1
  168. package/scripts/lib/config/handover-gate.mjs +2 -1
  169. package/scripts/lib/config/health-endpoints.mjs +7 -2
  170. package/scripts/lib/config/issue-budget.mjs +2 -1
  171. package/scripts/lib/config/loop-guard.mjs +2 -1
  172. package/scripts/lib/config/memory.mjs +2 -1
  173. package/scripts/lib/config/moc-staleness.mjs +2 -1
  174. package/scripts/lib/config/persona-gate-wave.mjs +2 -1
  175. package/scripts/lib/config/private-config-dir.mjs +67 -0
  176. package/scripts/lib/config/reconcile.mjs +2 -1
  177. package/scripts/lib/config/remote-hosts.mjs +2 -1
  178. package/scripts/lib/config/section-extractor.mjs +7 -1
  179. package/scripts/lib/config/skill-evolution.mjs +2 -1
  180. package/scripts/lib/config/slopcheck.mjs +2 -1
  181. package/scripts/lib/config/state-md-lock.mjs +2 -1
  182. package/scripts/lib/config/templates-first.mjs +2 -1
  183. package/scripts/lib/config/test.mjs +2 -1
  184. package/scripts/lib/config/vault-integration.mjs +7 -1
  185. package/scripts/lib/config/vault-mirror-quality.mjs +2 -1
  186. package/scripts/lib/config/vault-staleness.mjs +2 -1
  187. package/scripts/lib/config/vault-sync.mjs +2 -1
  188. package/scripts/lib/config/verification-auto-fix.mjs +2 -1
  189. package/scripts/lib/config/wave-reviewers.mjs +2 -1
  190. package/scripts/lib/config/worktree-orphans.mjs +2 -1
  191. package/scripts/lib/convergence-monitor.mjs +82 -16
  192. package/scripts/lib/dispatcher/rank.mjs +124 -48
  193. package/scripts/lib/ecosystem-health.mjs +16 -2
  194. package/scripts/lib/eval/engine.mjs +9 -1
  195. package/scripts/lib/eval/session-resolve.mjs +23 -4
  196. package/scripts/lib/events.mjs +22 -6
  197. package/scripts/lib/frontmatter-guard.mjs +131 -13
  198. package/scripts/lib/gates/gate-full.mjs +26 -0
  199. package/scripts/lib/gates/gate-helpers.mjs +76 -0
  200. package/scripts/lib/hardware-pattern-detector.mjs +18 -1
  201. package/scripts/lib/harness-audit/categories/category4.mjs +31 -11
  202. package/scripts/lib/host-identity.mjs +50 -11
  203. package/scripts/lib/instruction-budget-guard.mjs +171 -5
  204. package/scripts/lib/learnings/evolve-telemetry.mjs +178 -0
  205. package/scripts/lib/learnings/io.mjs +60 -6
  206. package/scripts/lib/memory-proposals/store.mjs +30 -22
  207. package/scripts/lib/owner-config-banner.mjs +43 -6
  208. package/scripts/lib/owner-config-loader.mjs +21 -10
  209. package/scripts/lib/owner-interview.mjs +3 -3
  210. package/scripts/lib/owner-yaml.mjs +207 -14
  211. package/scripts/lib/platform.mjs +108 -15
  212. package/scripts/lib/plugin-update-banner.mjs +406 -0
  213. package/scripts/lib/project-hygiene.mjs +38 -2
  214. package/scripts/lib/qg-command-drift-banner.mjs +50 -12
  215. package/scripts/lib/quality-gate.mjs +133 -44
  216. package/scripts/lib/reconcile/emitter.mjs +68 -6
  217. package/scripts/lib/reconcile/engine.mjs +13 -4
  218. package/scripts/lib/reconcile/idempotency.mjs +37 -4
  219. package/scripts/lib/reconcile/writer.mjs +40 -18
  220. package/scripts/lib/session-close-backfill.mjs +67 -9
  221. package/scripts/lib/session-id.mjs +12 -23
  222. package/scripts/lib/session-identity/own-session.mjs +125 -10
  223. package/scripts/lib/session-lock-shape.mjs +43 -0
  224. package/scripts/lib/session-lock.mjs +5 -10
  225. package/scripts/lib/session-registry.mjs +25 -9
  226. package/scripts/lib/session-schema/constants.mjs +36 -2
  227. package/scripts/lib/session-schema/validator.mjs +38 -4
  228. package/scripts/lib/session-start-probes.mjs +18 -1
  229. package/scripts/lib/sessions-staleness-banner.mjs +18 -11
  230. package/scripts/lib/skill-health/join.mjs +17 -4
  231. package/scripts/lib/state-md.mjs +78 -0
  232. package/scripts/lib/sunset/walker.mjs +6 -0
  233. package/scripts/lib/telemetry/schema.mjs +181 -9
  234. package/scripts/lib/telemetry/sync.mjs +368 -12
  235. package/scripts/lib/validate/check-agents-skills.mjs +327 -0
  236. package/scripts/lib/validate/check-agents.mjs +3 -3
  237. package/scripts/lib/validate/check-cursor-adapter.mjs +234 -72
  238. package/scripts/lib/validate/check-hooks-symmetry.mjs +45 -16
  239. package/scripts/lib/validate/check-owner-leakage.mjs +281 -20
  240. package/scripts/lib/validate/check-skill-links.mjs +163 -0
  241. package/scripts/lib/validate/check-skill-script-paths.mjs +47 -28
  242. package/scripts/lib/validate/check-unwired-features.mjs +0 -2
  243. package/scripts/lib/validate/check-validator-registration.mjs +10 -4
  244. package/scripts/lib/validate/enumerate-repo-files.mjs +317 -0
  245. package/scripts/lib/vault-backfill/template.mjs +63 -6
  246. package/scripts/lib/vault-mirror/process.mjs +165 -42
  247. package/scripts/lib/vault-mirror/telemetry.mjs +2 -2
  248. package/scripts/lib/vault-status/narrative-mirror.mjs +127 -18
  249. package/scripts/lib/wave-executor/dispatch-common.mjs +164 -0
  250. package/scripts/lib/wave-executor/foreign-dispatch.mjs +7 -142
  251. package/scripts/lib/wave-executor/remote-dispatch.mjs +5 -7
  252. package/scripts/lib/wave-resource-gate.mjs +8 -2
  253. package/scripts/lib/wave-sizing.mjs +4 -1
  254. package/scripts/lib/wave-transcript-tail.mjs +118 -4
  255. package/scripts/materialize-wave-scope.mjs +12 -5
  256. package/scripts/memory-propose.mjs +19 -5
  257. package/scripts/migrate-cold-start-seed.mjs +4 -1
  258. package/scripts/parse-config.mjs +60 -3
  259. package/scripts/release.mjs +337 -29
  260. package/scripts/repair-invalid-sessions.mjs +3 -3
  261. package/scripts/run-quality-gate.mjs +128 -11
  262. package/scripts/sweep-expired-learnings.mjs +90 -0
  263. package/scripts/sync-vault-schema.mjs +3 -1
  264. package/scripts/telemetry.mjs +2 -2
  265. package/scripts/validate-plugin.mjs +161 -0
  266. package/scripts/validate-wave-scope.mjs +28 -8
  267. package/scripts/wave-scope-binding.mjs +215 -0
  268. package/skills/_shared/instruction-file-resolution.md +10 -0
  269. package/skills/_shared/parallel-aware-preamble.md +1 -0
  270. package/skills/_shared/platform-tools.md +1 -1
  271. package/skills/_shared/state-ownership.md +1 -1
  272. package/skills/architecture/SKILL.md +7 -5
  273. package/skills/{domain-model/SKILL.md → architecture/references/domain-model.md} +9 -9
  274. package/skills/autopilot/SKILL.md +4 -18
  275. package/skills/claude-md-drift-check/SKILL.md +5 -1
  276. package/skills/claude-md-drift-check/checker.mjs +62 -2
  277. package/skills/convergence-monitoring/SIGNALS.md +55 -0
  278. package/skills/discovery/probes/vault-staleness.mjs +37 -13
  279. package/skills/discovery/probes-arch.md +20 -18
  280. package/skills/dispatcher/SKILL.md +3 -2
  281. package/skills/evolve/SKILL.md +65 -26
  282. package/skills/frontmatter-guard/SKILL.md +11 -5
  283. package/skills/npm-publish/SKILL.md +1 -1
  284. package/skills/reconcile/SKILL.md +33 -0
  285. package/skills/remote-offload/SKILL.md +1 -1
  286. package/skills/session-end/SKILL.md +18 -905
  287. package/skills/session-end/phase-3-6-tail.md +10 -3
  288. package/skills/session-end/plan-verification.md +221 -155
  289. package/skills/session-end/references/phase-2-quality-gate.md +93 -0
  290. package/skills/session-end/references/phase-3-documentation-updates.md +229 -0
  291. package/skills/session-end/references/phase-4a-worktree-cleanup.md +120 -0
  292. package/skills/session-end/references/phase-4b-worktree-orphan-sweep.md +58 -0
  293. package/skills/session-end/references/phase-5-issue-cleanup.md +104 -0
  294. package/skills/session-end/references/session-summary-template.md +62 -0
  295. package/skills/session-plan/SKILL.md +49 -0
  296. package/skills/session-start/SKILL.md +22 -904
  297. package/skills/session-start/phase-8-5-express-path.md +1 -1
  298. package/skills/session-start/references/phase-1-1-dispatcher-autonomy-capture.md +55 -0
  299. package/skills/session-start/references/phase-1-2-session-lock.md +140 -0
  300. package/skills/session-start/references/phase-1-5-session-continuity.md +254 -0
  301. package/skills/session-start/references/phase-1-7-vault-status-board.md +53 -0
  302. package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +75 -0
  303. package/skills/session-start/references/phase-4-ssot-environment-check.md +155 -0
  304. package/skills/session-start/references/phase-6-5-forced-reads.md +75 -0
  305. package/skills/session-start/references/phase-6-6-project-intelligence.md +81 -0
  306. package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +103 -0
  307. package/skills/vault-sync/validator.mjs +21 -27
  308. package/skills/wave-executor/SKILL.md +15 -1
  309. package/skills/wave-executor/references/wave-loop-dispatch.md +612 -0
  310. package/skills/wave-executor/references/wave-loop-review.md +570 -0
  311. package/skills/wave-executor/references/wave-loop-scope-manifest.md +162 -0
  312. package/skills/wave-executor/wave-loop.md +14 -1309
  313. package/templates/_shared/journey-manifest.md +10 -6
  314. package/.cursor/commands/autopilot-multi.md +0 -14
  315. package/.cursor/commands/contract-version-bump.md +0 -14
  316. package/.cursor/commands/journey-audit.md +0 -14
  317. package/.cursor/skills/contract-version-bump/SKILL.md +0 -12
  318. package/.cursor/skills/daily/SKILL.md +0 -12
  319. package/.cursor/skills/domain-model/SKILL.md +0 -13
  320. package/.cursor/skills/journey-audit/SKILL.md +0 -13
  321. package/.cursor/skills/skill-creator/SKILL.md +0 -13
  322. package/.cursor/skills/ubiquitous-language/SKILL.md +0 -13
  323. package/commands/autopilot-multi.md +0 -74
  324. package/commands/contract-version-bump.md +0 -28
  325. package/commands/journey-audit.md +0 -43
  326. package/pi/prompts/autopilot-multi.md +0 -12
  327. package/pi/prompts/contract-version-bump.md +0 -12
  328. package/pi/prompts/journey-audit.md +0 -12
  329. package/scripts/autopilot-multi.mjs +0 -885
  330. package/scripts/backfill-learnings-expires.mjs +0 -196
  331. package/scripts/backfill-learnings.mjs +0 -203
  332. package/scripts/fleet-instruction-scan.mjs +0 -141
  333. package/scripts/lib/autopilot/dep-graph.mjs +0 -417
  334. package/scripts/lib/autopilot/multi-killswitch.mjs +0 -184
  335. package/scripts/lib/webhook-url.mjs +0 -105
  336. package/scripts/lifecycle-sim-v6.mjs +0 -347
  337. package/scripts/migrate-learnings-jsonl.mjs +0 -189
  338. package/scripts/migrate-subagents-jsonl.mjs +0 -196
  339. package/scripts/upload-social-preview.mjs +0 -316
  340. package/skills/_shared/model-selection.md +0 -64
  341. package/skills/contract-version-bump/SKILL.md +0 -219
  342. package/skills/daily/SKILL.md +0 -222
  343. package/skills/daily/generate.sh +0 -92
  344. package/skills/daily/templates/daily.md.tpl +0 -36
  345. package/skills/journey-audit/SKILL.md +0 -270
  346. package/skills/skill-creator/SKILL.md +0 -168
  347. package/skills/ubiquitous-language/SKILL.md +0 -97
  348. package/skills/vault-sync/package-lock.json +0 -40
  349. /package/skills/{domain-model → architecture/references}/ADR-FORMAT.md +0 -0
  350. /package/skills/{domain-model → architecture/references}/CONTEXT-FORMAT.md +0 -0
@@ -65,6 +65,10 @@ let readJson;
65
65
  let findScopeFile;
66
66
  let extractBashWriteTargets;
67
67
  let pathMatchesPattern;
68
+ /** @type {typeof import('../scripts/lib/session-identity/own-session.mjs').readProcessLocalSessionIds} */
69
+ let readProcessLocalSessionIds;
70
+ /** @type {typeof import('../scripts/lib/session-identity/own-session.mjs').classifyManifestSession} */
71
+ let classifyManifestSession;
68
72
  /**
69
73
  * The `command-blocker.mjs` namespace, imported DIRECTLY (not via the
70
74
  * hardening.mjs barrel — which does not carry the `headFallback` recovery, and
@@ -150,6 +154,10 @@ async function bootstrap() {
150
154
  headFallback: true,
151
155
  requires: ['commandMatchesBlocked', 'suggestForCommandBlock'],
152
156
  },
157
+ // #1153 P1. Bound through the same loader as every other repo dependency,
158
+ // so a SyntaxError in the identity module banners GUARD INACTIVE instead
159
+ // of link-crashing this hook into a silent fail-open (#993).
160
+ sessionIdentity: { specifier: lib('session-identity/own-session.mjs') },
153
161
  },
154
162
  {
155
163
  hookName: HOOK_NAME,
@@ -163,6 +171,7 @@ async function bootstrap() {
163
171
  ({ resolveProjectDir } = modules.platform);
164
172
  ({ readJson } = modules.common);
165
173
  ({ findScopeFile, extractBashWriteTargets, pathMatchesPattern } = modules.hardening);
174
+ ({ readProcessLocalSessionIds, classifyManifestSession } = modules.sessionIdentity);
166
175
  blocker = modules.blocker;
167
176
  degradedLabels = degraded;
168
177
  }
@@ -245,6 +254,66 @@ async function main() {
245
254
  } catch {
246
255
  scope = {};
247
256
  }
257
+ // G3b (#1153 P1) — is this manifest even MINE?
258
+ //
259
+ // `wave-scope.json` lives in the WORKING COPY, not in the session, so two live
260
+ // sessions sharing one checkout read the SAME file. Before this gate, session
261
+ // A's `blockedCommands` decided session B's Bash calls, with a deny reason
262
+ // pointing at a wave plan B does not own (#1082, the sibling defect
263
+ // hooks/enforce-scope.mjs Gate 3b closed for Edit/Write under #1123).
264
+ //
265
+ // Deliberately AFTER the parse block: a corrupt manifest folds to `{}`, hence
266
+ // no ids, hence `'unknown'` — and keeps being enforced. `'own'` and
267
+ // `'unknown'` fall through completely unchanged; a legacy manifest without a
268
+ // `session` field stays enforced. Only what is PROVABLY foreign stands down.
269
+ //
270
+ // IDENTITY TIER: `readProcessLocalSessionIds` (hook payload +
271
+ // `CLAUDE_CODE_SESSION_ID`) and deliberately NOT `readOwnSessionIds`, whose
272
+ // third tier is the repo-global `session.lock` — one file shared by every
273
+ // session in the checkout, which made a peer's manifest classify as `'own'`
274
+ // (#1194).
275
+ //
276
+ // CEILING (BV-004): on a harness that exports no session env var and puts no
277
+ // `session_id` in the hook payload (Codex CLI, Cursor today) both tiers are
278
+ // empty, so this gate is permanently `'unknown'` = enforce = pre-#1153
279
+ // behaviour there. Revisit when those hook payloads carry a session id.
280
+ {
281
+ // `new Set(...)` is load-bearing: `readProcessLocalSessionIds` returns a
282
+ // string[], and `classifyManifestSession` does `ownIds instanceof Set ?
283
+ // ownIds : new Set()` — a bare array would silently become EMPTY.
284
+ const ownIds = new Set(readProcessLocalSessionIds({ hookInput: input }));
285
+ const { verdict, manifestIds } = classifyManifestSession(scope, ownIds);
286
+ if (verdict === 'foreign') {
287
+ // Observability only, and deliberately NOT emitWarn: this branch is hit on
288
+ // every Bash call of the non-owning session, so a stderr line per call
289
+ // would be noise. Awaited before the allow — flushNotices() exits the
290
+ // process, which would discard a pending append.
291
+ try {
292
+ const { emitEvent } = await import('../scripts/lib/events.mjs');
293
+ await emitEvent(
294
+ 'orchestrator.scope.foreign_session_ignored',
295
+ {
296
+ hook: HOOK_NAME,
297
+ manifest: scopePath,
298
+ manifest_session: manifestIds,
299
+ own_session: [...ownIds],
300
+ wave: scope.wave,
301
+ command,
302
+ },
303
+ { repoRoot: projectRoot },
304
+ );
305
+ } catch { /* observability is best-effort — never blocks the decision */ }
306
+ return flushNotices(notices);
307
+ }
308
+ }
309
+
310
+ // ADVERSARIAL CEILING: `wave-scope.json` is a file ANY process in this working
311
+ // copy can write, so writing a foreign id into it is a per-file kill switch for
312
+ // this session's own gate. Accepted because "no manifest" already means allow
313
+ // (G3), so a foreign manifest grants nothing an `rm` would not — but the change
314
+ // MUST stay visible: see post-bash-write-verify's control-file notice (#938),
315
+ // which is the only thing that distinguishes this from a legitimate peer wave.
316
+
248
317
  const enforcement = scope.enforcement || 'strict';
249
318
  const blockedCommands = Array.isArray(scope.blockedCommands)
250
319
  ? scope.blockedCommands
@@ -7,7 +7,7 @@
7
7
  "hooks": [
8
8
  {
9
9
  "type": "command",
10
- "command": "echo '🎯 Session Orchestrator v3.24.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
10
+ "command": "echo '🎯 Session Orchestrator v4.0.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
11
11
  "async": false
12
12
  },
13
13
  {
@@ -112,6 +112,11 @@
112
112
  "type": "command",
113
113
  "command": "sh \"$CURSOR_PLUGIN_ROOT/hooks/run-node.sh\" \"$CURSOR_PLUGIN_ROOT/hooks/post-tooluse-frontend-slop.mjs\"",
114
114
  "timeout": 5
115
+ },
116
+ {
117
+ "type": "command",
118
+ "command": "sh \"$CURSOR_PLUGIN_ROOT/hooks/run-node.sh\" \"$CURSOR_PLUGIN_ROOT/hooks/post-edit-import-probe.mjs\"",
119
+ "timeout": 5
115
120
  }
116
121
  ]
117
122
  },
@@ -139,6 +144,11 @@
139
144
  "type": "command",
140
145
  "command": "sh \"$CURSOR_PLUGIN_ROOT/hooks/run-node.sh\" \"$CURSOR_PLUGIN_ROOT/hooks/post-tooluse-frontend-slop.mjs\"",
141
146
  "timeout": 5
147
+ },
148
+ {
149
+ "type": "command",
150
+ "command": "sh \"$CURSOR_PLUGIN_ROOT/hooks/run-node.sh\" \"$CURSOR_PLUGIN_ROOT/hooks/post-edit-import-probe.mjs\"",
151
+ "timeout": 5
142
152
  }
143
153
  ]
144
154
  }
@@ -85,6 +85,11 @@
85
85
  "type": "command",
86
86
  "command": "sh \"$PI_PLUGIN_ROOT/hooks/run-node.sh\" \"$PI_PLUGIN_ROOT/hooks/post-tooluse-frontend-slop.mjs\"",
87
87
  "timeout": 5
88
+ },
89
+ {
90
+ "type": "command",
91
+ "command": "sh \"$PI_PLUGIN_ROOT/hooks/run-node.sh\" \"$PI_PLUGIN_ROOT/hooks/post-edit-import-probe.mjs\"",
92
+ "timeout": 5
88
93
  }
89
94
  ]
90
95
  },
package/hooks/hooks.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",
9
- "command": "echo '🎯 Session Orchestrator v3.24.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
9
+ "command": "echo '🎯 Session Orchestrator v4.0.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
10
10
  "async": false
11
11
  },
12
12
  {
@@ -135,6 +135,11 @@
135
135
  "type": "command",
136
136
  "command": "sh \"$CLAUDE_PLUGIN_ROOT/hooks/run-node.sh\" \"$CLAUDE_PLUGIN_ROOT/hooks/post-tooluse-frontend-slop.mjs\"",
137
137
  "timeout": 5
138
+ },
139
+ {
140
+ "type": "command",
141
+ "command": "sh \"$CLAUDE_PLUGIN_ROOT/hooks/run-node.sh\" \"$CLAUDE_PLUGIN_ROOT/hooks/post-edit-import-probe.mjs\"",
142
+ "timeout": 5
138
143
  }
139
144
  ]
140
145
  },
@@ -33,7 +33,7 @@ import os from 'node:os';
33
33
  import path from 'node:path';
34
34
  import crypto from 'node:crypto';
35
35
 
36
- import { SO_PROJECT_DIR } from '../scripts/lib/platform.mjs';
36
+ import { getProjectDir } from '../scripts/lib/platform.mjs';
37
37
  import { emitEvent } from '../scripts/lib/events.mjs';
38
38
  import { writeJsonAtomicSync } from '../scripts/lib/io.mjs';
39
39
  import { _parseLoopGuard } from '../scripts/lib/config/loop-guard.mjs';
@@ -84,8 +84,8 @@ function readStdinJson() {
84
84
  */
85
85
  async function loadConfig() {
86
86
  const candidates = [
87
- path.join(SO_PROJECT_DIR, 'CLAUDE.md'),
88
- path.join(SO_PROJECT_DIR, 'AGENTS.md'),
87
+ path.join(getProjectDir(), 'CLAUDE.md'),
88
+ path.join(getProjectDir(), 'AGENTS.md'),
89
89
  ];
90
90
  for (const file of candidates) {
91
91
  try {
@@ -35,7 +35,7 @@ import { shouldRunHook } from './_lib/profile-gate.mjs';
35
35
  if (!shouldRunHook('on-session-end')) process.exit(0);
36
36
 
37
37
  import { emitEvent } from '../scripts/lib/events.mjs';
38
- import { SO_PROJECT_DIR } from '../scripts/lib/platform.mjs';
38
+ import { getProjectDir } from '../scripts/lib/platform.mjs';
39
39
  import {
40
40
  backfillAbandonedSession,
41
41
  backfillCompletedFromStateMd,
@@ -551,7 +551,7 @@ async function emitFinalWaveCompleted(
551
551
 
552
552
  async function main() {
553
553
  const input = await readStdinJson();
554
- const projectRoot = SO_PROJECT_DIR;
554
+ const projectRoot = getProjectDir();
555
555
 
556
556
  const reason =
557
557
  typeof input?.reason === 'string' && input.reason.length > 0 ? input.reason : 'other';
@@ -42,7 +42,7 @@ if (!shouldRunHook('on-session-start')) process.exit(0);
42
42
  import { emitEvent, eventsFilePath } from '../scripts/lib/events.mjs';
43
43
  import { maybeRotate } from '../scripts/lib/events-rotation.mjs';
44
44
  import { readConfigFile, parseSessionConfig } from '../scripts/lib/config.mjs';
45
- import { SO_PLATFORM, resolveProjectDir } from '../scripts/lib/platform.mjs';
45
+ import { getPlatform, resolveProjectDir } from '../scripts/lib/platform.mjs';
46
46
  import {
47
47
  registerSelf,
48
48
  detectPeers,
@@ -410,6 +410,13 @@ async function resolveSessionId(input, projectRoot) {
410
410
  if (Object.prototype.hasOwnProperty.call(prev, 'last_wave_completed')) {
411
411
  preserved.last_wave_completed = prev.last_wave_completed;
412
412
  }
413
+ // #980 — the OPEN half of the wave-diff pair, written by
414
+ // post-tool-batch-wave-signal.mjs. Dropping it across a /clear leaves
415
+ // the running wave with no start point, so the next
416
+ // `orchestrator.wave.completed` silently omits `files_changed`.
417
+ if (Object.prototype.hasOwnProperty.call(prev, 'wave_start_sha')) {
418
+ preserved.wave_start_sha = prev.wave_start_sha;
419
+ }
413
420
  }
414
421
  } catch { /* absent / unparseable → no preservation (reset) */ }
415
422
  }
@@ -705,7 +712,15 @@ async function main() {
705
712
  // must never break the hook, which is informational-only.
706
713
  const input = await stdinPromise;
707
714
  const { sessionId, semanticSessionId, mode } = await resolveSessionId(input, projectRoot);
708
- const platform = process.env.SO_PLATFORM ?? SO_PLATFORM;
715
+ // getPlatform() ALREADY implements the SO_PLATFORM override as step 1 of its
716
+ // precedence — and, unlike a bare `??`, it validates the value against the
717
+ // four-platform allowlist and trims it. The former `process.env.SO_PLATFORM ??
718
+ // getPlatform()` therefore added nothing except the empty-string trap
719
+ // (`.claude/rules/development.md` § Env-var fallback whitespace trap): a key
720
+ // exported as `SO_PLATFORM=` is not nullish, so `??` short-circuited and this
721
+ // hook stamped `platform: ''` on the session event and handed '' down to the
722
+ // update probe. Measured 2026-09-06 by the codex test below.
723
+ const platform = getPlatform();
709
724
 
710
725
  // Epic #583 P3 — mechanical session.lock writer (closes D1+D2+D4 gaps).
711
726
  // Bootstrap the session-lock so discoverActiveSessions() picks us up even
@@ -947,6 +962,40 @@ async function main() {
947
962
  } catch { /* hook must remain non-blocking */ }
948
963
  }
949
964
 
965
+ // Plugin-update probe (#nnn, d7 R2) — "is the plugin that is RUNNING behind
966
+ // the plugin that is PUBLISHED?".
967
+ //
968
+ // MEASURED 2026-09-06: this host ran the marketplace-cache copy at 3.19.0
969
+ // (installed 2026-08-09) while repo and npm stood at the 3.24 line — five minors,
970
+ // four weeks, zero warnings, because NO code anywhere compared installed
971
+ // against available (`grep -rln "registry.npmjs.org\|dist-tags" scripts/
972
+ // hooks/ skills/` → only scripts/release.mjs, the publisher).
973
+ //
974
+ // Placement mirrors the probe block above: BEFORE the payload (the event
975
+ // carries both versions) and BEFORE flushBanner(), so the line rides the ONE
976
+ // systemMessage envelope. A second stdout object would be silently discarded
977
+ // (HR-106) — the failure mode the bannerLines docstring describes.
978
+ //
979
+ // Same measurement/display split as the probes: the request and the event
980
+ // fields happen regardless, `pushBanner` is gated on `bannerData`. Network,
981
+ // cache and kill-switch behaviour live in the module.
982
+ let pluginUpdate = null;
983
+ try {
984
+ const { checkPluginUpdate } = await import('../scripts/lib/plugin-update-banner.mjs');
985
+ pluginUpdate = await checkPluginUpdate({
986
+ cacheDir: path.join(projectRoot, '.orchestrator', 'runtime'),
987
+ // The probe's own fallback is env-only (SO_PLATFORM, then each harness's
988
+ // compat var) and cannot run detectPlatform()'s third step, the marker-
989
+ // directory walk from cwd. This hook already resolved the platform at the
990
+ // top of run() through exactly that full precedence, so handing it over
991
+ // makes the remedy instruction MEASURED rather than guessed — a Codex or
992
+ // Cursor consumer with no compat var set would otherwise be told to run
993
+ // `/plugin update`, a command its harness does not have.
994
+ platform,
995
+ });
996
+ if (pluginUpdate && bannerData) pushBanner(pluginUpdate.message);
997
+ } catch { /* hook must remain non-blocking */ }
998
+
950
999
  const payload = {
951
1000
  platform,
952
1001
  project: projectName,
@@ -958,6 +1007,34 @@ async function main() {
958
1007
  // makes the supersession rate measurable instead of inferred (HR-105).
959
1008
  peers_superseded: mechanicalPeersSuperseded,
960
1009
  };
1010
+
1011
+ // #nnn — installed/latest plugin version on the session record.
1012
+ //
1013
+ // HR-105: a verdict nothing records is unfalsifiable. `installed` is stamped
1014
+ // on EVERY start (it is a local file read that cannot fail for network
1015
+ // reasons), and `latest` whenever it is known — including the runs where
1016
+ // checkPluginUpdate() stayed silent because the versions matched. Those runs
1017
+ // are the DENOMINATOR of the probe's firing rate; without them the fleet can
1018
+ // only ever see how often it warned, never how often it could have.
1019
+ //
1020
+ // Both keys are OMITTED rather than nulled when unknown (docs/events-schema.md
1021
+ // optional-field convention: absent ≠ zero) — a kill-switched or offline
1022
+ // session must not read as "latest is 0/unknown-but-measured".
1023
+ try {
1024
+ const { readInstalledPluginVersion, readCachedLatest, isUpdateCheckDisabled } =
1025
+ await import('../scripts/lib/plugin-update-banner.mjs');
1026
+ const installedVersion = readInstalledPluginVersion();
1027
+ if (installedVersion) payload.plugin_version_installed = installedVersion;
1028
+ // `latest` is gated on the kill switch, `installed` is not: reading this
1029
+ // repo's own package.json is not the thing an operator switched off.
1030
+ if (!isUpdateCheckDisabled()) {
1031
+ const latestVersion =
1032
+ pluginUpdate?.latest
1033
+ ?? readCachedLatest({ cacheDir: path.join(projectRoot, '.orchestrator', 'runtime') })?.version
1034
+ ?? null;
1035
+ if (latestVersion) payload.plugin_version_latest = latestVersion;
1036
+ }
1037
+ } catch { /* telemetry never blocks the hook */ }
961
1038
  if (bannerData) {
962
1039
  payload.host_class = bannerData.host.host_class;
963
1040
  payload.ram_free_gb = bannerData.resources.ram_free_gb;
@@ -993,6 +1070,30 @@ async function main() {
993
1070
  // only stdout write in the hook.
994
1071
  flushBanner();
995
1072
 
1073
+ // #1229 — refresh the session-lock heartbeat at the END of this hook's run.
1074
+ //
1075
+ // MEASURED (STATE.md deviation 2026-09-05T06:08:46Z): session-20's lock was
1076
+ // reaped as stale at 06:06Z by an UNRELATED session's SessionEnd, its
1077
+ // heartbeat 11.3 h old against a 4 h TTL. The session was not idle — it was
1078
+ // still inside session-start / session-plan, where no heartbeat writer
1079
+ // existed: `updateHeartbeat()` was called only from the wave loop, from
1080
+ // on-stop and from session-end. A session that never reached /go therefore
1081
+ // aged out of its own lock while working.
1082
+ //
1083
+ // bootstrapLock() above stamps `last_heartbeat = started_at` at LOCK GENESIS,
1084
+ // i.e. before the peer scan, the backfill and the Phase-4 probes have run.
1085
+ // Stamping again here moves the clock to the end of the hook and — more
1086
+ // importantly — makes session-start a heartbeat CALL SITE at all, so the
1087
+ // cadence no longer begins only when the first wave starts.
1088
+ //
1089
+ // `updateHeartbeat()` already carries the same-session guard this needs (it
1090
+ // returns false without writing when the lock belongs to another session),
1091
+ // so no `touchIfOwn()` wrapper is warranted — see scripts/lib/session-lock.mjs.
1092
+ try {
1093
+ const { updateHeartbeat } = await import('../scripts/lib/session-lock.mjs');
1094
+ updateHeartbeat({ repoRoot: projectRoot, sessionId });
1095
+ } catch { /* hook must remain non-blocking */ }
1096
+
996
1097
  // Size-based rotation of events.jsonl (#251). Session-start is the single
997
1098
  // rotation trigger — per-append overhead is rejected design. Any failure
998
1099
  // is swallowed: rotation must NEVER block the hook.
package/hooks/on-stop.mjs CHANGED
@@ -16,7 +16,9 @@
16
16
  * JSONL format (`.orchestrator/metrics/events.jsonl`) — emitted via the canonical
17
17
  * `emitEvent()` so the JSONL record and the optional Clank webhook always carry the
18
18
  * SAME dotted event name (was: bare `stop`/`subagent_stop` in JSONL vs dotted in webhook):
19
- * Stop: {"timestamp":<ISO>,"event":"orchestrator.session.stopped","session_id":"...","semantic_session_id":"...","wave":<int>,"branch":"...","commit":"...","duration_ms":<int>,"duration_source":"session-lock"}
19
+ * Stop: {"timestamp":<ISO>,"event":"orchestrator.turn.stopped","session_id":"...","semantic_session_id":"...","wave":<int>,"branch":"...","commit":"...","duration_ms":<int>,"duration_source":"session-lock"}
20
+ * + the SAME payload under the DEPRECATED legacy name (one generation, removal 2027-03-06):
21
+ * {"timestamp":<ISO>,"event":"orchestrator.session.stopped",…same fields…,"deprecated":true}
20
22
  * (`session_id` / `semantic_session_id` are omitted when unresolvable — #1068 AC1.
21
23
  * `duration_ms` + `duration_source` are omitted TOGETHER when no OWNED
22
24
  * session.lock is readable — never a fabricated 0, see K5 below.)
@@ -46,7 +48,7 @@ import { AGENT_ID_RE, resolveSubagentSidecar } from './_lib/subagent-paths.mjs';
46
48
  if (!shouldRunHook('on-stop')) process.exit(0);
47
49
 
48
50
  import { emitEvent } from '../scripts/lib/events.mjs';
49
- import { SO_PROJECT_DIR } from '../scripts/lib/platform.mjs';
51
+ import { getProjectDir } from '../scripts/lib/platform.mjs';
50
52
  import { parseSessionId } from '../scripts/lib/session-id.mjs';
51
53
  import { heartbeat, logSweepEvent } from '../scripts/lib/session-registry.mjs';
52
54
  import { readLock, updateHeartbeat } from '../scripts/lib/session-lock.mjs';
@@ -366,7 +368,7 @@ function resolveStopDuration(input, projectRoot) {
366
368
  * @param {object|null} input
367
369
  */
368
370
  async function handleStop(input) {
369
- const projectRoot = SO_PROJECT_DIR;
371
+ const projectRoot = getProjectDir();
370
372
 
371
373
  const wave = await readWaveNumber(projectRoot);
372
374
  const { commit, branch } = await gitInfo(projectRoot);
@@ -457,15 +459,38 @@ async function handleStop(input) {
457
459
  // #1183 — a malformed record throws EventValidationError BEFORE any side
458
460
  // effect (scripts/lib/events.mjs); this hook must never abort on that, so
459
461
  // the emit is wrapped rather than left to propagate.
462
+ //
463
+ // ── THE RENAME (GitLab #1234) ──────────────────────────────────────────────
464
+ // `orchestrator.session.stopped` is a TURN counter with a session's name. The
465
+ // Stop hook fires once per ASSISTANT TURN, not once per session: measured
466
+ // 2026-09-06 over 90 days, 15.538 records against 2.016 distinct
467
+ // `session.started` ids = 7,7 per session, with 184 for a single id. Six
468
+ // consumers read it as a session-lifecycle signal and are wrong by that factor.
469
+ //
470
+ // So the same payload now leaves under BOTH names for one generation:
471
+ // - `orchestrator.turn.stopped` — the correct name, what new readers use.
472
+ // - `orchestrator.session.stopped` — unchanged shape PLUS `deprecated: true`,
473
+ // so an existing reader keeps working and a NEW reader can tell at a glance
474
+ // that it picked the legacy name.
475
+ // Removal of the legacy name: 2027-03-06 (docs/events-schema.md).
476
+ //
477
+ // The SubagentStop branch (`orchestrator.agent.stopped`) is deliberately NOT
478
+ // renamed — it is a different event and its per-agent cardinality is correct.
479
+ const stoppedPayload = {
480
+ ...(sessionId !== null ? { session_id: sessionId } : {}),
481
+ ...(semanticSessionId !== null ? { semantic_session_id: semanticSessionId } : {}),
482
+ wave,
483
+ ...(branch !== null ? { branch } : {}),
484
+ ...(commit !== null ? { commit } : {}),
485
+ ...duration,
486
+ };
487
+
460
488
  try {
461
- await emitEvent('orchestrator.session.stopped', {
462
- ...(sessionId !== null ? { session_id: sessionId } : {}),
463
- ...(semanticSessionId !== null ? { semantic_session_id: semanticSessionId } : {}),
464
- wave,
465
- ...(branch !== null ? { branch } : {}),
466
- ...(commit !== null ? { commit } : {}),
467
- ...duration,
468
- });
489
+ await emitEvent('orchestrator.turn.stopped', stoppedPayload);
490
+ } catch { /* telemetry never blocks the hook (#1183) */ }
491
+
492
+ try {
493
+ await emitEvent('orchestrator.session.stopped', { ...stoppedPayload, deprecated: true });
469
494
  } catch { /* telemetry never blocks the hook (#1183) */ }
470
495
  }
471
496
 
@@ -37,14 +37,14 @@ import { shouldRunHook } from './_lib/profile-gate.mjs';
37
37
  // Exit 0 immediately when disabled via SO_HOOK_PROFILE / SO_DISABLED_HOOKS.
38
38
  if (!shouldRunHook('operator-steer')) process.exit(0);
39
39
 
40
- import { SO_PROJECT_DIR } from '../scripts/lib/platform.mjs';
40
+ import { getProjectDir } from '../scripts/lib/platform.mjs';
41
41
 
42
42
  // ---------------------------------------------------------------------------
43
43
  // Main
44
44
  // ---------------------------------------------------------------------------
45
45
 
46
46
  async function main() {
47
- const steerPath = path.join(SO_PROJECT_DIR, '.orchestrator', 'STEER.md');
47
+ const steerPath = path.join(getProjectDir(), '.orchestrator', 'STEER.md');
48
48
 
49
49
  // File absent — nothing to do.
50
50
  if (!existsSync(steerPath)) return;
@@ -163,6 +163,13 @@ import { findScopeFile, pathMatchesPattern } from '../scripts/lib/hardening.mjs'
163
163
  // is unchanged. Two byte-identical copies of a clock is exactly the one-fact-two-
164
164
  // copies class this repo keeps paying for.
165
165
  import { sessionAgeMs, PEER_RECORD_PREFIX, isPeerRecordId } from '../scripts/lib/scope-gate.mjs';
166
+ // #1153 P1 — the same process-local ownership check hooks/enforce-scope.mjs
167
+ // applies at Gate 3b. This hook reads the SAME working-copy `wave-scope.json`,
168
+ // so without it a peer session's manifest drives this session's advisories.
169
+ import {
170
+ readProcessLocalSessionIds,
171
+ classifyManifestSession,
172
+ } from '../scripts/lib/session-identity/own-session.mjs';
166
173
 
167
174
  // ---------------------------------------------------------------------------
168
175
  // Constants
@@ -854,6 +861,17 @@ async function main() {
854
861
  }
855
862
  if (raw === null) return; // file vanished between findScopeFile and read
856
863
 
864
+ // The control-file identity of THIS call, computed BEFORE the G3b
865
+ // stand-down (W4/F7). Ordering is the whole fix: G3b used to `return` above
866
+ // this, so a peer or agent that REBOUND `wave-scope.json` to a fabricated
867
+ // `session_id` with one `cat >` redirect disarmed this session's gates AND
868
+ // suppressed the #938 control-file notice at the same time. Deleting the file
869
+ // was reported (G3 above); rebinding it was not — the louder half of the
870
+ // tamper trail was the one behind the gate the tamper opens.
871
+ //
872
+ // Nothing but the hash/enforcement snapshot is derived here: no path
873
+ // evaluation, no allowedPaths read, no report — those still wait for the
874
+ // classification below to judge the manifest ours.
857
875
  const relScopePath = path.relative(repoRoot, scopePath) || scopePath;
858
876
  const currentScopeState = {
859
877
  hash: createHash('sha1').update(raw).digest('hex').slice(0, 16),
@@ -863,6 +881,73 @@ async function main() {
863
881
  gateOn: scope ? scope?.gates?.['bash-write-verify'] !== false : true,
864
882
  };
865
883
 
884
+ // G3b (#1153 P1) — is this manifest even MINE?
885
+ //
886
+ // `wave-scope.json` is a WORKING-COPY artefact; two live sessions in one
887
+ // checkout read the same bytes. Everything below this point — the path
888
+ // report, the control-file notice, the snapshot — describes THIS session's
889
+ // relationship to a wave plan. When the manifest provably names another
890
+ // session, none of it is about us, and the advisories were a peer's
891
+ // `allowedPaths` scolding an unrelated session (#1082 / #1123).
892
+ //
893
+ // Deliberately AFTER the parse: an unparseable manifest leaves `scope === null`,
894
+ // which classifies as `'unknown'` and keeps every pre-#1153 behaviour —
895
+ // including the #938 tamper notices, which must never be reachable via a
896
+ // manifest a Bash call just corrupted. `'own'` and `'unknown'` (legacy,
897
+ // unbound, or no resolvable own identity) fall through unchanged.
898
+ //
899
+ // CEILING (BV-004): on a harness that supplies no session id at all (Codex
900
+ // CLI, Cursor today) the own-id set is empty, so this gate is permanently
901
+ // `'unknown'` = full pre-#1153 behaviour there.
902
+ {
903
+ const ownIds = new Set(readProcessLocalSessionIds({ hookInput: input }));
904
+ const { verdict, manifestIds } = classifyManifestSession(scope ?? {}, ownIds);
905
+ if (verdict === 'foreign') {
906
+ // Observability only — one event per stand-down decision, awaited so the
907
+ // append cannot be lost to the process exiting.
908
+ try {
909
+ const { emitEvent } = await import('../scripts/lib/events.mjs');
910
+ await emitEvent(
911
+ 'orchestrator.scope.foreign_session_ignored',
912
+ {
913
+ hook: 'post-bash-write-verify',
914
+ manifest: scopePath,
915
+ manifest_session: manifestIds,
916
+ own_session: [...ownIds],
917
+ wave: scope?.wave,
918
+ },
919
+ { repoRoot },
920
+ );
921
+ } catch { /* observability is best-effort — never blocks the decision */ }
922
+
923
+ // The stand-down itself is a control-file event when the binding CHANGED
924
+ // since the last call: the manifest this session was baselined against is
925
+ // now bound to someone else, which switches every scope gate off. Reported
926
+ // on the visible channel exactly once — the snapshot below records the new
927
+ // hash, so a stable foreign manifest (an ordinary peer wave) is silent from
928
+ // the next call on, and a rebind back to us reports again.
929
+ if (prevScopeState
930
+ && prevScopeState.hash !== 'absent'
931
+ && prevScopeState.hash !== currentScopeState.hash) {
932
+ writeSnapshot(snapFile, { ...carriedRecord, scopeState: currentScopeState });
933
+ emitMessages([
934
+ `bash-write-verify: control file ${relScopePath} changed its SESSION BINDING `
935
+ + `since the last Bash call (now names ${manifestIds.join(', ') || 'an unnamed session'}) — `
936
+ + 'this session\'s scope gates are standing down. '
937
+ + 'Scope-control changes are never exempt from reporting (#938).',
938
+ ], true);
939
+ }
940
+ return;
941
+ }
942
+ }
943
+
944
+ // ADVERSARIAL CEILING: `wave-scope.json` is a file ANY process in this working
945
+ // copy can write, so writing a foreign id into it is a per-file kill switch for
946
+ // this session's own gate. Accepted because "no manifest" already means allow
947
+ // (G3), so a foreign manifest grants nothing an `rm` would not — but the change
948
+ // MUST stay visible: see post-bash-write-verify's control-file notice (#938),
949
+ // which is the only thing that distinguishes this from a legitimate peer wave.
950
+
866
951
  let scopeMtimeMs = null;
867
952
  try {
868
953
  scopeMtimeMs = statSync(scopePath).mtimeMs;