session-orchestrator 3.23.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 (393) 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 +13 -0
  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 +1401 -0
  81. package/NOTICE +11 -6
  82. package/README.md +127 -92
  83. package/agents/db-specialist.md +0 -1
  84. package/agents/eval-judge.md +1 -1
  85. package/agents/skill-applied-judge.md +1 -1
  86. package/assets/wave-lifecycle.svg +98 -0
  87. package/commands/release.md +6 -3
  88. package/commands/session.md +18 -3
  89. package/docs/README.md +4 -0
  90. package/{agents/AGENTS.md → docs/agent-authoring.md} +19 -26
  91. package/docs/baseline.md +67 -0
  92. package/docs/ci-setup.md +249 -48
  93. package/docs/codex-setup.md +66 -22
  94. package/docs/components.md +37 -16
  95. package/docs/cursor-setup.md +6 -2
  96. package/docs/events-schema.md +51 -10
  97. package/docs/instruction-delivery.md +62 -0
  98. package/{agents/memory-proposal-collector.md → docs/memory-proposal-flow.md} +1 -8
  99. package/docs/migration-v4.md +341 -0
  100. package/docs/pi-setup.md +6 -1
  101. package/docs/plugin-architecture-v3.md +1 -1
  102. package/docs/rule-authoring.md +85 -19
  103. package/docs/scope-collision-guard.md +8 -8
  104. package/docs/session-config-reference.md +120 -61
  105. package/docs/session-config-template.md +40 -33
  106. package/docs/telemetry/telemetry-claims.md +11 -10
  107. package/docs/telemetry.md +187 -4
  108. package/docs/vault-docs-architecture.md +50 -11
  109. package/hooks/_lib/atomic-json.mjs +111 -0
  110. package/hooks/_lib/hook-import-set.json +1487 -0
  111. package/hooks/_lib/subagent-paths.mjs +143 -0
  112. package/hooks/_lib/subagent-transcript.mjs +562 -0
  113. package/hooks/config-protection.mjs +2 -2
  114. package/hooks/cwd-change-restore.mjs +11 -31
  115. package/hooks/enforce-commands.mjs +69 -0
  116. package/hooks/enforce-scope.mjs +35 -6
  117. package/hooks/hooks-codex.json +1 -1
  118. package/hooks/hooks-cursor.json +10 -0
  119. package/hooks/hooks-pi.json +5 -0
  120. package/hooks/hooks.json +6 -1
  121. package/hooks/loop-guard.mjs +3 -3
  122. package/hooks/on-session-end.mjs +280 -14
  123. package/hooks/on-session-start.mjs +153 -4
  124. package/hooks/on-stop.mjs +371 -17
  125. package/hooks/operator-steer.mjs +2 -2
  126. package/hooks/post-bash-write-verify.mjs +189 -4
  127. package/hooks/post-edit-import-probe.mjs +344 -0
  128. package/hooks/post-subagent-discovery-validator.mjs +278 -392
  129. package/hooks/post-tool-batch-wave-signal.mjs +272 -44
  130. package/hooks/post-tool-failure-corrective-context.mjs +11 -34
  131. package/hooks/post-tooluse-frontend-slop.mjs +3 -3
  132. package/hooks/pre-bash-destructive-guard.mjs +39 -13
  133. package/hooks/pre-bash-memory-propose-audit.mjs +13 -7
  134. package/hooks/skill-invocation-telemetry.mjs +17 -5
  135. package/hooks/subagent-telemetry.mjs +24 -30
  136. package/monitors/monitors.json +3 -3
  137. package/package.json +9 -1
  138. package/pi/prompts/session.md +2 -2
  139. package/plugin.json +27 -0
  140. package/scripts/autopilot.mjs +26 -12
  141. package/scripts/backfill-abandoned-sessions.mjs +130 -15
  142. package/scripts/backfill-learnings-from-vault.mjs +9 -3
  143. package/scripts/dialectic-deriver.mjs +73 -8
  144. package/scripts/emit-event.mjs +10 -2
  145. package/scripts/export-hw-learnings.mjs +113 -1
  146. package/scripts/generate-agents-skills.mjs +378 -0
  147. package/scripts/generate-cursor-adapter.mjs +45 -8
  148. package/scripts/generate-hook-import-set.mjs +249 -0
  149. package/scripts/lib/agent-status.mjs +13 -2
  150. package/scripts/lib/auq/parse.mjs +5 -29
  151. package/scripts/lib/auto-dialectic.mjs +68 -0
  152. package/scripts/lib/auto-dream.mjs +38 -36
  153. package/scripts/lib/autonomy/suitability.mjs +6 -0
  154. package/scripts/lib/autopilot/loop.mjs +2 -2
  155. package/scripts/lib/autopilot/worktree-pipeline.mjs +82 -6
  156. package/scripts/lib/build-live-signals.mjs +25 -22
  157. package/scripts/lib/ci-status-banner.mjs +220 -75
  158. package/scripts/lib/codex/plugin-contract.mjs +82 -6
  159. package/scripts/lib/cold-start-detector.mjs +23 -14
  160. package/scripts/lib/config/auto-dream.mjs +2 -1
  161. package/scripts/lib/config/block-header.mjs +63 -0
  162. package/scripts/lib/config/block-preprocess.mjs +177 -0
  163. package/scripts/lib/config/broken-window.mjs +2 -1
  164. package/scripts/lib/config/cold-start.mjs +2 -1
  165. package/scripts/lib/config/config-protection.mjs +22 -2
  166. package/scripts/lib/config/context-coverage.mjs +2 -1
  167. package/scripts/lib/config/cross-repo.mjs +2 -1
  168. package/scripts/lib/config/custom-phases.mjs +2 -1
  169. package/scripts/lib/config/dialectic.mjs +2 -1
  170. package/scripts/lib/config/discovery-validator.mjs +9 -3
  171. package/scripts/lib/config/dispatcher-autonomy-capture.mjs +24 -1
  172. package/scripts/lib/config/dispatcher-autonomy.mjs +2 -1
  173. package/scripts/lib/config/docs-orchestrator.mjs +2 -1
  174. package/scripts/lib/config/docs-staleness.mjs +2 -1
  175. package/scripts/lib/config/drift-check.mjs +2 -1
  176. package/scripts/lib/config/eval.mjs +2 -1
  177. package/scripts/lib/config/events-rotation.mjs +2 -1
  178. package/scripts/lib/config/evolve.mjs +8 -2
  179. package/scripts/lib/config/frontend-slop-hook.mjs +7 -3
  180. package/scripts/lib/config/gitlab-portfolio.mjs +2 -1
  181. package/scripts/lib/config/handover-gate.mjs +2 -1
  182. package/scripts/lib/config/health-endpoints.mjs +388 -0
  183. package/scripts/lib/config/issue-budget.mjs +2 -1
  184. package/scripts/lib/config/loop-guard.mjs +2 -1
  185. package/scripts/lib/config/memory.mjs +2 -1
  186. package/scripts/lib/config/moc-staleness.mjs +2 -1
  187. package/scripts/lib/config/persona-gate-wave.mjs +2 -1
  188. package/scripts/lib/config/private-config-dir.mjs +67 -0
  189. package/scripts/lib/config/reconcile.mjs +2 -1
  190. package/scripts/lib/config/remote-hosts.mjs +234 -0
  191. package/scripts/lib/config/section-extractor.mjs +7 -1
  192. package/scripts/lib/config/skill-evolution.mjs +2 -1
  193. package/scripts/lib/config/slopcheck.mjs +2 -1
  194. package/scripts/lib/config/state-md-lock.mjs +2 -1
  195. package/scripts/lib/config/templates-first.mjs +2 -1
  196. package/scripts/lib/config/test.mjs +2 -1
  197. package/scripts/lib/config/vault-integration.mjs +7 -1
  198. package/scripts/lib/config/vault-mirror-quality.mjs +2 -1
  199. package/scripts/lib/config/vault-staleness.mjs +2 -1
  200. package/scripts/lib/config/vault-sync.mjs +2 -1
  201. package/scripts/lib/config/verification-auto-fix.mjs +2 -1
  202. package/scripts/lib/config/wave-reviewers.mjs +2 -1
  203. package/scripts/lib/config/worktree-orphans.mjs +2 -1
  204. package/scripts/lib/config.mjs +31 -3
  205. package/scripts/lib/convergence-monitor.mjs +82 -16
  206. package/scripts/lib/dispatcher/enumerate.mjs +2 -17
  207. package/scripts/lib/dispatcher/rank.mjs +124 -48
  208. package/scripts/lib/ecosystem-health.mjs +16 -2
  209. package/scripts/lib/eval/engine.mjs +9 -1
  210. package/scripts/lib/eval/session-resolve.mjs +23 -4
  211. package/scripts/lib/events-schema.mjs +48 -0
  212. package/scripts/lib/events.mjs +256 -7
  213. package/scripts/lib/evolve/autonomy-verdict.mjs +9 -4
  214. package/scripts/lib/evolve/autopilot-effectiveness.mjs +18 -1
  215. package/scripts/lib/frontmatter-guard.mjs +131 -13
  216. package/scripts/lib/gates/gate-full.mjs +26 -0
  217. package/scripts/lib/gates/gate-helpers.mjs +76 -0
  218. package/scripts/lib/gitlab-portfolio/cli.mjs +3 -15
  219. package/scripts/lib/hardware-pattern-detector.mjs +18 -1
  220. package/scripts/lib/harness-audit/categories/category1.mjs +17 -6
  221. package/scripts/lib/harness-audit/categories/category4.mjs +31 -11
  222. package/scripts/lib/host-identity.mjs +50 -11
  223. package/scripts/lib/instruction-budget-guard.mjs +171 -5
  224. package/scripts/lib/learnings/evolve-telemetry.mjs +178 -0
  225. package/scripts/lib/learnings/io.mjs +60 -6
  226. package/scripts/lib/memory-banner.mjs +20 -8
  227. package/scripts/lib/memory-proposals/store.mjs +30 -22
  228. package/scripts/lib/owner-config-banner.mjs +43 -6
  229. package/scripts/lib/owner-config-loader.mjs +21 -10
  230. package/scripts/lib/owner-interview.mjs +3 -3
  231. package/scripts/lib/owner-yaml.mjs +207 -14
  232. package/scripts/lib/peer-discovery.mjs +20 -2
  233. package/scripts/lib/platform.mjs +108 -15
  234. package/scripts/lib/plugin-update-banner.mjs +406 -0
  235. package/scripts/lib/project-hygiene.mjs +38 -2
  236. package/scripts/lib/qg-command-drift-banner.mjs +50 -12
  237. package/scripts/lib/quality-gate.mjs +133 -44
  238. package/scripts/lib/reconcile/emitter.mjs +68 -6
  239. package/scripts/lib/reconcile/engine.mjs +249 -9
  240. package/scripts/lib/reconcile/idempotency.mjs +37 -4
  241. package/scripts/lib/reconcile/writer.mjs +40 -18
  242. package/scripts/lib/scope-gate.mjs +36 -0
  243. package/scripts/lib/session-close-backfill.mjs +125 -18
  244. package/scripts/lib/session-discovery.mjs +57 -3
  245. package/scripts/lib/session-end/phase-skip.mjs +2 -2
  246. package/scripts/lib/session-id.mjs +12 -23
  247. package/scripts/lib/session-identity/own-session.mjs +187 -11
  248. package/scripts/lib/session-lock-shape.mjs +43 -0
  249. package/scripts/lib/session-lock.mjs +5 -10
  250. package/scripts/lib/session-registry.mjs +25 -9
  251. package/scripts/lib/session-schema/constants.mjs +36 -2
  252. package/scripts/lib/session-schema/validator.mjs +38 -4
  253. package/scripts/lib/session-start-probes.mjs +18 -1
  254. package/scripts/lib/session-transition.mjs +1 -1
  255. package/scripts/lib/sessions-canonical.mjs +446 -0
  256. package/scripts/lib/sessions-staleness-banner.mjs +18 -11
  257. package/scripts/lib/skill-health/join.mjs +17 -4
  258. package/scripts/lib/state-md.mjs +78 -0
  259. package/scripts/lib/sunset/walker.mjs +6 -0
  260. package/scripts/lib/telemetry/schema.mjs +255 -17
  261. package/scripts/lib/telemetry/sync.mjs +417 -24
  262. package/scripts/lib/tmux-layout/telemetry.mjs +14 -2
  263. package/scripts/lib/validate/check-agents-skills.mjs +327 -0
  264. package/scripts/lib/validate/check-agents.mjs +3 -3
  265. package/scripts/lib/validate/check-cursor-adapter.mjs +234 -72
  266. package/scripts/lib/validate/check-doc-cli-commands.mjs +9 -33
  267. package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +370 -0
  268. package/scripts/lib/validate/check-hooks-symmetry.mjs +45 -16
  269. package/scripts/lib/validate/check-owner-leakage.mjs +281 -20
  270. package/scripts/lib/validate/check-skill-links.mjs +163 -0
  271. package/scripts/lib/validate/check-skill-script-paths.mjs +455 -0
  272. package/scripts/lib/validate/check-untracked-test-deps.mjs +10 -0
  273. package/scripts/lib/validate/check-unwired-features.mjs +0 -9
  274. package/scripts/lib/validate/check-validator-registration.mjs +254 -0
  275. package/scripts/lib/validate/check-vcs-repo-flag.mjs +6 -28
  276. package/scripts/lib/validate/enumerate-repo-files.mjs +317 -0
  277. package/scripts/lib/validate/markdown-fences.mjs +196 -0
  278. package/scripts/lib/vault-backfill/template.mjs +63 -6
  279. package/scripts/lib/vault-mirror/process.mjs +165 -42
  280. package/scripts/lib/vault-mirror/telemetry.mjs +2 -2
  281. package/scripts/lib/vault-status/board-lock.mjs +185 -0
  282. package/scripts/lib/vault-status/board-writer.mjs +174 -135
  283. package/scripts/lib/vault-status/narrative-mirror.mjs +129 -37
  284. package/scripts/lib/wave-executor/dispatch-common.mjs +164 -0
  285. package/scripts/lib/wave-executor/foreign-dispatch.mjs +7 -142
  286. package/scripts/lib/wave-executor/remote-dispatch.mjs +502 -0
  287. package/scripts/lib/wave-resource-gate.mjs +133 -7
  288. package/scripts/lib/wave-sizing.mjs +4 -1
  289. package/scripts/lib/wave-transcript-tail.mjs +142 -8
  290. package/scripts/materialize-wave-scope.mjs +32 -9
  291. package/scripts/memory-propose.mjs +146 -8
  292. package/scripts/migrate-cold-start-seed.mjs +4 -1
  293. package/scripts/parse-config.mjs +60 -3
  294. package/scripts/promote-vault-strict.mjs +4 -15
  295. package/scripts/release.mjs +337 -29
  296. package/scripts/repair-invalid-sessions.mjs +3 -3
  297. package/scripts/run-quality-gate.mjs +128 -11
  298. package/scripts/site-numbers.mjs +36 -4
  299. package/scripts/sweep-expired-learnings.mjs +90 -0
  300. package/scripts/sync-vault-schema.mjs +3 -1
  301. package/scripts/telemetry.mjs +2 -2
  302. package/scripts/validate-plugin.mjs +187 -0
  303. package/scripts/validate-wave-scope.mjs +28 -8
  304. package/scripts/vault-consolidate.mjs +3 -11
  305. package/scripts/vault-integration-watcher.mjs +2 -4
  306. package/scripts/vault-mirror.mjs +111 -26
  307. package/scripts/wave-scope-binding.mjs +215 -0
  308. package/skills/_shared/instruction-file-resolution.md +10 -0
  309. package/skills/_shared/parallel-aware-auq.md +31 -2
  310. package/skills/_shared/parallel-aware-preamble.md +18 -4
  311. package/skills/_shared/platform-tools.md +1 -1
  312. package/skills/_shared/state-ownership.md +1 -1
  313. package/skills/architecture/SKILL.md +7 -5
  314. package/skills/{domain-model/SKILL.md → architecture/references/domain-model.md} +9 -9
  315. package/skills/autopilot/SKILL.md +4 -18
  316. package/skills/claude-md-drift-check/SKILL.md +5 -1
  317. package/skills/claude-md-drift-check/checker.mjs +62 -2
  318. package/skills/convergence-monitoring/SIGNALS.md +55 -0
  319. package/skills/discovery/probes/vault-staleness.mjs +37 -13
  320. package/skills/discovery/probes-arch.md +20 -18
  321. package/skills/dispatcher/SKILL.md +3 -2
  322. package/skills/ecosystem-health/SKILL.md +4 -1
  323. package/skills/ecosystem-health/wizard.md +5 -0
  324. package/skills/evolve/SKILL.md +87 -11
  325. package/skills/frontmatter-guard/SKILL.md +11 -5
  326. package/skills/npm-publish/SKILL.md +1 -1
  327. package/skills/reconcile/SKILL.md +38 -2
  328. package/skills/remote-offload/SKILL.md +89 -0
  329. package/skills/session-end/SKILL.md +18 -905
  330. package/skills/session-end/phase-3-6-tail.md +19 -9
  331. package/skills/session-end/plan-verification.md +221 -155
  332. package/skills/session-end/references/phase-2-quality-gate.md +93 -0
  333. package/skills/session-end/references/phase-3-documentation-updates.md +229 -0
  334. package/skills/session-end/references/phase-4a-worktree-cleanup.md +120 -0
  335. package/skills/session-end/references/phase-4b-worktree-orphan-sweep.md +58 -0
  336. package/skills/session-end/references/phase-5-issue-cleanup.md +104 -0
  337. package/skills/session-end/references/session-summary-template.md +62 -0
  338. package/skills/session-plan/SKILL.md +49 -0
  339. package/skills/session-start/SKILL.md +41 -900
  340. package/skills/session-start/phase-8-5-express-path.md +1 -1
  341. package/skills/session-start/references/phase-1-1-dispatcher-autonomy-capture.md +55 -0
  342. package/skills/session-start/references/phase-1-2-session-lock.md +140 -0
  343. package/skills/session-start/references/phase-1-5-session-continuity.md +254 -0
  344. package/skills/session-start/references/phase-1-7-vault-status-board.md +53 -0
  345. package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +75 -0
  346. package/skills/session-start/references/phase-4-ssot-environment-check.md +155 -0
  347. package/skills/session-start/references/phase-6-5-forced-reads.md +75 -0
  348. package/skills/session-start/references/phase-6-6-project-intelligence.md +81 -0
  349. package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +103 -0
  350. package/skills/vault-sync/validator.mjs +21 -27
  351. package/skills/wave-executor/SKILL.md +16 -2
  352. package/skills/wave-executor/references/wave-loop-dispatch.md +612 -0
  353. package/skills/wave-executor/references/wave-loop-review.md +570 -0
  354. package/skills/wave-executor/references/wave-loop-scope-manifest.md +162 -0
  355. package/skills/wave-executor/wave-loop.md +14 -1271
  356. package/templates/_shared/journey-manifest.md +10 -6
  357. package/.cursor/commands/autopilot-multi.md +0 -14
  358. package/.cursor/commands/contract-version-bump.md +0 -14
  359. package/.cursor/commands/journey-audit.md +0 -14
  360. package/.cursor/skills/contract-version-bump/SKILL.md +0 -12
  361. package/.cursor/skills/daily/SKILL.md +0 -12
  362. package/.cursor/skills/domain-model/SKILL.md +0 -13
  363. package/.cursor/skills/journey-audit/SKILL.md +0 -13
  364. package/.cursor/skills/skill-creator/SKILL.md +0 -13
  365. package/.cursor/skills/ubiquitous-language/SKILL.md +0 -13
  366. package/commands/autopilot-multi.md +0 -74
  367. package/commands/contract-version-bump.md +0 -28
  368. package/commands/journey-audit.md +0 -43
  369. package/pi/prompts/autopilot-multi.md +0 -12
  370. package/pi/prompts/contract-version-bump.md +0 -12
  371. package/pi/prompts/journey-audit.md +0 -12
  372. package/scripts/autopilot-multi.mjs +0 -885
  373. package/scripts/backfill-learnings-expires.mjs +0 -196
  374. package/scripts/backfill-learnings.mjs +0 -203
  375. package/scripts/fleet-instruction-scan.mjs +0 -141
  376. package/scripts/lib/autopilot/dep-graph.mjs +0 -417
  377. package/scripts/lib/autopilot/multi-killswitch.mjs +0 -184
  378. package/scripts/lib/webhook-url.mjs +0 -105
  379. package/scripts/lifecycle-sim-v6.mjs +0 -347
  380. package/scripts/migrate-learnings-jsonl.mjs +0 -189
  381. package/scripts/migrate-subagents-jsonl.mjs +0 -196
  382. package/scripts/upload-social-preview.mjs +0 -316
  383. package/skills/_shared/model-selection.md +0 -64
  384. package/skills/contract-version-bump/SKILL.md +0 -219
  385. package/skills/daily/SKILL.md +0 -222
  386. package/skills/daily/generate.sh +0 -92
  387. package/skills/daily/templates/daily.md.tpl +0 -36
  388. package/skills/journey-audit/SKILL.md +0 -269
  389. package/skills/skill-creator/SKILL.md +0 -168
  390. package/skills/ubiquitous-language/SKILL.md +0 -97
  391. package/skills/vault-sync/package-lock.json +0 -40
  392. /package/skills/{domain-model → architecture/references}/ADR-FORMAT.md +0 -0
  393. /package/skills/{domain-model → architecture/references}/CONTEXT-FORMAT.md +0 -0
