session-orchestrator 3.24.0 → 4.0.1

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 (435) 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 +3 -2
  47. package/.codex-plugin/skills/architecture/SKILL.md +20 -0
  48. package/.codex-plugin/skills/autopilot/SKILL.md +21 -0
  49. package/.codex-plugin/skills/autopilot/agents/openai.yaml +5 -0
  50. package/.codex-plugin/skills/bootstrap/SKILL.md +22 -0
  51. package/.codex-plugin/skills/bootstrap/agents/openai.yaml +5 -0
  52. package/.codex-plugin/skills/brainstorm/SKILL.md +22 -0
  53. package/.codex-plugin/skills/brainstorm/agents/openai.yaml +5 -0
  54. package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +17 -0
  55. package/.codex-plugin/skills/close/SKILL.md +21 -0
  56. package/.codex-plugin/skills/close/agents/openai.yaml +5 -0
  57. package/.codex-plugin/skills/convergence-monitoring/SKILL.md +24 -0
  58. package/.codex-plugin/skills/debug/SKILL.md +21 -0
  59. package/.codex-plugin/skills/debug/agents/openai.yaml +5 -0
  60. package/.codex-plugin/skills/discovery/SKILL.md +21 -0
  61. package/.codex-plugin/skills/discovery/agents/openai.yaml +5 -0
  62. package/.codex-plugin/skills/dispatcher/SKILL.md +21 -0
  63. package/.codex-plugin/skills/dispatcher/agents/openai.yaml +5 -0
  64. package/.codex-plugin/skills/docs-orchestrator/SKILL.md +20 -0
  65. package/.codex-plugin/skills/ecosystem-health/SKILL.md +22 -0
  66. package/.codex-plugin/skills/eli5/SKILL.md +21 -0
  67. package/.codex-plugin/skills/eli5/agents/openai.yaml +5 -0
  68. package/.codex-plugin/skills/eval/SKILL.md +21 -0
  69. package/.codex-plugin/skills/eval/agents/openai.yaml +5 -0
  70. package/.codex-plugin/skills/evolve/SKILL.md +21 -0
  71. package/.codex-plugin/skills/evolve/agents/openai.yaml +5 -0
  72. package/.codex-plugin/skills/frontmatter-guard/SKILL.md +17 -0
  73. package/.codex-plugin/skills/gitlab-ops/SKILL.md +22 -0
  74. package/.codex-plugin/skills/gitlab-portfolio/SKILL.md +17 -0
  75. package/.codex-plugin/skills/go/SKILL.md +22 -0
  76. package/.codex-plugin/skills/go/agents/openai.yaml +5 -0
  77. package/.codex-plugin/skills/grill/SKILL.md +21 -0
  78. package/.codex-plugin/skills/grill/agents/openai.yaml +5 -0
  79. package/.codex-plugin/skills/harness-audit/SKILL.md +19 -0
  80. package/.codex-plugin/skills/harness-audit/agents/openai.yaml +5 -0
  81. package/.codex-plugin/skills/hook-development/SKILL.md +17 -0
  82. package/.codex-plugin/skills/mcp-builder/SKILL.md +17 -0
  83. package/.codex-plugin/skills/memory-cleanup/SKILL.md +21 -0
  84. package/.codex-plugin/skills/memory-cleanup/agents/openai.yaml +5 -0
  85. package/.codex-plugin/skills/mode-selector/SKILL.md +19 -0
  86. package/.codex-plugin/skills/npm-publish/SKILL.md +18 -0
  87. package/.codex-plugin/skills/peekaboo-driver/SKILL.md +20 -0
  88. package/.codex-plugin/skills/persona-panel/SKILL.md +22 -0
  89. package/.codex-plugin/skills/persona-panel/agents/openai.yaml +5 -0
  90. package/.codex-plugin/skills/plan/SKILL.md +22 -0
  91. package/.codex-plugin/skills/plan/agents/openai.yaml +5 -0
  92. package/.codex-plugin/skills/playwright-driver/SKILL.md +22 -0
  93. package/.codex-plugin/skills/portfolio/SKILL.md +21 -0
  94. package/.codex-plugin/skills/portfolio/agents/openai.yaml +5 -0
  95. package/.codex-plugin/skills/quality-gates/SKILL.md +22 -0
  96. package/.codex-plugin/skills/reconcile/SKILL.md +21 -0
  97. package/.codex-plugin/skills/reconcile/agents/openai.yaml +5 -0
  98. package/.codex-plugin/skills/release/SKILL.md +22 -0
  99. package/.codex-plugin/skills/release/agents/openai.yaml +5 -0
  100. package/.codex-plugin/skills/remote-offload/SKILL.md +22 -0
  101. package/.codex-plugin/skills/repo-audit/SKILL.md +19 -0
  102. package/.codex-plugin/skills/repo-audit/agents/openai.yaml +5 -0
  103. package/.codex-plugin/skills/session/SKILL.md +21 -0
  104. package/.codex-plugin/skills/session/agents/openai.yaml +5 -0
  105. package/.codex-plugin/skills/session-end/SKILL.md +22 -0
  106. package/.codex-plugin/skills/session-plan/SKILL.md +22 -0
  107. package/.codex-plugin/skills/session-start/SKILL.md +22 -0
  108. package/.codex-plugin/skills/spinout/SKILL.md +21 -0
  109. package/.codex-plugin/skills/spinout/agents/openai.yaml +5 -0
  110. package/.codex-plugin/skills/sunset-review/SKILL.md +21 -0
  111. package/.codex-plugin/skills/sunset-review/agents/openai.yaml +5 -0
  112. package/.codex-plugin/skills/templates-ack/SKILL.md +21 -0
  113. package/.codex-plugin/skills/templates-ack/agents/openai.yaml +5 -0
  114. package/.codex-plugin/skills/test/SKILL.md +21 -0
  115. package/.codex-plugin/skills/test/agents/openai.yaml +5 -0
  116. package/.codex-plugin/skills/test-runner/SKILL.md +22 -0
  117. package/.codex-plugin/skills/tmux-layout/SKILL.md +23 -0
  118. package/.codex-plugin/skills/using-orchestrator/SKILL.md +19 -0
  119. package/.codex-plugin/skills/vault-mirror/SKILL.md +17 -0
  120. package/.codex-plugin/skills/vault-sync/SKILL.md +17 -0
  121. package/.codex-plugin/skills/wave-executor/SKILL.md +22 -0
  122. package/.codex-plugin/skills/write-executable-plan/SKILL.md +24 -0
  123. package/.cursor/commands/autopilot.md +2 -2
  124. package/.cursor/commands/bootstrap.md +1 -1
  125. package/.cursor/commands/brainstorm.md +1 -1
  126. package/.cursor/commands/debug.md +1 -1
  127. package/.cursor/commands/discovery.md +1 -1
  128. package/.cursor/commands/dispatcher.md +2 -2
  129. package/.cursor/commands/eli5.md +2 -2
  130. package/.cursor/commands/eval.md +2 -2
  131. package/.cursor/commands/evolve.md +1 -1
  132. package/.cursor/commands/go.md +1 -1
  133. package/.cursor/commands/grill.md +2 -2
  134. package/.cursor/commands/memory-cleanup.md +2 -2
  135. package/.cursor/commands/persona-panel.md +1 -1
  136. package/.cursor/commands/plan.md +1 -1
  137. package/.cursor/commands/portfolio.md +1 -1
  138. package/.cursor/commands/reconcile.md +2 -2
  139. package/.cursor/commands/release.md +2 -2
  140. package/.cursor/commands/session.md +2 -2
  141. package/.cursor/commands/spinout.md +2 -2
  142. package/.cursor/commands/sunset-review.md +2 -2
  143. package/.cursor/commands/templates-ack.md +2 -2
  144. package/.cursor/commands/test.md +2 -2
  145. package/.cursor/skills/brainstorm/SKILL.md +1 -1
  146. package/.cursor/skills/eval/SKILL.md +1 -1
  147. package/.cursor/skills/quality-gates/SKILL.md +1 -1
  148. package/.cursor/skills/remote-offload/SKILL.md +1 -1
  149. package/.cursor-plugin/plugin.json +30 -0
  150. package/.orchestrator/policy/blocked-commands.json +121 -0
  151. package/.orchestrator/policy/ecosystem.schema.json +66 -0
  152. package/.orchestrator/policy/quality-gates.example.json +16 -0
  153. package/.orchestrator/policy/quality-gates.schema.json +38 -0
  154. package/.orchestrator/policy/templates-policy.json +27 -0
  155. package/.orchestrator/policy/test-profiles.json +47 -0
  156. package/AGENTS.md +225 -0
  157. package/CHANGELOG.md +1314 -2
  158. package/NOTICE +11 -6
  159. package/README.md +135 -94
  160. package/agents/eval-judge.md +1 -1
  161. package/agents/skill-applied-judge.md +1 -1
  162. package/assets/wave-lifecycle.svg +98 -0
  163. package/commands/release.md +6 -3
  164. package/commands/session.md +18 -3
  165. package/docs/README.md +4 -0
  166. package/{agents/AGENTS.md → docs/agent-authoring.md} +19 -26
  167. package/docs/baseline.md +67 -0
  168. package/docs/ci-setup.md +108 -62
  169. package/docs/codex-setup.md +107 -29
  170. package/docs/components.md +38 -16
  171. package/docs/cursor-setup.md +6 -2
  172. package/docs/events-schema.md +9 -6
  173. package/docs/instruction-delivery.md +69 -0
  174. package/{agents/memory-proposal-collector.md → docs/memory-proposal-flow.md} +1 -8
  175. package/docs/migration-v4.md +365 -0
  176. package/docs/pi-setup.md +6 -1
  177. package/docs/plugin-architecture-v3.md +1 -1
  178. package/docs/rule-authoring.md +85 -19
  179. package/docs/scope-collision-guard.md +5 -5
  180. package/docs/session-config-reference.md +57 -56
  181. package/docs/session-config-template.md +6 -29
  182. package/docs/telemetry.md +157 -3
  183. package/docs/vault-docs-architecture.md +50 -11
  184. package/hooks/_lib/hook-import-set.json +1488 -0
  185. package/hooks/_lib/subagent-transcript.mjs +562 -0
  186. package/hooks/config-protection.mjs +2 -2
  187. package/hooks/cwd-change-restore.mjs +2 -2
  188. package/hooks/enforce-commands.mjs +69 -0
  189. package/hooks/hooks-codex.json +1 -1
  190. package/hooks/hooks-cursor.json +10 -0
  191. package/hooks/hooks-pi.json +5 -0
  192. package/hooks/hooks.json +6 -1
  193. package/hooks/loop-guard.mjs +3 -3
  194. package/hooks/on-session-end.mjs +2 -2
  195. package/hooks/on-session-start.mjs +103 -2
  196. package/hooks/on-stop.mjs +60 -14
  197. package/hooks/operator-steer.mjs +2 -2
  198. package/hooks/post-bash-write-verify.mjs +85 -0
  199. package/hooks/post-edit-import-probe.mjs +344 -0
  200. package/hooks/post-subagent-discovery-validator.mjs +187 -431
  201. package/hooks/post-tool-batch-wave-signal.mjs +118 -4
  202. package/hooks/post-tool-failure-corrective-context.mjs +2 -2
  203. package/hooks/post-tooluse-frontend-slop.mjs +3 -3
  204. package/hooks/pre-bash-destructive-guard.mjs +39 -13
  205. package/hooks/skill-invocation-telemetry.mjs +17 -5
  206. package/hooks/subagent-telemetry.mjs +13 -4
  207. package/monitors/monitors.json +3 -3
  208. package/package.json +9 -1
  209. package/pi/prompts/session.md +2 -2
  210. package/scripts/backfill-abandoned-sessions.mjs +50 -4
  211. package/scripts/backfill-learnings-from-vault.mjs +9 -3
  212. package/scripts/dialectic-deriver.mjs +73 -8
  213. package/scripts/export-hw-learnings.mjs +113 -1
  214. package/scripts/generate-agents-skills.mjs +378 -0
  215. package/scripts/generate-codex-skills.mjs +246 -0
  216. package/scripts/generate-cursor-adapter.mjs +45 -8
  217. package/scripts/generate-hook-import-set.mjs +292 -0
  218. package/scripts/lib/agent-status.mjs +13 -2
  219. package/scripts/lib/auto-dream.mjs +38 -36
  220. package/scripts/lib/autonomy/suitability.mjs +6 -0
  221. package/scripts/lib/autopilot/loop.mjs +2 -2
  222. package/scripts/lib/ci-status-banner.mjs +220 -75
  223. package/scripts/lib/codex/plugin-contract.mjs +88 -6
  224. package/scripts/lib/config/auto-dream.mjs +2 -1
  225. package/scripts/lib/config/block-header.mjs +8 -0
  226. package/scripts/lib/config/block-preprocess.mjs +177 -0
  227. package/scripts/lib/config/broken-window.mjs +2 -1
  228. package/scripts/lib/config/cold-start.mjs +2 -1
  229. package/scripts/lib/config/config-protection.mjs +22 -2
  230. package/scripts/lib/config/context-coverage.mjs +2 -1
  231. package/scripts/lib/config/cross-repo.mjs +2 -1
  232. package/scripts/lib/config/custom-phases.mjs +2 -1
  233. package/scripts/lib/config/dialectic.mjs +2 -1
  234. package/scripts/lib/config/discovery-validator.mjs +2 -1
  235. package/scripts/lib/config/dispatcher-autonomy-capture.mjs +24 -1
  236. package/scripts/lib/config/dispatcher-autonomy.mjs +2 -1
  237. package/scripts/lib/config/docs-orchestrator.mjs +2 -1
  238. package/scripts/lib/config/docs-staleness.mjs +2 -1
  239. package/scripts/lib/config/drift-check.mjs +2 -1
  240. package/scripts/lib/config/eval.mjs +2 -1
  241. package/scripts/lib/config/events-rotation.mjs +2 -1
  242. package/scripts/lib/config/evolve.mjs +8 -2
  243. package/scripts/lib/config/frontend-slop-hook.mjs +7 -3
  244. package/scripts/lib/config/gitlab-portfolio.mjs +2 -1
  245. package/scripts/lib/config/handover-gate.mjs +2 -1
  246. package/scripts/lib/config/health-endpoints.mjs +7 -2
  247. package/scripts/lib/config/host-paths.mjs +20 -4
  248. package/scripts/lib/config/issue-budget.mjs +2 -1
  249. package/scripts/lib/config/loop-guard.mjs +2 -1
  250. package/scripts/lib/config/memory.mjs +2 -1
  251. package/scripts/lib/config/moc-staleness.mjs +2 -1
  252. package/scripts/lib/config/persona-gate-wave.mjs +2 -1
  253. package/scripts/lib/config/private-config-dir.mjs +67 -0
  254. package/scripts/lib/config/reconcile.mjs +2 -1
  255. package/scripts/lib/config/remote-hosts.mjs +2 -1
  256. package/scripts/lib/config/section-extractor.mjs +7 -1
  257. package/scripts/lib/config/skill-evolution.mjs +2 -1
  258. package/scripts/lib/config/slopcheck.mjs +2 -1
  259. package/scripts/lib/config/state-md-lock.mjs +2 -1
  260. package/scripts/lib/config/templates-first.mjs +2 -1
  261. package/scripts/lib/config/test.mjs +2 -1
  262. package/scripts/lib/config/vault-integration.mjs +7 -1
  263. package/scripts/lib/config/vault-mirror-quality.mjs +2 -1
  264. package/scripts/lib/config/vault-staleness.mjs +2 -1
  265. package/scripts/lib/config/vault-sync.mjs +2 -1
  266. package/scripts/lib/config/verification-auto-fix.mjs +2 -1
  267. package/scripts/lib/config/wave-reviewers.mjs +2 -1
  268. package/scripts/lib/config/worktree-orphans.mjs +2 -1
  269. package/scripts/lib/convergence-monitor.mjs +82 -16
  270. package/scripts/lib/dispatcher/rank.mjs +124 -48
  271. package/scripts/lib/ecosystem-health.mjs +16 -2
  272. package/scripts/lib/eval/engine.mjs +9 -1
  273. package/scripts/lib/eval/session-resolve.mjs +23 -4
  274. package/scripts/lib/events.mjs +22 -6
  275. package/scripts/lib/frontmatter-guard.mjs +131 -13
  276. package/scripts/lib/gates/gate-full.mjs +30 -0
  277. package/scripts/lib/gates/gate-helpers.mjs +76 -0
  278. package/scripts/lib/hardware-pattern-detector.mjs +18 -1
  279. package/scripts/lib/harness-audit/categories/category4.mjs +31 -11
  280. package/scripts/lib/host-identity.mjs +50 -11
  281. package/scripts/lib/instruction-budget-guard.mjs +171 -5
  282. package/scripts/lib/learnings/evolve-telemetry.mjs +178 -0
  283. package/scripts/lib/learnings/io.mjs +60 -6
  284. package/scripts/lib/memory-proposals/store.mjs +30 -22
  285. package/scripts/lib/owner-config-banner.mjs +41 -6
  286. package/scripts/lib/owner-config-loader.mjs +21 -10
  287. package/scripts/lib/owner-interview.mjs +3 -3
  288. package/scripts/lib/owner-yaml.mjs +215 -15
  289. package/scripts/lib/platform.mjs +108 -15
  290. package/scripts/lib/plugin-update-banner.mjs +414 -0
  291. package/scripts/lib/project-hygiene.mjs +38 -2
  292. package/scripts/lib/qg-command-drift-banner.mjs +50 -12
  293. package/scripts/lib/quality-gate.mjs +133 -44
  294. package/scripts/lib/reconcile/emitter.mjs +68 -6
  295. package/scripts/lib/reconcile/engine.mjs +51 -11
  296. package/scripts/lib/reconcile/idempotency.mjs +37 -4
  297. package/scripts/lib/reconcile/writer.mjs +40 -18
  298. package/scripts/lib/session-close-backfill.mjs +67 -9
  299. package/scripts/lib/session-id.mjs +12 -23
  300. package/scripts/lib/session-identity/own-session.mjs +125 -10
  301. package/scripts/lib/session-lock-shape.mjs +43 -0
  302. package/scripts/lib/session-lock.mjs +5 -10
  303. package/scripts/lib/session-registry.mjs +25 -9
  304. package/scripts/lib/session-schema/constants.mjs +64 -3
  305. package/scripts/lib/session-schema/validator.mjs +38 -4
  306. package/scripts/lib/session-start-probes.mjs +30 -1
  307. package/scripts/lib/sessions-staleness-banner.mjs +18 -11
  308. package/scripts/lib/skill-health/join.mjs +17 -4
  309. package/scripts/lib/state-md.mjs +78 -0
  310. package/scripts/lib/sunset/walker.mjs +6 -0
  311. package/scripts/lib/telemetry/schema.mjs +202 -9
  312. package/scripts/lib/telemetry/sync.mjs +368 -12
  313. package/scripts/lib/telemetry-flush-health-banner.mjs +211 -0
  314. package/scripts/lib/validate/check-agents-skills.mjs +327 -0
  315. package/scripts/lib/validate/check-agents.mjs +3 -3
  316. package/scripts/lib/validate/check-codex-skills.mjs +191 -0
  317. package/scripts/lib/validate/check-cursor-adapter.mjs +234 -72
  318. package/scripts/lib/validate/check-hooks-symmetry.mjs +45 -16
  319. package/scripts/lib/validate/check-owner-leakage.mjs +319 -22
  320. package/scripts/lib/validate/check-skill-links.mjs +193 -0
  321. package/scripts/lib/validate/check-skill-script-paths.mjs +47 -28
  322. package/scripts/lib/validate/check-test-git-config-target.mjs +192 -12
  323. package/scripts/lib/validate/check-unwired-features.mjs +163 -15
  324. package/scripts/lib/validate/check-validator-registration.mjs +10 -4
  325. package/scripts/lib/validate/confidential-names.mjs +95 -30
  326. package/scripts/lib/validate/enumerate-repo-files.mjs +317 -0
  327. package/scripts/lib/validate/repo-files.mjs +48 -14
  328. package/scripts/lib/vault-backfill/template.mjs +63 -6
  329. package/scripts/lib/vault-mirror/process.mjs +165 -42
  330. package/scripts/lib/vault-mirror/telemetry.mjs +2 -2
  331. package/scripts/lib/vault-status/narrative-mirror.mjs +127 -18
  332. package/scripts/lib/wave-executor/dispatch-common.mjs +164 -0
  333. package/scripts/lib/wave-executor/foreign-dispatch.mjs +7 -142
  334. package/scripts/lib/wave-executor/remote-dispatch.mjs +5 -7
  335. package/scripts/lib/wave-resource-gate.mjs +8 -2
  336. package/scripts/lib/wave-sizing.mjs +4 -1
  337. package/scripts/lib/wave-transcript-tail.mjs +118 -4
  338. package/scripts/materialize-wave-scope.mjs +12 -5
  339. package/scripts/memory-propose.mjs +19 -5
  340. package/scripts/migrate-cold-start-seed.mjs +4 -1
  341. package/scripts/parse-config.mjs +60 -3
  342. package/scripts/release.mjs +430 -31
  343. package/scripts/repair-invalid-sessions.mjs +3 -3
  344. package/scripts/run-quality-gate.mjs +128 -11
  345. package/scripts/site-numbers.mjs +344 -8
  346. package/scripts/sweep-expired-learnings.mjs +90 -0
  347. package/scripts/sync-vault-schema.mjs +3 -1
  348. package/scripts/telemetry.mjs +2 -2
  349. package/scripts/validate-plugin.mjs +164 -0
  350. package/scripts/validate-wave-scope.mjs +28 -8
  351. package/scripts/wave-scope-binding.mjs +215 -0
  352. package/skills/_shared/instruction-file-resolution.md +10 -0
  353. package/skills/_shared/parallel-aware-preamble.md +1 -0
  354. package/skills/_shared/platform-tools.md +1 -1
  355. package/skills/_shared/state-ownership.md +1 -1
  356. package/skills/architecture/SKILL.md +7 -5
  357. package/skills/{domain-model/SKILL.md → architecture/references/domain-model.md} +9 -9
  358. package/skills/autopilot/SKILL.md +4 -18
  359. package/skills/claude-md-drift-check/SKILL.md +5 -1
  360. package/skills/claude-md-drift-check/checker.mjs +62 -2
  361. package/skills/convergence-monitoring/SIGNALS.md +55 -0
  362. package/skills/discovery/probes/vault-staleness.mjs +37 -13
  363. package/skills/discovery/probes-arch.md +20 -18
  364. package/skills/dispatcher/SKILL.md +3 -2
  365. package/skills/evolve/SKILL.md +65 -26
  366. package/skills/frontmatter-guard/SKILL.md +11 -5
  367. package/skills/npm-publish/SKILL.md +1 -1
  368. package/skills/reconcile/SKILL.md +33 -0
  369. package/skills/remote-offload/SKILL.md +1 -1
  370. package/skills/session-end/SKILL.md +18 -905
  371. package/skills/session-end/phase-3-6-tail.md +10 -3
  372. package/skills/session-end/plan-verification.md +221 -155
  373. package/skills/session-end/references/phase-2-quality-gate.md +93 -0
  374. package/skills/session-end/references/phase-3-documentation-updates.md +229 -0
  375. package/skills/session-end/references/phase-4a-worktree-cleanup.md +120 -0
  376. package/skills/session-end/references/phase-4b-worktree-orphan-sweep.md +58 -0
  377. package/skills/session-end/references/phase-5-issue-cleanup.md +104 -0
  378. package/skills/session-end/references/session-summary-template.md +62 -0
  379. package/skills/session-plan/SKILL.md +49 -0
  380. package/skills/session-start/SKILL.md +22 -904
  381. package/skills/session-start/phase-8-5-express-path.md +1 -1
  382. package/skills/session-start/references/phase-1-1-dispatcher-autonomy-capture.md +55 -0
  383. package/skills/session-start/references/phase-1-2-session-lock.md +140 -0
  384. package/skills/session-start/references/phase-1-5-session-continuity.md +254 -0
  385. package/skills/session-start/references/phase-1-7-vault-status-board.md +53 -0
  386. package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +75 -0
  387. package/skills/session-start/references/phase-4-ssot-environment-check.md +160 -0
  388. package/skills/session-start/references/phase-6-5-forced-reads.md +75 -0
  389. package/skills/session-start/references/phase-6-6-project-intelligence.md +81 -0
  390. package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +103 -0
  391. package/skills/vault-sync/SKILL.md +10 -0
  392. package/skills/vault-sync/validator.mjs +21 -27
  393. package/skills/wave-executor/SKILL.md +15 -1
  394. package/skills/wave-executor/references/wave-loop-dispatch.md +612 -0
  395. package/skills/wave-executor/references/wave-loop-review.md +570 -0
  396. package/skills/wave-executor/references/wave-loop-scope-manifest.md +162 -0
  397. package/skills/wave-executor/wave-loop.md +14 -1309
  398. package/templates/_shared/journey-manifest.md +10 -6
  399. package/.cursor/commands/autopilot-multi.md +0 -14
  400. package/.cursor/commands/contract-version-bump.md +0 -14
  401. package/.cursor/commands/journey-audit.md +0 -14
  402. package/.cursor/skills/contract-version-bump/SKILL.md +0 -12
  403. package/.cursor/skills/daily/SKILL.md +0 -12
  404. package/.cursor/skills/domain-model/SKILL.md +0 -13
  405. package/.cursor/skills/journey-audit/SKILL.md +0 -13
  406. package/.cursor/skills/skill-creator/SKILL.md +0 -13
  407. package/.cursor/skills/ubiquitous-language/SKILL.md +0 -13
  408. package/commands/autopilot-multi.md +0 -74
  409. package/commands/contract-version-bump.md +0 -28
  410. package/commands/journey-audit.md +0 -43
  411. package/pi/prompts/autopilot-multi.md +0 -12
  412. package/pi/prompts/contract-version-bump.md +0 -12
  413. package/pi/prompts/journey-audit.md +0 -12
  414. package/scripts/autopilot-multi.mjs +0 -885
  415. package/scripts/backfill-learnings-expires.mjs +0 -196
  416. package/scripts/backfill-learnings.mjs +0 -203
  417. package/scripts/fleet-instruction-scan.mjs +0 -141
  418. package/scripts/lib/autopilot/dep-graph.mjs +0 -417
  419. package/scripts/lib/autopilot/multi-killswitch.mjs +0 -184
  420. package/scripts/lib/webhook-url.mjs +0 -105
  421. package/scripts/lifecycle-sim-v6.mjs +0 -347
  422. package/scripts/migrate-learnings-jsonl.mjs +0 -189
  423. package/scripts/migrate-subagents-jsonl.mjs +0 -196
  424. package/scripts/upload-social-preview.mjs +0 -316
  425. package/skills/_shared/model-selection.md +0 -64
  426. package/skills/contract-version-bump/SKILL.md +0 -219
  427. package/skills/daily/SKILL.md +0 -222
  428. package/skills/daily/generate.sh +0 -92
  429. package/skills/daily/templates/daily.md.tpl +0 -36
  430. package/skills/journey-audit/SKILL.md +0 -270
  431. package/skills/skill-creator/SKILL.md +0 -168
  432. package/skills/ubiquitous-language/SKILL.md +0 -97
  433. package/skills/vault-sync/package-lock.json +0 -40
  434. /package/skills/{domain-model → architecture/references}/ADR-FORMAT.md +0 -0
  435. /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
 
