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
@@ -42,6 +42,21 @@
42
42
  * under B's identity. `last_batch` stays UNGATED — it is a batch-resolution
43
43
  * breadcrumb, not a claim about whose wave lifecycle this is.
44
44
  *
45
+ * DIFF-SIZE MEASUREMENT (#980). The mechanical fallback below persists
46
+ * `wave_start_sha` (`git rev-parse HEAD`, best-effort) at every wave-OPEN
47
+ * transition and attaches `files_changed` + `files_changed_source:
48
+ * 'worktree-vs-wave-start-sha'` to the `orchestrator.wave.completed` it emits
49
+ * for the wave that sha opened. `files_changed` counts the DEDUPED union of
50
+ * `git diff --name-only <wave_start_sha>` (working tree vs the wave's start
51
+ * commit — the coordinator normally commits only at close, so uncommitted
52
+ * edits must count) and `git ls-files --others --exclude-standard` (untracked).
53
+ * Every git call is best-effort with a 1.5 s timeout; any failure, or a missing
54
+ * `wave_start_sha`, OMITS the key — the convergence-monitor reader treats an
55
+ * absent key as null and the `shrinking_diff` signal simply does not fire.
56
+ * `wave_start_sha` is written under the SAME ownership gate as the wave keys.
57
+ * OUT OF SCOPE: `hooks/on-session-end.mjs`'s final-wave `.completed` carries no
58
+ * `files_changed` — no wave-open transition runs there, so it has no start sha.
59
+ *
45
60
  * MONOTONICITY RULE (#1193 W4c Q3-MED-3). Three writers touch
46
61
  * `last_wave_completed` (this hook's explicit branch, this hook's fallback,
47
62
  * `hooks/on-session-end.mjs`). Every one of them writes through `maxWave()`,
@@ -51,6 +66,7 @@
51
66
  * hooks.json wiring is managed separately (W3-C4 scope).
52
67
  */
53
68
 
69
+ import { execFileSync } from 'node:child_process';
54
70
  import { readFile } from 'node:fs/promises';
55
71
  import path from 'node:path';
56
72
 
@@ -58,7 +74,7 @@ import { shouldRunHook } from './_lib/profile-gate.mjs';
58
74
  // Exit 0 immediately when disabled via SO_HOOK_PROFILE / SO_DISABLED_HOOKS.
59
75
  if (!shouldRunHook('post-tool-batch-wave-signal')) process.exit(0);
60
76
 
61
- import { SO_PROJECT_DIR } from '../scripts/lib/platform.mjs';
77
+ import { getProjectDir } from '../scripts/lib/platform.mjs';
62
78
  import { emitEvent } from '../scripts/lib/events.mjs';
63
79
  import { findScopeFile } from '../scripts/lib/scope-gate.mjs';
64
80
  import { atomicMutateJson } from './_lib/atomic-json.mjs';
@@ -208,6 +224,87 @@ async function resolveWaveNumber(projectDir) {
208
224
  }
209
225
  }
210
226
 
227
+ /**
228
+ * The only environment variables forwarded to the `git` children below.
229
+ * Everything else — `GIT_DIR`/`GIT_WORK_TREE`/`GIT_COMMON_DIR` above all — is
230
+ * dropped, so an ambient redirect cannot make these probes measure a FOREIGN
231
+ * repository while `cwd` points at this one (`GIT_DIR` outranks `cwd`; that is
232
+ * exactly the 2026-08-19 redirect incident class).
233
+ *
234
+ * Same 6-entry allowlist as `scripts/lib/git-config-drift.mjs:170` and
235
+ * `check-banner-parity.mjs:63`, COPIED rather than imported on purpose: this is
236
+ * a live hook, and importing a scripts/lib module for six strings would add an
237
+ * edge to the hook import graph (`hooks/_lib/hook-import-set.json`) for no
238
+ * behavioural gain — the constant is cheaper to copy than a shared module is to
239
+ * maintain (BV-001, the same call git-config-drift.mjs documents).
240
+ */
241
+ const GIT_ENV_ALLOWLIST = Object.freeze(['PATH', 'HOME', 'LANG', 'LC_ALL', 'TMPDIR', 'TZ']);
242
+
243
+ /** @returns {Record<string,string>} the allowlisted subset of `process.env`. */
244
+ function filteredGitEnv() {
245
+ /** @type {Record<string,string>} */
246
+ const out = {};
247
+ for (const key of GIT_ENV_ALLOWLIST) {
248
+ const value = process.env[key];
249
+ if (typeof value === 'string') out[key] = value;
250
+ }
251
+ return out;
252
+ }
253
+
254
+ /** Shared spawn options for the best-effort git probes below (#980). */
255
+ const GIT_OPTS = (projectDir) => ({
256
+ cwd: projectDir,
257
+ encoding: 'utf8',
258
+ timeout: 1_500,
259
+ stdio: ['ignore', 'pipe', 'ignore'],
260
+ env: filteredGitEnv(),
261
+ });
262
+
263
+ /**
264
+ * `git rev-parse HEAD` in `projectDir`, best-effort (#980).
265
+ * Returns null on ANY failure — not a git repo, detached/empty HEAD, timeout,
266
+ * git absent. A null is persisted as null so a STALE sha from the previous wave
267
+ * can never be mistaken for this wave's start point.
268
+ *
269
+ * @param {string} projectDir
270
+ * @returns {string|null}
271
+ */
272
+ function readHeadSha(projectDir) {
273
+ try {
274
+ const out = execFileSync('git', ['rev-parse', 'HEAD'], GIT_OPTS(projectDir)).trim();
275
+ return /^[0-9a-f]{40}$/i.test(out) ? out : null;
276
+ } catch {
277
+ return null;
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Count the files this wave touched: the DEDUPED union of the working tree's
283
+ * diff against `sha` and the untracked (non-ignored) files (#980).
284
+ *
285
+ * Working-tree-vs-sha rather than `sha..HEAD` on purpose: the coordinator
286
+ * commits at session close, not per wave, so a commit-only diff reads 0 for
287
+ * every wave of a normal session.
288
+ *
289
+ * @param {string} projectDir
290
+ * @param {unknown} sha Persisted `wave_start_sha` (may be null/absent).
291
+ * @returns {number|null} null when unmeasurable — caller OMITS the key.
292
+ */
293
+ function countFilesChangedSince(projectDir, sha) {
294
+ if (typeof sha !== 'string' || !/^[0-9a-f]{40}$/i.test(sha)) return null;
295
+ try {
296
+ const opts = GIT_OPTS(projectDir);
297
+ const tracked = execFileSync('git', ['diff', '--name-only', sha], opts);
298
+ const untracked = execFileSync('git', ['ls-files', '--others', '--exclude-standard'], opts);
299
+ const paths = new Set(
300
+ `${tracked}\n${untracked}`.split('\n').map((l) => l.trim()).filter((l) => l.length > 0),
301
+ );
302
+ return paths.size;
303
+ } catch {
304
+ return null;
305
+ }
306
+ }
307
+
211
308
  // ---------------------------------------------------------------------------
212
309
  // Main
213
310
  // ---------------------------------------------------------------------------
@@ -248,7 +345,7 @@ async function main() {
248
345
  ...(waveSignal !== null ? { wave_signal: waveSignal } : {}),
249
346
  };
250
347
 
251
- const sessionFile = path.join(SO_PROJECT_DIR, '.orchestrator', 'current-session.json');
348
+ const sessionFile = path.join(getProjectDir(), '.orchestrator', 'current-session.json');
252
349
 
253
350
  const lastBatchResult = await atomicMutateJson(sessionFile, {}, (current) => ({
254
351
  ...current,
@@ -277,7 +374,7 @@ async function main() {
277
374
  const sessionId = await resolveSessionIdForHeartbeat(input, sessionFile);
278
375
  if (typeof sessionId === 'string' && sessionId.length > 0) {
279
376
  const { updateHeartbeat } = await import('../scripts/lib/session-lock.mjs');
280
- updateHeartbeat({ repoRoot: SO_PROJECT_DIR, sessionId });
377
+ updateHeartbeat({ repoRoot: getProjectDir(), sessionId });
281
378
  }
282
379
  } catch { /* best effort — hook must remain non-blocking */ }
283
380
 
@@ -357,16 +454,19 @@ async function main() {
357
454
  // when an N+1 transition already closed wave N, SessionEnd sees
358
455
  // last_wave_completed === last_wave and stays silent.
359
456
  try {
360
- const wave = await resolveWaveNumber(SO_PROJECT_DIR);
457
+ const wave = await resolveWaveNumber(getProjectDir());
361
458
  if (wave > 0) {
362
459
  // Read last_wave from the just-written session file (after the
363
460
  // last_batch RMW above, so we observe the latest persisted value).
364
461
  let lastWave = 0;
365
462
  let lastWaveCompleted = 0;
463
+ let waveStartSha = null;
366
464
  try {
367
465
  const raw = await readFile(sessionFile, 'utf8');
368
466
  const parsed = JSON.parse(raw);
369
467
  if (typeof parsed.last_wave === 'number') lastWave = parsed.last_wave;
468
+ // #980 — the sha persisted when THIS `last_wave` was opened.
469
+ if (typeof parsed.wave_start_sha === 'string') waveStartSha = parsed.wave_start_sha;
370
470
  // #1193 review F2 — read in the SAME read as `last_wave`: a `/clear`
371
471
  // mid-wave already emitted `completed(lastWave)` via SessionEnd, and
372
472
  // `on-session-start.mjs` preserves that marker across the restart, so
@@ -383,8 +483,17 @@ async function main() {
383
483
  // prior wave is already closed too, and a non-integer marker counts
384
484
  // as absent rather than as "different".
385
485
  if (lastWave > lastWaveCompleted) {
486
+ // #980 — measure the closing wave's diff size BEFORE the new wave's
487
+ // start sha is persisted below. Unmeasurable → both keys omitted.
488
+ const filesChanged = countFilesChangedSince(getProjectDir(), waveStartSha);
386
489
  await emitEvent('orchestrator.wave.completed', {
387
490
  wave_number: lastWave,
491
+ ...(filesChanged !== null
492
+ ? {
493
+ files_changed: filesChanged,
494
+ files_changed_source: 'worktree-vs-wave-start-sha',
495
+ }
496
+ : {}),
388
497
  ...(batchId !== null ? { batch_id: batchId } : {}),
389
498
  ...(batchSize !== null ? { batch_size: batchSize } : {}),
390
499
  });
@@ -408,8 +517,13 @@ async function main() {
408
517
  // that session's own SessionStart takes over the file. Revisit if the
409
518
  // ledger shows repeated started{N} for one wave in a shared checkout.
410
519
  if (await ownsSessionFile(input, sessionFile)) {
520
+ // #980 — the OPEN half: stamp the sha this new wave starts from.
521
+ // Written as null (not omitted) when git is unreadable, so the
522
+ // PREVIOUS wave's sha can never linger and inflate the next count.
523
+ const startSha = readHeadSha(getProjectDir());
411
524
  const markResult = await atomicMutateJson(sessionFile, {}, (current) => ({
412
525
  ...current,
526
+ wave_start_sha: startSha,
413
527
  last_wave: maxWave(current?.last_wave, wave),
414
528
  ...(Math.max(lastWave, lastWaveCompleted) > 0
415
529
  ? {
@@ -30,7 +30,7 @@ import { shouldRunHook } from './_lib/profile-gate.mjs';
30
30
  // Exit 0 immediately when disabled via SO_HOOK_PROFILE / SO_DISABLED_HOOKS.
31
31
  if (!shouldRunHook('post-tool-failure-corrective-context')) process.exit(0);
32
32
 
33
- import { SO_PROJECT_DIR } from '../scripts/lib/platform.mjs';
33
+ import { getProjectDir } from '../scripts/lib/platform.mjs';
34
34
  import { atomicMutateJson } from './_lib/atomic-json.mjs';
35
35
 
36
36
  // ---------------------------------------------------------------------------
@@ -179,7 +179,7 @@ async function main() {
179
179
  exit_code: exitCode,
180
180
  };
181
181
 
182
- const sessionFile = path.join(SO_PROJECT_DIR, '.orchestrator', 'current-session.json');
182
+ const sessionFile = path.join(getProjectDir(), '.orchestrator', 'current-session.json');
183
183
 
184
184
  const result = await atomicMutateJson(sessionFile, {}, (current) => {
185
185
  const existing = Array.isArray(current.corrective_context)
@@ -29,7 +29,7 @@ if (!shouldRunHook('frontend-slop-hook')) process.exit(0);
29
29
 
30
30
  import path from 'node:path';
31
31
 
32
- import { SO_PROJECT_DIR } from '../scripts/lib/platform.mjs';
32
+ import { getProjectDir } from '../scripts/lib/platform.mjs';
33
33
  import { emitEvent } from '../scripts/lib/events.mjs';
34
34
  import { detectFiles, SCANNABLE_EXTS } from '../scripts/lib/frontend-detect/detect.mjs';
35
35
  import { loadFrontendSlopHookConfig } from '../scripts/lib/config/frontend-slop-hook.mjs';
@@ -98,7 +98,7 @@ function resolveSessionId(input) {
98
98
  */
99
99
  function toRelPath(filePath) {
100
100
  try {
101
- const rel = path.relative(SO_PROJECT_DIR, filePath);
101
+ const rel = path.relative(getProjectDir(), filePath);
102
102
  if (rel && !rel.startsWith('..') && !path.isAbsolute(rel)) return rel;
103
103
  } catch { /* fall through */ }
104
104
  return path.basename(filePath);
@@ -147,7 +147,7 @@ async function main() {
147
147
  if (!SCANNABLE_EXTS.has(ext)) return;
148
148
 
149
149
  // Session Config gate — OPT-IN (default off).
150
- const config = await loadFrontendSlopHookConfig({ repoRoot: SO_PROJECT_DIR });
150
+ const config = await loadFrontendSlopHookConfig({ repoRoot: getProjectDir() });
151
151
  if (config.enabled !== true) return;
152
152
 
153
153
  // Run the deterministic detector (fail-soft: unreadable files → []).
@@ -95,6 +95,8 @@ let blocker = null;
95
95
  let readConfigFile;
96
96
  let loadEffectivePolicy;
97
97
  let isSessionConfigHeading;
98
+ let preprocessBlockLinesNoDash;
99
+ let findUnterminatedComment;
98
100
  let emitEvent;
99
101
 
100
102
  /**
@@ -164,6 +166,7 @@ async function bootstrap() {
164
166
  config: { specifier: lib('config.mjs') },
165
167
  blockedCommandsPolicy: { specifier: lib('blocked-commands-policy.mjs') },
166
168
  sectionExtractor: { specifier: lib('config', 'section-extractor.mjs') },
169
+ blockPreprocess: { specifier: lib('config', 'block-preprocess.mjs') },
167
170
  events: { specifier: lib('events.mjs') },
168
171
  blocker: {
169
172
  specifier: lib('command-blocker.mjs'),
@@ -192,6 +195,7 @@ async function bootstrap() {
192
195
  ({ readConfigFile } = modules.config);
193
196
  ({ loadEffectivePolicy } = modules.blockedCommandsPolicy);
194
197
  ({ isSessionConfigHeading } = modules.sectionExtractor);
198
+ ({ findUnterminatedComment, preprocessBlockLinesNoDash } = modules.blockPreprocess);
195
199
  ({ emitEvent } = modules.events);
196
200
  blocker = modules.blocker;
197
201
  degradedLabels = degraded;
@@ -760,19 +764,41 @@ async function main() {
760
764
  const mdContent = await readConfigFile(projectDir);
761
765
  // Look for "allow-destructive-ops: true" in the Session Config block.
762
766
  // Simple line-based scan: find lines under ## Session Config, stop at next ##.
763
- const lines = mdContent.split(/\r?\n/);
764
- let inConfig = false;
765
- for (const line of lines) {
766
- // SSOT predicate (#968) never re-derive this comparison. A local copy
767
- // that drifts LOOSER than the extractor silently disagrees with the
768
- // runtime about where the config block starts.
769
- if (isSessionConfigHeading(line)) { inConfig = true; continue; }
770
- if (inConfig && /^## /.test(line)) break;
771
- if (inConfig) {
772
- const m = line.match(/^\s*(?:-\s+\*\*)?allow-destructive-ops(?::\*\*)?\s*:\s*(\S+)/);
773
- if (m && m[1].toLowerCase() === 'true') {
774
- process.stderr.write('ℹ destructive-guard bypassed\n');
775
- return emitAllow();
767
+ // W4/F1b HTML-comment stripping is MANDATORY here (same class as #1162a).
768
+ // With a raw `split(/\r?\n/)` a COMMENTED-OUT
769
+ // `<!--\nallow-destructive-ops: true\n-->` under `## Session Config` DISABLED
770
+ // the whole 14-rule guard for the session: commenting a key out is the most
771
+ // ordinary way to turn it off and must never turn it ON. `NoDash` (not the
772
+ // full `preprocessBlockLines`) on purpose bold-subkey normalisation is not
773
+ // wanted; the regex below already tolerates `- **key:**` and stays as-is.
774
+ //
775
+ // UNTERMINATED `<!--` — the fail-closed direction INVERTS for a bypass scan.
776
+ // `stripHtmlCommentBlocks` returns the lines UNFILTERED when a comment is
777
+ // never closed (right for a block PARSER, nothing may silently vanish), which
778
+ // here would hand the commented-out bypass line back and re-arm the guard's
779
+ // own off switch. For this scan "I cannot tell where the comments end" must
780
+ // mean NOT ARMED: the bypass is opt-in, so refusing to grant it is the safe
781
+ // reading of an ambiguous document. Said out loud on stderr, because a
782
+ // silently-ignored bypass would look identical to a typo the operator meant.
783
+ if (findUnterminatedComment(mdContent.split(/\r?\n/)) !== null) {
784
+ process.stderr.write(
785
+ 'ℹ destructive-guard: unterminated <!-- in CLAUDE.md — bypass ignored\n'
786
+ );
787
+ } else {
788
+ const lines = preprocessBlockLinesNoDash(mdContent);
789
+ let inConfig = false;
790
+ for (const line of lines) {
791
+ // SSOT predicate (#968) — never re-derive this comparison. A local copy
792
+ // that drifts LOOSER than the extractor silently disagrees with the
793
+ // runtime about where the config block starts.
794
+ if (isSessionConfigHeading(line)) { inConfig = true; continue; }
795
+ if (inConfig && /^## /.test(line)) break;
796
+ if (inConfig) {
797
+ const m = line.match(/^\s*(?:-\s+\*\*)?allow-destructive-ops(?::\*\*)?\s*:\s*(\S+)/);
798
+ if (m && m[1].toLowerCase() === 'true') {
799
+ process.stderr.write('ℹ destructive-guard bypassed\n');
800
+ return emitAllow();
801
+ }
776
802
  }
777
803
  }
778
804
  }
@@ -33,7 +33,7 @@ import { spawn } from 'node:child_process';
33
33
 
34
34
  import { shouldRunHook } from './_lib/profile-gate.mjs';
35
35
  import { appendSkillInvocation } from '../scripts/lib/skill-invocations-schema.mjs';
36
- import { SO_PROJECT_DIR } from '../scripts/lib/platform.mjs';
36
+ import { getProjectDir } from '../scripts/lib/platform.mjs';
37
37
  import { shouldDailyFlush } from '../scripts/lib/telemetry/sync.mjs';
38
38
  import { resolveConsent, readTelemetryState } from '../scripts/lib/telemetry/consent.mjs';
39
39
  import { loadOwnerConfig } from '../scripts/lib/owner-yaml.mjs';
@@ -42,9 +42,21 @@ import { loadOwnerConfig } from '../scripts/lib/owner-yaml.mjs';
42
42
  // Constants
43
43
  // ---------------------------------------------------------------------------
44
44
 
45
- const METRICS_DIR = path.join(SO_PROJECT_DIR, '.orchestrator', 'metrics');
45
+ /**
46
+ * Metrics directory / ledger path. FUNCTIONS, not module-level consts:
47
+ * `getProjectDir()` walks the filesystem on first call, and this hook must not
48
+ * pay that at import time (#1153 P5).
49
+ *
50
+ * @returns {string}
51
+ */
52
+ function metricsDirPath() {
53
+ return path.join(getProjectDir(), '.orchestrator', 'metrics');
54
+ }
46
55
 
47
- const JSONL_PATH = path.join(METRICS_DIR, 'skill-invocations.jsonl');
56
+ /** @returns {string} */
57
+ function jsonlPath() {
58
+ return path.join(metricsDirPath(), 'skill-invocations.jsonl');
59
+ }
48
60
 
49
61
  /** Absolute path to the telemetry CLI (carries the hidden `_flush` subcommand). */
50
62
  const TELEMETRY_CLI_PATH = path.resolve(
@@ -123,7 +135,7 @@ export function maybeSpawnDailyFlush({
123
135
  now = Date.now(),
124
136
  statePath,
125
137
  queuePath,
126
- metricsDir = METRICS_DIR,
138
+ metricsDir = metricsDirPath(),
127
139
  } = {}) {
128
140
  try {
129
141
  // 1. Cheapest gate: env kill-switches, no file I/O.
@@ -187,7 +199,7 @@ async function main() {
187
199
  schema_version: 1,
188
200
  };
189
201
 
190
- await appendSkillInvocation(JSONL_PATH, record);
202
+ await appendSkillInvocation(jsonlPath(), record);
191
203
 
192
204
  // Daily-fallback telemetry flush — non-blocking, best-effort, never throws.
193
205
  maybeSpawnDailyFlush();
@@ -84,14 +84,23 @@ if (!shouldRunHook('subagent-telemetry')) process.exit(0);
84
84
  import fs from 'node:fs';
85
85
  import path from 'node:path';
86
86
  import { appendSubagent } from '../scripts/lib/subagents-schema.mjs';
87
- import { SO_PROJECT_DIR } from '../scripts/lib/platform.mjs';
87
+ import { getProjectDir } from '../scripts/lib/platform.mjs';
88
88
  import { resolveSubagentSidecar } from './_lib/subagent-paths.mjs';
89
89
 
90
90
  // ---------------------------------------------------------------------------
91
91
  // Constants
92
92
  // ---------------------------------------------------------------------------
93
93
 
94
- const JSONL_PATH = path.join(SO_PROJECT_DIR, '.orchestrator', 'metrics', 'subagents.jsonl');
94
+ /**
95
+ * Absolute path to the subagent ledger. A FUNCTION, not a module-level const:
96
+ * `getProjectDir()` walks the filesystem on first call, and this hook must not
97
+ * pay that at import time (#1153 P5).
98
+ *
99
+ * @returns {string}
100
+ */
101
+ function jsonlPath() {
102
+ return path.join(getProjectDir(), '.orchestrator', 'metrics', 'subagents.jsonl');
103
+ }
95
104
 
96
105
  /**
97
106
  * Defense-in-depth byte ceiling for transcript reads (#624). The transcript path
@@ -669,7 +678,7 @@ async function main() {
669
678
  // duration measurement and the explicit orphan discriminator below.
670
679
  // 'unknown' is the agent-id fallback for a payload with no usable id —
671
680
  // joining on it would collide across unrelated agents, so never scan for it.
672
- const startedAtMs = agentId === 'unknown' ? null : findStartTimestampMs(JSONL_PATH, agentId);
681
+ const startedAtMs = agentId === 'unknown' ? null : findStartTimestampMs(jsonlPath(), agentId);
673
682
 
674
683
  // duration_ms (#917): prefer the harness value, else join backwards to this
675
684
  // agent's own start record, else null. Never 0 — see resolveDurationMs().
@@ -717,7 +726,7 @@ async function main() {
717
726
  record['gen_ai.system'] = 'anthropic';
718
727
  }
719
728
 
720
- await appendSubagent(JSONL_PATH, record);
729
+ await appendSubagent(jsonlPath(), record);
721
730
  }
722
731
 
723
732
  // Exit 0 always — informational hook must never block Claude.
@@ -2,13 +2,13 @@
2
2
  {
3
3
  "name": "ecosystem-health",
4
4
  "command": "node \"${CLAUDE_PLUGIN_ROOT}\"/scripts/lib/ecosystem-health.mjs --watch",
5
- "description": "Ecosystem repo health watcher (CI status, branch drift, stale baselines across vault-registered projects). Emits one NDJSON line per detected state change.",
6
- "when": "on-skill-invoke:ecosystem-health"
5
+ "description": "Ecosystem repo health watcher (CI status, branch drift, stale baselines across vault-registered projects). Emits one NDJSON line per detected state change. Hung on session-start because the previous trigger (on-skill-invoke:ecosystem-health) was self-referential: it could only start when the ecosystem-health skill ran, and that skill has 0 recorded invocations fleet-wide, so the watcher never started once.",
6
+ "when": "on-skill-invoke:session-start"
7
7
  },
8
8
  {
9
9
  "name": "convergence-monitor",
10
10
  "command": "node \"${CLAUDE_PLUGIN_ROOT}\"/scripts/lib/convergence-monitor.mjs --tail",
11
- "description": "Wave convergence telemetry monitor — tails .orchestrator/metrics/events.jsonl + sessions.jsonl, surfaces convergence signals (shrinking diff, pass-rate plateau, velocity).",
11
+ "description": "Wave convergence telemetry monitor — tails .orchestrator/metrics/events.jsonl (that file only; it never opens sessions.jsonl) and surfaces convergence signals (shrinking diff, pass-rate plateau, velocity).",
12
12
  "when": "on-skill-invoke:wave-executor"
13
13
  },
14
14
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "session-orchestrator",
3
- "version": "3.24.0",
3
+ "version": "4.0.0",
4
4
  "description": "Loop engineering for AI coding agents — turn ad-hoc sessions into a repeatable research → plan → wave-execute → close loop with verification gates. Runs on Claude Code, Codex CLI, Cursor, and Pi.",
5
5
  "type": "module",
6
6
  "homepage": "https://session-orchestrator.com",
@@ -23,7 +23,9 @@
23
23
  },
24
24
  "files": [
25
25
  ".mcp.json",
26
+ "AGENTS.md",
26
27
  "CHANGELOG.md",
28
+ "plugin.json",
27
29
  "pi/",
28
30
  "skills/",
29
31
  "agents/",
@@ -37,11 +39,14 @@
37
39
  "assets/",
38
40
  ".claude-plugin/",
39
41
  ".codex-plugin/",
42
+ ".agents/",
40
43
  ".cursor/",
44
+ ".orchestrator/policy/",
41
45
  "docs/*.md",
42
46
  "docs/telemetry/telemetry-claims.md",
43
47
  "NOTICE",
44
48
  "SECURITY.md",
49
+ "!skills/vault-sync/package-lock.json",
45
50
  "!scripts/tests/**",
46
51
  "!skills/vault-sync/tests/**",
47
52
  "!**/node_modules/**",
@@ -66,9 +71,12 @@
66
71
  "ajv": "^8.17.1",
67
72
  "js-yaml": "^4.3.1",
68
73
  "mdast-util-to-string": "^4.0.0",
74
+ "picomatch": "^4.0.4",
69
75
  "remark": "^15.0.0",
70
76
  "remark-parse": "^11.0.0",
71
77
  "unified": "^11.0.0",
78
+ "yaml": "^2.9.0",
79
+ "zod": "^3.25.76",
72
80
  "zx": "^8.1.0"
73
81
  },
74
82
  "devDependencies": {
@@ -1,6 +1,6 @@
1
1
  ---
2
- description: Start a development session (housekeeping, feature, deep)
3
- argument-hint: "[housekeeping|feature|deep]"
2
+ description: Start a development session (housekeeping, feature, deep; ultradeep = deep + profile)
3
+ argument-hint: "[housekeeping|feature|deep|ultradeep]"
4
4
  ---
5
5
 
6
6
  # /session
package/plugin.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
+ "name": "session-orchestrator",
4
+ "version": "4.0.0",
5
+ "description": "Loop engineering for AI coding agents — turn ad-hoc sessions into a repeatable research → plan → wave-execute → close loop with verification gates. Runs on Claude Code, Codex CLI, Cursor, and Pi.",
6
+ "author": {
7
+ "name": "Bernhard Goetzendorfer"
8
+ },
9
+ "homepage": "https://session-orchestrator.com",
10
+ "repository": "https://github.com/Kanevry/session-orchestrator",
11
+ "license": "MIT",
12
+ "keywords": [
13
+ "session",
14
+ "orchestration",
15
+ "waves",
16
+ "gitlab",
17
+ "github",
18
+ "quality-gates",
19
+ "verification-gates",
20
+ "multi-agent",
21
+ "codex",
22
+ "cursor",
23
+ "pi"
24
+ ],
25
+ "skills": "./skills/",
26
+ "mcpServers": "./.mcp.json"
27
+ }
@@ -43,7 +43,7 @@ import { fileURLToPath } from 'node:url';
43
43
 
44
44
  import { backfillAbandonedSession, isUuid } from './lib/session-close-backfill.mjs';
45
45
  import { emitEvent } from './lib/events.mjs';
46
- import { SO_PROJECT_DIR } from './lib/platform.mjs';
46
+ import { getProjectDir } from './lib/platform.mjs';
47
47
 
48
48
  const LOCK_ACQUIRED = 'orchestrator.session.lock.acquired';
49
49
  const SESSION_STARTED = 'orchestrator.session.started';
@@ -382,13 +382,59 @@ export async function backfillOnSessionStart({ repoRoot, limit = SESSION_START_L
382
382
  if (typeof repoRoot !== 'string' || repoRoot.length === 0) return null;
383
383
  // newestFirst is mandatory here — see runMigration's param docs for the
384
384
  // measured failure (budget burned on ancient already-recorded candidates).
385
- return await runMigration({ repoRoot, apply: true, limit, newestFirst: true });
385
+ const summary = await runMigration({ repoRoot, apply: true, limit, newestFirst: true });
386
+ // The telemetry flush rides the SAME start-time trigger, for the same reason
387
+ // the backfill does — see flushTelemetryOnSessionStart.
388
+ await flushTelemetryOnSessionStart(repoRoot);
389
+ return summary;
386
390
  } catch {
387
391
  // Swallowed by contract — see the docblock. The hook is informational-only.
388
392
  return null;
389
393
  }
390
394
  }
391
395
 
396
+ /** POST budget for the start-time telemetry drain (ms). Deliberately tighter than
397
+ * the close-time budget: a session START must not wait on the network. Expiry is
398
+ * LOSSLESS — flush() routes a timed-out batch into the offline queue. */
399
+ const STARTUP_FLUSH_TIMEOUT_MS = 1500;
400
+
401
+ /**
402
+ * SECOND FLUSH TRIGGER (GitLab #1234) — drain the PREVIOUS session's telemetry
403
+ * queue at the NEXT session's start.
404
+ *
405
+ * THE BUG THIS FIXES: the mechanical flush (#1138) lives only in
406
+ * `hooks/on-session-end.mjs`, which fires on a REGULAR close. Measured 2026-09-06
407
+ * over 90 fleet days, the real close rate is 21,3 % (429 clean closes / 2.016
408
+ * distinct `session.started` ids) — so roughly four sessions in five never reach
409
+ * the only code path that sends. This is the exact argument
410
+ * `backfillOnSessionStart` already makes for the ledger, applied to the ping:
411
+ * SessionStart is the trigger that survives whatever killed the last session.
412
+ *
413
+ * Three properties make this safe to run on the hot start path:
414
+ * - LAZY IMPORT. `sync.mjs` is pulled in at CALL time, not module load. Loading
415
+ * it statically would put the whole telemetry subtree onto the SessionStart
416
+ * hook's import graph — the graph `tests/hooks/on-stop.test.mjs` guards for
417
+ * bare specifiers, and the cost would be paid on every start whether or not
418
+ * telemetry is even consented to.
419
+ * - GATED, ALWAYS. `flush()` opens with `resolveConsent()`; with no consent it
420
+ * returns before any network, queue write, or anon-ID mint.
421
+ * - NEVER THROWS, NEVER BLOCKS A START. Every failure is swallowed, and the POST
422
+ * is bounded at STARTUP_FLUSH_TIMEOUT_MS; a timeout queues the batch rather
423
+ * than losing it.
424
+ *
425
+ * @param {string} repoRoot
426
+ * @returns {Promise<void>}
427
+ */
428
+ async function flushTelemetryOnSessionStart(repoRoot) {
429
+ try {
430
+ const { flush } = await import('./lib/telemetry/sync.mjs');
431
+ await flush({
432
+ metricsDir: path.join(repoRoot, '.orchestrator', 'metrics'),
433
+ timeoutMs: STARTUP_FLUSH_TIMEOUT_MS,
434
+ });
435
+ } catch { /* fire-and-forget by contract */ }
436
+ }
437
+
392
438
  function renderHuman(summary) {
393
439
  const lines = [];
394
440
  lines.push(`Backfill abandoned sessions — ${summary.mode}`);
@@ -441,7 +487,7 @@ async function main() {
441
487
  ' --dry-run preview only (DEFAULT — nothing is written)\n' +
442
488
  ' --apply synthesize + append abandoned-session stubs\n' +
443
489
  ' --json emit the summary as JSON to stdout\n' +
444
- ' --repo-root override the project root (default: resolved SO_PROJECT_DIR)\n' +
490
+ ' --repo-root override the project root (default: resolved project dir)\n' +
445
491
  ' --assume-dead-before ISO-8601 cutoff; a candidate whose last known event\n' +
446
492
  ' predates it bypasses a live foreign session.lock\n' +
447
493
  'Exit codes: 0 ok, 1 arg error, 2 system error\n'
@@ -451,7 +497,7 @@ async function main() {
451
497
 
452
498
  // --apply is an explicit opt-in; absent it (or with --dry-run) we never write.
453
499
  const apply = values.apply === true && values['dry-run'] !== true;
454
- const repoRoot = values['repo-root'] || SO_PROJECT_DIR;
500
+ const repoRoot = values['repo-root'] || getProjectDir();
455
501
 
456
502
  let assumeDeadBeforeMs = null;
457
503
  if (typeof values['assume-dead-before'] === 'string' && values['assume-dead-before'].length > 0) {
@@ -71,7 +71,7 @@ import { parseSessionConfig } from './lib/config.mjs';
71
71
  import { subjectToSlug, parseFrontmatter } from './lib/vault-mirror/utils.mjs';
72
72
  import { kebab } from './lib/learnings/kebab.mjs';
73
73
  import { validateLearning } from './lib/learnings/schema.mjs';
74
- import { appendLearning } from './lib/learnings/io.mjs';
74
+ import { appendLearning, isBackupOf } from './lib/learnings/io.mjs';
75
75
 
76
76
  const DEFAULT_RULES_DIR = '.claude/rules';
77
77
  const DEFAULT_STORE = '.orchestrator/metrics/learnings.jsonl';
@@ -620,7 +620,13 @@ function idsInStore(filePath) {
620
620
  return ids;
621
621
  }
622
622
 
623
- /** List `<store>.bak-*` siblings (a verbatim original beats any reconstruction). */
623
+ /**
624
+ * List `<store>.bak[-.]*` siblings (a verbatim original beats any
625
+ * reconstruction). Both delimiter forms count — the legacy dot form
626
+ * (`learnings.jsonl.bak.evolve-<ts>`) is still on disk in consumer repos and
627
+ * was silently skipped here, i.e. a backup that exists and is not found in the
628
+ * one case it is for (#1173).
629
+ */
624
630
  function backupPaths(storePath) {
625
631
  const dir = dirname(storePath);
626
632
  const base = basename(storePath);
@@ -633,7 +639,7 @@ function backupPaths(storePath) {
633
639
  return [];
634
640
  }
635
641
  return names
636
- .filter((n) => n.startsWith(`${base}.bak-`))
642
+ .filter((n) => isBackupOf(base, n))
637
643
  .sort()
638
644
  .map((n) => join(dir, n));
639
645
  }