package/hooks/on-stop.mjs CHANGED
@@ -16,23 +16,42 @@
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>}
20
- * (`session_id` / `semantic_session_id` are omitted when unresolvable #1068 AC1.)
21
- * SubagentStop: {"timestamp":<ISO>,"event":"orchestrator.agent.stopped","agent":"<name>"}
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}
22
+ * (`session_id` / `semantic_session_id` are omitted when unresolvable — #1068 AC1.
23
+ * `duration_ms` + `duration_source` are omitted TOGETHER when no OWNED
24
+ * session.lock is readable — never a fabricated 0, see K5 below.)
25
+ * SubagentStop: {"timestamp":<ISO>,"event":"orchestrator.agent.stopped","agent":"<name>","agent_id":"...",
26
+ * "agent_type_meta":"...","tool_use_id":"...","transcript_found":true,
27
+ * "duration_ms":<int>,"duration_source":"meta-birthtime","status":"done"}
28
+ * (#1190 — every key after `event` is OPTIONAL and OMITTED when the
29
+ * measurement could not be made; `agent` too. See docs/events-schema.md.)
22
30
  */
23
31
 
24
32
  import path from 'node:path';
25
- import { promises as fs, statSync, writeFileSync } from 'node:fs';
33
+ import {
34
+ promises as fs,
35
+ closeSync,
36
+ existsSync,
37
+ fstatSync,
38
+ openSync,
39
+ readFileSync,
40
+ readSync,
41
+ statSync,
42
+ writeFileSync,
43
+ } from 'node:fs';
26
44
 
