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
@@ -67,7 +67,7 @@ import { fileURLToPath } from 'node:url';
67
67
  import { emitEvent, sessionAttribution } from './events.mjs';
68
68
  import { admitSuiteCounts, extractTestCounts } from './gates/gate-helpers.mjs';
69
69
  import { redactDiagnosticsBundle } from './quality-gate/diagnostics.mjs';
70
- import { readLock } from './session-lock.mjs';
70
+ import { readProcessLocalSessionIds } from './session-identity/own-session.mjs';
71
71
 
72
72
  export { redactDiagnosticsBundle } from './quality-gate/diagnostics.mjs';
73
73
 
@@ -114,30 +114,99 @@ function resolveRepoRoot(explicit) {
114
114
  }
115
115
 
116
116
  /**
117
- * Load default commands from Session Config via `scripts/parse-config.mjs`.
118
- * Returns a partial object — keys that fail to resolve are simply absent
119
- * (the caller falls through to DEFAULT_COMMANDS for those).
117
+ * Closed set of `degraded` reasons for {@link loadCommandsFromSessionConfigDetailed}.
118
+ *
119
+ * Its own enum, deliberately not shared with `ci-status-banner.mjs`'s
120
+ * `DEGRADED_REASONS`: the members below are exactly the three ways THIS
121
+ * config-read can fail, and an enum whose members are not exhaustively
122
+ * reachable cannot be switched on exhaustively.
123
+ *
124
+ * - `script-missing` — `scripts/parse-config.mjs` is not on disk.
125
+ * - `spawn-failed` — the subprocess exited non-zero, timed out, or wrote
126
+ * nothing to stdout.
127
+ * - `parse-error` — stdout was not parseable JSON (or the read threw).
128
+ */
129
+ export const CONFIG_READ_DEGRADED_REASONS = Object.freeze([
130
+ 'script-missing',
131
+ 'spawn-failed',
132
+ 'parse-error',
133
+ ]);
134
+
135
+ /**
136
+ * Does `repoRoot` carry a Session Config file at all?
137
+ *
138
+ * Mirrors the file-name precedence `scripts/parse-config.mjs::resolveConfigFile`
139
+ * uses (`SO_CONFIG_FILE` → `CLAUDE.md` → `AGENTS.md`) but NOT its upward
140
+ * project-root walk — see the ceiling note at the call site.
141
+ *
142
+ * @param {string} repoRoot
143
+ * @returns {boolean}
144
+ */
145
+ function sessionConfigFileExists(repoRoot) {
146
+ try {
147
+ const soConfigFile = process.env.SO_CONFIG_FILE;
148
+ if (soConfigFile && existsSync(join(repoRoot, soConfigFile))) return true;
149
+ return existsSync(join(repoRoot, 'CLAUDE.md')) || existsSync(join(repoRoot, 'AGENTS.md'));
150
+ } catch {
151
+ return false;
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Load default commands from Session Config via `scripts/parse-config.mjs`,
157
+ * distinguishing "the config declares no `*-command` keys" from "the config
158
+ * could not be read at all".
159
+ *
160
+ * Both cases yield `commands: {}` — that half is unchanged, and every command
161
+ * resolution keeps falling through to DEFAULT_COMMANDS exactly as before. What
162
+ * is new is the second channel: on failure the result also carries a
163
+ * `degraded` reason from {@link CONFIG_READ_DEGRADED_REASONS}. Without it,
164
+ * `checkQgCommandDrift` reported "no drift" for a config it never managed to
165
+ * read — a silent all-clear derived from an absent measurement (#1031's
166
+ * failure class, one consumer over).
167
+ *
168
+ * `degraded` is OMITTED, not set to null, on the success path, so a strict
169
+ * `toEqual({commands: {…}})` pin holds for every readable config.
120
170
  *
121
171
  * Never throws.
122
172
  *
123
173
  * @param {string} repoRoot
124
- * @returns {{lint?: string, typecheck?: string, test?: string}}
174
+ * @returns {{commands: {lint?: string, typecheck?: string, test?: string},
175
+ * degraded?: 'script-missing'|'spawn-failed'|'parse-error'}}
125
176
  */
126
- export function loadCommandsFromSessionConfig(repoRoot) {
177
+ export function loadCommandsFromSessionConfigDetailed(repoRoot) {
127
178
  try {
128
179
  const scriptPath = join(
129
180
  dirname(fileURLToPath(import.meta.url)),
130
181
  '..',
131
182
  'parse-config.mjs',
132
183
  );
133
- if (!existsSync(scriptPath)) return {};
184
+ if (!existsSync(scriptPath)) return { commands: {}, degraded: 'script-missing' };
134
185
  const result = spawnSync('node', [scriptPath], {
135
186
  cwd: repoRoot,
136
187
  encoding: 'utf8',
137
188
  stdio: ['ignore', 'pipe', 'pipe'],
138
189
  timeout: 10_000,
139
190
  });
140
- if (result.status !== 0 || !result.stdout) return {};
191
+ if (result.status !== 0 || !result.stdout) {
192
+ // `parse-config.mjs` exits 1 for EVERY failure, including the benign
193
+ // "this repo has no CLAUDE.md / AGENTS.md at all" — measured 2026-09-05
194
+ // in an empty tmp dir: stderr `CLAUDE.md or AGENTS.md required`, exit 1.
195
+ // A repo with no Session Config has no `*-command` value that could
196
+ // drift, so reporting that as a failed READ would fire a warning on every
197
+ // such repo (`.claude/rules/host-resources.md` § HR-101 — a warning class
198
+ // that is usually present is a broken instrument).
199
+ //
200
+ // Ceiling (BV-004): `parse-config.mjs` resolves its config via
201
+ // `findProjectRoot(cwd)` walking UPWARD, this check looks only at
202
+ // `repoRoot` itself. For a `repoRoot` nested below the project root the
203
+ // two disagree — and the disagreement resolves toward SILENCE (no
204
+ // `degraded`), i.e. the pre-#1031 behaviour, never toward a false alarm.
205
+ // Revisit if a caller starts passing sub-directory roots.
206
+ return sessionConfigFileExists(repoRoot)
207
+ ? { commands: {}, degraded: 'spawn-failed' }
208
+ : { commands: {} };
209
+ }
141
210
  const cfg = JSON.parse(result.stdout);
142
211
  const out = {};
143
212
  if (typeof cfg['lint-command'] === 'string' && cfg['lint-command'].trim()) {
@@ -149,12 +218,30 @@ export function loadCommandsFromSessionConfig(repoRoot) {
149
218
  if (typeof cfg['test-command'] === 'string' && cfg['test-command'].trim()) {
150
219
  out.test = cfg['test-command'];
151
220
  }
152
- return out;
221
+ return { commands: out };
153
222
  } catch {
154
- return {};
223
+ return { commands: {}, degraded: 'parse-error' };
155
224
  }
156
225
  }
157
226
 
227
+ /**
228
+ * Load default commands from Session Config via `scripts/parse-config.mjs`.
229
+ * Returns a partial object — keys that fail to resolve are simply absent
230
+ * (the caller falls through to DEFAULT_COMMANDS for those).
231
+ *
232
+ * Thin wrapper over {@link loadCommandsFromSessionConfigDetailed}; byte-identical
233
+ * return value for every input, including every failure path. Callers that need
234
+ * to tell a failed read from an empty config use the detailed variant.
235
+ *
236
+ * Never throws.
237
+ *
238
+ * @param {string} repoRoot
239
+ * @returns {{lint?: string, typecheck?: string, test?: string}}
240
+ */
241
+ export function loadCommandsFromSessionConfig(repoRoot) {
242
+ return loadCommandsFromSessionConfigDetailed(repoRoot).commands;
243
+ }
244
+
158
245
  /**
159
246
  * Resolve the three gate commands. Precedence: override > session config > defaults.
160
247
  *
@@ -420,44 +507,32 @@ function listChangedFiles(repoRoot, ref) {
420
507
  * The id-space THIS process belongs to, for comparison against a repo-global
421
508
  * file that any session in the working copy may have written.
422
509
  *
423
- * Two sources, both read rather than invented — this deliberately adds no third
424
- * way of answering "who am I" (`scripts/lib/lock-reaper.mjs` takes the id from
425
- * its caller; `scripts/lib/peer-discovery.mjs` reads `readLock()`):
426
- *
427
- * 1. `CLAUDE_CODE_SESSION_ID` the only PER-PROCESS source, and therefore
428
- * the only one a foreign session cannot spoof by writing a file. Measured
429
- * 2026-08-23 inside a dispatched wave agent: present, and equal to the
430
- * coordinator's `current-session.json` `session_id` (a child session
431
- * inherits the parent's id, which is what makes it usable here).
432
- * `scripts/lib/spiral-carryover.mjs` reads the same variable, under the
433
- * same measured premise.
434
- * 2. `session.lock` `session_id` / `semantic_session_id` — a repo-global
435
- * FALLBACK for harnesses that export no session env var. Weaker on
436
- * purpose: the lock is one more shared file in the same working copy, so
437
- * it can name a peer rather than us. It is used only when (1) is absent,
438
- * where the alternative is no check at all.
510
+ * Process-local witnesses ONLY (`CLAUDE_CODE_SESSION_ID`, via
511
+ * {@link readProcessLocalSessionIds}) the `session.lock` fallback this
512
+ * function used to carry is deliberately GONE, not merely deprioritised.
513
+ * `session.lock` is a repo-GLOBAL artefact any session in the working copy
514
+ * can hold; unioning or falling back to it made a shared resource stand in
515
+ * for a process-local identity, which is exactly the #1194 hazard class
516
+ * (`.claude/rules/host-resources.md` § HR-102 "a better signal REPLACES a
517
+ * worse one, it does not merely suppress it"). Concretely for this module
518
+ * (#1205): without the env var, a process that is NOT the lock holder used to
519
+ * silently adopt the lock holder's id as its own, so a `current-session.json`
520
+ * written by that same holder passed the ownership check by construction.
521
+ * `readProcessLocalSessionIds()` (`./session-identity/own-session.mjs`) is
522
+ * the shared, already-hardened implementation of this exact question see
523
+ * its JSDoc for the full lock/STATE.md exclusion rationale.
524
+ *
525
+ * An empty result is not a mismatch: {@link classifyCurrentSessionOwnership}
526
+ * treats an empty `ownIds` as `'unknown'`, never `'foreign'`, so
527
+ * `corrective_context` is kept — the existing fail-open contract from #1058,
528
+ * unchanged by this fix.
439
529
  *
440
- * @param {string} repoRoot
441
530
  * @returns {Set<string>} possibly empty — an empty set means "identity
442
531
  * unresolvable", which the classifier below treats as `unknown`, never as a
443
532
  * mismatch.
444
533
  */
445
- function readOwnSessionIds(repoRoot) {
446
- const ids = new Set();
447
- // `.trim()` first: a whitespace-only env var is truthy and would otherwise
448
- // enter the set as a phantom id (`development.md` § env-var whitespace trap).
449
- const fromEnv = (process.env.CLAUDE_CODE_SESSION_ID || '').trim();
450
- if (fromEnv) ids.add(fromEnv);
451
- if (ids.size === 0) {
452
- try {
453
- const lock = readLock({ repoRoot });
454
- for (const key of ['session_id', 'semantic_session_id']) {
455
- const value = typeof lock?.[key] === 'string' ? lock[key].trim() : '';
456
- if (value) ids.add(value);
457
- }
458
- } catch { /* readLock never throws, but the contract is not ours to trust */ }
459
- }
460
- return ids;
534
+ function readOwnSessionIds() {
535
+ return new Set(readProcessLocalSessionIds({ env: process.env, hookInput: null }));
461
536
  }
462
537
 
463
538
  /**
@@ -534,7 +609,8 @@ function readCorrectiveContext(repoRoot) {
534
609
  if (!existsSync(p)) return [];
535
610
  const raw = readFileSync(p, 'utf8');
536
611
  const parsed = JSON.parse(raw);
537
- const { verdict, fileIds } = classifyCurrentSessionOwnership(parsed, readOwnSessionIds(repoRoot));
612
+ const ownIds = readOwnSessionIds();
613
+ const { verdict, fileIds } = classifyCurrentSessionOwnership(parsed, ownIds);
538
614
  if (verdict === 'foreign') {
539
615
  process.stderr.write(
540
616
  `⚠️ quality-gate: .orchestrator/current-session.json belongs to another session ` +
@@ -543,6 +619,19 @@ function readCorrectiveContext(repoRoot) {
543
619
  );
544
620
  return [];
545
621
  }
622
+ // The fail-open half of `verdict === 'unknown'`: the file DOES name an id,
623
+ // but this process has no process-local witness of its own (ownIds is
624
+ // empty — no `CLAUDE_CODE_SESSION_ID`, e.g. Codex/Cursor). Ownership is
625
+ // unprovable, so the content is kept per the #1058 contract — but silently
626
+ // is the wrong word for that: make the fail-open visible on stderr rather
627
+ // than indistinguishable from a verified 'own' match.
628
+ if (verdict === 'unknown' && fileIds.length > 0 && ownIds.size === 0) {
629
+ process.stderr.write(
630
+ '⚠ quality-gate: cannot verify ownership of .orchestrator/current-session.json ' +
631
+ '(no process-local session id — CLAUDE_CODE_SESSION_ID unset); keeping corrective_context ' +
632
+ `from session ${fileIds.join(', ')} UNVERIFIED\n`,
633
+ );
634
+ }
546
635
  const arr = Array.isArray(parsed?.corrective_context) ? parsed.corrective_context : [];
547
636
  return arr.slice(-CORRECTIVE_CONTEXT_TAIL);
548
637
  } catch {
@@ -172,18 +172,37 @@ const GLOB_METACHAR_RE = /[*?[\]{}()]|[!@+](?=\()/;
172
172
  * {@link UNSAFE_PATH_QUOTE_RE}). Results are deduped, order-preserving on
173
173
  * first occurrence.
174
174
  *
175
+ * #1153 P13: skipping used to be entirely silent, so a learning whose ONLY
176
+ * `file_paths` entry was unusable produced the generic "no activation axis"
177
+ * rejection with nothing naming WHY the axis was empty. Every skipped entry is
178
+ * therefore recorded into the optional `dropped` sink (value + one-word
179
+ * reason), which {@link formatDroppedFilePaths} renders into that rejection
180
+ * reason. The sink is an out-parameter rather than a changed return type so
181
+ * every existing caller stays byte-identical.
182
+ *
175
183
  * @param {string[]} filePaths
184
+ * @param {{value: string, reason: string}[]} [dropped] - out-param sink
176
185
  * @returns {string[]}
177
186
  */
178
- function globsFromFilePaths(filePaths) {
187
+ function globsFromFilePaths(filePaths, dropped) {
179
188
  const out = [];
180
189
  const seen = new Set();
181
190
  for (const raw of filePaths) {
182
191
  if (typeof raw !== 'string' || raw === '') continue;
183
- if (GLOB_METACHAR_RE.test(raw)) continue;
192
+ if (GLOB_METACHAR_RE.test(raw)) {
193
+ dropped?.push({ value: raw, reason: 'glob-metachar' });
194
+ continue;
195
+ }
184
196
  // #1015: frontmatter-structure guard. Skipping (never escaping) keeps this
185
197
  // idempotent and non-overlapping with the renderer's sanitiser.
186
- if (CONTROL_CHARS_TEST_RE.test(raw) || UNSAFE_PATH_QUOTE_RE.test(raw)) continue;
198
+ if (CONTROL_CHARS_TEST_RE.test(raw)) {
199
+ dropped?.push({ value: raw, reason: 'control-char' });
200
+ continue;
201
+ }
202
+ if (UNSAFE_PATH_QUOTE_RE.test(raw)) {
203
+ dropped?.push({ value: raw, reason: 'quote' });
204
+ continue;
205
+ }
187
206
  const normalized = raw.replace(/\\/g, '/');
188
207
  const dir = dirname(normalized);
189
208
  const pattern = dir === '.' ? normalized : `${dir}/**`;
@@ -195,6 +214,42 @@ function globsFromFilePaths(filePaths) {
195
214
  return out;
196
215
  }
197
216
 
217
+ /** Max chars of a single dropped `file_paths` value in a rejection reason. */
218
+ const DROPPED_VALUE_MAX = 60;
219
+
220
+ /**
221
+ * Render the `dropped` sink of {@link globsFromFilePaths} into a suffix for the
222
+ * never-always-on rejection reason (#1153 P13).
223
+ *
224
+ * The reason string is OPERATOR-VISIBLE (engine.mjs forwards it verbatim into
225
+ * `reason: emit/render error: …` and from there into the candidates ledger), so
226
+ * a hostile `file_paths` entry must not be able to inject newlines or ANSI into
227
+ * it. Each value is therefore `JSON.stringify`-escaped — which turns every
228
+ * control char into a `\uXXXX` literal — and then hard-truncated to
229
+ * {@link DROPPED_VALUE_MAX} chars, with any trailing partial backslash escape
230
+ * removed so the cut cannot end mid-sequence.
231
+ *
232
+ * @param {{value: string, reason: string}[]} dropped
233
+ * @returns {string} `''` when nothing was dropped, else a ` — dropped …` suffix
234
+ */
235
+ function formatDroppedFilePaths(dropped) {
236
+ if (!Array.isArray(dropped) || dropped.length === 0) return '';
237
+ const rendered = dropped.map(({ value, reason }) => {
238
+ const escaped = JSON.stringify(String(value));
239
+ const clipped =
240
+ escaped.length <= DROPPED_VALUE_MAX
241
+ ? escaped
242
+ // A `\uXXXX` escape is 6 chars, so the cut can land at FIVE distinct
243
+ // depths inside one — and `/\\+$/` only ever caught the shallowest
244
+ // (`\`). Measured at DROPPED_VALUE_MAX=60 with a NUL at offsets 53..56:
245
+ // the old form left `\u000`, `\u00`, `\u0` and `\u` standing. Strip the
246
+ // backslash together with up to three hex digits of a partial `u`-escape.
247
+ : `${escaped.slice(0, DROPPED_VALUE_MAX - 1).replace(/\\(u[0-9a-fA-F]{0,3})?$/, '')}…`;
248
+ return `${clipped} (${reason})`;
249
+ });
250
+ return ` — dropped file_paths: ${dropped.length} (${rendered.join(', ')})`;
251
+ }
252
+
198
253
  /**
199
254
  * Build a single-line, frontmatter-safe description from a learning.
200
255
  *
@@ -320,7 +375,8 @@ export function toActivationMetadata(learning, { ruleExpiryDays, now, minRuleDay
320
375
  }
321
376
 
322
377
  const filePaths = Array.isArray(learning.file_paths) ? learning.file_paths : [];
323
- const globs = globsFromFilePaths(filePaths);
378
+ const droppedFilePaths = [];
379
+ const globs = globsFromFilePaths(filePaths, droppedFilePaths);
324
380
 
325
381
  // #1015: `host_class` is copied straight into an UNQUOTED `host-class:`
326
382
  // frontmatter line by the renderer, so its SHAPE is load-bearing. Reject a
@@ -358,16 +414,22 @@ export function toActivationMetadata(learning, { ruleExpiryDays, now, minRuleDay
358
414
  // The brandmauer: an auto-generated rule must carry ≥1 activation axis. If we
359
415
  // could derive neither a glob nor a host-class, refuse — never emit always-on.
360
416
  if (globs.length === 0 && hostClass === undefined) {
417
+ // #1153 P13: name the entries that were silently skipped on the way here —
418
+ // otherwise "no activation axis" is indistinguishable from "no file_paths
419
+ // at all", and the operator has no way to see that a malformed record (not
420
+ // an empty one) caused the rejection.
421
+ const droppedSuffix = formatDroppedFilePaths(droppedFilePaths);
361
422
  if (hostClassDroppedByType) {
362
423
  throw new Error(
363
424
  `emitter: host_class ${JSON.stringify(learning.host_class)} is present and well-formed, but type ` +
364
425
  `${JSON.stringify(learning.type)} is not host-specific (#1090 — host-class is only copied through for ` +
365
426
  `${[...HOST_SPECIFIC_TYPES].join(', ')}) — refusing to emit always-on auto-generated rule ` +
366
- '(never-always-on invariant); no other activation axis (globs) is available either.',
427
+ `(never-always-on invariant); no other activation axis (globs) is available either.${droppedSuffix}`,
367
428
  );
368
429
  }
369
430
  throw new Error(
370
- 'emitter: no activation axis (globs/host-class) — refusing to emit always-on auto-generated rule (never-always-on invariant)',
431
+ 'emitter: no activation axis (globs/host-class) — refusing to emit always-on auto-generated rule ' +
432
+ `(never-always-on invariant)${droppedSuffix}`,
371
433
  );
372
434
  }
373
435
 
@@ -33,10 +33,16 @@
33
33
  * the engine performs is into the reconcile-candidates sidecar (via
34
34
  * idempotency.mjs), and even that is skipped under `dryRun`.
35
35
  *
36
- * ── never-throws contract ────────────────────────────────────────────────────
37
- * `runReconcile` NEVER throws to its caller. A per-learning emit/render failure
38
- * degrades to a recorded rejection (never a crash); any unexpected top-level
39
- * error returns a zeroed result with an `error` field.
36
+ * ── never-throws contract (PIPELINE errors only) ─────────────────────────────
37
+ * Once the pipeline is running, `runReconcile` NEVER throws to its caller: a
38
+ * per-learning emit/render failure degrades to a recorded rejection (never a
39
+ * crash), and any unexpected top-level pipeline error returns a zeroed result
40
+ * with an `error` field. The ONE exception is a caller-INPUT validation error
41
+ * at the very top of `runReconcile` — an explicitly-passed `trigger` outside
42
+ * `KNOWN_TRIGGERS` (issue #1201 Part A) THROWS synchronously before the
43
+ * pipeline ever starts, the same class of failure as passing a malformed
44
+ * `params` object to any other function in this codebase; see
45
+ * `assertKnownTrigger` below.
40
46
  *
41
47
  * Plain Node ESM, no external deps — Node 20+ stdlib + the four siblings only.
42
48
  *
@@ -154,8 +160,10 @@ function zeroedResult(error) {
154
160
  /**
155
161
  * Default learnings loader — read + parse `<repoRoot>/.orchestrator/metrics/learnings.jsonl`
156
162
  * line-by-line, migrate/normalize records through the learnings schema SSOT,
157
- * and skip blank/malformed lines. A missing file yields `[]`. Never throws (a
158
- * read error degrades to `[]`).
163
+ * and skip blank/malformed lines. A missing file (ENOENT) yields `[]`
164
+ * silently; an unreadable one (EACCES/EISDIR/…) yields `[]` with a stderr
165
+ * WARN (#1210 — ENOENT and other read failures are different facts, same
166
+ * split as `sessions-canonical.mjs` `readCanonicalSessions`).
159
167
  *
160
168
  * @param {string|undefined} repoRoot
161
169
  * @returns {Array<Record<string, unknown>>}
@@ -169,8 +177,15 @@ function defaultLoadLearnings(repoRoot) {
169
177
  let raw;
170
178
  try {
171
179
  raw = readFileSync(absPath, 'utf8');
172
- } catch {
173
- return []; // ENOENT or any read error → empty corpus.
180
+ } catch (err) {
181
+ if (!err || err.code !== 'ENOENT') {
182
+ process.stderr.write(
183
+ `⚠ defaultLoadLearnings: cannot read ${absPath} ` +
184
+ `(${err?.code ?? '?'}: ${err?.message ?? String(err)}) — ` +
185
+ 'treating as EMPTY, counts below are floors\n',
186
+ );
187
+ }
188
+ return [];
174
189
  }
175
190
 
176
191
  /** @type {Array<Record<string, unknown>>} */
@@ -463,7 +478,7 @@ function defaultReadMaterializedProvenance(repoRoot) {
463
478
  * @param {boolean} [opts.dryRun] - when true, compute proposals but SKIP the merge entirely.
464
479
  * @returns {Promise<ReconcileResult>}
465
480
  */
466
- export async function runReconcile(
481
+ async function runReconcileInner(
467
482
  {
468
483
  repoRoot,
469
484
  ruleExpiryDays,
@@ -780,3 +795,228 @@ export async function runReconcile(
780
795
  return zeroedResult(msg);
781
796
  }
782
797
  }
798
+
799
+ /**
800
+ * Ledger name of the reconcile run event (issue #1192). Catalogued in
801
+ * `docs/events-schema.md`; `events-schema.mjs` needs no registration — it
802
+ * validates the NAME shape only, and this name already satisfies it.
803
+ */
804
+ export const RECONCILE_EVENT = 'orchestrator.reconcile.completed';
805
+
806
+ /**
807
+ * Closed enum of `trigger` values a caller may explicitly pass to
808
+ * {@link runReconcile} (issue #1201 Part A / Discovery D8). Two of the three
809
+ * real call sites pinned `trigger` by PROSE inside a `.md` skill file —
810
+ * `skill` (`skills/reconcile/SKILL.md`) and `session-end`
811
+ * (`skills/session-end/phase-3-6-tail.md`) — where nothing enforced the
812
+ * string ever matched this list; only `phase-skip`
813
+ * (`scripts/lib/session-end/phase-skip.mjs`) pinned it in code. The two `.md`
814
+ * callers are migrated onto {@link runReconcileFromSkill} /
815
+ * {@link runReconcileAtSessionEnd} in this same change, so "prose sets the
816
+ * trigger" stops being possible for them at all.
817
+ */
818
+ export const KNOWN_TRIGGERS = Object.freeze(['skill', 'session-end', 'phase-skip']);
819
+
820
+ /**
821
+ * Reject an explicitly-passed `trigger` that is not in {@link KNOWN_TRIGGERS}
822
+ * — including the literal `'unknown'`, which is a DEFAULT this module mints
823
+ * for an ABSENT trigger, never a value a caller should pass on purpose. An
824
+ * absent trigger (`undefined`) is deliberately let through unchanged: it is
825
+ * legacy behaviour for a caller that has not yet been migrated onto a pinned
826
+ * wrapper, and `buildReconcilePayload` already defaults it to `'unknown'` so
827
+ * the per-trigger denominator in the ledger stays honest about which runs are
828
+ * unattributed rather than silently breaking them.
829
+ *
830
+ * BV-004 revisit trigger: `scripts/lib/session-end/phase-skip.mjs` is the one
831
+ * remaining caller passing `trigger: 'phase-skip'` as a hand-written literal
832
+ * rather than through {@link runReconcileFromPhaseSkip} (out of this task's
833
+ * file scope) — once every caller is migrated onto a pinned wrapper, flip the
834
+ * absent-trigger default below from "let it run" to a throw, closing the last
835
+ * legacy path.
836
+ *
837
+ * @param {unknown} trigger
838
+ * @throws {Error} when `trigger` is defined but not in `KNOWN_TRIGGERS`.
839
+ * @returns {void}
840
+ */
841
+ export function assertKnownTrigger(trigger) {
842
+ if (trigger === undefined) return;
843
+ if (!KNOWN_TRIGGERS.includes(trigger)) {
844
+ throw new Error(
845
+ `runReconcile: invalid trigger ${JSON.stringify(trigger)} — must be one of: ` +
846
+ `${KNOWN_TRIGGERS.join(', ')} (or omitted entirely, for a not-yet-migrated caller).`,
847
+ );
848
+ }
849
+ }
850
+
851
+ /** Clamp for the `reason` string on the abort path — a message can be long. */
852
+ const REASON_MAX_CHARS = 300;
853
+
854
+ /** The closed target enum `resolveEffectiveTargets` recognises. */
855
+ const KNOWN_TARGETS = ['repo-local', 'baseline'];
856
+
857
+ /**
858
+ * Build the `orchestrator.reconcile.completed` payload from a finished run.
859
+ *
860
+ * Counter fields are written INCLUDING `0`: each was MEASURED over the whole
861
+ * run, so a written zero is the payload (same contract as the vault-mirror run
862
+ * event). The two absence-preserving exceptions are `store_records_dropped`
863
+ * (absent ⇒ the candidate store was never inspected — dryRun, empty
864
+ * short-circuit, error path) and `targets` (absent ⇒ the caller asserted no
865
+ * target list). `aborted`/`reason` appear only when the never-throws guard
866
+ * fired; their absence means "ran to the end", never "unknown".
867
+ *
868
+ * @param {ReconcileResult} result
869
+ * @param {{ trigger?: string, targets?: string[], dryRun?: boolean, durationMs: number }} ctx
870
+ * @returns {Record<string, unknown>}
871
+ */
872
+ function buildReconcilePayload(result, ctx) {
873
+ const summary = (result && result.summary) || {};
874
+ /** @type {Record<string, unknown>} */
875
+ const payload = {
876
+ trigger: typeof ctx.trigger === 'string' && ctx.trigger.trim() !== '' ? ctx.trigger : 'unknown',
877
+ dry_run: ctx.dryRun === true,
878
+ learnings_total: summary.totalLearnings ?? 0,
879
+ eligible: summary.eligible ?? 0,
880
+ proposals: summary.proposed ?? 0,
881
+ rejected: summary.rejected ?? 0,
882
+ capped: summary.capped ?? 0,
883
+ already_materialized: summary.alreadyMaterialized ?? 0,
884
+ written: summary.written === true,
885
+ duration_ms: ctx.durationMs,
886
+ };
887
+ // `targets` originates in operator-authored Session Config (`reconcile.targets`)
888
+ // and is unbounded there. Allowlisted to the CLOSED enum `resolveEffectiveTargets`
889
+ // recognises before it enters the ledger and the optional Clank webhook (Q2-F4):
890
+ // anything else is not a target this engine can act on, so recording it would
891
+ // be a verbatim echo of untrusted text, never a measurement. Omitted when empty.
892
+ const targets = Array.isArray(ctx.targets)
893
+ ? [...new Set(ctx.targets.filter((t) => KNOWN_TARGETS.includes(t)))]
894
+ : [];
895
+ if (targets.length > 0) payload.targets = targets;
896
+ if (typeof summary.skipped === 'number') payload.store_records_dropped = summary.skipped;
897
+ if (typeof result?.error === 'string' && result.error !== '') {
898
+ payload.aborted = 'engine-error';
899
+ payload.reason = result.error.slice(0, REASON_MAX_CHARS);
900
+ }
901
+ return payload;
902
+ }
903
+
904
+ /**
905
+ * Record one reconcile run in the repo's event ledger — best-effort.
906
+ *
907
+ * Refuses the ambient `SO_PROJECT_DIR` destination when no `repoRoot` was
908
+ * given: most engine tests call `runReconcile` without one, and a fallback
909
+ * would append synthetic records to the operator's REAL fleet ledger on every
910
+ * `npm test` (#1119, `scripts/lib/express-path.mjs`). Diagnostics on stderr.
911
+ *
912
+ * @param {ReconcileResult} result
913
+ * @param {{ repoRoot?: string, trigger?: string, targets?: string[], dryRun?: boolean, durationMs: number }} ctx
914
+ */
915
+ async function emitReconcileCompleted(result, ctx) {
916
+ const { repoRoot } = ctx;
917
+ if (typeof repoRoot !== 'string' || repoRoot.trim() === '') {
918
+ process.stderr.write(
919
+ `reconcile: skipped ${RECONCILE_EVENT} — no repoRoot given; ` +
920
+ 'refusing the ambient SO_PROJECT_DIR destination (#1119).\n',
921
+ );
922
+ return;
923
+ }
924
+ const { emitEvent } = await import('../events.mjs');
925
+ await emitEvent(RECONCILE_EVENT, buildReconcilePayload(result, ctx), { repoRoot });
926
+ }
927
+
928
+ /**
929
+ * Public boundary: run the reconciliation pipeline and record the run.
930
+ *
931
+ * A thin WRAPPER, deliberately: the pipeline has three return points (empty
932
+ * short-circuit, normal tail, never-throws catch), and an inline emit would
933
+ * miss two of them — including the empty corpus and the error path, the two
934
+ * runs an operator most needs recorded (`.claude/rules/host-resources.md`
935
+ * § HR-105). Same shape as `runNarrativeMirror` + `mirrorNarrative` in
936
+ * `scripts/lib/vault-status/narrative-mirror.mjs`.
937
+ *
938
+ * The emit is wrapped in try/catch because `emitEvent` THROWS
939
+ * `EventValidationError` on an invalid record — without the catch, telemetry
940
+ * would break this function's never-throws contract. The pipeline's result is
941
+ * returned UNTOUCHED whether or not the ledger accepted the record.
942
+ *
943
+ * @param {Object} [params] - see {@link runReconcileInner}, plus:
944
+ * @param {'skill'|'session-end'|'phase-skip'} [params.trigger] - which caller
945
+ * invoked this run; recorded ALWAYS (default `'unknown'` when omitted) so
946
+ * the per-trigger denominator is complete. Not read by the pipeline.
947
+ * VALIDATED against {@link KNOWN_TRIGGERS} via {@link assertKnownTrigger}
948
+ * when explicitly passed — an unknown string, including the literal
949
+ * `'unknown'`, THROWS synchronously; an absent trigger is still let
950
+ * through unchanged (issue #1201 Part A — see `assertKnownTrigger`'s
951
+ * BV-004 revisit-trigger note for why the absent case is not also a
952
+ * throw yet). Prefer {@link runReconcileFromSkill},
953
+ * {@link runReconcileAtSessionEnd}, or {@link runReconcileFromPhaseSkip}
954
+ * over passing `trigger` here directly.
955
+ * @param {string[]} [params.targets] - the caller's effective target list
956
+ * (`resolveEffectiveTargets`); recorded when non-empty, omitted otherwise.
957
+ * Not read by the pipeline.
958
+ * @param {Object} [opts] - see {@link runReconcileInner}.
959
+ * @returns {Promise<ReconcileResult>}
960
+ */
961
+ export async function runReconcile(params = {}, opts = {}) {
962
+ assertKnownTrigger(params.trigger);
963
+ const t0 = Date.now();
964
+ const result = await runReconcileInner(params, opts);
965
+ try {
966
+ await emitReconcileCompleted(result, {
967
+ repoRoot: params.repoRoot,
968
+ trigger: params.trigger,
969
+ targets: params.targets,
970
+ dryRun: params.dryRun === true || opts.dryRun === true,
971
+ durationMs: Date.now() - t0,
972
+ });
973
+ } catch {
974
+ // Best-effort telemetry — never the reason a reconcile run fails.
975
+ }
976
+ return result;
977
+ }
978
+
979
+ /**
980
+ * Pin `trigger: 'skill'` in code for the `/reconcile` skill (Phase 2.3,
981
+ * `skills/reconcile/SKILL.md`) — the skill's own `.md` prose no longer sets
982
+ * the trigger string itself (issue #1201 Part A). Any `trigger` the caller
983
+ * passes in `params` is OVERWRITTEN; every other field forwards unchanged.
984
+ *
985
+ * @param {Object} [params] - see {@link runReconcile} `params`.
986
+ * @param {Object} [opts] - see {@link runReconcile} `opts` (DI seams).
987
+ * @returns {Promise<ReconcileResult>}
988
+ */
989
+ export async function runReconcileFromSkill(params = {}, opts = {}) {
990
+ return runReconcile({ ...params, trigger: 'skill' }, opts);
991
+ }
992
+
993
+ /**
994
+ * Pin `trigger: 'session-end'` in code for session-end Phase 3.6.8
995
+ * (`skills/session-end/phase-3-6-tail.md`) — see
996
+ * {@link runReconcileFromSkill} for the rationale and contract.
997
+ *
998
+ * @param {Object} [params] - see {@link runReconcile} `params`.
999
+ * @param {Object} [opts] - see {@link runReconcile} `opts` (DI seams).
1000
+ * @returns {Promise<ReconcileResult>}
1001
+ */
1002
+ export async function runReconcileAtSessionEnd(params = {}, opts = {}) {
1003
+ return runReconcile({ ...params, trigger: 'session-end' }, opts);
1004
+ }
1005
+
1006
+ /**
1007
+ * Pin `trigger: 'phase-skip'` in code for the session-end skip-plan
1008
+ * aggregator (`scripts/lib/session-end/phase-skip.mjs`, the highest-volume
1009
+ * probe-only caller) — see {@link runReconcileFromSkill} for the rationale
1010
+ * and contract. NOT YET wired into `phase-skip.mjs` itself, which is out of
1011
+ * this task's file scope and still passes `trigger: 'phase-skip'` as a
1012
+ * hand-written literal directly to `runReconcile`; exported here so that
1013
+ * migration is a one-line import swap (see `assertKnownTrigger`'s BV-004
1014
+ * revisit-trigger note).
1015
+ *
1016
+ * @param {Object} [params] - see {@link runReconcile} `params`.
1017
+ * @param {Object} [opts] - see {@link runReconcile} `opts` (DI seams).
1018
+ * @returns {Promise<ReconcileResult>}
1019
+ */
1020
+ export async function runReconcileFromPhaseSkip(params = {}, opts = {}) {
1021
+ return runReconcile({ ...params, trigger: 'phase-skip' }, opts);
1022
+ }