@@ -160,8 +160,10 @@ function zeroedResult(error) {
160
160
  /**
161
161
  * Default learnings loader — read + parse `<repoRoot>/.orchestrator/metrics/learnings.jsonl`
162
162
  * line-by-line, migrate/normalize records through the learnings schema SSOT,
163
- * and skip blank/malformed lines. A missing file yields `[]`. Never throws (a
164
- * 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`).
165
167
  *
166
168
  * @param {string|undefined} repoRoot
167
169
  * @returns {Array<Record<string, unknown>>}
@@ -175,8 +177,15 @@ function defaultLoadLearnings(repoRoot) {
175
177
  let raw;
176
178
  try {
177
179
  raw = readFileSync(absPath, 'utf8');
178
- } catch {
179
- 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 [];
180
189
  }
181
190
 
182
191
  /** @type {Array<Record<string, unknown>>} */
@@ -380,6 +389,28 @@ const BODY_LEARNING_ID_RE = /-\s*learning-id:\s*`([^`]+)`/g;
380
389
  * on disk: re-proposing it is the issue #484 defect (9 of 10 proposals in one
381
390
  * run were learnings a `.claude/rules/` file already covered).
382
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
+ *
383
414
  * Gated the same way as {@link defaultLoadCandidatesForDedupe}: an absent
384
415
  * `repoRoot` yields empty sets rather than falling back to `process.cwd()`.
385
416
  * Never throws — a missing `.claude/rules/` dir or an unreadable file
@@ -546,14 +577,23 @@ async function runReconcileInner(
546
577
  // new learning could use — the #484 defect measured on a real repo was
547
578
  // exactly this: 9 of 10 proposals in one run were learnings that already
548
579
  // had a `.claude/rules/` file on disk, crowding out the tenth new one.
549
- // Two independent sources both count as terminal, either is sufficient:
550
- // - the idempotency sidecar already carries a `processed_at` stamp for
551
- // this `learning_key` (`isProcessed`, previously computed but NEVER
552
- // called from this module — the other half of #484);
553
- // - a `.claude/rules/*.md` file already carries a matching
580
+ // Two independent sources both count as terminal, either is sufficient
581
+ // but they are NOT peers (#1242). Their contract is:
582
+ // - AUTHORITATIVE: a `.claude/rules/*.md` file already carries a matching
554
583
  // `learning-key`/`learning-id` provenance marker, discovered by
555
- // scanning disk directly (covers the case where a rule was written
556
- // without ever going through this sidecar, e.g. hand-authored).
584
+ // scanning disk directly. Those files are TRACKED, so this source alone
585
+ // is sufficient on a fresh clone and covers rules written without ever
586
+ // going through the sidecar (hand-authored, or consolidated by hand).
587
+ // - CACHE: the idempotency sidecar already carries a `processed_at` stamp
588
+ // for this `learning_key` (`isProcessed`, previously computed but NEVER
589
+ // called from this module — the other half of #484). It lives in the
590
+ // GITIGNORED `.orchestrator/runtime/` (`.gitignore:114`), so it is
591
+ // absent on a fresh clone. It only SHORT-CIRCUITS the scan above (and
592
+ // carries forward verdicts the scan can no longer see, e.g. an operator
593
+ // decline (#1042) or a rule file since removed) — it never replaces it.
594
+ // Consequence: an empty sidecar must never change the verdict for a
595
+ // learning whose rule file exists. Pinned by the "fresh clone, consolidated
596
+ // shape" test in `tests/lib/reconcile/engine.test.mjs`.
557
597
  const { records: existingCandidates } = loadCandidatesForDedupe(repoRoot) ?? { records: [] };
558
598
  const materialized = readMaterializedProvenance(repoRoot) ?? { keys: new Set(), ids: new Set() };
559
599
 
@@ -391,7 +391,14 @@ export function mergeCandidates({ candidates, repoRoot, storePath } = {}) {
391
391
  * @param {number} [params.fallbackConfidence] - used only when no existing record is found.
392
392
  * @param {string} [params.repoRoot]
393
393
  * @param {string} [params.storePath]
394
- * @returns {{ written: boolean, stamped: ReconcileCandidate|null }}
394
+ * @returns {{ written: boolean, stamped: ReconcileCandidate|null, alreadyProcessed?: boolean }}
395
+ * `stamped` is the record as PERSISTED (read back out of `mergeCandidates`'s
396
+ * merged array), never the one this call merely proposed — when a prior
397
+ * terminal verdict wins the dedupe, the two differ and only the former is
398
+ * true. `alreadyProcessed: true` marks exactly that case; `written` stays
399
+ * `true` there, because the store holds the intended terminal state and
400
+ * `written: false` means a WRITE FAILURE to the one production caller
401
+ * (`writer.mjs`, which turns it into an operator-visible error).
395
402
  */