27
45
  import { shouldRunHook } from './_lib/profile-gate.mjs';
46
+ import { AGENT_ID_RE, resolveSubagentSidecar } from './_lib/subagent-paths.mjs';
28
47
  // #211: exit 0 immediately (silent allow) when this hook is disabled via profile/env
29
48
  if (!shouldRunHook('on-stop')) process.exit(0);
30
49
 
31
50
  import { emitEvent } from '../scripts/lib/events.mjs';
32
- import { SO_PROJECT_DIR } from '../scripts/lib/platform.mjs';
51
+ import { getProjectDir } from '../scripts/lib/platform.mjs';
33
52
  import { parseSessionId } from '../scripts/lib/session-id.mjs';
34
53
  import { heartbeat, logSweepEvent } from '../scripts/lib/session-registry.mjs';
35
- import { updateHeartbeat } from '../scripts/lib/session-lock.mjs';
54
+ import { readLock, updateHeartbeat } from '../scripts/lib/session-lock.mjs';
36
55
 
37
56
  // ---------------------------------------------------------------------------
38
57
  // stdin reading (inline — no io.mjs because Stop hooks exit 0 always, never deny)
@@ -247,7 +266,8 @@ async function readWaveNumber(projectRoot) {
247
266
  */
248
267
  async function resolveSessionId(input, projectRoot) {
249
268
  const fromStdin = input?.session_id ?? input?.sessionId ?? null;
250
- let sessionId = parseSessionId(fromStdin)?.format === 'uuid' ? fromStdin : null;
269
+ const rawStdinId = parseSessionId(fromStdin)?.format === 'uuid' ? fromStdin : null;
270
+ let sessionId = rawStdinId;
251
271
 
252
272
  let recordedId = null;
253
273
  let semanticSessionId = null;
@@ -265,21 +285,90 @@ async function resolveSessionId(input, projectRoot) {
265
285
  }
266
286
  } catch { /* missing or unparseable is fine */ }
267
287
 
288
+ // Actor-identity fallback ONLY — `sessionId` drives the registry heartbeat and
289
+ // the lock heartbeat, both of which need an id even when stdin carried none.
268
290
  if (sessionId === null) sessionId = recordedId;
269
291
 
270
- const isRecordedSession = sessionId !== null && sessionId === recordedId;
292
+ // W4a review F-A, second site of the same defect (census: exactly 2, both in
293
+ // hooks/, `grep -rn "= recordedId;" hooks/ scripts/` 2026-09-02). Comparing
294
+ // the RESOLVED id against `recordedId` is self-fulfilling: the line above may
295
+ // have just copied one into the other, so an id-less or non-UUID turn-end
296
+ // always read as "I am the recorded session" and inherited a live PEER's
297
+ // semantic identity — the exact #863 defect (c) contamination this gate
298
+ // exists to refuse. Decided on the RAW stdin UUID instead.
299
+ const isRecordedSession = rawStdinId !== null && rawStdinId === recordedId;
271
300
  return {
272
301
  sessionId,
273
302
  semanticSessionId: isRecordedSession ? semanticSessionId : null,
274
303
  };
275
304
  }
