session-orchestrator 5.1.0 → 5.3.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 (484) hide show
  1. package/.agents/skills/architecture/SKILL.md +3 -1
  2. package/.agents/skills/autopilot/SKILL.md +6 -1
  3. package/.agents/skills/autopilot/agents/openai.yaml +5 -0
  4. package/.agents/skills/bootstrap/SKILL.md +7 -1
  5. package/.agents/skills/bootstrap/agents/openai.yaml +5 -0
  6. package/.agents/skills/brainstorm/SKILL.md +8 -1
  7. package/.agents/skills/brainstorm/agents/openai.yaml +5 -0
  8. package/.agents/skills/claude-md-drift-check/SKILL.md +3 -1
  9. package/.agents/skills/close/SKILL.md +21 -0
  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 +7 -1
  13. package/.agents/skills/debug/agents/openai.yaml +5 -0
  14. package/.agents/skills/discovery/SKILL.md +7 -2
  15. package/.agents/skills/discovery/agents/openai.yaml +5 -0
  16. package/.agents/skills/dispatcher/SKILL.md +7 -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 +7 -1
  21. package/.agents/skills/eli5/agents/openai.yaml +5 -0
  22. package/.agents/skills/eval/SKILL.md +7 -2
  23. package/.agents/skills/eval/agents/openai.yaml +5 -0
  24. package/.agents/skills/evolve/SKILL.md +8 -3
  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 +22 -0
  30. package/.agents/skills/go/agents/openai.yaml +5 -0
  31. package/.agents/skills/grill/SKILL.md +7 -1
  32. package/.agents/skills/grill/agents/openai.yaml +5 -0
  33. package/.agents/skills/harness-audit/SKILL.md +20 -0
  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 +6 -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/npm-publish/SKILL.md +4 -2
  41. package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
  42. package/.agents/skills/persona-panel/SKILL.md +6 -1
  43. package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
  44. package/.agents/skills/plan/SKILL.md +8 -2
  45. package/.agents/skills/plan/agents/openai.yaml +5 -0
  46. package/.agents/skills/playwright-driver/SKILL.md +3 -1
  47. package/.agents/skills/portfolio/SKILL.md +21 -0
  48. package/.agents/skills/portfolio/agents/openai.yaml +5 -0
  49. package/.agents/skills/quality-gates/SKILL.md +3 -1
  50. package/.agents/skills/reconcile/SKILL.md +6 -1
  51. package/.agents/skills/reconcile/agents/openai.yaml +5 -0
  52. package/.agents/skills/release/SKILL.md +22 -0
  53. package/.agents/skills/release/agents/openai.yaml +5 -0
  54. package/.agents/skills/remote-offload/SKILL.md +3 -1
  55. package/.agents/skills/repo-audit/SKILL.md +6 -1
  56. package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
  57. package/.agents/skills/session/SKILL.md +21 -0
  58. package/.agents/skills/session/agents/openai.yaml +5 -0
  59. package/.agents/skills/session-end/SKILL.md +3 -1
  60. package/.agents/skills/session-plan/SKILL.md +3 -1
  61. package/.agents/skills/session-start/SKILL.md +3 -1
  62. package/.agents/skills/spinout/SKILL.md +6 -1
  63. package/.agents/skills/spinout/agents/openai.yaml +5 -0
  64. package/.agents/skills/sunset-review/SKILL.md +7 -1
  65. package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
  66. package/.agents/skills/templates-ack/SKILL.md +21 -0
  67. package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
  68. package/.agents/skills/test/SKILL.md +21 -0
  69. package/.agents/skills/test/agents/openai.yaml +5 -0
  70. package/.agents/skills/test-runner/SKILL.md +3 -1
  71. package/.agents/skills/tmux-layout/SKILL.md +3 -1
  72. package/.agents/skills/using-orchestrator/SKILL.md +3 -1
  73. package/.agents/skills/ux-grill/SKILL.md +7 -1
  74. package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
  75. package/.agents/skills/vault-mirror/SKILL.md +3 -1
  76. package/.agents/skills/vault-sync/SKILL.md +3 -1
  77. package/.agents/skills/wave-executor/SKILL.md +3 -1
  78. package/.agents/skills/write-executable-plan/SKILL.md +3 -1
  79. package/.claude-plugin/marketplace.json +1 -1
  80. package/.claude-plugin/plugin.json +1 -1
  81. package/.codex-plugin/plugin.json +4 -4
  82. package/.codex-plugin/skills/autopilot/SKILL.md +5 -4
  83. package/.codex-plugin/skills/bootstrap/SKILL.md +8 -4
  84. package/.codex-plugin/skills/brainstorm/SKILL.md +11 -4
  85. package/.codex-plugin/skills/close/SKILL.md +3 -3
  86. package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -1
  87. package/.codex-plugin/skills/debug/SKILL.md +11 -4
  88. package/.codex-plugin/skills/discovery/SKILL.md +8 -4
  89. package/.codex-plugin/skills/dispatcher/SKILL.md +4 -4
  90. package/.codex-plugin/skills/eli5/SKILL.md +9 -4
  91. package/.codex-plugin/skills/eval/SKILL.md +9 -4
  92. package/.codex-plugin/skills/evolve/SKILL.md +9 -4
  93. package/.codex-plugin/skills/go/SKILL.md +3 -3
  94. package/.codex-plugin/skills/grill/SKILL.md +11 -4
  95. package/.codex-plugin/skills/harness-audit/SKILL.md +4 -3
  96. package/.codex-plugin/skills/memory-cleanup/SKILL.md +9 -4
  97. package/.codex-plugin/skills/npm-publish/SKILL.md +1 -1
  98. package/.codex-plugin/skills/persona-panel/SKILL.md +5 -5
  99. package/.codex-plugin/skills/plan/SKILL.md +8 -4
  100. package/.codex-plugin/skills/portfolio/SKILL.md +3 -3
  101. package/.codex-plugin/skills/reconcile/SKILL.md +9 -4
  102. package/.codex-plugin/skills/release/SKILL.md +3 -3
  103. package/.codex-plugin/skills/repo-audit/SKILL.md +6 -4
  104. package/.codex-plugin/skills/session/SKILL.md +1 -1
  105. package/.codex-plugin/skills/spinout/SKILL.md +4 -4
  106. package/.codex-plugin/skills/sunset-review/SKILL.md +5 -4
  107. package/.codex-plugin/skills/test/SKILL.md +3 -3
  108. package/.codex-plugin/skills/ux-grill/SKILL.md +11 -4
  109. package/.cursor/commands/autopilot.md +4 -4
  110. package/.cursor/commands/bootstrap.md +5 -4
  111. package/.cursor/commands/brainstorm.md +5 -4
  112. package/.cursor/commands/close.md +4 -3
  113. package/.cursor/commands/debug.md +4 -4
  114. package/.cursor/commands/discovery.md +4 -4
  115. package/.cursor/commands/dispatcher.md +4 -4
  116. package/.cursor/commands/eli5.md +4 -4
  117. package/.cursor/commands/eval.md +4 -4
  118. package/.cursor/commands/evolve.md +4 -4
  119. package/.cursor/commands/go.md +4 -3
  120. package/.cursor/commands/grill.md +4 -4
  121. package/.cursor/commands/harness-audit.md +3 -3
  122. package/.cursor/commands/memory-cleanup.md +4 -4
  123. package/.cursor/commands/persona-panel.md +4 -4
  124. package/.cursor/commands/plan.md +5 -4
  125. package/.cursor/commands/portfolio.md +3 -3
  126. package/.cursor/commands/reconcile.md +4 -4
  127. package/.cursor/commands/release.md +4 -3
  128. package/.cursor/commands/repo-audit.md +4 -4
  129. package/.cursor/commands/session.md +1 -1
  130. package/.cursor/commands/spinout.md +4 -4
  131. package/.cursor/commands/sunset-review.md +4 -4
  132. package/.cursor/commands/test.md +3 -3
  133. package/.cursor/commands/ux-grill.md +4 -4
  134. package/.cursor/rules/000-session-orchestrator.mdc +0 -2
  135. package/.cursor/rules/010-session-workflow.mdc +2 -2
  136. package/.cursor/rules/050-plan.mdc +1 -1
  137. package/.cursor/skills/bootstrap/SKILL.md +1 -0
  138. package/.cursor/skills/close/SKILL.md +13 -0
  139. package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
  140. package/.cursor/skills/debug/SKILL.md +0 -1
  141. package/.cursor/skills/discovery/SKILL.md +0 -1
  142. package/.cursor/skills/dispatcher/SKILL.md +0 -1
  143. package/.cursor/skills/eli5/SKILL.md +0 -1
  144. package/.cursor/skills/eval/SKILL.md +1 -1
  145. package/.cursor/skills/evolve/SKILL.md +0 -1
  146. package/.cursor/skills/go/SKILL.md +13 -0
  147. package/.cursor/skills/grill/SKILL.md +0 -1
  148. package/.cursor/skills/harness-audit/SKILL.md +12 -0
  149. package/.cursor/skills/npm-publish/SKILL.md +1 -0
  150. package/.cursor/skills/portfolio/SKILL.md +12 -0
  151. package/.cursor/skills/release/SKILL.md +13 -0
  152. package/.cursor/skills/repo-audit/SKILL.md +0 -1
  153. package/.cursor/skills/sunset-review/SKILL.md +0 -1
  154. package/.cursor/skills/test/SKILL.md +12 -0
  155. package/.cursor/skills/ux-grill/SKILL.md +0 -1
  156. package/.cursor-plugin/plugin.json +1 -1
  157. package/.orchestrator/policy/blocked-commands.json +13 -4
  158. package/AGENTS.md +3 -2
  159. package/CHANGELOG.md +197 -0
  160. package/README.md +11 -9
  161. package/SECURITY.md +12 -0
  162. package/agents/dialectic-deriver.md +13 -10
  163. package/agents/eval-judge.md +67 -45
  164. package/agents/skill-applied-judge.md +34 -19
  165. package/commands/session.md +17 -3
  166. package/docs/baseline.md +12 -6
  167. package/docs/ci-setup.md +53 -0
  168. package/docs/codex-setup.md +15 -3
  169. package/docs/components.md +13 -6
  170. package/docs/events-schema.md +59 -9
  171. package/docs/install.md +16 -0
  172. package/docs/persona-panel.md +1 -1
  173. package/docs/pi-setup.md +1 -1
  174. package/docs/rule-authoring.md +135 -14
  175. package/docs/scope-collision-guard.md +2 -0
  176. package/docs/session-config-reference.md +106 -11
  177. package/docs/session-config-template.md +31 -2
  178. package/docs/telemetry.md +2 -0
  179. package/hooks/_lib/hook-import-set.json +125 -8
  180. package/hooks/_lib/subagent-paths.mjs +15 -0
  181. package/hooks/_lib/subagent-transcript.mjs +582 -31
  182. package/hooks/_lib/vcs-create-matcher.mjs +217 -62
  183. package/hooks/config-protection.mjs +11 -3
  184. package/hooks/cwd-change-restore.mjs +11 -3
  185. package/hooks/enforce-commands.mjs +70 -23
  186. package/hooks/enforce-scope.mjs +143 -33
  187. package/hooks/hooks-codex.json +1 -1
  188. package/hooks/hooks.json +1 -1
  189. package/hooks/loop-guard.mjs +11 -3
  190. package/hooks/on-session-end.mjs +72 -25
  191. package/hooks/on-session-start.mjs +48 -11
  192. package/hooks/on-stop.mjs +211 -23
  193. package/hooks/operator-steer.mjs +11 -3
  194. package/hooks/post-bash-issue-budget-refund.mjs +18 -8
  195. package/hooks/post-bash-write-verify.mjs +6 -2
  196. package/hooks/post-edit-import-probe.mjs +17 -9
  197. package/hooks/post-edit-validate.mjs +13 -5
  198. package/hooks/post-subagent-discovery-validator.mjs +98 -13
  199. package/hooks/post-tool-batch-wave-signal.mjs +200 -38
  200. package/hooks/post-tool-failure-corrective-context.mjs +11 -5
  201. package/hooks/post-tooluse-frontend-slop.mjs +10 -4
  202. package/hooks/pre-auq-clarity.mjs +18 -2
  203. package/hooks/pre-bash-destructive-guard.mjs +80 -9
  204. package/hooks/pre-bash-issue-budget.mjs +119 -28
  205. package/hooks/pre-bash-memory-propose-audit.mjs +86 -54
  206. package/hooks/pre-bash-sessions-ledger-guard.mjs +391 -20
  207. package/hooks/pre-bash-staging-fence.mjs +335 -31
  208. package/hooks/pre-bash-templates-first.mjs +19 -14
  209. package/hooks/pre-task-scope-disjoint.mjs +385 -5
  210. package/hooks/skill-invocation-telemetry.mjs +2 -1
  211. package/hooks/subagent-telemetry.mjs +15 -19
  212. package/hooks/wave-scope-commit-guard.mjs +197 -100
  213. package/monitors/monitors.json +1 -1
  214. package/output-styles/wave-summary.md +1 -1
  215. package/package.json +2 -1
  216. package/pi/prompts/autopilot.md +3 -3
  217. package/pi/prompts/bootstrap.md +3 -3
  218. package/pi/prompts/brainstorm.md +3 -3
  219. package/pi/prompts/close.md +2 -2
  220. package/pi/prompts/debug.md +3 -3
  221. package/pi/prompts/discovery.md +3 -3
  222. package/pi/prompts/dispatcher.md +3 -3
  223. package/pi/prompts/eli5.md +3 -3
  224. package/pi/prompts/eval.md +3 -3
  225. package/pi/prompts/evolve.md +3 -3
  226. package/pi/prompts/go.md +2 -2
  227. package/pi/prompts/grill.md +3 -3
  228. package/pi/prompts/harness-audit.md +2 -3
  229. package/pi/prompts/memory-cleanup.md +3 -3
  230. package/pi/prompts/persona-panel.md +3 -3
  231. package/pi/prompts/plan.md +3 -3
  232. package/pi/prompts/portfolio.md +2 -2
  233. package/pi/prompts/reconcile.md +3 -3
  234. package/pi/prompts/release.md +3 -3
  235. package/pi/prompts/repo-audit.md +3 -4
  236. package/pi/prompts/session.md +2 -2
  237. package/pi/prompts/spinout.md +3 -3
  238. package/pi/prompts/sunset-review.md +3 -3
  239. package/pi/prompts/templates-ack.md +1 -1
  240. package/pi/prompts/test.md +3 -3
  241. package/pi/prompts/ux-grill.md +3 -3
  242. package/rules/README.md +1 -1
  243. package/rules/opt-in-domain/prompt-caching.md +1 -1
  244. package/rules/opt-in-stack/backend-data.md +1 -1
  245. package/rules/opt-in-stack/backend.md +3 -3
  246. package/rules/opt-in-stack/frontend.md +1 -1
  247. package/rules/opt-in-stack/security-web.md +3 -3
  248. package/rules/opt-in-stack/swift.md +1 -1
  249. package/scripts/archive-closed-prds.mjs +2 -2
  250. package/scripts/auq-audit.mjs +2 -3
  251. package/scripts/autopilot.mjs +23 -2
  252. package/scripts/backfill-abandoned-sessions.mjs +171 -15
  253. package/scripts/backfill-evidence-digest.mjs +2 -1
  254. package/scripts/backfill-learnings-from-vault.mjs +2 -2
  255. package/scripts/check-package-manager.mjs +2 -2
  256. package/scripts/check-sessions-integrity.mjs +300 -0
  257. package/scripts/ci/assert-vitest-green.mjs +2 -1
  258. package/scripts/dialectic-deriver.mjs +50 -13
  259. package/scripts/emit-session.mjs +77 -32
  260. package/scripts/eval-session.mjs +65 -3
  261. package/scripts/export-hw-learnings.mjs +2 -1
  262. package/scripts/express-path.mjs +1 -1
  263. package/scripts/gc-stale-worktrees.mjs +2 -1
  264. package/scripts/generate-agents-skills.mjs +102 -29
  265. package/scripts/generate-codex-skills.mjs +48 -4
  266. package/scripts/generate-cursor-adapter.mjs +220 -11
  267. package/scripts/generate-hook-import-set.mjs +12 -27
  268. package/scripts/generate-pi-prompts.mjs +183 -13
  269. package/scripts/github-protection-audit.mjs +2 -3
  270. package/scripts/lib/agent-frontmatter.mjs +23 -1
  271. package/scripts/lib/agent-status.mjs +2 -31
  272. package/scripts/lib/auq/clarity.mjs +10 -2
  273. package/scripts/lib/auq/parse.mjs +12 -31
  274. package/scripts/lib/auq/schema.mjs +56 -41
  275. package/scripts/lib/auto-dialectic.mjs +304 -15
  276. package/scripts/lib/autopilot/flags.mjs +12 -1
  277. package/scripts/lib/autopilot/kill-switches.mjs +6 -3
  278. package/scripts/lib/autopilot/loop.mjs +14 -1
  279. package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
  280. package/scripts/lib/ci-status-banner.mjs +376 -16
  281. package/scripts/lib/claude-md-budget-lint.mjs +2 -5
  282. package/scripts/lib/command-blocker.mjs +408 -33
  283. package/scripts/lib/config/dialectic.mjs +12 -3
  284. package/scripts/lib/config/drift-check.mjs +19 -0
  285. package/scripts/lib/config/gate.mjs +74 -0
  286. package/scripts/lib/config/reaper.mjs +162 -0
  287. package/scripts/lib/config.mjs +14 -0
  288. package/scripts/lib/convergence-monitor.mjs +76 -13
  289. package/scripts/lib/cursor-hook-bridge.mjs +2 -2
  290. package/scripts/lib/description-surface.mjs +2 -5
  291. package/scripts/lib/dispatcher/cli.mjs +2 -1
  292. package/scripts/lib/ecosystem-health.mjs +11 -0
  293. package/scripts/lib/ecosystem-wizard.mjs +2 -1
  294. package/scripts/lib/eval/engine.mjs +421 -53
  295. package/scripts/lib/eval/judge.mjs +463 -40
  296. package/scripts/lib/eval/schema.mjs +10 -1
  297. package/scripts/lib/events-rotation.mjs +221 -25
  298. package/scripts/lib/events-schema.mjs +114 -0
  299. package/scripts/lib/events.mjs +524 -5
  300. package/scripts/lib/fetch-baseline.mjs +3 -8
  301. package/scripts/lib/frontmatter-guard.mjs +21 -10
  302. package/scripts/lib/gates/gate-baseline.mjs +27 -2
  303. package/scripts/lib/gates/gate-full.mjs +28 -3
  304. package/scripts/lib/gates/gate-helpers.mjs +243 -21
  305. package/scripts/lib/gates/gate-incremental.mjs +28 -3
  306. package/scripts/lib/gates/gate-per-file.mjs +27 -2
  307. package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +2 -1
  308. package/scripts/lib/gitlab-portfolio/cli.mjs +2 -1
  309. package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
  310. package/scripts/lib/instruction-budget-guard.mjs +332 -50
  311. package/scripts/lib/io.mjs +42 -8
  312. package/scripts/lib/is-main-module.mjs +82 -0
  313. package/scripts/lib/issue-close-strip-labels.mjs +207 -49
  314. package/scripts/lib/js-mask.mjs +197 -0
  315. package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
  316. package/scripts/lib/locks/index.mjs +32 -25
  317. package/scripts/lib/maintenance-due-banner.mjs +122 -91
  318. package/scripts/lib/orphan-reaper.mjs +1588 -0
  319. package/scripts/lib/peer-cards/merger.mjs +48 -10
  320. package/scripts/lib/peer-cards/reader.mjs +78 -2
  321. package/scripts/lib/peer-discovery.mjs +2 -5
  322. package/scripts/lib/playwright-driver/runner.mjs +2 -1
  323. package/scripts/lib/process-group.mjs +899 -0
  324. package/scripts/lib/quality-gate.mjs +107 -28
  325. package/scripts/lib/reconcile/backlog.mjs +368 -0
  326. package/scripts/lib/reconcile/engine.mjs +55 -188
  327. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +884 -0
  328. package/scripts/lib/reconcile/sanitize.mjs +69 -3
  329. package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
  330. package/scripts/lib/resource-probe/parsers.mjs +31 -0
  331. package/scripts/lib/rule-loader.mjs +41 -12
  332. package/scripts/lib/rules-sync.mjs +2 -5
  333. package/scripts/lib/scope-echo.mjs +429 -7
  334. package/scripts/lib/scope-gate.mjs +605 -1
  335. package/scripts/lib/session-close-backfill.mjs +91 -12
  336. package/scripts/lib/session-id.mjs +9 -20
  337. package/scripts/lib/session-invocation.mjs +20 -0
  338. package/scripts/lib/session-schema/constants.mjs +30 -2
  339. package/scripts/lib/session-schema/normalizer.mjs +56 -4
  340. package/scripts/lib/session-schema.mjs +8 -3
  341. package/scripts/lib/session-start-probes.mjs +95 -10
  342. package/scripts/lib/sessions-canonical.mjs +23 -0
  343. package/scripts/lib/sessions-integrity-banner.mjs +7 -1
  344. package/scripts/lib/sessions-staleness-banner.mjs +193 -51
  345. package/scripts/lib/skill-evidence-window.mjs +891 -0
  346. package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
  347. package/scripts/lib/skill-evolution/engine.mjs +18 -9
  348. package/scripts/lib/skill-judge.mjs +45 -3
  349. package/scripts/lib/state-md.mjs +84 -3
  350. package/scripts/lib/sunset/walker.mjs +31 -4
  351. package/scripts/lib/tail-window.mjs +56 -0
  352. package/scripts/lib/telemetry/schema.mjs +30 -0
  353. package/scripts/lib/telemetry/sync.mjs +61 -6
  354. package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
  355. package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
  356. package/scripts/lib/tests-src-ratio.mjs +2 -6
  357. package/scripts/lib/tmux-layout/telemetry-stats.mjs +74 -14
  358. package/scripts/lib/user-invocable-skills.mjs +205 -0
  359. package/scripts/lib/ux-grill/reconcile.mjs +48 -22
  360. package/scripts/lib/validate/check-agents-skills.mjs +26 -15
  361. package/scripts/lib/validate/check-banner-parity.mjs +2 -2
  362. package/scripts/lib/validate/check-cursor-adapter.mjs +3 -2
  363. package/scripts/lib/validate/check-dead-bridge.mjs +2 -2
  364. package/scripts/lib/validate/check-doc-cli-commands.mjs +2 -2
  365. package/scripts/lib/validate/check-entry-guard.mjs +329 -0
  366. package/scripts/lib/validate/check-guard-requires-parity.mjs +2 -2
  367. package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
  368. package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +2 -2
  369. package/scripts/lib/validate/check-learning-provenance.mjs +2 -2
  370. package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
  371. package/scripts/lib/validate/check-rules.mjs +7 -5
  372. package/scripts/lib/validate/check-skill-links.mjs +35 -6
  373. package/scripts/lib/validate/check-skill-script-paths.mjs +241 -29
  374. package/scripts/lib/validate/check-test-git-config-target.mjs +26 -36
  375. package/scripts/lib/validate/check-unicode-safety.mjs +2 -2
  376. package/scripts/lib/validate/check-untracked-test-deps.mjs +9 -104
  377. package/scripts/lib/validate/check-unwired-features.mjs +220 -33
  378. package/scripts/lib/validate/check-validator-registration.mjs +36 -12
  379. package/scripts/lib/validate/check-vcs-repo-flag.mjs +2 -2
  380. package/scripts/lib/validate/confidential-names.mjs +10 -0
  381. package/scripts/lib/validate-vendored-rules.mjs +39 -12
  382. package/scripts/lib/vault-mirror/namespace.mjs +46 -8
  383. package/scripts/lib/vault-mirror/process.mjs +10 -3
  384. package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
  385. package/scripts/lib/vault-status/narrative-mirror.mjs +31 -7
  386. package/scripts/lib/vault-yaml.mjs +118 -0
  387. package/scripts/lib/wave-transcript-tail.mjs +2 -2
  388. package/scripts/lib/worktree/lifecycle.mjs +153 -1
  389. package/scripts/lock-reaper.mjs +2 -1
  390. package/scripts/materialize-wave-scope.mjs +87 -4
  391. package/scripts/migrate-sessions-jsonl.mjs +2 -1
  392. package/scripts/migrate-vault-paths.mjs +2 -3
  393. package/scripts/release-session-lock.mjs +305 -0
  394. package/scripts/release.mjs +109 -39
  395. package/scripts/relocate-vault-corpus.mjs +2 -3
  396. package/scripts/repair-invalid-sessions.mjs +2 -2
  397. package/scripts/resolve-session-invocation.mjs +59 -0
  398. package/scripts/run-quality-gate.mjs +156 -17
  399. package/scripts/session-shape.mjs +2 -2
  400. package/scripts/site-numbers.mjs +35 -11
  401. package/scripts/sweep-expired-rules.mjs +227 -0
  402. package/scripts/validate-plugin.mjs +21 -0
  403. package/scripts/validate-wave-scope.mjs +32 -105
  404. package/scripts/vault-consolidate.mjs +2 -2
  405. package/scripts/vault-mirror.mjs +11 -4
  406. package/scripts/wave-scope-binding.mjs +2 -3
  407. package/skills/_shared/bootstrap-gate.md +1 -1
  408. package/skills/_shared/monitor-patterns.md +1 -1
  409. package/skills/_shared/platform-tools.md +23 -11
  410. package/skills/_shared/research-evidence.md +53 -0
  411. package/skills/_shared/state-ownership.md +3 -0
  412. package/skills/autopilot/SKILL.md +80 -11
  413. package/skills/bootstrap/SKILL.md +51 -1
  414. package/skills/brainstorm/SKILL.md +16 -0
  415. package/skills/claude-md-drift-check/SKILL.md +1 -1
  416. package/skills/claude-md-drift-check/checker.mjs +49 -11
  417. package/{commands/close.md → skills/close/SKILL.md} +9 -3
  418. package/skills/convergence-monitoring/README.md +8 -1
  419. package/skills/convergence-monitoring/SIGNALS.md +50 -6
  420. package/skills/convergence-monitoring/SKILL.md +15 -6
  421. package/skills/debug/SKILL.md +10 -0
  422. package/skills/discovery/SKILL.md +24 -1
  423. package/skills/discovery/probes-session.md +2 -2
  424. package/skills/dispatcher/SKILL.md +38 -7
  425. package/skills/eli5/SKILL.md +11 -0
  426. package/skills/eval/SKILL.md +52 -23
  427. package/skills/eval/rubric-v1.md +1 -0
  428. package/skills/eval/rubric-v2.md +457 -0
  429. package/skills/evolve/SKILL.md +9 -2
  430. package/skills/evolve/references/evolve-dialectic-mode.md +46 -25
  431. package/skills/gitlab-ops/SKILL.md +3 -2
  432. package/{commands/go.md → skills/go/SKILL.md} +9 -1
  433. package/skills/grill/SKILL.md +19 -0
  434. package/{commands/harness-audit.md → skills/harness-audit/SKILL.md} +7 -2
  435. package/skills/hook-development/SKILL.md +46 -41
  436. package/skills/memory-cleanup/SKILL.md +7 -0
  437. package/skills/npm-publish/SKILL.md +2 -2
  438. package/skills/persona-panel/SKILL.md +56 -1
  439. package/skills/persona-panel/persona-format.md +1 -1
  440. package/skills/plan/SKILL.md +28 -1
  441. package/{commands/portfolio.md → skills/portfolio/SKILL.md} +8 -2
  442. package/skills/reconcile/SKILL.md +21 -0
  443. package/{commands/release.md → skills/release/SKILL.md} +16 -2
  444. package/skills/repo-audit/SKILL.md +7 -0
  445. package/skills/session-end/SKILL.md +13 -16
  446. package/skills/session-end/discovery-scan.md +1 -1
  447. package/skills/session-end/phase-3-6-tail.md +55 -9
  448. package/skills/session-end/plan-verification.md +2 -2
  449. package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
  450. package/skills/session-end/session-metrics-write.md +10 -0
  451. package/skills/session-plan/SKILL.md +18 -6
  452. package/skills/session-plan/references/session-plan-task-classification.md +2 -2
  453. package/skills/session-start/SKILL.md +5 -4
  454. package/skills/session-start/phase-8-5-express-path.md +6 -6
  455. package/skills/session-start/references/phase-1-5-session-continuity.md +1 -1
  456. package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +1 -1
  457. package/skills/session-start/references/phase-4-ssot-environment-check.md +6 -4
  458. package/skills/spinout/SKILL.md +12 -1
  459. package/skills/sunset-review/SKILL.md +13 -0
  460. package/{commands/test.md → skills/test/SKILL.md} +10 -4
  461. package/skills/ux-grill/SKILL.md +20 -2
  462. package/skills/wave-executor/SKILL.md +14 -7
  463. package/skills/wave-executor/circuit-breaker.md +2 -0
  464. package/skills/wave-executor/references/wave-executor-state-init.md +18 -4
  465. package/skills/wave-executor/references/wave-loop-dispatch.md +5 -2
  466. package/skills/wave-executor/references/wave-loop-review.md +17 -1
  467. package/commands/autopilot.md +0 -80
  468. package/commands/bootstrap.md +0 -56
  469. package/commands/brainstorm.md +0 -48
  470. package/commands/debug.md +0 -36
  471. package/commands/discovery.md +0 -32
  472. package/commands/dispatcher.md +0 -59
  473. package/commands/eli5.md +0 -33
  474. package/commands/eval.md +0 -28
  475. package/commands/evolve.md +0 -10
  476. package/commands/grill.md +0 -45
  477. package/commands/memory-cleanup.md +0 -26
  478. package/commands/persona-panel.md +0 -121
  479. package/commands/plan.md +0 -15
  480. package/commands/reconcile.md +0 -23
  481. package/commands/repo-audit.md +0 -24
  482. package/commands/spinout.md +0 -15
  483. package/commands/sunset-review.md +0 -27
  484. package/commands/ux-grill.md +0 -51
