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
@@ -7,12 +7,52 @@
7
7
  * Part of v3.0.0 migration (Epic #124, issue #133).
8
8
  * Issue #228: removed hardcoded personal-domain default URL. Clank Event Bus URL
9
9
  * must now be supplied explicitly via CLANK_EVENT_URL when CLANK_EVENT_SECRET is set.
10
+ *
11
+ * ## Correlation envelope (#1177 FA3)
12
+ *
13
+ * Measured 2026-09-02 @ c3ab480 over 33,608 ledger records: only 22.1% carry
14
+ * `session_id` and 4.8% carry `wave`, because filling them was every call
15
+ * site's own job and 32 of 34 call sites pass no options at all. `emitEvent()`
16
+ * now fills those keys itself — under three hard rules:
17
+ *
18
+ * 1. **Additive, never overriding.** The correlation keys are spread BEFORE
19
+ * `payload`, so any caller-supplied `session_id` / `semantic_session_id` /
20
+ * `wave` wins byte-for-byte. A payload that supplies EITHER session key
21
+ * suppresses the session fill entirely (both keys), so a caller that
22
+ * deliberately pins attribution elsewhere — `vault-mirror/telemetry.mjs`
23
+ * pins to `SO_PROJECT_DIR` and passes both keys — is untouched.
24
+ * 2. **Omit, never fabricate.** When attribution cannot be PROVEN, both keys
25
+ * are left ABSENT — never `null`, never `''`. An absent key is the only
26
+ * honest encoding of "not attributable" (see `sessionAttribution()`).
27
+ * 3. **Never a peer's id (#1123).** A shared working copy means
28
+ * `session.lock` can name a PEER session that won the acquire race. The
29
+ * lock alone therefore does not prove ownership; the fill happens only
30
+ * when a PROCESS-LOCAL id (`CLAUDE_CODE_SESSION_ID`, or a hook payload's
31
+ * `session_id`) equals the lock's raw `session_id`. STATE.md is NOT a
32
+ * witness here (#1177 FX1): it is a shared working-copy file written by
33
+ * the lock holder, so under a peer-owned lock both agreed about the peer
34
+ * and the union stamped the peer's ids. See {@link attributionForRecord}.
35
+ *
36
+ * The attribution root is the SAME root the ledger line is pinned to
37
+ * (`opts.repoRoot ?? SO_PROJECT_DIR`), never `process.cwd()`. Measured cost of
38
+ * the whole envelope (lock + wave manifest, 100 calls, this repo):
39
+ * 0.0961 ms/call.
10
40
  */
11
41
 
12
- import { promises as fs } from 'node:fs';
42
+ import { promises as fs, existsSync, readFileSync } from 'node:fs';
13
43
  import path from 'node:path';
14
- import { SO_PROJECT_DIR, SO_SHARED_DIR } from './platform.mjs';
44
+ import { getProjectDir, SO_SHARED_DIR } from './platform.mjs';
15
45
  import { readLock } from './session-lock.mjs';
46
+ import { resolveStateMdPath } from './state-md/frontmatter-mutators.mjs';
47
+ import {
48
+ classifyManifestSession,
49
+ readProcessLocalSessionIds,
50
+ } from './session-identity/own-session.mjs';
51
+ import {
52
+ EventValidationError,
53
+ stampEventSchemaVersion,
54
+ validateEventRecord,
55
+ } from './events-schema.mjs';
16
56
 
17
57
  // ---------------------------------------------------------------------------
18
58
  // Public API
@@ -30,7 +70,7 @@ import { readLock } from './session-lock.mjs';
30
70
  * @param {string} [repoRoot=SO_PROJECT_DIR] — project root the events log lives under.
31
71
  * @returns {string}
32
72
  */
33
- export function eventsFilePath(repoRoot = SO_PROJECT_DIR) {
73
+ export function eventsFilePath(repoRoot = getProjectDir()) {
34
74
  return path.join(repoRoot, SO_SHARED_DIR, 'metrics', 'events.jsonl');
35
75
  }
36
76
 
@@ -69,16 +109,189 @@ export function sessionAttribution(repoRoot) {
69
109
  }
70
110
  }
71
111
 
112
+ // ---------------------------------------------------------------------------
113
+ // Correlation envelope (#1177 FA3)
114
+ // ---------------------------------------------------------------------------
115
+
116
+ /** State-dir candidates, in the same order `state-md` resolves them. */
117
+ const STATE_DIR_CANDIDATES = ['.claude', '.codex', '.cursor', '.pi'];
118
+
119
+ /**
120
+ * Session-correlation keys for a record pinned to `root` — `{}` when ownership
121
+ * is not provable.
122
+ *
123
+ * Decision, in one line: **only a PROCESS-LOCAL id may confirm the lock, and
124
+ * when one exists it decides alone.**
125
+ *
126
+ * - No lock (CI, a bare script) → `{}`. Nothing to attribute to.
127
+ * - No process-local id (`CLAUDE_CODE_SESSION_ID` absent) → `{}`. Ownership is
128
+ * UNPROVEN, and an unproven attribution is exactly the peer-id write #1123
129
+ * forbids; an absent key costs a correlation, a wrong key costs a false one.
130
+ * - A process-local id that equals the lock's raw `session_id` → fill BOTH
131
+ * keys, verbatim from the lock.
132
+ * - A process-local id that DISAGREES → `{}` (the lock names a peer that won
133
+ * the acquire race).
134
+ *
135
+ * **Why STATE.md is not a witness (#1177 FX1).** It used to be one, unioned
136
+ * with the env id — and the union was the bug: `.claude/STATE.md` is a SHARED
137
+ * working-copy artefact written by the session that OWNS the working copy,
138
+ * i.e. normally the lock holder. When a peer holds the lock, the peer also
139
+ * wrote STATE.md, so both "independent" witnesses name the PEER and a
140
+ * disagreeing process-local id could not veto them. Measured: lock=peer,
141
+ * STATE.md=peer, `CLAUDE_CODE_SESSION_ID`=me → the peer's ids were stamped on
142
+ * this session's records. A shared file cannot prove which PROCESS is emitting;
143
+ * see `readProcessLocalSessionIds()` for the tiering rationale (HR-102: a
144
+ * better signal replaces a worse one, it does not merely get outvoted by it).
145
+ *
146
+ * CEILING (BV-004): the comparison is against the lock's RAW `session_id`, so a
147
+ * harness that ROTATES its session id mid-session (see
148
+ * `tests/hooks/on-session-end.test.mjs` `new-rotated-uuid`) has an env id that
149
+ * no longer equals the lock's raw id, and BOTH keys are then omitted — honest
150
+ * absence, never misattribution. REVISIT when the rotation rate is measured in
151
+ * `events.jsonl` (count `orchestrator.session.started` against lock rewrites):
152
+ * if rotation is common, the lock must be refreshed on rotation rather than
153
+ * this comparison widened.
154
+ *
155
+ * **THE manifest-binding writer contract (#1207).** This function is not only
156
+ * `emitEvent()`'s correlation fill — it is the canonical primitive for every
157
+ * caller that needs to name a `.orchestrator/`-adjacent artefact as "mine"
158
+ * without risking a peer's id. `skills/wave-executor/wave-loop.md` § Scope
159
+ * Manifest step 1 calls it directly to derive `wave-scope.json`'s `session_id` /
160
+ * `semantic_session_id` binding — a hand-written prose comparison against
161
+ * STATE.md previously stood in for exactly this check, and (per the STATE.md
162
+ * caveat above) that comparison could not veto a peer-owned lock. Any new
163
+ * writer facing the same "is this working-copy-shared artefact mine to
164
+ * stamp?" question should call this function rather than re-deriving the
165
+ * raw-id-vs-process-local comparison inline (see `scripts/memory-propose.mjs`
166
+ * `resolveRunningWaveId()` for a case that reads the SAME lock but needs the
167
+ * semantic id plus diagnostic detail this function's `{}`-on-any-mismatch
168
+ * contract intentionally does not expose, and keeps its own comparison for
169
+ * that reason).
170
+ *
171
+ * @param {string} [root=SO_PROJECT_DIR] — the repo the record is pinned to.
172
+ * @returns {{session_id?: string, semantic_session_id?: string}}
173
+ */
174
+ export function attributionForRecord(root = getProjectDir()) {
175
+ const attribution = sessionAttribution(root);
176
+ const lockRawId =
177
+ typeof attribution.session_id === 'string' ? attribution.session_id.trim() : '';
178
+ if (!lockRawId) return {};
179
+ const processLocal = readProcessLocalSessionIds();
180
+ if (processLocal.length === 0) return {};
181
+ return processLocal.includes(lockRawId) ? { ...attribution } : {};
182
+ }
183
+
184
+ /**
185
+ * Absolute path of the active `wave-scope.json`, or `null` when none exists.
186
+ *
187
+ * The active platform's state dir is tried first (via `resolveStateMdPath()`,
188
+ * the repo's existing resolver), then the remaining candidates — so a Codex or
189
+ * Cursor run finds its own manifest rather than a stale `.claude/` one.
190
+ *
191
+ * @param {string} root
192
+ * @returns {string|null}
193
+ */
194
+ function waveScopePath(root) {
195
+ const dirs = [];
196
+ try {
197
+ dirs.push(path.dirname(resolveStateMdPath(root)));
198
+ } catch {
199
+ /* fall through to the fixed candidate list */
200
+ }
201
+ for (const dir of STATE_DIR_CANDIDATES) {
202
+ const abs = path.join(root, dir);
203
+ if (!dirs.includes(abs)) dirs.push(abs);
204
+ }
205
+ for (const dir of dirs) {
206
+ const candidate = path.join(dir, 'wave-scope.json');
207
+ try {
208
+ if (existsSync(candidate)) return candidate;
209
+ } catch {
210
+ /* unreadable candidate — try the next one */
211
+ }
212
+ }
213
+ return null;
214
+ }
215
+
216
+ /**
217
+ * `{ wave }` from the live wave-scope manifest — `{}` when the manifest is
218
+ * missing, waveless, or belongs to another session.
219
+ *
220
+ * The manifest is a SHARED working-copy artefact (`.claude/wave-scope.json`),
221
+ * so a peer session's manifest is readable here and would stamp this session's
222
+ * events with a foreign wave number. Ownership is classified with
223
+ * `classifyManifestSession()` against the PROCESS-LOCAL id set plus whatever
224
+ * `attributionForRecord()` actually filled — the same tiering as the session
225
+ * keys, for the same reason (a shared file cannot prove which process emits):
226
+ *
227
+ * - manifest classified `own` → fill (as a NUMBER, see below).
228
+ * - anything else → omit. That includes an UNBOUND manifest (no `session_id` /
229
+ * `semantic_session_id`): since #1123 BOTH writers stamp the binding, so a
230
+ * manifest without one is a peer's or a stale artefact, never a legacy own
231
+ * one. It also includes `unknown` because we cannot resolve our own
232
+ * identity — stricter than `classifyManifestSession()`'s own `unknown`
233
+ * doctrine on purpose: a wave number is data on the record, not a feature
234
+ * gate, so "cannot tell" must not become "stamp it anyway".
235
+ *
236
+ * @param {string} root
237
+ * @param {{session_id?: string, semantic_session_id?: string}} attribution
238
+ * @returns {{wave?: number}}
239
+ */
240
+ function waveForRecord(root, attribution) {
241
+ let scope;
242
+ try {
243
+ const file = waveScopePath(root);
244
+ if (!file) return {};
245
+ scope = JSON.parse(readFileSync(file, 'utf8'));
246
+ } catch {
247
+ return {};
248
+ }
249
+ // The ledger's `wave` is numeric in 1876 of 1876 live records; a manifest
250
+ // carrying `"wave": "3"` used to write the STRING through verbatim and split
251
+ // every downstream group-by. Coerce, and omit anything that is not an integer
252
+ // (`"abc"`, `2.5`) rather than writing a NaN or a fraction.
253
+ const waveNum = Number(scope?.wave);
254
+ if (scope?.wave === null || scope?.wave === '' || !Number.isInteger(waveNum)) return {};
255
+
256
+ const ownIds = new Set([
257
+ ...readProcessLocalSessionIds(),
258
+ ...[attribution.session_id, attribution.semantic_session_id].filter(Boolean),
259
+ ]);
260
+ const { verdict } = classifyManifestSession(scope, ownIds);
261
+ return verdict === 'own' ? { wave: waveNum } : {};
262
+ }
263
+
72
264
  /**
73
265
  * Append a JSONL event record and optionally POST to the Clank Event Bus webhook.
74
266
  *
75
- * Writes `{ts, event, ...payload}` as a single JSON line to
76
- * `.orchestrator/metrics/events.jsonl` (creates parent directory if needed).
267
+ * Writes `{timestamp, event, schema_version, ...payload}` as a single JSON line
268
+ * to `.orchestrator/metrics/events.jsonl` (creates parent directory if needed).
77
269
  * If both `CLANK_EVENT_SECRET` and `CLANK_EVENT_URL` are set, fires an async
78
270
  * fire-and-forget POST to `CLANK_EVENT_URL` with a 3-second timeout. Network
79
271
  * errors are swallowed. Write errors propagate to the caller. No personal-domain
80
272
  * default URL exists — both vars must be set explicitly (#228).
81
273
  *
274
+ * Validation + versioning (#1177). Every record is stamped via
275
+ * `stampEventSchemaVersion()` (the schema module's own stamper — it fills the
276
+ * field only when absent, so a caller keeps authority over it) and run through
277
+ * `validateEventRecord()` BEFORE any side effect. An invalid record throws
278
+ * `EventValidationError` and produces NO ledger line and NO webhook POST —
279
+ * a malformed event is dropped at the producer rather than written and
280
+ * discovered by a downstream reader. The stamp is applied AFTER the payload
281
+ * spread, and still yields to it: the helper fills the field only when it is
282
+ * absent or null, so a caller supplying its own `schema_version` wins.
283
+ *
284
+ * Correlation envelope (#1177 FA3). When the payload carries neither session
285
+ * key, `session_id`/`semantic_session_id` are filled from
286
+ * {@link attributionForRecord}; when it carries no `wave`, `wave` is filled
287
+ * from the OWN wave-scope manifest. Both are additive and omitted whenever
288
+ * ownership is unproven — see the module header for the three rules.
289
+ *
290
+ * The webhook body deliberately stays `{ event_type, source, payload }` with the
291
+ * RAW payload — the wire format is a published contract with an external
292
+ * consumer; `schema_version` describes the JSONL record, not the webhook
293
+ * envelope, and is not added to it.
294
+ *
82
295
  * @param {string} type — event type (e.g. "orchestrator.session.started")
83
296
  * @param {object} [payload={}] — additional fields shallow-merged into the record
84
297
  * @param {object} [opts={}] — emission options.
@@ -95,8 +308,44 @@ export function sessionAttribution(repoRoot) {
95
308
  * @returns {Promise<void>}
96
309
  */
97
310
  export async function emitEvent(type, payload = {}, opts = {}) {
98
- // Build the JSONL record: ts + event come first, payload spreads last.
99
- const record = { timestamp: new Date().toISOString(), event: type, ...payload };
311
+ // Correlation envelope (#1177 FA3) computed against the SAME root the line
312
+ // is pinned to. Both fills are gated on the payload NOT already carrying the
313
+ // key, and both spread BEFORE `payload`, so a caller always wins twice over.
314
+ // A payload that supplies EITHER session key suppresses BOTH: mixing a
315
+ // caller's `session_id` with a lock-derived `semantic_session_id` would
316
+ // silently produce a record whose two id fields name different sessions.
317
+ const attributionRoot = opts.repoRoot ?? getProjectDir();
318
+ const correlation = {};
319
+ if (payload.session_id === undefined && payload.semantic_session_id === undefined) {
320
+ Object.assign(correlation, attributionForRecord(attributionRoot));
321
+ }
322
+ if (payload.wave === undefined) {
323
+ Object.assign(correlation, waveForRecord(attributionRoot, correlation));
324
+ }
325
+
326
+ // Build the JSONL record: timestamp + event first, payload spreads last, and
327
+ // `stampEventSchemaVersion()` — the schema module's own stamper, which only
328
+ // fills an ABSENT/null field — adds the version. Routing through the helper
329
+ // instead of inlining `schema_version: CURRENT_SCHEMA_VERSION` keeps the
330
+ // stamp rule in ONE place: a caller-supplied version still wins, because the
331
+ // helper never overwrites a value the spread already put there.
332
+ const record = stampEventSchemaVersion({
333
+ timestamp: new Date().toISOString(),
334
+ event: type,
335
+ ...correlation,
336
+ ...payload,
337
+ });
338
+
339
+ // Validate BEFORE any side effect — no line, no directory, no webhook (#1177).
340
+ const verdict = validateEventRecord(record);
341
+ if (!verdict.valid) {
342
+ throw new EventValidationError(
343
+ `invalid event record for "${String(type)}": ${verdict.errors.join('; ')}`,
344
+ verdict.errors,
345
+ typeof type === 'string' ? type : undefined,
346
+ );
347
+ }
348
+
100
349
  const line = JSON.stringify(record) + '\n';
101
350
 
102
351
  // Ensure the destination directory exists before appending. Resolution order:
@@ -223,10 +223,15 @@ function readinessConfidence(autopilotSummary, judgmentSummary, score) {
223
223
  /**
224
224
  * Summarize autopilot run history plus type-8 mode effectiveness rollups.
225
225
  *
226
- * Abandoned-session filtering (#834): `sessions` is passed straight through
227
- * to `groupByMode()`, which filters phantom `status: 'abandoned'` stubs
228
- * before bucketing this function inherits that guarantee transitively and
229
- * does not duplicate the filter. See `autopilot-effectiveness.mjs` `groupByMode()`.
226
+ * Abandoned-session filtering (#834) AND duplicate-identity collapse (#1167):
227
+ * `sessions` is passed straight through to `groupByMode()`, which canonicalizes
228
+ * the array (one record per physical session) and then filters phantom
229
+ * `status: 'abandoned'` stubs before bucketing this function inherits BOTH
230
+ * guarantees transitively and duplicates neither. The abandoned filter alone
231
+ * was not enough: a `supersedes` pair and an exact same-id duplicate are two
232
+ * records of one session that both survive it. See
233
+ * `autopilot-effectiveness.mjs` `groupByMode()` (which canonicalizes via
234
+ * `canonicalizeSessions(sessions, { keepUnidentified: true })` before filtering).
230
235
  *
231
236
  * @param {Array} autopilotRuns
232
237
  * @param {Array} sessions
@@ -27,6 +27,7 @@
27
27
  import { randomUUID } from 'node:crypto';
28
28
 
29
29
  import { filterRealSessions } from '../session-schema.mjs';
30
+ import { canonicalizeSessions } from '../sessions-canonical.mjs';
30
31
 
31
32
  // ---------------------------------------------------------------------------
32
33
  // Constants
@@ -181,7 +182,23 @@ export function groupByMode(autopilotRuns, sessions) {
181
182
  const out = new Map();
182
183
  if (!Array.isArray(sessions) || sessions.length === 0) return out;
183
184
 
184
- const realSessions = filterRealSessions(sessions);
185
+ // Canonicalize BEFORE filtering (#1167). `filterRealSessions` drops phantom
186
+ // `abandoned` stubs, but two records of ONE physical session that are both
187
+ // real survive it — a `supersedes` pair (the backfilled stub plus the
188
+ // authoritative `completed` record that refutes it) and an exact same-id
189
+ // duplicate line both do. Each of those inflates `n_manual` / `n_autopilot`
190
+ // and skews every mean computed from the bucket, so the identity collapse
191
+ // has to happen first; the two compose in exactly this order (see
192
+ // `sessions-canonical.mjs` § "What this module does not do").
193
+ //
194
+ // `keepUnidentified: true` because effectiveness analysis must not LOSE the
195
+ // id-less rows `canonicalizeSessions` drops by default: legacy ledger rows
196
+ // and every in-repo fixture of the pre-id era carry `session_type` and
197
+ // metrics but no id, and dropping them would silently shrink `n_manual`
198
+ // instead of de-duplicating it.
199
+ const realSessions = filterRealSessions(
200
+ canonicalizeSessions(sessions, { keepUnidentified: true }),
201
+ );
185
202
  if (realSessions.length === 0) return out;
186
203
 
187
204
  // Optional: known autopilot_run_id set for stricter pairing. Empty set means
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Frontmatter-Guard library (issue #328).
3
3
  *
4
- * Reads the canonical vault-frontmatter Zod schema source and exposes helpers
4
+ * Reads the canonical vault-frontmatter Zod schema source when a
5
+ * projects-baseline checkout is reachable on this host — and exposes helpers
5
6
  * for generating a contextual schema snippet that can be injected into agent
6
7
  * prompts before vault-write tasks.
7
8
  *
@@ -10,15 +11,103 @@
10
11
  */
11
12
 
12
13
  import { digestSha256Short } from './crypto-digest-utils.mjs';
14
+ import { resolveHostPath } from './config/host-paths.mjs';
13
15
  import { readFileSync, statSync } from 'node:fs';
14
16
  import { homedir } from 'node:os';
15
- import { join } from 'node:path';
17
+ import { dirname, join, resolve } from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
16
19
 
17
- /** Absolute path to the canonical vault-frontmatter schema source. */
18
- const SCHEMA_SOURCE_PATH = join(
19
- homedir(),
20
- 'Projects/projects-baseline/packages/zod-schemas/src/vault-frontmatter.ts',
21
- );
20
+ /** Path of the schema source RELATIVE to a projects-baseline checkout root. */
21
+ const SCHEMA_REL_PATH = 'packages/zod-schemas/src/vault-frontmatter.ts';
22
+
23
+ /** This file lives at `<repoRoot>/scripts/lib/` — two levels up is the repo root. */
24
+ const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
25
+
26
+ /**
27
+ * Candidate projects-baseline checkout roots.
28
+ *
29
+ * The baseline is OPTIONAL and PRIVATE (see `docs/baseline.md`), so this list
30
+ * must never assume a particular operator layout — it carries no host-specific
31
+ * directory names.
32
+ *
33
+ * When the host-local override (`SO_BASELINE_PATH` / `owner.yaml`
34
+ * `paths.baseline-path`) is set it is used ALONE: probing past a wrong explicit
35
+ * value would silently read a DIFFERENT baseline than the one named. Degrading
36
+ * to `null` (and the documented fallback enum set) is the honest outcome there.
37
+ * Only when nothing is configured do the two CONVENTIONS apply — the sibling
38
+ * checkout `scripts/sync-vault-schema.mjs` already uses, then the legacy
39
+ * `~/Projects` default this module shipped with.
40
+ *
41
+ * @returns {string[]}
42
+ */
43
+ function baselineCandidates() {
44
+ const configured = resolveHostPath('baseline-path', null);
45
+ if (typeof configured === 'string' && configured.trim() !== '') return [configured.trim()];
46
+ return [
47
+ resolve(REPO_ROOT, '..', 'projects-baseline'),
48
+ join(homedir(), 'Projects', 'projects-baseline'),
49
+ ];
50
+ }
51
+
52
+ /** @type {{ resolved: boolean, value: string|null }} */
53
+ const _pathCache = { resolved: false, value: null };
54
+
55
+ /**
56
+ * Resolve the canonical vault-frontmatter schema source, or `null` when no
57
+ * baseline checkout is reachable on this host.
58
+ *
59
+ * Ceiling: at most two `statSync` calls per invocation, and the result is
60
+ * memoised for the process lifetime. Revisit if the candidate list ever grows
61
+ * past a handful of entries — then it needs a real search, not a probe loop.
62
+ *
63
+ * @param {{ refresh?: boolean }} [opts] — `refresh: true` re-probes (tests only)
64
+ * @returns {string|null}
65
+ */
66
+ export function resolveSchemaSourcePath({ refresh = false } = {}) {
67
+ if (!refresh && _pathCache.resolved) return _pathCache.value;
68
+ let found = null;
69
+ for (const base of baselineCandidates()) {
70
+ const candidate = join(base, SCHEMA_REL_PATH);
71
+ try {
72
+ if (statSync(candidate).isFile()) {
73
+ found = candidate;
74
+ break;
75
+ }
76
+ } catch {
77
+ /* candidate absent — try the next one */
78
+ }
79
+ }
80
+ _pathCache.resolved = true;
81
+ _pathCache.value = found;
82
+ return found;
83
+ }
84
+
85
+ /**
86
+ * Fallback enum/field set used when no baseline checkout is reachable.
87
+ *
88
+ * Values mirror `skills/vault-sync/validator.mjs` (`vaultNoteTypeSchema` /
89
+ * `vaultNoteStatusSchema`), which is this repo's own in-tree copy of the
90
+ * canonical schema and is what `vault-sync` actually validates against. Using
91
+ * it means a baseline-less host injects a snippet the local validator accepts,
92
+ * rather than throwing.
93
+ */
94
+ const FALLBACK_SCHEMA = Object.freeze({
95
+ typeEnum: Object.freeze([
96
+ 'note', 'daily', 'project', 'person', 'reference',
97
+ 'idea', 'learning', 'session', 'peer-card', 'board',
98
+ ]),
99
+ statusEnum: Object.freeze([
100
+ 'draft', 'active', 'verified', 'archived', 'production',
101
+ 'mvp', 'idea', 'maintenance', 'planned', 'paused', 'dead',
102
+ ]),
103
+ requiredFields: Object.freeze(['id', 'type', 'created', 'updated']),
104
+ idRegex: '^[a-z0-9]+(?:-[a-z0-9]+)*$',
105
+ tagsRegex: '^[a-z0-9]+(?:-[a-z0-9]+)*(?:/[a-z0-9]+(?:-[a-z0-9]+)*)*$',
106
+ schemaText: null,
107
+ });
108
+
109
+ /** One WARN per process, not one per call — the condition is constant. */
110
+ let _warnedFallback = false;
22
111
 
23
112
  /**
24
113
  * In-memory mtime cache so repeated calls within a single process invocation
@@ -93,9 +182,12 @@ function _parseSchema(text) {
93
182
  * @returns {{ typeEnum: string[], statusEnum: string[], requiredFields: string[], idRegex: string, tagsRegex: string, schemaText: string } | null}
94
183
  */
95
184
  export function readVaultSchema() {
185
+ const sourcePath = resolveSchemaSourcePath();
186
+ if (sourcePath === null) return null;
187
+
96
188
  let mtime;
97
189
  try {
98
- mtime = statSync(SCHEMA_SOURCE_PATH).mtimeMs;
190
+ mtime = statSync(sourcePath).mtimeMs;
99
191
  } catch {
100
192
  // File missing or inaccessible
101
193
  return null;
@@ -107,7 +199,7 @@ export function readVaultSchema() {
107
199
 
108
200
  let text;
109
201
  try {
110
- text = readFileSync(SCHEMA_SOURCE_PATH, 'utf8');
202
+ text = readFileSync(sourcePath, 'utf8');
111
203
  } catch {
112
204
  return null;
113
205
  }
@@ -122,10 +214,17 @@ export function readVaultSchema() {
122
214
  * Compute an 8-character SHA-256 hex prefix of the given schema source text.
123
215
  * Stable across calls for the same input — useful as a cache-busting token.
124
216
  *
125
- * @param {string} schemaText
126
- * @returns {string}
217
+ * Returns `null` when there is NO schema text (absent baseline checkout,
218
+ * `readVaultSchema()` → `null`). Hashing "nothing" previously produced
219
+ * `e3b0c442` — the SHA-256 of the empty string — which is a real-looking token
220
+ * that compares equal across every baseline-less host, so a cache keyed on it
221
+ * would report "schema unchanged" while having measured nothing at all.
222
+ *
223
+ * @param {string|null|undefined} schemaText
224
+ * @returns {string|null} 8-char hex prefix, or `null` when there is no schema
127
225
  */
128
226
  export function computeSchemaHash(schemaText) {
227
+ if (typeof schemaText !== 'string' || schemaText.length === 0) return null;
129
228
  return digestSha256Short(schemaText);
130
229
  }
131
230
 
@@ -133,11 +232,30 @@ export function computeSchemaHash(schemaText) {
133
232
  * Generate a deterministic Markdown snippet documenting the vault frontmatter
134
233
  * schema, suitable for injection into agent prompts before vault-write tasks.
135
234
  *
136
- * @param {{ typeEnum: string[], statusEnum: string[], requiredFields: string[], idRegex: string, tagsRegex: string }} schema
235
+ * Degrades instead of throwing when no schema is available: a host without a
236
+ * projects-baseline checkout gets `readVaultSchema() === null`, and destructuring
237
+ * that killed the caller with `Cannot destructure property 'typeEnum' of
238
+ * 'schema' as it is undefined`. The guard below falls back to FALLBACK_SCHEMA
239
+ * and warns ONCE on stderr — an injected snippet that is one schema-version
240
+ * behind is worth incomparably more than a crashed pre-dispatch hook.
241
+ *
242
+ * @param {{ typeEnum: string[], statusEnum: string[], requiredFields: string[], idRegex: string, tagsRegex: string }|null} [schema]
137
243
  * @returns {string}
138
244
  */
139
245
  export function generateFrontmatterSnippet(schema) {
140
- const { typeEnum, statusEnum, requiredFields, idRegex, tagsRegex: _tagsRegex } = schema;
246
+ let source = schema;
247
+ if (source === null || typeof source !== 'object') {
248
+ if (!_warnedFallback) {
249
+ _warnedFallback = true;
250
+ process.stderr.write(
251
+ 'frontmatter-guard: no projects-baseline schema reachable — using the in-module ' +
252
+ 'fallback enum set (see docs/baseline.md). Set SO_BASELINE_PATH or ' +
253
+ 'owner.yaml paths.baseline-path to read the canonical schema.\n',
254
+ );
255
+ }
256
+ source = FALLBACK_SCHEMA;
257
+ }
258
+ const { typeEnum, statusEnum, requiredFields, idRegex, tagsRegex: _tagsRegex } = source;
141
259
 
142
260
  const typeList = typeEnum.map((v) => `\`${v}\``).join(' | ');
143
261
  const statusList = statusEnum.map((v) => `\`${v}\``).join(' | ');
@@ -9,6 +9,7 @@ import {
9
9
  runCheck,
10
10
  extractCount,
11
11
  extractTestCounts,
12
+ extractFailedTestFiles,
12
13
  collectDebugArtifacts,
13
14
  } from './gate-helpers.mjs';
14
15
 
@@ -64,11 +65,26 @@ const testCounts =
64
65
  // from a measured one — with `suite_died: false` derived from it, stating a
65
66
  // verdict nobody had checked. Absent, not zero: the same contract this
66
67
  // envelope's `counts` field already keeps (`admitSuiteCounts`).
68
+ //
69
+ // `failed_files` (this change) is the fifth member of that same set and joins
70
+ // it for the same reason: it is DERIVED from the runner's file-level report, so
71
+ // it is published exactly when the file-level measurement exists. A count with
72
+ // no name is what made the 2026-09-06 pre-push block unusable —
73
+ // `files_failed: 1` out of 662, and reconstructing WHICH file cost a manual
74
+ // re-materialisation of the tracked tree. An EMPTY array here is meaningful and
75
+ // is NOT the absent case: it says the file-level summary was parsed and no path
76
+ // could be read out of it (a non-vitest reporter, a truncated capture), which
77
+ // is a parser gap worth seeing — the unmeasured case is the absent key.
78
+ const failedTestFiles = testResult.status === 'fail'
79
+ ? extractFailedTestFiles(testResult.fullOutput ?? testResult.output ?? '')
80
+ : [];
81
+
67
82
  const fileFields = testCounts.files
68
83
  ? {
69
84
  files_total: testCounts.files.total,
70
85
  files_passed: testCounts.files.passed,
71
86
  files_failed: testCounts.files.failed,
87
+ failed_files: failedTestFiles,
72
88
  // Self-diagnosing: true exactly when `status: 'fail'` sits beside a
73
89
  // test-case `failed: 0` that a file-level failure explains. Greppable —
74
90
  // a consumer no longer has to recompute the contradiction by hand.
@@ -154,6 +170,16 @@ const failed = [tcResult, testResult, lintResult].some(
154
170
  // of silence" the hook promises still holds; and the operator gets the failure
155
171
  // at the moment of the block instead of a second run to find it.
156
172
  if (failed) {
173
+ // Names FIRST, raw output after. Under the pre-push hook the raw block is
174
+ // hundreds of lines; the operator reads the top of it, and the one fact he
175
+ // needs to act (`npx vitest run <file>`) must not sit at the bottom.
176
+ if (failedTestFiles.length > 0) {
177
+ process.stderr.write(
178
+ `\n──── failing test files (${failedTestFiles.length}) ────\n` +
179
+ failedTestFiles.map((f) => ` ${f}\n`).join('') +
180
+ ` reproduce: npx vitest run ${failedTestFiles.join(' ')}\n`,
181
+ );
182
+ }
157
183
  for (const [name, result] of [
158
184
  ['typecheck', tcResult],
159
185
  ['test', testResult],