276
305
 
306
+ /**
307
+ * Resolve an elapsed span for this Stop event — and WHICH span depends on the
308
+ * source, which is why `duration_source` ships beside the number (W4a F-C).
309
+ *
310
+ * `session-lock` — SESSION-elapsed, measured at this turn's end. Stop fires
311
+ * once per TURN, and `started_at` is stamped once per
312
+ * SESSION, so this number GROWS MONOTONICALLY across the
313
+ * turns of one session: the last Stop of a 3-hour session
314
+ * reports ~3 hours, not the length of its final turn.
315
+ * `stdin-start-ms` — TURN-elapsed, the only first-party measurement of the
316
+ * turn itself. Measured today: the harness never sends it,
317
+ * so this source has never appeared in the fleet stream.
318
+ *
319
+ * Consumers must therefore not aggregate the two: summing `session-lock` spans
320
+ * over a session double-counts, where summing `stdin-start-ms` spans would not.
321
+ *
322
+ * OWNERSHIP-GATED, on the raw stdin `session_id` only. A lock in this working
323
+ * copy may name a PEER session (two windows share one checkout routinely), and
324
+ * a working-copy artefact is not a process-local identity witness — so the
325
+ * lock's `session_id` must match the id the STOPPING session was handed on
326
+ * stdin. `resolveSessionId()`'s resolved id is deliberately NOT used here: it
327
+ * falls back to `current-session.json`, which is exactly the foreign-identity
328
+ * inheritance this guard exists to refuse.
329
+ *
330
+ * Returns an object to SPREAD into the payload: `{}` when nothing is
331
+ * attestable, so `duration_ms` and `duration_source` are omitted together
332
+ * rather than written as a fabricated 0 (K5).
333
+ *
334
+ * `start_ms` is honoured first when the harness ever starts sending it — it is
335
+ * the only first-party measurement of the turn itself; measured today it is
336
+ * never present.
337
+ *
338
+ * @param {object|null} input parsed stdin payload
339
+ * @param {string} projectRoot
340
+ * @returns {{duration_ms?: number, duration_source?: string}}
341
+ */
342
+ function resolveStopDuration(input, projectRoot) {
343
+ if (typeof input?.start_ms === 'number' && Number.isFinite(input.start_ms)) {
344
+ const ms = Date.now() - input.start_ms;
345
+ if (Number.isFinite(ms) && ms >= 0) {
346
+ return { duration_ms: Math.round(ms), duration_source: 'stdin-start-ms' };
347
+ }
348
+ }
349
+
350
+ const stdinId = input?.session_id ?? input?.sessionId ?? null;
351
+ if (typeof stdinId !== 'string' || stdinId.length === 0) return {};
352
+
353
+ try {
354
+ const lock = readLock({ repoRoot: projectRoot });
355
+ if (lock === null || lock.session_id !== stdinId) return {};
356
+ const startedAt = Date.parse(lock.started_at);
357
+ if (Number.isNaN(startedAt)) return {};
358
+ const ms = Date.now() - startedAt;
359
+ if (!Number.isFinite(ms) || ms < 0) return {};
360
+ return { duration_ms: Math.round(ms), duration_source: 'session-lock' };
361
+ } catch {
362
+ return {}; // best-effort — an unreadable lock measures nothing
363
+ }
364
+ }
365
+
277
366
  /**
278
367
  * Handle a Stop event. Reads wave from scope file + git info, appends JSONL.
279
368
  * @param {object|null} input
280
369
  */
