session-orchestrator 5.2.0 → 5.4.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 (427) hide show
  1. package/.agents/skills/architecture/SKILL.md +3 -1
  2. package/.agents/skills/autopilot/SKILL.md +5 -1
  3. package/.agents/skills/autopilot/agents/openai.yaml +5 -0
  4. package/.agents/skills/bootstrap/SKILL.md +5 -1
  5. package/.agents/skills/bootstrap/agents/openai.yaml +5 -0
  6. package/.agents/skills/brainstorm/SKILL.md +5 -1
  7. package/.agents/skills/brainstorm/agents/openai.yaml +5 -0
  8. package/.agents/skills/claude-md-drift-check/SKILL.md +4 -2
  9. package/.agents/skills/close/SKILL.md +5 -1
  10. package/.agents/skills/close/agents/openai.yaml +5 -0
  11. package/.agents/skills/convergence-monitoring/SKILL.md +4 -2
  12. package/.agents/skills/debug/SKILL.md +5 -1
  13. package/.agents/skills/debug/agents/openai.yaml +5 -0
  14. package/.agents/skills/discovery/SKILL.md +5 -1
  15. package/.agents/skills/discovery/agents/openai.yaml +5 -0
  16. package/.agents/skills/dispatcher/SKILL.md +5 -1
  17. package/.agents/skills/dispatcher/agents/openai.yaml +5 -0
  18. package/.agents/skills/docs-orchestrator/SKILL.md +3 -1
  19. package/.agents/skills/ecosystem-health/SKILL.md +3 -1
  20. package/.agents/skills/eli5/SKILL.md +5 -1
  21. package/.agents/skills/eli5/agents/openai.yaml +5 -0
  22. package/.agents/skills/eval/SKILL.md +6 -2
  23. package/.agents/skills/eval/agents/openai.yaml +5 -0
  24. package/.agents/skills/evolve/SKILL.md +6 -2
  25. package/.agents/skills/evolve/agents/openai.yaml +5 -0
  26. package/.agents/skills/frontmatter-guard/SKILL.md +3 -1
  27. package/.agents/skills/gitlab-ops/SKILL.md +3 -1
  28. package/.agents/skills/gitlab-portfolio/SKILL.md +3 -1
  29. package/.agents/skills/go/SKILL.md +5 -1
  30. package/.agents/skills/go/agents/openai.yaml +5 -0
  31. package/.agents/skills/grill/SKILL.md +5 -1
  32. package/.agents/skills/grill/agents/openai.yaml +5 -0
  33. package/.agents/skills/harness-audit/SKILL.md +5 -1
  34. package/.agents/skills/harness-audit/agents/openai.yaml +5 -0
  35. package/.agents/skills/hook-development/SKILL.md +3 -1
  36. package/.agents/skills/mcp-builder/SKILL.md +3 -1
  37. package/.agents/skills/memory-cleanup/SKILL.md +5 -1
  38. package/.agents/skills/memory-cleanup/agents/openai.yaml +5 -0
  39. package/.agents/skills/mode-selector/SKILL.md +3 -1
  40. package/.agents/skills/navigator/SKILL.md +22 -0
  41. package/.agents/skills/navigator/agents/openai.yaml +5 -0
  42. package/.agents/skills/npm-publish/SKILL.md +4 -2
  43. package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
  44. package/.agents/skills/persona-panel/SKILL.md +5 -1
  45. package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
  46. package/.agents/skills/plan/SKILL.md +5 -1
  47. package/.agents/skills/plan/agents/openai.yaml +5 -0
  48. package/.agents/skills/playwright-driver/SKILL.md +3 -1
  49. package/.agents/skills/portfolio/SKILL.md +5 -1
  50. package/.agents/skills/portfolio/agents/openai.yaml +5 -0
  51. package/.agents/skills/quality-gates/SKILL.md +3 -1
  52. package/.agents/skills/reconcile/SKILL.md +5 -1
  53. package/.agents/skills/reconcile/agents/openai.yaml +5 -0
  54. package/.agents/skills/release/SKILL.md +5 -1
  55. package/.agents/skills/release/agents/openai.yaml +5 -0
  56. package/.agents/skills/remote-offload/SKILL.md +3 -1
  57. package/.agents/skills/repo-audit/SKILL.md +5 -1
  58. package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
  59. package/.agents/skills/session/SKILL.md +21 -0
  60. package/.agents/skills/session/agents/openai.yaml +5 -0
  61. package/.agents/skills/session-end/SKILL.md +3 -1
  62. package/.agents/skills/session-plan/SKILL.md +3 -1
  63. package/.agents/skills/session-start/SKILL.md +3 -1
  64. package/.agents/skills/spinout/SKILL.md +5 -1
  65. package/.agents/skills/spinout/agents/openai.yaml +5 -0
  66. package/.agents/skills/sunset-review/SKILL.md +5 -1
  67. package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
  68. package/.agents/skills/templates-ack/SKILL.md +21 -0
  69. package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
  70. package/.agents/skills/test/SKILL.md +5 -1
  71. package/.agents/skills/test/agents/openai.yaml +5 -0
  72. package/.agents/skills/test-audit/SKILL.md +21 -0
  73. package/.agents/skills/test-audit/agents/openai.yaml +5 -0
  74. package/.agents/skills/test-runner/SKILL.md +3 -1
  75. package/.agents/skills/tmux-layout/SKILL.md +3 -1
  76. package/.agents/skills/using-orchestrator/SKILL.md +3 -1
  77. package/.agents/skills/ux-grill/SKILL.md +5 -1
  78. package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
  79. package/.agents/skills/vault-mirror/SKILL.md +3 -1
  80. package/.agents/skills/vault-sync/SKILL.md +3 -1
  81. package/.agents/skills/wave-executor/SKILL.md +3 -1
  82. package/.agents/skills/write-executable-plan/SKILL.md +3 -1
  83. package/.claude-plugin/marketplace.json +1 -1
  84. package/.claude-plugin/plugin.json +1 -1
  85. package/.codex-plugin/plugin.json +4 -4
  86. package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -1
  87. package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -3
  88. package/.codex-plugin/skills/eval/SKILL.md +1 -1
  89. package/.codex-plugin/skills/evolve/SKILL.md +1 -1
  90. package/.codex-plugin/skills/navigator/SKILL.md +22 -0
  91. package/.codex-plugin/skills/{convergence-monitoring → navigator}/agents/openai.yaml +2 -2
  92. package/.codex-plugin/skills/npm-publish/SKILL.md +1 -3
  93. package/.codex-plugin/skills/session/SKILL.md +1 -1
  94. package/.codex-plugin/skills/test-audit/SKILL.md +21 -0
  95. package/.codex-plugin/skills/{npm-publish → test-audit}/agents/openai.yaml +1 -1
  96. package/.cursor/commands/eval.md +1 -1
  97. package/.cursor/commands/navigator.md +15 -0
  98. package/.cursor/commands/session.md +1 -1
  99. package/.cursor/commands/test-audit.md +14 -0
  100. package/.cursor/rules/000-session-orchestrator.mdc +0 -2
  101. package/.cursor/rules/050-plan.mdc +1 -1
  102. package/.cursor/rules/060-evolve.mdc +27 -12
  103. package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
  104. package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
  105. package/.cursor/skills/eval/SKILL.md +1 -1
  106. package/.cursor/skills/navigator/SKILL.md +13 -0
  107. package/.cursor/skills/npm-publish/SKILL.md +1 -0
  108. package/.cursor/skills/test-audit/SKILL.md +12 -0
  109. package/.cursor-plugin/plugin.json +1 -1
  110. package/.orchestrator/policy/blocked-commands.json +12 -3
  111. package/AGENTS.md +9 -30
  112. package/CHANGELOG.md +206 -0
  113. package/README.md +13 -9
  114. package/SECURITY.md +12 -0
  115. package/agents/dialectic-deriver.md +13 -10
  116. package/agents/eval-judge.md +67 -45
  117. package/agents/skill-applied-judge.md +34 -19
  118. package/commands/session.md +7 -3
  119. package/docs/agent-authoring.md +2 -2
  120. package/docs/baseline.md +22 -7
  121. package/docs/ci-setup.md +57 -4
  122. package/docs/codex-setup.md +14 -2
  123. package/docs/components.md +12 -10
  124. package/docs/events-schema.md +65 -15
  125. package/docs/persona-panel.md +5 -4
  126. package/docs/rule-authoring.md +84 -17
  127. package/docs/scope-collision-guard.md +26 -4
  128. package/docs/session-config-reference.md +154 -12
  129. package/docs/session-config-template.md +41 -3
  130. package/docs/telemetry.md +2 -0
  131. package/hooks/_lib/hook-import-set.json +87 -6
  132. package/hooks/_lib/subagent-transcript.mjs +582 -31
  133. package/hooks/config-protection.mjs +11 -3
  134. package/hooks/cwd-change-restore.mjs +11 -3
  135. package/hooks/enforce-commands.mjs +70 -23
  136. package/hooks/enforce-scope.mjs +143 -33
  137. package/hooks/hooks-codex.json +1 -1
  138. package/hooks/hooks-cursor.json +5 -0
  139. package/hooks/hooks-pi.json +5 -0
  140. package/hooks/hooks.json +6 -1
  141. package/hooks/loop-guard.mjs +11 -3
  142. package/hooks/on-session-end.mjs +58 -23
  143. package/hooks/on-session-start.mjs +101 -11
  144. package/hooks/on-stop.mjs +170 -22
  145. package/hooks/operator-steer.mjs +11 -3
  146. package/hooks/post-bash-issue-budget-refund.mjs +18 -8
  147. package/hooks/post-bash-write-verify.mjs +3 -2
  148. package/hooks/post-edit-import-probe.mjs +17 -9
  149. package/hooks/post-edit-validate.mjs +13 -5
  150. package/hooks/post-subagent-discovery-validator.mjs +98 -13
  151. package/hooks/post-tool-batch-wave-signal.mjs +202 -38
  152. package/hooks/post-tool-failure-corrective-context.mjs +11 -5
  153. package/hooks/post-tooluse-frontend-slop.mjs +10 -4
  154. package/hooks/pre-auq-clarity.mjs +15 -2
  155. package/hooks/pre-bash-destructive-guard.mjs +80 -9
  156. package/hooks/pre-bash-issue-budget.mjs +16 -11
  157. package/hooks/pre-bash-memory-propose-audit.mjs +137 -56
  158. package/hooks/pre-bash-sessions-ledger-guard.mjs +419 -22
  159. package/hooks/pre-bash-staging-fence.mjs +335 -31
  160. package/hooks/pre-bash-templates-first.mjs +19 -14
  161. package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
  162. package/hooks/pre-task-scope-disjoint.mjs +792 -74
  163. package/hooks/subagent-telemetry.mjs +111 -38
  164. package/hooks/wave-scope-commit-guard.mjs +197 -100
  165. package/monitors/monitors.json +1 -1
  166. package/output-styles/wave-summary.md +1 -1
  167. package/package.json +3 -3
  168. package/pi/prompts/eval.md +1 -1
  169. package/pi/prompts/navigator.md +12 -0
  170. package/pi/prompts/session.md +1 -1
  171. package/pi/prompts/test-audit.md +12 -0
  172. package/rules/README.md +7 -2
  173. package/rules/_index.md +3 -3
  174. package/rules/always-on/bash-harness-pitfalls.md +7 -1
  175. package/rules/opt-in-domain/prompt-caching.md +6 -2
  176. package/rules/opt-in-stack/backend-data.md +10 -2
  177. package/rules/opt-in-stack/backend.md +10 -4
  178. package/rules/opt-in-stack/frontend.md +7 -2
  179. package/rules/opt-in-stack/security-web.md +10 -4
  180. package/rules/opt-in-stack/swift.md +6 -2
  181. package/scripts/apply-session-learnings.mjs +492 -0
  182. package/scripts/archive-closed-prds.mjs +2 -1
  183. package/scripts/autopilot.mjs +70 -53
  184. package/scripts/backfill-abandoned-sessions.mjs +118 -16
  185. package/scripts/baseline-archetypes.mjs +1 -1
  186. package/scripts/check-package-manager.mjs +62 -5
  187. package/scripts/check-sessions-integrity.mjs +300 -0
  188. package/scripts/cursor-install.mjs +1 -4
  189. package/scripts/dialectic-deriver.mjs +50 -13
  190. package/scripts/emit-session.mjs +371 -31
  191. package/scripts/eval-session.mjs +65 -3
  192. package/scripts/export-hw-learnings.mjs +22 -3
  193. package/scripts/fleet-checkin.mjs +147 -0
  194. package/scripts/generate-agents-skills.mjs +102 -29
  195. package/scripts/generate-cursor-adapter.mjs +61 -16
  196. package/scripts/lib/agent-frontmatter.mjs +3 -3
  197. package/scripts/lib/agent-status.mjs +2 -31
  198. package/scripts/lib/auq/clarity.mjs +10 -2
  199. package/scripts/lib/auq/parse.mjs +12 -31
  200. package/scripts/lib/auq/schema.mjs +56 -41
  201. package/scripts/lib/auto-dialectic.mjs +304 -15
  202. package/scripts/lib/auto-dream.mjs +7 -6
  203. package/scripts/lib/autopilot/flags.mjs +18 -2
  204. package/scripts/lib/autopilot/kill-switches.mjs +6 -3
  205. package/scripts/lib/autopilot/loop.mjs +19 -2
  206. package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
  207. package/scripts/lib/baseline-archetypes.mjs +41 -22
  208. package/scripts/lib/ci-status-banner.mjs +376 -16
  209. package/scripts/lib/command-blocker.mjs +275 -28
  210. package/scripts/lib/config/dialectic.mjs +12 -3
  211. package/scripts/lib/config/gate.mjs +74 -0
  212. package/scripts/lib/config/host-paths.mjs +79 -0
  213. package/scripts/lib/config/persona-gate-wave.mjs +3 -3
  214. package/scripts/lib/config/reaper.mjs +163 -0
  215. package/scripts/lib/config-schema.mjs +13 -0
  216. package/scripts/lib/config.mjs +85 -10
  217. package/scripts/lib/convergence-monitor.mjs +74 -11
  218. package/scripts/lib/ecosystem-health.mjs +11 -0
  219. package/scripts/lib/eval/engine.mjs +421 -53
  220. package/scripts/lib/eval/judge.mjs +463 -40
  221. package/scripts/lib/eval/schema.mjs +10 -1
  222. package/scripts/lib/events-rotation.mjs +221 -25
  223. package/scripts/lib/events-schema.mjs +114 -0
  224. package/scripts/lib/events.mjs +545 -7
  225. package/scripts/lib/fleet-protocol.mjs +270 -0
  226. package/scripts/lib/frontmatter-guard.mjs +21 -10
  227. package/scripts/lib/gates/gate-baseline.mjs +40 -5
  228. package/scripts/lib/gates/gate-full.mjs +35 -5
  229. package/scripts/lib/gates/gate-helpers.mjs +336 -23
  230. package/scripts/lib/gates/gate-incremental.mjs +49 -23
  231. package/scripts/lib/gates/gate-per-file.mjs +42 -8
  232. package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
  233. package/scripts/lib/instruction-budget-guard.mjs +208 -8
  234. package/scripts/lib/io.mjs +42 -8
  235. package/scripts/lib/issue-close-strip-labels.mjs +207 -49
  236. package/scripts/lib/js-mask.mjs +197 -0
  237. package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
  238. package/scripts/lib/learnings/expiry-sweep.mjs +34 -3
  239. package/scripts/lib/learnings/io.mjs +154 -26
  240. package/scripts/lib/learnings/schema.mjs +163 -15
  241. package/scripts/lib/learnings/sizing-subject.mjs +2 -2
  242. package/scripts/lib/maintenance-due-banner.mjs +53 -88
  243. package/scripts/lib/memory-proposals/schema.mjs +11 -5
  244. package/scripts/lib/named-baseline-resolver.mjs +53 -28
  245. package/scripts/lib/named-vault-resolver.mjs +104 -5
  246. package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
  247. package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
  248. package/scripts/lib/orphan-reaper/reaper-audit.mjs +291 -0
  249. package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
  250. package/scripts/lib/orphan-reaper/scan-throttle.mjs +92 -0
  251. package/scripts/lib/orphan-reaper/scan.mjs +506 -0
  252. package/scripts/lib/orphan-reaper.mjs +316 -0
  253. package/scripts/lib/owner-yaml.mjs +104 -6
  254. package/scripts/lib/peer-cards/merger.mjs +48 -10
  255. package/scripts/lib/peer-cards/reader.mjs +78 -2
  256. package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
  257. package/scripts/lib/process-group.mjs +950 -0
  258. package/scripts/lib/project-hygiene.mjs +21 -1
  259. package/scripts/lib/quality-gate.mjs +152 -33
  260. package/scripts/lib/reconcile/backlog.mjs +368 -0
  261. package/scripts/lib/reconcile/engine.mjs +55 -188
  262. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +302 -60
  263. package/scripts/lib/reconcile/sanitize.mjs +69 -3
  264. package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
  265. package/scripts/lib/resource-probe/parsers.mjs +31 -0
  266. package/scripts/lib/resource-probe.mjs +1 -1
  267. package/scripts/lib/rule-loader.mjs +141 -74
  268. package/scripts/lib/rules-sync.mjs +27 -12
  269. package/scripts/lib/scope-baseline.mjs +3 -4
  270. package/scripts/lib/scope-echo.mjs +39 -2
  271. package/scripts/lib/scope-gate.mjs +605 -1
  272. package/scripts/lib/session-close-backfill.mjs +33 -6
  273. package/scripts/lib/session-id.mjs +9 -20
  274. package/scripts/lib/session-invocation.mjs +20 -0
  275. package/scripts/lib/session-record-repair.mjs +183 -26
  276. package/scripts/lib/session-registry.mjs +27 -2
  277. package/scripts/lib/session-schema/constants.mjs +56 -2
  278. package/scripts/lib/session-schema/normalizer.mjs +56 -4
  279. package/scripts/lib/session-schema/validator.mjs +28 -1
  280. package/scripts/lib/session-schema.mjs +8 -3
  281. package/scripts/lib/session-start-probes.mjs +95 -10
  282. package/scripts/lib/session-token-rollup.mjs +245 -52
  283. package/scripts/lib/sessions-canonical.mjs +118 -4
  284. package/scripts/lib/sessions-integrity-banner.mjs +7 -1
  285. package/scripts/lib/sessions-staleness-banner.mjs +193 -51
  286. package/scripts/lib/sh-quote.mjs +29 -0
  287. package/scripts/lib/skill-evidence-window.mjs +891 -0
  288. package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
  289. package/scripts/lib/skill-evolution/engine.mjs +18 -9
  290. package/scripts/lib/skill-judge.mjs +45 -3
  291. package/scripts/lib/subagents-schema.mjs +30 -3
  292. package/scripts/lib/tail-window.mjs +56 -0
  293. package/scripts/lib/telemetry/pricing.mjs +118 -71
  294. package/scripts/lib/telemetry/schema.mjs +30 -0
  295. package/scripts/lib/telemetry/sync.mjs +61 -6
  296. package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
  297. package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
  298. package/scripts/lib/tests-src-ratio.mjs +140 -23
  299. package/scripts/lib/tmux-layout/layouts.mjs +1 -11
  300. package/scripts/lib/tmux-layout/telemetry-stats.mjs +72 -13
  301. package/scripts/lib/tmux-layout/tmux-shell.mjs +2 -11
  302. package/scripts/lib/user-invocable-skills.mjs +23 -3
  303. package/scripts/lib/ux-grill/reconcile.mjs +48 -22
  304. package/scripts/lib/validate/check-agents-skills.mjs +26 -15
  305. package/scripts/lib/validate/check-agents.mjs +1 -1
  306. package/scripts/lib/validate/check-cursor-adapter.mjs +1 -0
  307. package/scripts/lib/validate/check-entry-guard.mjs +13 -50
  308. package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
  309. package/scripts/lib/validate/check-hooks-symmetry.mjs +5 -0
  310. package/scripts/lib/validate/check-learnings-shell-writes.mjs +214 -0
  311. package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
  312. package/scripts/lib/validate/check-rules.mjs +7 -5
  313. package/scripts/lib/validate/check-skill-links.mjs +9 -1
  314. package/scripts/lib/validate/check-skill-script-paths.mjs +239 -27
  315. package/scripts/lib/validate/check-test-git-config-target.mjs +24 -34
  316. package/scripts/lib/validate/check-test-value-bans.mjs +87 -14
  317. package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
  318. package/scripts/lib/validate/check-untracked-test-deps.mjs +7 -102
  319. package/scripts/lib/validate/check-unwired-features.mjs +130 -27
  320. package/scripts/lib/validate/check-validator-registration.mjs +34 -10
  321. package/scripts/lib/validate/confidential-names.mjs +10 -0
  322. package/scripts/lib/validate-vendored-rules.mjs +210 -84
  323. package/scripts/lib/vault-mirror/namespace.mjs +46 -8
  324. package/scripts/lib/vault-mirror/process.mjs +10 -3
  325. package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
  326. package/scripts/lib/vault-mirror/telemetry.mjs +8 -1
  327. package/scripts/lib/vault-status/board-writer.mjs +48 -11
  328. package/scripts/lib/vault-status/narrative-mirror.mjs +71 -16
  329. package/scripts/lib/vault-yaml.mjs +118 -0
  330. package/scripts/lib/wave-executor/dispatch-common.mjs +10 -18
  331. package/scripts/lib/wave-executor/foreign-dispatch.mjs +1 -1
  332. package/scripts/lib/wave-executor/remote-dispatch.mjs +1 -1
  333. package/scripts/lib/wave-resource-gate.mjs +34 -8
  334. package/scripts/lib/worktree/lifecycle.mjs +153 -1
  335. package/scripts/memory-propose.mjs +18 -5
  336. package/scripts/release-session-lock.mjs +306 -0
  337. package/scripts/release.mjs +40 -8
  338. package/scripts/repair-invalid-sessions.mjs +37 -4
  339. package/scripts/resolve-session-invocation.mjs +59 -0
  340. package/scripts/run-migrate-v2-cross-repo.mjs +158 -30
  341. package/scripts/run-quality-gate.mjs +357 -197
  342. package/scripts/site-numbers.mjs +163 -5
  343. package/scripts/sweep-expired-learnings.mjs +56 -5
  344. package/scripts/sweep-expired-rules.mjs +16 -4
  345. package/scripts/validate-plugin.mjs +19 -0
  346. package/scripts/validate-wave-scope.mjs +32 -105
  347. package/scripts/vault-integration-watcher.mjs +2 -1
  348. package/scripts/vault-mirror.mjs +81 -47
  349. package/scripts/wave-scope-binding.mjs +2 -1
  350. package/skills/_shared/config-reading.md +3 -3
  351. package/skills/_shared/fleet-protocol.md +131 -0
  352. package/skills/_shared/platform-tools.md +23 -11
  353. package/skills/_shared/state-ownership.md +5 -0
  354. package/skills/autopilot/SKILL.md +47 -18
  355. package/skills/bootstrap/SKILL.md +1 -1
  356. package/skills/bootstrap/_shared-template.md +1 -1
  357. package/skills/bootstrap/fast-template.md +1 -1
  358. package/skills/bootstrap/private-contract.md +2 -1
  359. package/skills/bootstrap/public-fallback.md +6 -3
  360. package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
  361. package/skills/claude-md-drift-check/SKILL.md +10 -10
  362. package/skills/claude-md-drift-check/checker.mjs +26 -18
  363. package/skills/convergence-monitoring/README.md +8 -1
  364. package/skills/convergence-monitoring/SIGNALS.md +50 -6
  365. package/skills/convergence-monitoring/SKILL.md +15 -6
  366. package/skills/eval/SKILL.md +39 -24
  367. package/skills/eval/rubric-v1.md +1 -0
  368. package/skills/eval/rubric-v2.md +457 -0
  369. package/skills/evolve/SKILL.md +3 -3
  370. package/skills/evolve/references/evolve-analyze-mode.md +10 -6
  371. package/skills/evolve/references/evolve-dialectic-mode.md +42 -25
  372. package/skills/gitlab-ops/SKILL.md +3 -2
  373. package/skills/memory-cleanup/SKILL.md +1 -1
  374. package/skills/navigator/SKILL.md +193 -0
  375. package/skills/navigator/references/account-check.sh +87 -0
  376. package/skills/navigator/references/fleet-decisions.mjs +231 -0
  377. package/skills/navigator/references/slot-check.sh +114 -0
  378. package/skills/npm-publish/SKILL.md +1 -1
  379. package/skills/persona-panel/SKILL.md +2 -2
  380. package/skills/persona-panel/persona-format.md +2 -2
  381. package/skills/persona-panel/presets/designer-lens.md +1 -1
  382. package/skills/persona-panel/presets/engineer-lens.md +1 -1
  383. package/skills/persona-panel/presets/pm-lens.md +1 -1
  384. package/skills/plan/mode-retro.md +19 -4
  385. package/skills/quality-gates/SKILL.md +6 -4
  386. package/skills/reconcile/SKILL.md +11 -0
  387. package/skills/remote-offload/SKILL.md +4 -2
  388. package/skills/session-end/SKILL.md +13 -16
  389. package/skills/session-end/discovery-scan.md +1 -1
  390. package/skills/session-end/learning-patterns.md +22 -12
  391. package/skills/session-end/metrics-collection.md +5 -6
  392. package/skills/session-end/phase-3-6-tail.md +55 -9
  393. package/skills/session-end/references/phase-3-documentation-updates.md +2 -2
  394. package/skills/session-end/references/phase-4a-worktree-cleanup.md +2 -0
  395. package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
  396. package/skills/session-end/references/session-summary-template.md +1 -1
  397. package/skills/session-end/session-metrics-write.md +32 -60
  398. package/skills/session-plan/SKILL.md +17 -8
  399. package/skills/session-plan/references/session-plan-task-classification.md +2 -2
  400. package/skills/session-start/SKILL.md +6 -0
  401. package/skills/session-start/references/operations-contract.md +4 -0
  402. package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
  403. package/skills/session-start/references/phase-4-ssot-environment-check.md +4 -3
  404. package/skills/test-audit/SKILL.md +152 -0
  405. package/skills/test-audit/references/campaign.md +111 -0
  406. package/skills/test-audit/references/mutate.sh +50 -0
  407. package/skills/ux-grill/SKILL.md +1 -1
  408. package/skills/wave-executor/SKILL.md +9 -5
  409. package/skills/wave-executor/circuit-breaker.md +2 -0
  410. package/skills/wave-executor/references/wave-executor-state-init.md +8 -3
  411. package/skills/wave-executor/references/wave-loop-dispatch.md +5 -2
  412. package/skills/wave-executor/references/wave-loop-review.md +3 -5
  413. package/templates/personas/README.md +5 -5
  414. package/templates/personas/accounting-compliance.v1.md +1 -1
  415. package/templates/personas/accounting-tax-advisor.v1.md +1 -1
  416. package/templates/personas/buyer-p1-cto.v1.md +1 -1
  417. package/templates/personas/buyer-p2-kanzlei.v1.md +1 -1
  418. package/templates/personas/buyer-p3-build.v1.md +1 -1
  419. package/templates/personas/buyer-p4-tech-veto.v1.md +1 -1
  420. package/templates/personas/buyer-p5-solo.v1.md +1 -1
  421. package/templates/personas/buyer-p6-ld.v1.md +1 -1
  422. package/templates/personas/klima-ai-expert.v1.md +1 -1
  423. package/templates/personas/klima-physicist.v1.md +1 -1
  424. package/.cursor/commands/convergence-monitoring.md +0 -13
  425. package/.cursor/commands/npm-publish.md +0 -13
  426. package/pi/prompts/convergence-monitoring.md +0 -11
  427. package/pi/prompts/npm-publish.md +0 -11