@@ -78,6 +78,7 @@
78
78
 
79
79
  import { existsSync, readdirSync, readFileSync } from 'node:fs';
80
80
  import { basename, join } from 'node:path';
81
+ import { scanEventsBackwards } from './events.mjs';
81
82
  import { loadApplicableRules, parseGlobsFrontmatter } from './rule-loader.mjs';
82
83
 
83
84
  /** Default directive ceiling (operator-chosen growth ratchet just above the ~457 baseline). */
@@ -255,17 +256,17 @@ export const DEFAULT_BYTE_CEILING = 121000;
255
256
  * → rulesDir extracted from `c73c094f`: { bytes: 76114, files: 8 }
256
257
  * → rulesDir extracted from `e4674109`: { bytes: 89763, files: 43 }
257
258
  *
258
- * The CONCLUSION stands, on a wider base: firing rate is 0 of 3, not 0 of 2 —
259
- * the guard would not have fired at any of the three states this repo has
260
- * recorded. What does NOT stand is the headroom claim it rested on. The live
261
- * corpus is **2,427 B** below the historical peak, not 13,649 B: a single
262
- * /reconcile wave of the size this one just absorbed puts the generated corpus
263
- * back at its worst recorded state, and the ceiling still would not notice.
264
- * "Decorative" is therefore an understatement of the gap, not an overstatement
265
- * which strengthens the case for the operator decision this block defers,
266
- * and changes none of its terms. Still tracked rather than silently patched:
267
- * raising OR lowering a threshold inside a population fix is exactly the
268
- * conflation this comment exists to end.
259
+ * HISTORY as of 2026-09-16 every number in this paragraph described the
260
+ * 124,000 ceiling, which no longer exists on this axis. It read, correctly for
261
+ * 2026-09-11: firing rate 0 of 3, not 0 of 2; the generated corpus then sat
262
+ * **2,427 B** below the peak known at that time (87,336 of 89,763 B), not
263
+ * 13,649 B, so one /reconcile wave of the size that session absorbed would have
264
+ * restored its worst recorded state with the ceiling still silent. Superseded
265
+ * on BOTH terms by the RE-CALIBRATED block below (ceiling 95,000, true peak
266
+ * 99,774 B, live distance to the peak 29,833 B measured 2026-09-16). Kept
267
+ * because it records the rule that produced the eventual fix: raising OR
268
+ * lowering a threshold inside a population fix is exactly the conflation this
269
+ * comment exists to end.
269
270
  *