281
370
  async function handleStop(input) {
282
- const projectRoot = SO_PROJECT_DIR;
371
+ const projectRoot = getProjectDir();
283
372
 
284
373
  const wave = await readWaveNumber(projectRoot);
285
374
  const { commit, branch } = await gitInfo(projectRoot);
@@ -350,9 +439,15 @@ async function handleStop(input) {
350
439
  } catch { /* best-effort */ }
351
440
  }
352
441
 
353
- // duration_ms: if input provides a start time we compute from it, else 0
354
- const durationMs =
355
- typeof input?.start_ms === 'number' ? Date.now() - input.start_ms : 0;
442
+ // K5 `duration_ms` was literally 0 in 8.127 of 8.127 fleet
443
+ // `orchestrator.session.stopped` records (measured 2026-09-02): the old
444
+ // expression fell back to a hard 0 because the harness never sends
445
+ // `start_ms`. A fabricated 0 is the exact failure class
446
+ // `scripts/lib/telemetry/subagents-schema.mjs:20-35` documents — it reads as
447
+ // a MEASURED zero-length session and is indistinguishable from one. So the
448
+ // span now comes from the session.lock's `started_at`, and BOTH keys are
449
+ // omitted when it cannot be measured.
450
+ const duration = resolveStopDuration(input, projectRoot);
356
451
 