396
403
  export function markCandidateProcessed({
397
404
  learningKey,
@@ -419,6 +426,7 @@ export function markCandidateProcessed({
419
426
  stamped = { ...found, processed_at: stampAt, outcome: typeof outcome === 'string' ? outcome : (found.outcome ?? null) };
420
427
  } else {
421
428
  const slug = typeof fallbackSlug === 'string' ? fallbackSlug : '';
429
+ const finalOutcome = typeof outcome === 'string' ? outcome : null;
422
430
  stamped = buildCandidate({
423
431
  id:
424
432
  typeof fallbackCandidateId === 'string' && fallbackCandidateId.length > 0
@@ -426,15 +434,40 @@ export function markCandidateProcessed({
426
434
  : makeCandidateId(learningKey, slug),
427
435
  learningKey,
428
436
  slug,
429
- status: 'proposed',
437
+ // A minted record is TERMINAL from birth (`processed_at` is set two lines
438
+ // below), so `status` must agree with `outcome` instead of always saying
439
+ // `'proposed'` — a record reading `status:'proposed'` while carrying
440
+ // `outcome:'rejected'` is self-contradictory and misreads as a live
441
+ // proposal to anything that renders `status`. The vocabulary is exactly
442
+ // the two members of the ReconcileCandidate typedef; nothing new is
443
+ // invented here: `'rejected'` for the operator-rejection outcome
444
+ // (writer.mjs, issue #1042), `'proposed'` for the accepted outcomes
445
+ // (`'written'` / `'already-on-disk'`), where the proposal stood.
446
+ status: finalOutcome === 'rejected' ? 'rejected' : 'proposed',
430
447
  reason: 'stamped without a prior sidecar record',
431
448
  confidence: typeof fallbackConfidence === 'number' ? fallbackConfidence : 0,
432
449
  createdAt: stampAt,
433
450
  });
434
451
  stamped.processed_at = stampAt;
435
- stamped.outcome = typeof outcome === 'string' ? outcome : null;
452
+ stamped.outcome = finalOutcome;
436
453
  }
437
454
 
438
455
  const result = mergeCandidates({ candidates: [stamped], repoRoot, storePath });
439
- return { written: result.written === true, stamped };
456
+
457
+ // Report what is PERSISTED, not what we asked for. `mergeCandidates`'s dedupe
458
+ // rule keeps an already-terminal existing record and DROPS the incoming one,
459
+ // so when `found` was already processed the store still holds the OLD
460
+ // `processed_at`/`outcome` — returning our freshly-built `stamped` would tell
461
+ // the caller a verdict that is nowhere on disk. `merged` IS the array that was
462
+ // just written, so reading the record back out of it is the honest answer.
463
+ const persisted = result.merged.find((rec) => rec && rec.learning_key === learningKey);
464
+ return {
465
+ written: result.written === true,
466
+ stamped: persisted ?? stamped,
467
+ // `true` iff a prior terminal verdict won and this call changed nothing on
468
+ // disk. `written` deliberately stays `true` in that case: the store IS in
469
+ // the intended terminal state, and writer.mjs treats `written:false` as a
470
+ // stamp FAILURE worth an operator-visible error.
471
+ alreadyProcessed: isTerminal(found),
472
+ };
440
473
  }
@@ -457,26 +457,37 @@ function frontmatterRefusalReason(content) {
457
457
  * not yet mature enough — silently, and with no way back short of editing the
458
458
  * sidecar by hand.
459
459
  *
460
- * The discriminator is the rendered `content`: `engine.mjs` pushes its own
461
- * rejections as `{learningKey, type, reason, status:'rejected'}` they never
462
- * reach the renderer, so they never carry `content`/`slug`/`path` while the
463
- * proposals the operator declines are full `ReconcileProposal` records whose
464
- * `content` is the very rule text the AUQ showed him.
465
- *
466
- * CEILING (BV-004): this reads an implicit signal, not an explicit marker,
467
- * because the one production caller (`skills/session-end/phase-3-6-tail.md`
468
- * step 6/7) concatenates engine rejections and operator-declined proposals into
469
- * ONE `rejected` array and marks neither. Revisit if that caller starts passing
470
- * rendered content on engine-side rejections, or if it gains an explicit
471
- * operator-rejection flag then key on the flag instead.
472
- *
473
- * @param {WriterRejectedItem & {content?: unknown, learningKey?: unknown}} item
460
+ * PRIMARY discriminator (#1153 P15): the explicit `operatorRejected: true`
461
+ * flag that `skills/session-end/phase-3-6-tail.md` step 6 stamps on every
462
+ * proposal the operator left unselected before concatenating it into the one
463
+ * `rejected` array. `engine.mjs` pushes its own rejections as
464
+ * `{learningKey, type, reason, status:'rejected'}` and never sets the flag, so
465
+ * the two shapes are now distinguishable by a marker rather than by inference.
466
+ *
467
+ * FALLBACK (@deprecated remove once no supported skill body predates the P15
468
+ * stamp): when the flag is absent, fall back to the old implicit signal — the
469
+ * rendered `content`. Engine rejections never reach the renderer, so they never
470
+ * carry `content`/`slug`/`path`, while an operator-declined proposal is a full
471
+ * `ReconcileProposal` whose `content` is the rule text the AUQ showed him. The
472
+ * fallback exists because the skill prose and this module ship together but
473
+ * consumer repos may pin an OLDER `phase-3-6-tail.md` that emits no flag;
474
+ * dropping it immediately would silently stop stamping their operator
475
+ * rejections terminal (the exact #1042 bug). REMOVAL TRIGGER: the next major
476
+ * release in which no supported consumer ships a pre-P15 skill body.
477
+ *
478
+ * Either way the decision stays conservative in the same direction: a false
479
+ * negative costs nothing (the learning is re-proposed next run); a false
480
+ * positive would permanently suppress a learning that was only capped.
481
+ *
482
+ * @param {WriterRejectedItem & {content?: unknown, learningKey?: unknown, operatorRejected?: unknown}} item
474
483
  * @returns {boolean}
475
484
  */
476
485
  function isOperatorRejection(item) {
477
486
  if (!item || typeof item !== 'object') return false;
478
- if (typeof item.content !== 'string' || item.content.length === 0) return false;
479
- return typeof item.learningKey === 'string' && item.learningKey.length > 0;
487
+ if (typeof item.learningKey !== 'string' || item.learningKey.length === 0) return false;
488
+ if (item.operatorRejected === true) return true;
489
+ // @deprecated fallback — pre-P15 skill bodies emit no flag; see above.
490
+ return typeof item.content === 'string' && item.content.length > 0;
480
491
  }
481
492
 
482
493
  // ---------------------------------------------------------------------------
@@ -663,8 +674,12 @@ export async function writeApprovedRules({
663
674
  repoRoot,
664
675
  });
665
676
  if (!stampResult.written) {
677
+ // Same discrimination as the rejected path below (identical shape,
678
+ // identical reader): an already-terminal record is not a lost stamp.
666
679
  errors.push(
667
- `sidecar-stamp failed for "${item.path ?? item.slug}" (learningKey=${item.learningKey}) — rule file was written but the idempotency sidecar was not updated`,
680
+ stampResult.alreadyProcessed
681
+ ? `sidecar-stamp for "${item.path ?? item.slug}" (learningKey=${item.learningKey}) wrote nothing — a prior terminal verdict is already on disk, so the rule file landed and the candidate stays terminal`
682
+ : `sidecar-stamp failed for "${item.path ?? item.slug}" (learningKey=${item.learningKey}) — rule file was written but the idempotency sidecar was not updated`,
668
683
  );
669
684
  }
670
685
  }
@@ -724,8 +739,15 @@ export async function writeApprovedRules({
724
739
  repoRoot,
725
740
  });
726
741
  if (!stampResult.written) {
742
+ // `alreadyProcessed` distinguishes the two failures that look
743
+ // identical from `written:false` alone. When a PRIOR terminal
744
+ // verdict is already on disk, the re-proposal warning is
745
+ // simply false — the sidecar holds the state we wanted — and
746
+ // telling the operator otherwise sends him after a non-bug.
727
747
  errors.push(
728
- `sidecar-stamp failed for rejected "${item.learningKey}" — the rejection was archived but the idempotency sidecar was not updated, so a later run may re-propose it`,
748
+ stampResult.alreadyProcessed
749
+ ? `sidecar-stamp for rejected "${item.learningKey}" wrote nothing — a prior terminal verdict is already on disk, so the rejection is recorded and it will NOT be re-proposed`
750
+ : `sidecar-stamp failed for rejected "${item.learningKey}" — the rejection was archived but the idempotency sidecar was not updated, so a later run may re-propose it`,
729
751
  );
730
752
  }
731
753
  }