270
271
  * FIRING-RATE AUDIT — widened from 3 states to 89 (#1308, 2026-09-11T17:26Z,
271
272
  * clean tree @ `4a49adc6`). HR-105 forbids a rule nothing records; "0 of 3"
@@ -273,35 +274,63 @@ export const DEFAULT_BYTE_CEILING = 121000;
273
274
  * guard was replayed over EVERY commit that touched `.claude/rules/`:
274
275
  * `git log --format=%h -- .claude/rules/` → 89 commits, each extracted with
275
276
  * `git archive <sha> .claude/rules | tar -x -C <tmp>` and fed to
276
- * `computeInstructionBudget({rulesDir})`:
277
+ * `computeInstructionBudget({rulesDir})`. Firing rates only — the headroom
278
+ * annotations this table used to carry are deliberately gone, because an
279
+ * absolute headroom rots between two edits of the corpus; read the live one
280
+ * from the axis itself (command in the RE-MEASURED block below):
277
281
  *
278
282
  * directive axis (480) → fires 4/89 = 4.5 % rare, healthy
279
- * byte axis (121,000) → fires 0/89 = 0.0 % live: 586 B headroom
280
- * generated axis (124,000) → fires 0/89 = 0.0 % peak ever 89,763 B
281
- * pathScoped axis (124,000) → fires 4/89 = 4.5 % falsifiable
282
- *
283
- * "Silently broken" is now RULED OUT: `pathScoped` runs the same code path off
284
- * the same walk and fires on 4 of the same 89 states. What remains is the
285
- * calibration gap — the all-time peak of this population is 89,763 B, i.e.
286
- * 72.4 % of the ceiling, so no state this repo has ever recorded could have
287
- * breached it, and none plausibly will before the corpus grows ~38 %.
288
- * A falsifiable ceiling for this axis would sit just above the recorded peak
289
- * (≈ 92,000 B, peak + 2.5 %); this module does NOT move it, because
290
- * `.claude/rules/development.md` § Guard & Threshold Design makes re-aiming a
291
- * threshold an operator decision, separate from measuring it. Revisit trigger:
292
- * re-run the 89-state replay above whenever the generated corpus passes
293
- * 92,000 B, or when this axis first fires (then it is calibrated, not
294
- * decorative).
295
- *
296
- * That trigger FIRED: the generated corpus stood at 99,774 B at `c16fb518`
297
- * (2026-09-12), past 92,000. It was answered by consolidation rather than by
298
- * a replay of this axis — the 8 generated files were merged down (prose only,
299
- * provenance unchanged) to 75,130 B / 8 files on the 2026-09-12 working tree
300
- * (#1316), back under the 92,000 mark. This ceiling is unchanged and the
301
- * 89-state replay of THIS axis was not re-run; the trigger stays armed for the
302
- * next pass of 92,000 B.
283
+ * byte axis (121,000) → fires 0/89 = 0.0 %
284
+ * generated axis (124,000) → fires 0/91 = 0.0 % superseded, see below
285
+ * pathScoped axis (124,000) → fires 5/91 = 5.5 % → calibrated
286
+ *
287
+ * RE-CALIBRATED 2026-09-16 (#1308). The replay was re-run over the FULL
288
+ * recorded history of this directory, not the 89-state subset:
289
+ *
290
+ * `git log --format=%h -- ./.claude/rules/` 91 states; each materialized
291
+ * with `git archive <sha> .claude/rules | tar -x -C <tmp>` and fed to
292
+ * `computeInstructionBudget({ rulesDir })`.
293
+ *
294
+ * True peak of the generated axis: **99,774 B @ `2ae08fe0`** — which CORRECTS
295
+ * the "peak ever 89,763 B" this docblock carried until now (that number came
296
+ * from a single replayed commit, not from the population). Median state:
297
+ * 4,802 B. Firing table over the same 91 states:
298
+ *
299
+ * 124,000 → 0/91 = 0.0 % (the standing, unfalsifiable value)
300
+ * 102,268 (peak+2.5%)→ 0/91 = 0.0 % (a ceiling above the peak never fires)
301
+ * 95,000 1/91 = 1.1 % ← chosen
302
+ * 88,000 → 3/91 = 3.3 %
303
+ * 80,000 → 5/91 = 5.5 %
304
+ *
305
+ * Why 95,000: it FIRES on recorded history (HR-105 a rule you cannot falsify
306
+ * is not a rule), at 1.1 % it sits in HR-101's rare band (a class above ~10 %
307
+ * is a broken instrument), and it stays silent on the corpus it lands on, so it
308
+ * does not go red before the #1367 consolidation has been committed. "Peak +
309
+ * 2.5 %" is REJECTED here for the reason the old text recommended it: above the
310
+ * peak, the axis is decorative by construction.
311
+ *
312
+ * RE-MEASURED 2026-09-16 @ `ca214376`, after the #1367 consolidation landed in
313
+ * the working tree (the state this ceiling was calibrated for is the state it
314
+ * now judges):
315
+ *
316
+ * node --input-type=module -e "const m = await import('./scripts/lib/instruction-budget-guard.mjs'); \
317
+ * console.log(JSON.stringify(m.computeInstructionBudget({ repoRoot: process.cwd() }).bySurface))"
318
+ * → generated { bytes: 69941, files: 7 } ×1.36 under 95,000
319
+ * pathScoped { bytes: 109932, files: 10 }
320
+ *
321
+ * Read the headroom off that command, never off a sentence in this block: the
322
+ * two numbers a verdict needs are the AXIS and its CEILING, and the distance
323
+ * between them changes with every rule edit. For the record at that SHA the
324
+ * generated axis sat 25,059 B under its ceiling and 29,833 B under the 99,774 B
325
+ * peak — i.e. one /reconcile wave the size of the largest recorded one would
326
+ * still not reach the peak, but a repeat of the growth that produced it would
327
+ * cross 95,000 first. That is the intended, rare signal: a cue to consolidate.
328
+ *
329
+ * BV-004 revisit trigger: re-run the 91-state replay when the generated corpus
330
+ * passes 90,000 B, or on this ceiling's FIRST fire — at which point it is
331
+ * calibrated by a real event rather than by a replay.
303
332
  */
304
- export const DEFAULT_GENERATED_BYTE_CEILING = 124000;
333
+ export const DEFAULT_GENERATED_BYTE_CEILING = 95000;
305
334
 
306
335
  /**
307
336
  * Default byte ceiling for the PATH-SCOPED rule surface — the fourth axis,
@@ -353,11 +382,14 @@ export const DEFAULT_GENERATED_BYTE_CEILING = 124000;
353
382
  * working tree (2026-09-11): 134,969 B / 11 files → FIRES
354
383
  *
355
384
  * Firing rate 2 of 3, falsifiable in both directions — the condition
356
- * {@link DEFAULT_GENERATED_BYTE_CEILING} does NOT currently meet (0 of 3).
357
- * Confirmed on a wider base (#1308, 2026-09-11T17:26Z @ `4a49adc6`): replayed
358
- * over all 89 commits that touched `.claude/rules/`, this axis fires 4/89
359
- * (4.5 %) against the generated axis's 0/89 — inside HR-101's rare band, and
360
- * falsifiable, which is exactly the property the sibling ceiling lacks.
385
+ * {@link DEFAULT_GENERATED_BYTE_CEILING} did not meet while it stood at
386
+ * 124,000 (0 of 3). Confirmed on a wider base (#1308, 2026-09-11T17:26Z @
387
+ * `4a49adc6`): replayed over all 89 commits that touched `.claude/rules/`, this
388
+ * axis fires 4/89 (4.5 %) — inside HR-101's rare band, and falsifiable.
389
+ * SUPERSEDED as a CONTRAST 2026-09-16: the sibling ceiling was re-calibrated to
390
+ * 95,000 in the same #1308 replay and now fires 1/91 (1.1 %) on recorded
391
+ * history, so "the property the sibling ceiling lacks" is no longer true of it —
392
+ * both axes are falsifiable, and this one is simply the looser of the two.
361
393
  * This is also why it is not re-derived upward off the live number: a ceiling
362
394
  * placed above 134,969 would be silent on all three states, i.e. the same
363
395
  * unfalsifiable shape, obtained by the threshold-patch move
@@ -385,9 +417,14 @@ export const DEFAULT_GENERATED_BYTE_CEILING = 124000;
385
417
  * the threshold-patch move `development.md` § Guard & Threshold Design
386
418
  * forbids.
387
419
  *
388
- * Headroom is 1,237 B. The next `/reconcile` rule (~2.5 KB written
389
- * standalone) trips this axis unless it is absorbed into a thematic file in
390
- * the SAME write step an obligation written in the /reconcile-overshoot
420
+ * Headroom moves with every rule edit, so it is measured, never quoted from
421
+ * here: `computeInstructionBudget({ repoRoot }).bySurface.pathScoped` against
422
+ * this constant. At 2026-09-16 @ `ca214376` that was 109,932 B / 10 files —
423
+ * 14,068 B under the ceiling, after the #1367 consolidation; on 2026-09-12 it
424
+ * was 1,237 B, which is how narrow this axis can get. The next `/reconcile`
425
+ * rule (~2.5 KB written standalone) trips it from a margin like that one
426
+ * unless it is absorbed into a thematic file in the SAME write step — an
427
+ * obligation written in the /reconcile-overshoot
391
428
  * learning of `.claude/rules/measurement-discipline.md` and checked by the
392
429
  * reconcile writer's budget pre-flight (`scripts/lib/reconcile/writer.mjs`).
393
430
  * A breach sets `overBudget` AND turns `tests/rules/receiving-review.test.mjs`
@@ -407,6 +444,35 @@ export const DEFAULT_GENERATED_BYTE_CEILING = 124000;
407
444
  */
408
445
  export const DEFAULT_PATH_SCOPED_BYTE_CEILING = 124000;
409
446
 
447
+ /**
448
+ * Share of {@link DEFAULT_PATH_SCOPED_BYTE_CEILING} at which the banner warns
449
+ * BEFORE the writer refuses (#1419).
450
+ *
451
+ * The defect #1419 records is not the ceiling but its TIMING: it becomes
452
+ * visible only inside `writeApprovedRules`, after the operator has already
453
+ * decided. Measured on that case (2026-09-20, /reconcile run): 117,973 of
454
+ * 124,000 B = 95.1 % — ~6 kB of headroom holding back `capped: 91`
455
+ * candidates, and `overPathScopedBudget` (a HARD `bytes > ceiling` test) did
456
+ * not fire, so nothing warned.
457
+ *
458
+ * 90 % is the distance of ~4 materialised rules: 10 % of 124,000 B = 12,400 B
459
+ * against the ~1.5 kB a generated rule file occupied on the #1419 tree — i.e.
460
+ * roughly four more accepted learnings until the hard break. A fixed fraction,
461
+ * deliberately not a per-repo calibration: it cannot know how large THIS
462
+ * repo's next rule is, and a second calibrated number would need its own
463
+ * falsification record.
464
+ *
465
+ * BV-004 ceiling + revisit trigger (HR-105): measure this line's firing rate
466
+ * after ~20 sessions from the probe records in
467
+ * `.orchestrator/metrics/events.jsonl`
468
+ * (`jq -c 'select(.event=="orchestrator.probes.completed")
469
+ * | .probes[] | select(.id=="instruction-budget")' …` — count `ran-warn`
470
+ * against the record total).
471
+ * Above ~10 % the instrument is RE-AIMED per HR-101 — neither lowered nor
472
+ * silenced; below it, leave it alone.
473
+ */
474
+ export const PATH_SCOPED_NEAR_THRESHOLD = 0.9;
475
+
410
476
  /**
411
477
  * Read the `instruction-budget:` nested block from the `## Session Config`
412
478
  * section of CLAUDE.md (or AGENTS.md) at `repoRoot`. Synchronous + never throws.
@@ -749,7 +815,7 @@ function sumBytes(entries) {
749
815
  * @param {Record<string, unknown>} meta
750
816
  * @returns {boolean}
751
817
  */
752
- function isMachineGeneratedRule(meta) {
818
+ export function isMachineGeneratedRule(meta) {
753
819
  if (!meta || typeof meta !== 'object') return false;
754
820
  return (
755
821
  meta['auto-generated'] === true ||
@@ -758,6 +824,110 @@ function isMachineGeneratedRule(meta) {
758
824
  );
759
825
  }
760
826
 
827
+ /** One day in milliseconds — local to this module, as in every other lib here. */
828
+ const MS_PER_DAY = 86_400_000;
829
+
830
+ /**
831
+ * Whole days from `now` until this rule's `expires-at`, or `null` when the rule
832
+ * declares no expiry or an unparseable one.
833
+ *
834
+ * Negative ⇒ already expired. The comparison basis is deliberately the SAME one
835
+ * `rule-loader.mjs` `applyGates` uses — `Date.parse(expires-at) < now`, i.e. a
836
+ * bare `YYYY-MM-DD` is UTC midnight — so "expired" here means exactly "the
837
+ * loader already filters it out of every wave's rule set", never a second
838
+ * definition of the same word. `Math.floor` therefore returns `-1` for a rule
839
+ * that expired at midnight today: past, but by less than a day.
840
+ *
841
+ * An unparseable date returns `null` (fail-OPEN, matching the loader, which
842
+ * warns and ignores the expiry rather than excluding the file) — the malformed
843
+ * value is a separate finding, not an expiry verdict.
844
+ *
845
+ * @param {{ meta?: Record<string, unknown>, now?: Date|number|string }} [opts]
846
+ * @returns {number|null}
847
+ */
848
+ export function daysUntilGeneratedRuleExpiry(opts = {}) {
849
+ const meta = opts.meta;
850
+ if (!meta || typeof meta !== 'object') return null;
851
+ if (!Object.prototype.hasOwnProperty.call(meta, 'expires-at')) return null;
852
+ const expiresAt = Date.parse(String(meta['expires-at']));
853
+ if (!Number.isFinite(expiresAt)) return null;
854
+
855
+ const raw = opts.now;
856
+ let now;
857
+ if (raw instanceof Date) now = raw.getTime();
858
+ else if (typeof raw === 'number' && Number.isFinite(raw)) now = raw;
859
+ else if (typeof raw === 'string' && Number.isFinite(Date.parse(raw))) now = Date.parse(raw);
860
+ else now = Date.now();
861
+
862
+ return Math.floor((expiresAt - now) / MS_PER_DAY);
863
+ }
864
+
865
+ /**
866
+ * Is this generated rule's `expires-at` already in the past?
867
+ *
868
+ * The defect it names (#1372): `rule-loader.mjs` filters an expired rule out at
869
+ * READ time, but nothing DELETES it — so the file keeps being counted by every
870
+ * byte ceiling above and keeps being loaded verbatim by Claude Code, which has
871
+ * no notion of `expires-at`.
872
+ *
873
+ * @param {{ meta?: Record<string, unknown>, now?: Date|number|string }} [opts]
874
+ * @returns {boolean}
875
+ */
876
+ export function isExpiredGeneratedRule(opts = {}) {
877
+ const days = daysUntilGeneratedRuleExpiry(opts);
878
+ return days !== null && days < 0;
879
+ }
880
+
881
+ /**
882
+ * Enumerate the machine-generated rule files in `rulesDir`.
883
+ *
884
+ * Same population, same filter and same frontmatter parser as
885
+ * {@link measureRuleCorpora}'s `generated` surface — that is the point of
886
+ * exporting it: a consumer that needs the FILES rather than the BYTES (the
887
+ * expiry test, the session-start maintenance probe) must not mirror the
888
+ * predicate, because a mirror agrees only until one side is edited.
889
+ *
890
+ * Three-state, like every reader in the banner family: `ok: false` means the
891
+ * directory exists but could not be enumerated, which a caller must record as
892
+ * "undeterminable" and never as "no generated rules". A MISSING directory is
893
+ * `ok: true` with an empty list — a repo without `.claude/rules/` genuinely has
894
+ * none.
895
+ *
896
+ * @param {{ rulesDir?: string, repoRoot?: string }} [opts]
897
+ * @returns {{ ok: boolean, rules: Array<{ file: string, meta: Record<string, unknown>, expiresAt: string|null }> }}
898
+ */
899
+ export function listMachineGeneratedRules(opts = {}) {
900
+ const dir =
901
+ typeof opts.rulesDir === 'string' && opts.rulesDir.length > 0
902
+ ? opts.rulesDir
903
+ : join(opts.repoRoot ?? process.cwd(), '.claude', 'rules');
904
+
905
+ /** @type {Array<{ file: string, meta: Record<string, unknown>, expiresAt: string|null }>} */
906
+ const rules = [];
907
+ let names;
908
+ try {
909
+ names = readdirSync(dir);
910
+ } catch (err) {
911
+ // ENOENT is an answer ("no generated rules"); anything else is not knowing.
912
+ return { ok: err?.code === 'ENOENT', rules };
913
+ }
914
+
915
+ for (const file of names) {
916
+ if (!file.endsWith('.md')) continue;
917
+ let meta;
918
+ try {
919
+ ({ meta } = parseGlobsFrontmatter(readFileSync(join(dir, file), 'utf8')));
920
+ } catch {
921
+ continue; // unreadable or unparseable — skip, never throw
922
+ }
923
+ if (!isMachineGeneratedRule(meta)) continue;
924
+ const raw = meta?.['expires-at'];
925
+ rules.push({ file, meta, expiresAt: typeof raw === 'string' ? raw : null });
926
+ }
927
+
928
+ return { ok: true, rules };
929
+ }
930
+
761
931
  /**
762
932
  * Single-pass scan of `.claude/rules/*.md` producing the TWO corpus measures
763
933
  * this guard's third axis needs, which #1297 proved are NOT the same set:
@@ -1104,6 +1274,83 @@ export function computeInstructionBudget(opts = {}) {
1104
1274
  };
1105
1275
  }
1106
1276
 
1277
+ /** Ledger event the backlog number below is read from (`reconcile/engine.mjs`). */
1278
+ const RECONCILE_EVENT = 'orchestrator.reconcile.completed';
1279
+
1280
+ /**
1281
+ * Wall-clock budget for the backwards ledger walk. Deliberately small: this
1282
+ * runs inside a session-start probe whose own budget is 2 s, and the number it
1283
+ * fetches is DECORATION on a line that is already actionable without it.
1284
+ */
1285
+ const RECONCILE_SCAN_BUDGET_MS = 150;
1286
+
1287
+ /**
1288
+ * `capped` from the most recent `orchestrator.reconcile.completed` record —
1289
+ * how many rule-eligible learnings the last /reconcile run held back.
1290
+ *
1291
+ * Three-state on purpose (`feedback_missing_measurement_looks_like_zero`):
1292
+ * `null` means NOT MEASURED (no ledger, no such record, unreadable source, or
1293
+ * the walk ran out of budget), which the caller must render as ABSENCE — never
1294
+ * as `0`, which is a measured "nothing waiting".
1295
+ *
1296
+ * @param {string|undefined} repoRoot
1297
+ * @returns {number|null}
1298
+ */
1299
+ function readLastReconcileCapped(repoRoot) {
1300
+ if (typeof repoRoot !== 'string' || repoRoot === '') return null;
1301
+ const filePath = join(repoRoot, '.orchestrator', 'metrics', 'events.jsonl');
1302
+ let capped = null;
1303
+ let scan;
1304
+ try {
1305
+ scan = scanEventsBackwards({
1306
+ filePath,
1307
+ budgetMs: RECONCILE_SCAN_BUDGET_MS,
1308
+ // Cheap raw-line pre-filter before JSON.parse — same reason as
1309
+ // `maintenance-due-banner.mjs`: parsing every line of a multi-MB ledger
1310
+ // to answer one question is the cost this option exists to avoid.
1311
+ filter: RECONCILE_EVENT,
1312
+ onRecord: (rec) => {
1313
+ if (rec?.event !== RECONCILE_EVENT) return false;
1314
+ capped = Number.isFinite(rec.capped) ? rec.capped : null;
1315
+ return true; // newest record wins; stop the walk
1316
+ },
1317
+ });
1318
+ } catch {
1319
+ return null; // never throw out of the banner wrapper
1320
+ }
1321
+ // A walk that did not finish has not proven anything about the backlog.
1322
+ if (scan.truncated || scan.unreadable.length > 0) return null;
1323
+ return capped;
1324
+ }
1325
+
1326
+ /**
1327
+ * The #1419 early-warning line: the path-scoped ceiling is close, not broken.
1328
+ *
1329
+ * Reports the numbers the RULE judged (HR-106) — the same `pathScoped.bytes`
1330
+ * and ceiling `writeApprovedRules` will compare later — plus, when the ledger
1331
+ * carries one, how many learnings the last /reconcile run already held back.
1332
+ * The percentage is formatted locale-free so the same corpus renders the same
1333
+ * string on every host.
1334
+ *
1335
+ * @param {ReturnType<typeof computeInstructionBudget>} budget
1336
+ * @param {string|undefined} repoRoot
1337
+ * @returns {string}
1338
+ */
1339
+ function formatNearPathScopedLine(budget, repoRoot) {
1340
+ const used = budget.bySurface.pathScoped.bytes;
1341
+ const ceiling = budget.pathScopedByteCeiling;
1342
+ const pct = ((used / ceiling) * 100).toFixed(1);
1343
+ const capped = readLastReconcileCapped(repoRoot);
1344
+ // Only a POSITIVE measured backlog is worth a clause; `0` and `null` both
1345
+ // add nothing the operator can act on, and printing `0` for `null` would be
1346
+ // the false-negative this helper's three-state contract exists to prevent.
1347
+ const backlog = typeof capped === 'number' && capped > 0 ? ` — ${capped} Learnings warten` : '';
1348
+ return (
1349
+ `path-scoped Regel-Decke zu ${pct} % belegt (${used}/${ceiling} B)${backlog} — ` +
1350
+ '/reconcile wird Learnings zurueckhalten'
1351
+ );
1352
+ }
1353
+
1107
1354
  /**
1108
1355
  * Banner wrapper — session-start Phase 4 convention.
1109
1356
  *
@@ -1128,10 +1375,15 @@ export function computeInstructionBudget(opts = {}) {
1128
1375
  * @param {number} [opts.byteCeiling] explicit byte-ceiling override (wins over config).
1129
1376
  * @param {number} [opts.pathScopedByteCeiling] explicit path-scoped-ceiling override.
1130
1377
  * @returns {{ severity: 'warn', message: string } | null}
1131
- * null when disabled / off / every axis at-or-under its ceiling OR on any
1132
- * read failure. Since #1316 each of the four axes — directives, bytes,
1378
+ * null when disabled / off / every axis comfortably under its ceiling OR on
1379
+ * any read failure. Since #1316 each of the four axes — directives, bytes,
1133
1380
  * generated, path-scoped — raises the banner on its own (see
1134
- * DEFAULT_PATH_SCOPED_BYTE_CEILING).
1381
+ * DEFAULT_PATH_SCOPED_BYTE_CEILING). Since #1419 the path-scoped axis ALSO
1382
+ * raises it at {@link PATH_SCOPED_NEAR_THRESHOLD} of its ceiling, before the
1383
+ * breach: that band used to be silent, and the ceiling then surfaced only
1384
+ * inside `writeApprovedRules` — after the operator had approved rules it
1385
+ * refused to write. The RETURN SHAPE is unchanged (`{severity:'warn',
1386
+ * message}` | null), so `session-start-probes.mjs` needs no new vocabulary.
1135
1387
  */
1136
1388
  export function checkInstructionBudget(opts = {}) {
1137
1389
  let cfg;
@@ -1191,7 +1443,33 @@ export function checkInstructionBudget(opts = {}) {
1191
1443
  return null; // never throw out of the banner wrapper
1192
1444
  }
1193
1445
 
1194
- if (!budget || !budget.overBudget) return null;
1446
+ if (!budget) return null;
1447
+
1448
+ // #1419 near-threshold stage. Strictly BELOW the hard test (a breach is
1449
+ // reported as a breach, never twice), and computed before the early return
1450
+ // so a corpus at 90–100 % of the path-scoped ceiling stops being silent —
1451
+ // that silence is the defect: the ceiling used to surface only inside
1452
+ // `writeApprovedRules`, after the operator had already approved rules.
1453
+ const nearPathScoped =
1454
+ !budget.overPathScopedBudget &&
1455
+ budget.pathScopedByteCeiling > 0 &&
1456
+ budget.bySurface.pathScoped.bytes >= budget.pathScopedByteCeiling * PATH_SCOPED_NEAR_THRESHOLD;
1457
+
1458
+ if (!budget.overBudget && !nearPathScoped) return null;
1459
+
1460
+ const nearLine = nearPathScoped ? formatNearPathScopedLine(budget, opts.repoRoot) : null;
1461
+
1462
+ // Near-threshold and nothing breached: a one-finding banner, carrying the
1463
+ // same remedy as the breach clause below (consolidate — never raise).
1464
+ if (!budget.overBudget) {
1465
+ return {
1466
+ severity: 'warn',
1467
+ message: [
1468
+ `⚠ ${nearLine}.`,
1469
+ ' Consolidate generated rules per docs/rule-authoring.md § Consolidated rules; never raise the ceiling.',
1470
+ ].join('\n'),
1471
+ };
1472
+ }
1195
1473
 
1196
1474
  // Name only the breached axes — listing a healthy axis would pad the line
1197
1475
  // without telling the operator anything they must act on.
@@ -1237,8 +1515,12 @@ export function checkInstructionBudget(opts = {}) {
1237
1515
  .map((f) => `${f.file} (${f.count} dir, ${f.bytes} B)`)
1238
1516
  .join(', ');
1239
1517
 
1518
+ // The near-threshold line rides ALONGSIDE the breach line rather than inside
1519
+ // `axes`: the path-scoped axis is not over, and folding a healthy-but-close
1520
+ // axis into a sentence that says "over" would misreport which ceiling broke.
1240
1521
  const message = [
1241
1522
  `⚠ Instruction budget over — ${axes.join(' · ')} across ${budget.perFile.length} always-on rules.`,
1523
+ ...(nearLine ? [` ${nearLine}.`] : []),
1242
1524
  ` Top files: ${top}`,
1243
1525
  ' See the instruction-budget audit (#687; archived in the private Meta-Vault) for the prune/demote list.',
1244
1526
  ].join('\n');
@@ -711,12 +711,33 @@ export function emitRewrite(updatedInput) {
711
711
  process.exit(0);
712
712
  };
713
713
 
714
- if (updatedInput === null || typeof updatedInput !== 'object' || Array.isArray(updatedInput)) {
715
- // `updatedInput` is a MAP in the bundle schema, so an array is as wrong as a
716
- // string — and an array would serialize into a shape the harness cannot use.
717
- bail(
718
- `was called with ${Array.isArray(updatedInput) ? 'an array' : String(updatedInput === null ? 'null' : typeof updatedInput)}, not a tool-input object`,
719
- );
714
+ // `updatedInput` is a MAP in the bundle schema, so an array is as wrong as a
715
+ // string and an array would serialize into a shape the harness cannot use.
716
+ //
717
+ // PLAIN object only, and `typeof === 'object'` is not that test. An object
718
+ // WRAPPER passes it and then serializes to something else entirely:
719
+ // `new Date()` and `new String('x')` become JSON STRINGS (their `toJSON` /
720
+ // primitive form), `new Map([...])` becomes `{}` — which looks valid and,
721
+ // per the doc above, deletes every field of the tool input. Each of the three
722
+ // reaches the harness as a schema-invalid `updatedInput`, and the bundle
723
+ // turns that into `behavior: "deny"`: on the `AskUserQuestion` path the
724
+ // operator's question is destroyed to save a rewrite. A prototype of
725
+ // `Object.prototype` (a literal) or `null` (`Object.create(null)`) is the
726
+ // whole admissible set; everything else takes the same no-op-plus-stderr
727
+ // contract as `null`, an array or a string.
728
+ const proto =
729
+ updatedInput !== null && typeof updatedInput === 'object'
730
+ ? Object.getPrototypeOf(updatedInput)
731
+ : undefined;
732
+ const isPlainObject =
733
+ !Array.isArray(updatedInput) && (proto === Object.prototype || proto === null);
734
+ if (!isPlainObject) {
735
+ let described;
736
+ if (Array.isArray(updatedInput)) described = 'an array';
737
+ else if (updatedInput === null) described = 'null';
738
+ else if (typeof updatedInput !== 'object') described = String(typeof updatedInput);
739
+ else described = `a non-plain object (${proto?.constructor?.name ?? 'unknown prototype'})`;
740
+ bail(`was called with ${described}, not a tool-input object`);
720
741
  }
721
742
 
722
743
  let line;
@@ -731,11 +752,24 @@ export function emitRewrite(updatedInput) {
731
752
  },
732
753
  });
733
754
  } catch (err) {
734
- // A cycle or a BigInt in the caller's object. JSON.stringify can also return
735
- // undefined (a toJSON that yields undefined) — caught by the same guard.
755
+ // A cycle or a BigInt in the caller's object.
736
756
  bail(`could not serialize the tool input (${err?.message ?? String(err)})`);
737
757
  }
738
758
 
759
+ // `JSON.stringify` returns `undefined` only for a TOP-LEVEL value it drops —
760
+ // and `updatedInput` sits NESTED inside the envelope literal above, so at this
761
+ // call site it never can. Measured 2026-09-18 with
762
+ // `{ toJSON() { return undefined; } }` as `updatedInput`: stdout is
763
+ // `{"hookSpecificOutput":{"hookEventName":"PreToolUse"}}` (53 bytes) — the
764
+ // property is silently OMITTED, not undefined, and the guard below does not
765
+ // fire. That degraded state is the safe one (an envelope without
766
+ // `updatedInput` carries no rewrite, so the tool call runs with its original
767
+ // input — exactly what `bail` produces), which is why it is documented rather
768
+ // than checked: re-parsing up to REWRITE_ENVELOPE_MAX_BYTES on every rewrite
769
+ // to recover one stderr line is a poor trade. The guard stays because it is
770
+ // the envelope SHAPE that makes the case unreachable: hoist `updatedInput` to
771
+ // the top level, or hand the literal to a helper that returns it, and
772
+ // `undefined` becomes reachable again.
739
773
  if (typeof line !== 'string') {
740
774
  bail('serialized the tool input to undefined (a toJSON returning undefined?)');
741
775
  }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * is-main-module.mjs — the one entry-guard predicate, symlink-safe.
3
+ *
4
+ * ## The incident class
5
+ *
6
+ * An ESM module that wants to run `main()` only when invoked as a CLI has to
7
+ * compare "the module I am" against "the script Node was told to run". The two
8
+ * sides are NOT the same kind of path:
9
+ *
10
+ * - `import.meta.url` is resolved by Node to the module's **realpath**.
11
+ * - `process.argv[1]` is the path **as typed on the command line**.
12
+ *
13
+ * With a symlink anywhere in the invocation path — a `node_modules/.bin/` shim,
14
+ * a `~/bin/foo -> /repo/scripts/foo.mjs` convenience link, a plugin directory
15
+ * symlinked into `~/.claude/plugins/`, or `/tmp` itself (on macOS `/tmp` is a
16
+ * symlink to `/private/tmp`) — the two strings differ and the guard is FALSE.
17
+ *
18
+ * The failure is silent and total: `main()` never runs, nothing is printed, and
19
+ * the process exits 0. Every caller — a hook, a CI job, an npm script, an
20
+ * operator — reads that as success. This is the single worst shape a defect can
21
+ * have in this repo, which is why the predicate is centralised here instead of
22
+ * being re-typed per file.
23
+ *
24
+ * ## The textual variants this replaces
25
+ *
26
+ * Measured 2026-09-16 @ ca214376 over the 50 files swept for #1371, the guard
27
+ * had been hand-written in 15 distinct textual forms (measured by extracting the
28
+ * guard expression from every file at HEAD), collapsing into four
29
+ * semantic variants — all four wrong in the same way:
30
+ *
31
+ * V1 import.meta.url === `file://${process.argv[1]}`
32
+ * (also breaks on any space or non-ASCII char in the path — no escaping)
33
+ * V2 import.meta.url === pathToFileURL(process.argv[1] ?? '').href
34
+ * V3 resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))
35
+ * / resolve(process.argv[1]) === resolve(__filename)
36
+ * V4 fileURLToPath(import.meta.url) === process.argv[1]
37
+ *
38
+ * V2/V3 fix the `undefined`-argv and relative-path cases; none of them resolve
39
+ * symlinks, because only `realpath` does.
40
+ *
41
+ * ## Non-goal: Windows drive-letter / case folding
42
+ *
43
+ * `realpathSync.native` delegates to the OS, so on NTFS it canonicalises the
44
+ * stored casing of every path component and the two sides agree. Where the
45
+ * realpath call FAILS (deleted path, permission error) the fallback is a raw
46
+ * string compare, which on Windows can be case-sensitive against a
47
+ * case-insensitive filesystem. That residual case is deliberately out of scope:
48
+ * this repo runs on macOS and Linux CI, and a case-only mismatch degrades to
49
+ * today's behaviour rather than to something worse.
50
+ *
51
+ * @module scripts/lib/is-main-module
52
+ */
53
+
54
+ import { realpathSync } from 'node:fs';
55
+ import { fileURLToPath } from 'node:url';
56
+
57
+ /**
58
+ * Whether the module identified by `importMetaUrl` is the script Node was
59
+ * invoked with.
60
+ *
61
+ * Symlink-safe: both sides are canonicalised via `realpathSync.native` before
62
+ * comparison, so an invocation through a symlink (shim, `~/bin` link, `/tmp` on
63
+ * macOS) still matches. When either path cannot be realpath'd — it was deleted
64
+ * between spawn and this call, or is unreadable — the comparison degrades to a
65
+ * raw string compare rather than throwing, because an entry guard must never be
66
+ * the thing that crashes an import.
67
+ *
68
+ * @param {string} importMetaUrl the calling module's `import.meta.url`
69
+ * @param {string | undefined} [argv1] the invoked script path; defaults to
70
+ * `process.argv[1]`, which is `undefined` under `node -e`, the REPL and some
71
+ * dynamic-import contexts — those return `false`
72
+ * @returns {boolean} true when this module IS the entrypoint
73
+ */
74
+ export function isMainModule(importMetaUrl, argv1 = process.argv[1]) {
75
+ if (!argv1) return false;
76
+ const self = fileURLToPath(importMetaUrl);
77
+ try {
78
+ return realpathSync.native(argv1) === realpathSync.native(self);
79
+ } catch {
80
+ return argv1 === self;
81
+ }
82
+ }