357
452
  // Single emission path: emitEvent writes the canonical {timestamp, event, ...payload}
358
453
  // JSONL record AND fires the optional Clank webhook with the SAME event name — no
@@ -361,14 +456,42 @@ async function handleStop(input) {
361
456
  // turn-end outcome is joinable by identity from events.jsonl alone. Omitted
362
457
  // (never `""`/`null`) when unattested: an unresolved identity stays visibly
363
458
  // unresolved rather than becoming a guessed id.
364
- await emitEvent('orchestrator.session.stopped', {
459
+ // #1183 — a malformed record throws EventValidationError BEFORE any side
460
+ // effect (scripts/lib/events.mjs); this hook must never abort on that, so
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 = {
365
480
  ...(sessionId !== null ? { session_id: sessionId } : {}),
366
481
  ...(semanticSessionId !== null ? { semantic_session_id: semanticSessionId } : {}),
367
482
  wave,
368
483
  ...(branch !== null ? { branch } : {}),
369
484
  ...(commit !== null ? { commit } : {}),
370
- duration_ms: durationMs,
371
- });
485
+ ...duration,
486
+ };
487
+
488
+ try {
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 });
494
+ } catch { /* telemetry never blocks the hook (#1183) */ }
372
495
  }
373
496
 
374
497
  /**
@@ -380,11 +503,242 @@ async function handleStop(input) {
380
503
  * @returns {Promise<string|null>} additionalContext or null
381
504
  */