@@ -0,0 +1,368 @@
1
+ /**
2
+ * backlog.mjs — LEAF module for the reconcile BACKLOG count (#1380 follow-up).
3
+ *
4
+ * Holds the side-effect-free half of the reconcile pipeline: the corpus/sidecar/
5
+ * provenance readers, the materialization partition, and the backlog count the
6
+ * session-start nudge judges on. Split out of `engine.mjs` so a read-only probe
7
+ * no longer has to import the WRITE-capable orchestrator to ask a counting
8
+ * question.
9
+ *
10
+ * ── Why a separate file, and what "leaf" binds to ───────────────────────────
11
+ * `scripts/lib/reconcile-nudge-banner.mjs` is a session-start probe: it renders
12
+ * one advisory line and touches nothing. Importing `countReconcileBacklog` from
13
+ * `engine.mjs` pulled the renderer, the emitter, the unicode-safety validator and
14
+ * the candidate-store writer into that probe's static closure — measured
15
+ * 2026-09-18: 11 modules / 151,946 B before the engine import, 19 modules /
16
+ * 322,897 B after (+112 %), inherited unasked by
17
+ * `scripts/lib/maintenance-due-banner.mjs`, which imports the probe.
18
+ * The runtime cost was never the problem (0.55 % of the 2-s probe budget); the
19
+ * DIRECTION was — a banner must not be able to reach a writer.
20
+ *
21
+ * So this module imports ONLY `node:*` plus light siblings that are themselves
22
+ * free of write paths: `../learnings/kebab.mjs`, `../learnings/schema.mjs`,
23
+ * `./eligibility.mjs` (pure partition), `../rule-loader.mjs` (#1387 — zero
24
+ * relative imports and no write paths of its own, measured 2026-09-18) and
25
+ * `./idempotency.mjs` (its READ half —
26
+ * `loadCandidates` / `isProcessed`; `mergeCandidates`, the only writer there, is
27
+ * never referenced from here). It MUST NOT import `./engine.mjs`,
28
+ * `./emitter.mjs`, `./renderer.mjs` or `./writer.mjs` — a future import of any
29
+ * of those re-creates exactly the coupling this file exists to break.
30
+ *
31
+ * `engine.mjs` imports from here and RE-EXPORTS `partitionMaterialized` +
32
+ * `countReconcileBacklog`, so every existing caller keeps working unchanged.
33
+ *
34
+ * Cross-references:
35
+ * - `scripts/lib/reconcile/engine.mjs` — the orchestrator (write side).
36
+ * - `scripts/lib/reconcile-nudge-banner.mjs` — the probe that imports this leaf.
37
+ * - `.claude/rules/host-resources.md` § HR-106 — the banner reports what the rule judges.
38
+ * - Issues #484 / #1242 (dedupe contract), #1380 (backlog vs. eligible).
39
+ */
40
+
41
+ import { readFileSync, readdirSync } from 'node:fs';
42
+ import { isAbsolute, join } from 'node:path';
43
+
44
+ import { learningKeyOf } from '../learnings/kebab.mjs';
45
+ import { migrateLegacyLearning, normalizeLearning } from '../learnings/schema.mjs';
46
+ import { isRuleExpired, parseGlobsFrontmatter } from '../rule-loader.mjs';
47
+ import { filterEligible } from './eligibility.mjs';
48
+ import { isProcessed, loadCandidates as realLoadCandidates } from './idempotency.mjs';
49
+
50
+ /** Default repo-relative location of the learnings corpus. */
51
+ export const DEFAULT_LEARNINGS_PATH = '.orchestrator/metrics/learnings.jsonl';
52
+
53
+ /**
54
+ * Default learnings loader — read + parse `<repoRoot>/.orchestrator/metrics/learnings.jsonl`
55
+ * line-by-line, migrate/normalize records through the learnings schema SSOT,
56
+ * and skip blank/malformed lines. A missing file (ENOENT) yields `[]`
57
+ * silently; an unreadable one (EACCES/EISDIR/…) yields `[]` with a stderr
58
+ * WARN (#1210 — ENOENT and other read failures are different facts, same
59
+ * split as `sessions-canonical.mjs` `readCanonicalSessions`).
60
+ *
61
+ * @param {string|undefined} repoRoot
62
+ * @returns {Array<Record<string, unknown>>}
63
+ */
64
+ export function defaultLoadLearnings(repoRoot) {
65
+ const root = typeof repoRoot === 'string' && repoRoot.length > 0 ? repoRoot : process.cwd();
66
+ const absPath = isAbsolute(DEFAULT_LEARNINGS_PATH)
67
+ ? DEFAULT_LEARNINGS_PATH
68
+ : join(root, DEFAULT_LEARNINGS_PATH);
69
+
70
+ let raw;
71
+ try {
72
+ raw = readFileSync(absPath, 'utf8');
73
+ } catch (err) {
74
+ if (!err || err.code !== 'ENOENT') {
75
+ process.stderr.write(
76
+ `⚠ defaultLoadLearnings: cannot read ${absPath} ` +
77
+ `(${err?.code ?? '?'}: ${err?.message ?? String(err)}) — ` +
78
+ 'treating as EMPTY, counts below are floors\n',
79
+ );
80
+ }
81
+ return [];
82
+ }
83
+
84
+ /** @type {Array<Record<string, unknown>>} */
85
+ const records = [];
86
+ for (const line of raw.split('\n')) {
87
+ const trimmed = line.trim();
88
+ if (trimmed.length === 0) continue;
89
+ let parsed;
90
+ try {
91
+ parsed = JSON.parse(trimmed);
92
+ } catch {
93
+ continue; // skip malformed line
94
+ }
95
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
96
+ records.push(
97
+ /** @type {Record<string, unknown>} */ (normalizeLearning(migrateLegacyLearning(parsed))),
98
+ );
99
+ }
100
+ }
101
+ return records;
102
+ }
103
+
104
+ /**
105
+ * Bring an INJECTED corpus onto the same dialect {@link defaultLoadLearnings}
106
+ * produces, so "which learnings were counted" cannot depend on WHO read the file.
107
+ *
108
+ * The nudge loads the corpus through `readLearnings` (`../learnings/io.mjs`),
109
+ * which applies `normalizeLearning` but NOT `migrateLegacyLearning` — so a
110
+ * record still carrying the legacy `files` key would reach the eligibility gate
111
+ * without its `file_paths` axis and be rejected here while `/reconcile` (which
112
+ * loads via `defaultLoadLearnings`) accepts it. Two populations in one banner
113
+ * line is the HR-106 defect; running the injected corpus through the same two
114
+ * functions removes the fork at its source rather than asking every caller to
115
+ * remember.
116
+ *
117
+ * Both functions are idempotent on already-normalized records (verified
118
+ * 2026-09-18 against this repo's corpus), so the pass is a no-op for a corpus
119
+ * that already went through the loader. A record the normalizer rejects is kept
120
+ * VERBATIM rather than dropped: the eligibility gate downstream is the place
121
+ * that decides a record's fate, and silently shrinking the denominator here
122
+ * would under-report the backlog.
123
+ *
124
+ * @param {Array<Record<string, unknown>>} learnings
125
+ * @returns {Array<Record<string, unknown>>}
126
+ */
127
+ function normalizeInjectedCorpus(learnings) {
128
+ return learnings.map((entry) => {
129
+ try {
130
+ return /** @type {Record<string, unknown>} */ (normalizeLearning(migrateLegacyLearning(entry)));
131
+ } catch {
132
+ return entry;
133
+ }
134
+ });
135
+ }
136
+
137
+ /**
138
+ * Default sidecar-candidate loader for the issue #484 idempotency dedupe
139
+ * check. Deliberately gated on `repoRoot` being a caller-supplied, non-empty
140
+ * string — UNLIKE `defaultLoadLearnings` and `mergeCandidates`, this does NOT
141
+ * fall back to `process.cwd()` when `repoRoot` is absent. Every existing engine
142
+ * test exercises the pipeline via injected learnings with no `repoRoot`,
143
+ * precisely to avoid touching this repo's OWN
144
+ * `.orchestrator/runtime/reconcile-candidates.jsonl`; a cwd fallback here would
145
+ * silently read it.
146
+ *
147
+ * @param {string|undefined} repoRoot
148
+ * @returns {{ records: import('./idempotency.mjs').ReconcileCandidate[] }}
149
+ */
150
+ export function defaultLoadCandidatesForDedupe(repoRoot) {
151
+ if (typeof repoRoot !== 'string' || repoRoot.length === 0) return { records: [] };
152
+ const { records } = realLoadCandidates({ repoRoot });
153
+ return { records };
154
+ }
155
+
156
+ /** Frontmatter form emitted by renderer.mjs: `learning-key: <value>` (no backticks, no leading dash). */
157
+ const FRONTMATTER_LEARNING_KEY_RE = /^learning-key:\s*(.+)$/gm;
158
+ /** Provenance-body form emitted by renderer.mjs: `` - learning-key: `<value>` ``. */
159
+ const BODY_LEARNING_KEY_RE = /-\s*learning-key:\s*`([^`]+)`/g;
160
+ /** Provenance-body form emitted by renderer.mjs: `` - learning-id: `<value>` ``. */
161
+ const BODY_LEARNING_ID_RE = /-\s*learning-id:\s*`([^`]+)`/g;
162
+
163
+ /**
164
+ * Scan `<repoRoot>/.claude/rules/*.md` for the provenance markers the
165
+ * renderer stamps on every machine-generated rule — the frontmatter
166
+ * `learning-key:` line and the body `## Provenance` block's `learning-key`/
167
+ * `learning-id` bullets (`renderer.mjs`) — and return the two identity sets a
168
+ * learning can already be materialized under. A learning whose derived
169
+ * `learning_key` OR raw `.id` appears in either set already has a rule file
170
+ * on disk: re-proposing it is the issue #484 defect (9 of 10 proposals in one
171
+ * run were learnings a `.claude/rules/` file already covered).
172
+ *
173
+ * **This scan is the AUTHORITATIVE half of the dedupe contract** (#1242). The
174
+ * `.claude/rules/*.md` files it reads are TRACKED, so they survive a fresh
175
+ * clone, a wiped working copy, and any loss of `.orchestrator/runtime/` (which
176
+ * is gitignored — `.gitignore:114`). The idempotency sidecar consulted beside
177
+ * it is a CACHE that can only SHORT-CIRCUIT this scan, never replace it: on a
178
+ * fresh clone the sidecar is empty and correctness rests entirely on the
179
+ * markers below. Measured 2026-09-07 on this repo: with the sidecar emptied,
180
+ * the run produced the identical 10 proposals and 30 "already materialized"
181
+ * rejections; with this scan disabled instead, 5 already-consolidated
182
+ * learnings were re-proposed.
183
+ *
184
+ * Both marker forms are load-bearing. Frontmatter `learning-key:` is a YAML
185
+ * SCALAR and can name exactly ONE learning, so a CONSOLIDATED rule file (one
186
+ * file absorbing N learnings) carries the remaining N-1 identities ONLY as
187
+ * `## Provenance` body bullets. Breaking {@link BODY_LEARNING_KEY_RE} would
188
+ * therefore silently re-propose most of a consolidated corpus while every
189
+ * single-learning file still deduped correctly — pinned by the "fresh clone,
190
+ * consolidated shape" test in `tests/lib/reconcile/engine.test.mjs`.
191
+ *
192
+ * An EXPIRED rule file is skipped WHOLE — frontmatter key and body bullets
193
+ * alike (#1387). `rule-loader.mjs` already refuses to inject such a file, so
194
+ * counting its markers here declared a learning materialized by a rule that no
195
+ * longer reaches any agent: materialized forever, re-proposable never. The
196
+ * expiry verdict comes from `isRuleExpired` in `rule-loader.mjs` — the SAME
197
+ * predicate the injection gate uses, and fail-open the same way (an absent or
198
+ * unparseable `expires-at` is NOT expired; fail-closed would un-materialize the
199
+ * whole corpus on one typo).
200
+ *
201
+ * KNOWN LIMIT (measured 2026-09-18): this covers only the on-disk half of
202
+ * {@link partitionMaterialized} (`sidecarTerminal || onDisk`). 79 of this
203
+ * repo's 92 provenance keys are ALSO terminal in the idempotency sidecar, so
204
+ * only 13 actually become re-proposable; the sidecar half is left untouched on
205
+ * purpose because it carries operator DECLINES. See `docs/rule-authoring.md`
206
+ * § Consolidated rules.
207
+ *
208
+ * Gated the same way as {@link defaultLoadCandidatesForDedupe}: an absent
209
+ * `repoRoot` yields empty sets rather than falling back to `process.cwd()`.
210
+ * Never throws — a missing `.claude/rules/` dir or an unreadable file
211
+ * degrades to "nothing materialized" for that source, never a crash.
212
+ * @param {string|undefined} repoRoot
213
+ * @returns {{ keys: Set<string>, ids: Set<string> }}
214
+ */
215
+ export function defaultReadMaterializedProvenance(repoRoot) {
216
+ const keys = new Set();
217
+ const ids = new Set();
218
+ if (typeof repoRoot !== 'string' || repoRoot.length === 0) return { keys, ids };
219
+
220
+ // One clock reading for the whole scan, so two files cannot be judged
221
+ // against two different "now"s. Positional-only signature by contract (the
222
+ // engine's DI seam types it as `(repoRoot) => …`), hence no injectable clock.
223
+ const nowMs = Date.now();
224
+ const rulesDir = join(repoRoot, '.claude', 'rules');
225
+ let entries;
226
+ try {
227
+ entries = readdirSync(rulesDir);
228
+ } catch {
229
+ return { keys, ids }; // no rules dir yet → nothing materialized
230
+ }
231
+
232
+ for (const entry of entries) {
233
+ if (!entry.endsWith('.md')) continue;
234
+ let content;
235
+ try {
236
+ content = readFileSync(join(rulesDir, entry), 'utf8');
237
+ } catch {
238
+ continue; // unreadable file — skip it, do not fail the whole scan
239
+ }
240
+ // #1387 — an expired rule no longer reaches any agent, so its markers must
241
+ // not keep their learnings materialized. Malformed frontmatter throws here
242
+ // and is treated as "no expiry" (fail-open, same direction as the gate).
243
+ let expiresAt;
244
+ try {
245
+ ({ 'expires-at': expiresAt } = parseGlobsFrontmatter(content).meta);
246
+ } catch {
247
+ expiresAt = undefined;
248
+ }
249
+ if (isRuleExpired(expiresAt, nowMs)) continue;
250
+ for (const m of content.matchAll(FRONTMATTER_LEARNING_KEY_RE)) {
251
+ const v = m[1].trim();
252
+ if (v) keys.add(v);
253
+ }
254
+ for (const m of content.matchAll(BODY_LEARNING_KEY_RE)) {
255
+ const v = m[1].trim();
256
+ if (v) keys.add(v);
257
+ }
258
+ for (const m of content.matchAll(BODY_LEARNING_ID_RE)) {
259
+ const v = m[1].trim();
260
+ if (v && v !== 'n/a') ids.add(v);
261
+ }
262
+ }
263
+ return { keys, ids };
264
+ }
265
+
266
+ /**
267
+ * Split rule-eligible learnings into the ones still awaiting a rule and the ones
268
+ * already materialized (#484/#1242 dedupe contract). A learning is materialized
269
+ * when the idempotency sidecar carries a terminal verdict for its `learning_key`
270
+ * (CACHE) OR a `.claude/rules/*.md` provenance marker names its key/id
271
+ * (AUTHORITATIVE).
272
+ *
273
+ * Pure: reads nothing, writes nothing, emits nothing — the engine and the
274
+ * session-start reconcile nudge share it so both judge the same backlog (#1380).
275
+ *
276
+ * @param {Array<Record<string, unknown>>} eligible - output of `filterEligible().eligible`.
277
+ * @param {{ existingCandidates: import('./idempotency.mjs').ReconcileCandidate[], materialized: { keys: Set<string>, ids: Set<string> } }} sources
278
+ * @returns {{
279
+ * stillEligible: Array<Record<string, unknown>>,
280
+ * materializedItems: Array<{ learning: Record<string, unknown>, learningKey: string|null, sidecarTerminal: boolean, onDisk: boolean }>,
281
+ * }}
282
+ */
283
+ export function partitionMaterialized(eligible, { existingCandidates, materialized }) {
284
+ /** @type {Array<Record<string, unknown>>} */
285
+ const stillEligible = [];
286
+ const materializedItems = [];
287
+ for (const learning of eligible) {
288
+ const learningKey = learningKeyOf(learning);
289
+ const learningId =
290
+ learning &&
291
+ typeof learning === 'object' &&
292
+ typeof learning.id === 'string' &&
293
+ learning.id.length > 0
294
+ ? learning.id
295
+ : null;
296
+
297
+ const sidecarTerminal =
298
+ learningKey !== null && isProcessed({ learning_key: learningKey }, existingCandidates);
299
+ const onDisk =
300
+ (learningKey !== null && materialized.keys.has(learningKey)) ||
301
+ (learningId !== null && materialized.ids.has(learningId));
302
+
303
+ if (!sidecarTerminal && !onDisk) {
304
+ stillEligible.push(learning);
305
+ } else {
306
+ materializedItems.push({ learning, learningKey, sidecarTerminal, onDisk });
307
+ }
308
+ }
309
+ return { stillEligible, materializedItems };
310
+ }
311
+
312
+ /**
313
+ * Count the reconcile BACKLOG the way a real `/reconcile` run would see it:
314
+ * rule-eligible learnings (expiry-gated by `now`) minus those already
315
+ * materialized in the sidecar or under `.claude/rules/`. This is the quantity
316
+ * `/reconcile` can actually drive to zero — the session-start nudge judges on it
317
+ * (#1380; HR-101: a probe that counts already-materialized learnings can never
318
+ * go green).
319
+ *
320
+ * Side-effect-free (no event, no sidecar write) so session-start probes may call
321
+ * it. Never throws — any failure degrades to all-zero counts.
322
+ *
323
+ * @param {object} [params]
324
+ * @param {string} [params.repoRoot] - repo root; required for the sidecar + rules scan
325
+ * (absent → both sources are empty, same gate as the engine defaults).
326
+ * @param {Array<Record<string, unknown>>} [params.learnings] - injected corpus;
327
+ * defaults to {@link defaultLoadLearnings} for
328
+ * `<repoRoot>/.orchestrator/metrics/learnings.jsonl`. An injected corpus is
329
+ * run through {@link normalizeInjectedCorpus} first, so injecting can never
330
+ * silently count a DIFFERENT population than loading would.
331
+ * @param {import('./idempotency.mjs').ReconcileCandidate[]} [params.existingCandidates]
332
+ * already-loaded sidecar records (DI seam for a caller that read the store
333
+ * itself — the session-start nudge does, for the last-run timestamp). Absent
334
+ * ⇒ this function reads the store via {@link defaultLoadCandidatesForDedupe}.
335
+ * @param {Date|number} [params.now] - clock for the expiry gate (defaults to `Date.now()`).
336
+ * @param {number} [params.minInsightChars] - forwarded to `filterEligible`.
337
+ * @returns {{ eligible: number, alreadyMaterialized: number, backlog: number }}
338
+ */
339
+ export function countReconcileBacklog({
340
+ repoRoot,
341
+ learnings,
342
+ existingCandidates,
343
+ now,
344
+ minInsightChars,
345
+ } = {}) {
346
+ try {
347
+ const corpus = Array.isArray(learnings)
348
+ ? normalizeInjectedCorpus(learnings)
349
+ : defaultLoadLearnings(repoRoot);
350
+ if (corpus.length === 0) return { eligible: 0, alreadyMaterialized: 0, backlog: 0 };
351
+ const nowMs =
352
+ now instanceof Date ? now.getTime() : typeof now === 'number' ? now : Date.now();
353
+ const { eligible } = filterEligible(corpus, { now: nowMs, minInsightChars });
354
+ const { stillEligible, materializedItems } = partitionMaterialized(eligible, {
355
+ existingCandidates: Array.isArray(existingCandidates)
356
+ ? existingCandidates
357
+ : defaultLoadCandidatesForDedupe(repoRoot).records,
358
+ materialized: defaultReadMaterializedProvenance(repoRoot),
359
+ });
360
+ return {
361
+ eligible: eligible.length,
362
+ alreadyMaterialized: materializedItems.length,
363
+ backlog: stillEligible.length,
364
+ };
365
+ } catch {
366
+ return { eligible: 0, alreadyMaterialized: 0, backlog: 0 };
367
+ }
368
+ }
@@ -104,26 +104,37 @@
104
104
  * @property {string} [error] - present only when the never-throws top-level guard fired.
105
105
  */
106
106
 
107
- import { readFileSync, readdirSync } from 'node:fs';
108
- import { isAbsolute, join } from 'node:path';
107
+ import { homedir } from 'node:os';
108
+ import { isAbsolute } from 'node:path';
109
109
 
110
110
  import { expandTilde } from '../common.mjs';
111
111
  import { learningKeyOf } from '../learnings/kebab.mjs';
112
- import { migrateLegacyLearning, normalizeLearning } from '../learnings/schema.mjs';
112
+ import {
113
+ countReconcileBacklog,
114
+ defaultLoadCandidatesForDedupe,
115
+ defaultLoadLearnings,
116
+ defaultReadMaterializedProvenance,
117
+ partitionMaterialized,
118
+ } from './backlog.mjs';
113
119
  import { filterEligible } from './eligibility.mjs';
114
120
  import { toActivationMetadata } from './emitter.mjs';
115
121
  import { renderRule } from './renderer.mjs';
116
122
  import {
117
123
  DEFAULT_STORE_PATH,
118
124
  buildCandidate,
119
- isProcessed,
120
125
  makeCandidateId,
121
- loadCandidates as realLoadCandidates,
122
126
  mergeCandidates as realMergeCandidates,
123
127
  } from './idempotency.mjs';
124
128
 
125
- /** Default repo-relative location of the learnings corpus. */
126
- const DEFAULT_LEARNINGS_PATH = '.orchestrator/metrics/learnings.jsonl';
129
+ /**
130
+ * Re-exported from the LEAF module `./backlog.mjs` (which now owns the
131
+ * side-effect-free backlog half) so every existing importer of these two names
132
+ * keeps working unchanged. A read-only probe should import them from
133
+ * `./backlog.mjs` DIRECTLY — importing them from here drags this orchestrator's
134
+ * whole closure (emitter, renderer, unicode validator, candidate-store writer)
135
+ * into the probe. See that module's header for the measured cost.
136
+ */
137
+ export { countReconcileBacklog, partitionMaterialized };
127
138
 
128
139
  /**
129
140
  * Default volume brake (issue #900 D) — mirrors the `reconcile.max-proposals-
@@ -157,57 +168,6 @@ function zeroedResult(error) {
157
168
  return result;
158
169
  }
159
170
 
160
- /**
161
- * Default learnings loader — read + parse `<repoRoot>/.orchestrator/metrics/learnings.jsonl`
162
- * line-by-line, migrate/normalize records through the learnings schema SSOT,
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`).
167
- *
168
- * @param {string|undefined} repoRoot
169
- * @returns {Array<Record<string, unknown>>}
170
- */
171
- function defaultLoadLearnings(repoRoot) {
172
- const root = typeof repoRoot === 'string' && repoRoot.length > 0 ? repoRoot : process.cwd();
173
- const absPath = isAbsolute(DEFAULT_LEARNINGS_PATH)
174
- ? DEFAULT_LEARNINGS_PATH
175
- : join(root, DEFAULT_LEARNINGS_PATH);
176
-
177
- let raw;
178
- try {
179
- raw = readFileSync(absPath, 'utf8');
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 [];
189
- }
190
-
191
- /** @type {Array<Record<string, unknown>>} */
192
- const records = [];
193
- for (const line of raw.split('\n')) {
194
- const trimmed = line.trim();
195
- if (trimmed.length === 0) continue;
196
- let parsed;
197
- try {
198
- parsed = JSON.parse(trimmed);
199
- } catch {
200
- continue; // skip malformed line
201
- }
202
- if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
203
- records.push(
204
- /** @type {Record<string, unknown>} */ (normalizeLearning(migrateLegacyLearning(parsed))),
205
- );
206
- }
207
- }
208
- return records;
209
- }
210
-
211
171
  /**
212
172
  * Best-effort logical key for a REJECTED learning (rejections never run the
213
173
  * emitter, so there is no `metadata.learningKey`). Delegates to the shared
@@ -352,109 +312,6 @@ export function resolveEffectiveTargets({ targets, baselineRoot } = {}) {
352
312
  };
353
313
  }
354
314
 
355
- /**
356
- * Default sidecar-candidate loader for the issue #484 idempotency dedupe
357
- * check (below, step 3a). Deliberately gated on `repoRoot` being a
358
- * caller-supplied, non-empty string — UNLIKE `defaultLoadLearnings` and
359
- * `realMergeCandidates`, this does NOT fall back to `process.cwd()` when
360
- * `repoRoot` is absent. Every existing engine test exercises this module via
361
- * `opts.learnings` with no `repoRoot`, precisely to avoid touching this repo's
362
- * OWN `.orchestrator/runtime/reconcile-candidates.jsonl`; a cwd fallback here
363
- * would silently read it. The disk-touching default is reserved for callers
364
- * that always pass an explicit `repoRoot` (the `/reconcile` skill resolves it
365
- * via `git rev-parse --show-toplevel`).
366
- * @param {string|undefined} repoRoot
367
- * @returns {{ records: import('./idempotency.mjs').ReconcileCandidate[] }}
368
- */
369
- function defaultLoadCandidatesForDedupe(repoRoot) {
370
- if (typeof repoRoot !== 'string' || repoRoot.length === 0) return { records: [] };
371
- const { records } = realLoadCandidates({ repoRoot });
372
- return { records };
373
- }
374
-
375
- /** Frontmatter form emitted by renderer.mjs: `learning-key: <value>` (no backticks, no leading dash). */
376
- const FRONTMATTER_LEARNING_KEY_RE = /^learning-key:\s*(.+)$/gm;
377
- /** Provenance-body form emitted by renderer.mjs: `` - learning-key: `<value>` ``. */
378
- const BODY_LEARNING_KEY_RE = /-\s*learning-key:\s*`([^`]+)`/g;
379
- /** Provenance-body form emitted by renderer.mjs: `` - learning-id: `<value>` ``. */
380
- const BODY_LEARNING_ID_RE = /-\s*learning-id:\s*`([^`]+)`/g;
381
-
382
- /**
383
- * Scan `<repoRoot>/.claude/rules/*.md` for the provenance markers the
384
- * renderer stamps on every machine-generated rule — the frontmatter
385
- * `learning-key:` line and the body `## Provenance` block's `learning-key`/
386
- * `learning-id` bullets (`renderer.mjs`) — and return the two identity sets a
387
- * learning can already be materialized under. A learning whose derived
388
- * `learning_key` OR raw `.id` appears in either set already has a rule file
389
- * on disk: re-proposing it is the issue #484 defect (9 of 10 proposals in one
390
- * run were learnings a `.claude/rules/` file already covered).
391
- *
392
- * **This scan is the AUTHORITATIVE half of the dedupe contract** (#1242). The
393
- * `.claude/rules/*.md` files it reads are TRACKED, so they survive a fresh
394
- * clone, a wiped working copy, and any loss of `.orchestrator/runtime/` (which
395
- * is gitignored — `.gitignore:114`). The idempotency sidecar consulted beside
396
- * it is a CACHE that can only SHORT-CIRCUIT this scan, never replace it: on a
397
- * fresh clone the sidecar is empty and correctness rests entirely on the
398
- * markers below. Measured 2026-09-07 on this repo: with the sidecar emptied,
399
- * the run produced the identical 10 proposals and 30 "already materialized"
400
- * rejections; with this scan disabled instead, 5 already-consolidated
401
- * learnings were re-proposed.
402
- *
403
- * Both marker forms are load-bearing. Frontmatter `learning-key:` is a YAML
404
- * SCALAR and can name exactly ONE learning, so a CONSOLIDATED rule file (one
405
- * file absorbing N learnings) carries the remaining N-1 identities ONLY as
406
- * `## Provenance` body bullets. Breaking {@link BODY_LEARNING_KEY_RE} would
407
- * therefore silently re-propose most of a consolidated corpus while every
408
- * single-learning file still deduped correctly — pinned by the "fresh clone,
409
- * consolidated shape" test in `tests/lib/reconcile/engine.test.mjs`.
410
- *
411
- * `rule-loader.mjs` only EXCLUDES expired rules from injection; it never
412
- * deletes a file, so an expired rule keeps deduping through these markers.
413
- *
414
- * Gated the same way as {@link defaultLoadCandidatesForDedupe}: an absent
415
- * `repoRoot` yields empty sets rather than falling back to `process.cwd()`.
416
- * Never throws — a missing `.claude/rules/` dir or an unreadable file
417
- * degrades to "nothing materialized" for that source, never a crash.
418
- * @param {string|undefined} repoRoot
419
- * @returns {{ keys: Set<string>, ids: Set<string> }}
420
- */
421
- function defaultReadMaterializedProvenance(repoRoot) {
422
- const keys = new Set();
423
- const ids = new Set();
424
- if (typeof repoRoot !== 'string' || repoRoot.length === 0) return { keys, ids };
425
-
426
- const rulesDir = join(repoRoot, '.claude', 'rules');
427
- let entries;
428
- try {
429
- entries = readdirSync(rulesDir);
430
- } catch {
431
- return { keys, ids }; // no rules dir yet → nothing materialized
432
- }
433
-
434
- for (const entry of entries) {
435
- if (!entry.endsWith('.md')) continue;
436
- let content;
437
- try {
438
- content = readFileSync(join(rulesDir, entry), 'utf8');
439
- } catch {
440
- continue; // unreadable file — skip it, do not fail the whole scan
441
- }
442
- for (const m of content.matchAll(FRONTMATTER_LEARNING_KEY_RE)) {
443
- const v = m[1].trim();
444
- if (v) keys.add(v);
445
- }
446
- for (const m of content.matchAll(BODY_LEARNING_KEY_RE)) {
447
- const v = m[1].trim();
448
- if (v) keys.add(v);
449
- }
450
- for (const m of content.matchAll(BODY_LEARNING_ID_RE)) {
451
- const v = m[1].trim();
452
- if (v && v !== 'n/a') ids.add(v);
453
- }
454
- }
455
- return { keys, ids };
456
- }
457
-
458
315
  /**
459
316
  * Run the reconciliation engine.
460
317
  *
@@ -597,32 +454,13 @@ async function runReconcileInner(
597
454
  const { records: existingCandidates } = loadCandidatesForDedupe(repoRoot) ?? { records: [] };
598
455
  const materialized = readMaterializedProvenance(repoRoot) ?? { keys: new Set(), ids: new Set() };
599
456
 
600
- /** @type {Array<Record<string, unknown>>} */
601
- const stillEligible = [];
602
- let alreadyMaterialized = 0;
603
-
604
- for (const learning of eligible) {
605
- const learningKey = rejectedLearningKey(learning);
606
- const learningId =
607
- learning &&
608
- typeof learning === 'object' &&
609
- typeof learning.id === 'string' &&
610
- learning.id.length > 0
611
- ? learning.id
612
- : null;
613
-
614
- const sidecarTerminal =
615
- learningKey !== null && isProcessed({ learning_key: learningKey }, existingCandidates);
616
- const onDisk =
617
- (learningKey !== null && materialized.keys.has(learningKey)) ||
618
- (learningId !== null && materialized.ids.has(learningId));
619
-
620
- if (!sidecarTerminal && !onDisk) {
621
- stillEligible.push(learning);
622
- continue;
623
- }
457
+ const { stillEligible, materializedItems } = partitionMaterialized(eligible, {
458
+ existingCandidates,
459
+ materialized,
460
+ });
461
+ const alreadyMaterialized = materializedItems.length;
624
462
 
625
- alreadyMaterialized += 1;
463
+ for (const { learning, learningKey, sidecarTerminal, onDisk } of materializedItems) {
626
464
  const type = learningType(learning);
627
465
  const reason = sidecarTerminal
628
466
  ? 'already processed — the idempotency sidecar already carries a terminal verdict for this learning-key'
@@ -882,6 +720,35 @@ export function assertKnownTrigger(trigger) {
882
720
  /** Clamp for the `reason` string on the abort path — a message can be long. */
883
721
  const REASON_MAX_CHARS = 300;
884
722
 
723
+ /**
724
+ * Replace the repo root and the home directory inside a free-text error message
725
+ * with the stable placeholders `<repo>` and `~`.
726
+ *
727
+ * An `fs` error message embeds the absolute path it failed on, so the abort
728
+ * `reason` otherwise carried `/Users/<name>/…` into the ledger and the optional
729
+ * webhook — sibling payloads never carry the absolute root (`docs/events-schema.md`).
730
+ * Runs BEFORE the clamp, so the 300 chars are spent on the tail, not on a prefix
731
+ * cut mid-path. The repo root goes first because it usually sits under home.
732
+ *
733
+ * Named ceiling (BV-004): only the two literal spellings are replaced. A path the
734
+ * OS reports in a different spelling — a symlink-resolved `/private/var/…` for a
735
+ * `/var/…` repo root — keeps its non-home prefix. REVISIT TRIGGER: a ledger
736
+ * `reason` carrying an absolute path outside both placeholders.
737
+ *
738
+ * @param {string} message
739
+ * @param {string|undefined} repoRoot
740
+ * @returns {string}
741
+ */
742
+ function redactLocalPaths(message, repoRoot) {
743
+ let out = message;
744
+ // A relative or bare-`/` root would match arbitrary substrings — absolute only.
745
+ const root = typeof repoRoot === 'string' ? repoRoot.trim().replace(/\/+$/, '') : '';
746
+ if (root !== '' && isAbsolute(root)) out = out.split(root).join('<repo>');
747
+ const home = homedir().replace(/\/+$/, '');
748
+ if (home !== '' && isAbsolute(home)) out = out.split(home).join('~');
749
+ return out;
750
+ }
751
+
885
752
  /** The closed target enum `resolveEffectiveTargets` recognises. */
886
753
  const KNOWN_TARGETS = ['repo-local', 'baseline'];
887
754
 
@@ -904,7 +771,7 @@ const KNOWN_TARGETS = ['repo-local', 'baseline'];
904
771
  * same boolean until 2027-03-13 (#1315).
905
772
  *
906
773
  * @param {ReconcileResult} result
907
- * @param {{ trigger?: string, targets?: string[], dryRun?: boolean, durationMs: number }} ctx
774
+ * @param {{ repoRoot?: string, trigger?: string, targets?: string[], dryRun?: boolean, durationMs: number }} ctx
908
775
  * @returns {Record<string, unknown>}
909
776
  */
910
777
  function buildReconcilePayload(result, ctx) {
@@ -945,7 +812,7 @@ function buildReconcilePayload(result, ctx) {
945
812
  if (typeof summary.skipped === 'number') payload.store_records_dropped = summary.skipped;
946
813
  if (typeof result?.error === 'string' && result.error !== '') {
947
814
  payload.aborted = 'engine-error';
948
- payload.reason = result.error.slice(0, REASON_MAX_CHARS);
815
+ payload.reason = redactLocalPaths(result.error, ctx.repoRoot).slice(0, REASON_MAX_CHARS);
949
816
  }
950
817
  return payload;
951
818
  }