382
505
  async function handleSubagentStop(input) {
383
- const agent = input?.agent_type ?? 'unknown';
384
- await emitEvent('orchestrator.agent.stopped', { agent });
506
+ /** @type {Record<string, unknown>} */
507
+ const payload = {};
508
+
509
+ // `agent` — the #1190 headline fix. The harness sends the EMPTY STRING, which
510
+ // `?? 'unknown'` never caught: 89.991 of 103.763 fleet records (86,7%,
511
+ // measured 2026-09-02) carry `agent: ""`. Trim, and emit the key ONLY when a
512
+ // real value remains — an unmeasured type stays visibly unmeasured rather
513
+ // than becoming `""` or a guessed `'unknown'`.
514
+ //
515
+ // Clamped with the SAME regex as `agent_type_meta` below (Q1-LOW-F3): this is
516
+ // the identical value class from a different source, it lands in the ledger
517
+ // and travels the optional Clank webhook, and it was the only unclamped
518
+ // string in this payload. A mismatch OMITS the key — never a truncation,
519
+ // which would look like a measured (but wrong) agent type.
520
+ const rawAgent = firstNonEmptyString(input, ['agent_type', 'subagent_type']);
521
+ const agent = rawAgent !== null && AGENT_TYPE_META_RE.test(rawAgent) ? rawAgent : null;
522
+ if (agent !== null) payload.agent = agent;
523
+
524
+ // `agent_id` — reader shape copied from hooks/subagent-telemetry.mjs (both
525
+ // naming conventions the harness may use). Charset-guarded before it is ever
526
+ // interpolated into a path (same precedent, resolveSubagentTranscriptPath)
527
+ // AND before it is emitted: an unbounded, unvalidated id (`../../etc/passwd`,
528
+ // or a 10 KB blob) would otherwise land verbatim in the ledger and travel
529
+ // over the optional Clank webhook unredacted. A rejected id is OMITTED — the
530
+ // same omission contract every other field in this payload uses — never
531
+ // sanitised into a different id that looks measured.
532
+ const rawAgentId = firstNonEmptyString(input, ['agent_id', 'subagent_id']);
533
+ const agentId = rawAgentId !== null && AGENT_ID_RE.test(rawAgentId) ? rawAgentId : null;
534
+ if (agentId !== null) payload.agent_id = agentId;
535
+
536
+ // Sidecar-derived fields. Every derivation is individually wrapped: a failure
537
+ // omits its own field and nothing else, and this function must never throw —
538
+ // a throw here would skip the emit and lose the record that already works.
539
+ // Consolidated derivation (#1196): hooks/_lib/subagent-paths.mjs now also
540
+ // rejects the literal 'unknown' agentId — this file's own AGENT_ID_RE-only
541
+ // check (above, for the emitted `agent_id` field) never did.
542
+ const sidecar = resolveSubagentSidecar({ transcriptPath: input?.transcript_path, agentId });
543
+ if (sidecar !== null) {
544
+ const transcriptPath = sidecar.transcript;
545
+ const metaPath = sidecar.meta;
546
+
547
+ let transcriptFound = false;
548
+ try {
549
+ transcriptFound = existsSync(transcriptPath);
550
+ payload.transcript_found = transcriptFound;
551
+ } catch { /* probe failed — omit rather than assert `false` */ }
552
+
553
+ try {
554
+ // One small read, one parse, two fields. `description` is operator prose
555
+ // and is deliberately NOT carried: this payload also travels over the
556
+ // optional Clank webhook unredacted.
557
+ const meta = JSON.parse(readFileSync(metaPath, 'utf8'));
558
+ // Both fields are CHARSET+LENGTH CLAMPED before they enter the payload,
559
+ // for the same reason `agent_id` above is: this record is appended to the
560
+ // ledger AND travels over the optional Clank webhook UNREDACTED, and
561
+ // meta.json is written by the harness, not by us. An oversized or
562
+ // structured value would land verbatim in both. A rejected value is
563
+ // OMITTED, never truncated into a shorter value that still looks measured.
564
+ if (typeof meta?.toolUseId === 'string' && TOOL_USE_ID_RE.test(meta.toolUseId.trim())) {
565
+ payload.tool_use_id = meta.toolUseId.trim();
566
+ }
567
+ // A SECOND witness for the type — never merged into `agent`, so the
568
+ // empty-`agent_type` rate stays measurable.
569
+ if (typeof meta?.agentType === 'string' && AGENT_TYPE_META_RE.test(meta.agentType.trim())) {
570
+ payload.agent_type_meta = meta.agentType.trim();
571
+ }
572
+ } catch { /* absent or corrupt sidecar meta — omit both fields */ }
573
+
574
+ try {
575
+ // meta.json carries no spawn timestamp; the harness writes the file AT
576
+ // spawn, so its birthtime IS the spawn moment. `duration_source` puts
577
+ // that provenance in the record instead of implying a measured span.
578
+ const span = spanFromBirthtime(Date.now(), statSync(metaPath).birthtimeMs);
579
+ if (span !== null) {
580
+ payload.duration_ms = span;
581
+ payload.duration_source = 'meta-birthtime';
582
+ }
583
+ } catch { /* stat failed — omit both keys (never a fabricated 0) */ }
584
+
585
+ if (transcriptFound) {
586
+ const status = readStatusFromTranscriptTail(transcriptPath);
587
+ if (status !== null) payload.status = status;
588
+ }
589
+ }
590
+
591
+ // #1183 — a malformed record throws EventValidationError BEFORE any side
592
+ // effect (scripts/lib/events.mjs); this hook must never abort on that, so
593
+ // the emit is wrapped rather than left to propagate.
594
+ try {
595
+ await emitEvent('orchestrator.agent.stopped', payload);
596
+ } catch { /* telemetry never blocks the hook (#1183) */ }
385
597
  return null;
386
598
  }
387
599
 
600
+ // ---------------------------------------------------------------------------
601
+ // SubagentStop payload derivations (#1190)
602
+ // ---------------------------------------------------------------------------
603
+
604
+ /**
605
+ * Harness tool-use ids, measured on-disk 2026-09-02 in a real sidecar meta.json:
606
+ * `toolu_01Xj6qZ3ApsVfomhByNftmxj` (25 chars, alphanumeric + one underscore).
607
+ */
608
+ const TOOL_USE_ID_RE = /^[A-Za-z0-9_.-]{1,64}$/;
609
+
610
+ /**
611
+ * Agent types carry a plugin qualifier, so the COLON is part of the real shape —
612
+ * measured in the same file: `session-orchestrator:code-implementer` (37 chars).
613
+ * A `tool_use_id`-identical charset would have rejected every plugin-qualified
614
+ * agent type in this repo, i.e. silently removed the field it clamps.
615
+ */
616
+ const AGENT_TYPE_META_RE = /^[A-Za-z0-9_.:-]{1,64}$/;
617
+
618
+ /**
619
+ * Upper bound on a `meta-birthtime` span, in ms. NAMED CEILING (BV-004): 7 days.
620
+ *
621
+ * No subagent runs for a week — the longest recorded here is minutes — so a
622
+ * span above this is not a long agent, it is a birthtime the filesystem did not
623
+ * supply. Node documents `birthtimeMs` as unavailable on filesystems that do
624
+ * not record it (overlayfs and some CI images among them), where it surfaces as
625
+ * 0 / 1970-01-01; the old `>= 0` lower bound accepted that verbatim and would
626
+ * ship a ~55-YEAR span stamped `duration_source: 'meta-birthtime'` — a
627
+ * fabricated measurement of exactly the class K5 removed.
628
+ *
629
+ * REVISIT TRIGGER: if a legitimately long-running agent class ever appears
630
+ * (a background agent measured in hours-to-days), raise this — but raise it to
631
+ * a measured bound, never remove it.
632
+ */
633
+ const META_BIRTHTIME_MAX_SPAN_MS = 7 * 24 * 60 * 60 * 1000;
634
+
635
+ /**
636
+ * Span between a birthtime and now, or `null` when the birthtime is not a
637
+ * usable measurement.
638
+ *
639
+ * Rejects a non-positive `birthtimeMs` (the "filesystem records no birthtime"
640
+ * signal) and any span outside `(0, META_BIRTHTIME_MAX_SPAN_MS)`. `null` means
641
+ * "omit both keys" at the call site — never a fabricated number.
642
+ *
643
+ * Pure, and separate from the call site so the two out-of-band cases are
644
+ * testable without a filesystem that can forge a birthtime.
645
+ *
646
+ * @param {number} nowMs
647
+ * @param {number} birthtimeMs
648
+ * @returns {number|null} rounded span in ms, or null
649
+ */
650
+ function spanFromBirthtime(nowMs, birthtimeMs) {
651
+ if (!Number.isFinite(birthtimeMs) || birthtimeMs <= 0) return null;
652
+ const span = nowMs - birthtimeMs;
653
+ if (!Number.isFinite(span) || span < 0 || span > META_BIRTHTIME_MAX_SPAN_MS) return null;
654
+ return Math.round(span);
655
+ }
656
+
657
+ /** Tail window for the STATUS scan — sidecars measured at 113–528 KB. */
658
+ const STATUS_TAIL_BYTES = 64 * 1024;
659
+
660
+ /**
661
+ * STATUS literals an agent reports at the START of a line (optionally bold).
662
+ *
663
+ * LINE-ANCHORED on purpose — the rationale and the measurement are at
664
+ * `scripts/lib/wave-transcript-tail.mjs:105-112`: a free-floating
665
+ * `/STATUS: partial/` fires on any agent that merely QUOTES the marker
666
+ * (2 of 2 live hits in that measurement were quotations, 0 were reports).
667
+ * NOT imported from that module because the two alternations are DIFFERENT
668
+ * literal sets, not two copies of one: the tailer's `STATUS_RE` matches only
669
+ * `partial|blocked|failed` (it classifies a single `status-partial` attention
670
+ * pattern), while this hook records the agent's SELF-REPORTED status and must
671
+ * therefore also match `done` and `no-tests-needed`. Importing its constant
672
+ * would silently drop the two success literals. (Import cost is NOT the reason —
673
+ * measured 2026-09-02 as equal.)
674
+ */
675
+ const AGENT_STATUS_RE = /^[ \t*_]*STATUS:\s*(done|partial|blocked|failed|no-tests-needed)\b/im;
676
+
677
+ /**
678
+ * Pick the first non-empty trimmed string among `keys` on `input`.
679
+ * @param {object|null|undefined} input
680
+ * @param {string[]} keys
681
+ * @returns {string|null}
682
+ */
683
+ function firstNonEmptyString(input, keys) {
684
+ for (const k of keys) {
685
+ const v = input?.[k];
686
+ if (typeof v === 'string' && v.trim()) return v.trim();
687
+ }
688
+ return null;
689
+ }
690
+
691
+ /**
692
+ * Read the LAST `STATUS_TAIL_BYTES` of an agent transcript and return the most
693
+ * recent line-anchored STATUS literal, or null when none is present.
694
+ *
695
+ * Never reads the whole file: measured sidecar sizes are 113–528 KB and this
696
+ * hook fires on every SubagentStop. Absence means NOT FOUND, never success —
697
+ * measured 2026-09-02, only 1 of 4 live sidecars carried a STATUS token in its
698
+ * last 64 KiB, so the miss is the common path.
699
+ *
700
+ * @param {string} transcriptPath
701
+ * @returns {string|null}
702
+ */
703
+ function readStatusFromTranscriptTail(transcriptPath) {
704
+ let fd = null;
705
+ try {
706
+ fd = openSync(transcriptPath, 'r');
707
+ const size = fstatSync(fd).size;
708
+ const length = Math.min(size, STATUS_TAIL_BYTES);
709
+ const start = size - length;
710
+ const buf = Buffer.alloc(length);
711
+ const read = readSync(fd, buf, 0, length, start);
712
+ let lines = buf.subarray(0, read).toString('utf8').split('\n');
713
+ // Drop the leading partial line when the window did not start at byte 0.
714
+ if (start > 0) lines = lines.slice(1);
715
+
716
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
717
+ const line = lines[i].trim();
718
+ if (!line) continue;
719
+ let rec;
720
+ try {
721
+ rec = JSON.parse(line);
722
+ } catch { continue; }
723
+ const content = rec?.message?.content;
724
+ if (!Array.isArray(content)) continue;
725
+ for (let j = content.length - 1; j >= 0; j -= 1) {
726
+ const block = content[j];
727
+ if (typeof block?.text !== 'string') continue;
728
+ const m = AGENT_STATUS_RE.exec(block.text);
729
+ if (m) return m[1].toLowerCase();
730
+ }
731
+ }
732
+ return null;
733
+ } catch {
734
+ return null;
735
+ } finally {
736
+ if (fd !== null) {
737
+ try { closeSync(fd); } catch { /* best-effort */ }
738
+ }
739
+ }
740
+ }
741
+
388
742
  // ---------------------------------------------------------------------------
389
743
  // main
390
744
  // ---------------------------------------------------------------------------
@@ -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;