session-orchestrator 5.0.0 → 5.2.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 (298) hide show
  1. package/.agents/skills/autopilot/SKILL.md +1 -0
  2. package/.agents/skills/bootstrap/SKILL.md +2 -0
  3. package/.agents/skills/brainstorm/SKILL.md +3 -0
  4. package/.agents/skills/close/SKILL.md +17 -0
  5. package/.agents/skills/debug/SKILL.md +2 -0
  6. package/.agents/skills/discovery/SKILL.md +2 -1
  7. package/.agents/skills/dispatcher/SKILL.md +2 -0
  8. package/.agents/skills/eli5/SKILL.md +2 -0
  9. package/.agents/skills/eval/SKILL.md +1 -0
  10. package/.agents/skills/evolve/SKILL.md +2 -1
  11. package/.agents/skills/go/SKILL.md +18 -0
  12. package/.agents/skills/grill/SKILL.md +2 -0
  13. package/.agents/skills/harness-audit/SKILL.md +16 -0
  14. package/.agents/skills/memory-cleanup/SKILL.md +1 -0
  15. package/.agents/skills/persona-panel/SKILL.md +1 -0
  16. package/.agents/skills/plan/SKILL.md +3 -1
  17. package/.agents/skills/portfolio/SKILL.md +17 -0
  18. package/.agents/skills/reconcile/SKILL.md +1 -0
  19. package/.agents/skills/release/SKILL.md +18 -0
  20. package/.agents/skills/repo-audit/SKILL.md +1 -0
  21. package/.agents/skills/spinout/SKILL.md +1 -0
  22. package/.agents/skills/sunset-review/SKILL.md +2 -0
  23. package/.agents/skills/test/SKILL.md +17 -0
  24. package/.agents/skills/ux-grill/SKILL.md +2 -0
  25. package/.claude-plugin/marketplace.json +3 -3
  26. package/.claude-plugin/plugin.json +2 -2
  27. package/.codex-plugin/plugin.json +2 -2
  28. package/.codex-plugin/skills/autopilot/SKILL.md +5 -4
  29. package/.codex-plugin/skills/bootstrap/SKILL.md +8 -4
  30. package/.codex-plugin/skills/brainstorm/SKILL.md +11 -4
  31. package/.codex-plugin/skills/close/SKILL.md +3 -3
  32. package/.codex-plugin/skills/convergence-monitoring/SKILL.md +2 -0
  33. package/.codex-plugin/skills/convergence-monitoring/agents/openai.yaml +5 -0
  34. package/.codex-plugin/skills/debug/SKILL.md +11 -4
  35. package/.codex-plugin/skills/discovery/SKILL.md +8 -4
  36. package/.codex-plugin/skills/dispatcher/SKILL.md +4 -4
  37. package/.codex-plugin/skills/eli5/SKILL.md +9 -4
  38. package/.codex-plugin/skills/eval/SKILL.md +9 -4
  39. package/.codex-plugin/skills/evolve/SKILL.md +9 -4
  40. package/.codex-plugin/skills/go/SKILL.md +3 -3
  41. package/.codex-plugin/skills/grill/SKILL.md +11 -4
  42. package/.codex-plugin/skills/harness-audit/SKILL.md +4 -3
  43. package/.codex-plugin/skills/memory-cleanup/SKILL.md +9 -4
  44. package/.codex-plugin/skills/npm-publish/SKILL.md +2 -0
  45. package/.codex-plugin/skills/npm-publish/agents/openai.yaml +5 -0
  46. package/.codex-plugin/skills/persona-panel/SKILL.md +5 -5
  47. package/.codex-plugin/skills/plan/SKILL.md +8 -4
  48. package/.codex-plugin/skills/portfolio/SKILL.md +3 -3
  49. package/.codex-plugin/skills/reconcile/SKILL.md +9 -4
  50. package/.codex-plugin/skills/release/SKILL.md +3 -3
  51. package/.codex-plugin/skills/repo-audit/SKILL.md +6 -4
  52. package/.codex-plugin/skills/spinout/SKILL.md +4 -4
  53. package/.codex-plugin/skills/sunset-review/SKILL.md +5 -4
  54. package/.codex-plugin/skills/test/SKILL.md +3 -3
  55. package/.codex-plugin/skills/ux-grill/SKILL.md +11 -4
  56. package/.cursor/commands/autopilot.md +4 -4
  57. package/.cursor/commands/bootstrap.md +5 -4
  58. package/.cursor/commands/brainstorm.md +5 -4
  59. package/.cursor/commands/close.md +4 -3
  60. package/.cursor/commands/convergence-monitoring.md +13 -0
  61. package/.cursor/commands/debug.md +4 -4
  62. package/.cursor/commands/discovery.md +4 -4
  63. package/.cursor/commands/dispatcher.md +4 -4
  64. package/.cursor/commands/eli5.md +4 -4
  65. package/.cursor/commands/eval.md +4 -4
  66. package/.cursor/commands/evolve.md +4 -4
  67. package/.cursor/commands/go.md +4 -3
  68. package/.cursor/commands/grill.md +4 -4
  69. package/.cursor/commands/harness-audit.md +3 -3
  70. package/.cursor/commands/memory-cleanup.md +4 -4
  71. package/.cursor/commands/npm-publish.md +13 -0
  72. package/.cursor/commands/persona-panel.md +4 -4
  73. package/.cursor/commands/plan.md +5 -4
  74. package/.cursor/commands/portfolio.md +3 -3
  75. package/.cursor/commands/reconcile.md +4 -4
  76. package/.cursor/commands/release.md +4 -3
  77. package/.cursor/commands/repo-audit.md +4 -4
  78. package/.cursor/commands/spinout.md +4 -4
  79. package/.cursor/commands/sunset-review.md +4 -4
  80. package/.cursor/commands/test.md +3 -3
  81. package/.cursor/commands/ux-grill.md +4 -4
  82. package/.cursor/rules/010-session-workflow.mdc +2 -2
  83. package/.cursor/skills/bootstrap/SKILL.md +1 -0
  84. package/.cursor/skills/close/SKILL.md +13 -0
  85. package/.cursor/skills/debug/SKILL.md +0 -1
  86. package/.cursor/skills/discovery/SKILL.md +0 -1
  87. package/.cursor/skills/dispatcher/SKILL.md +0 -1
  88. package/.cursor/skills/eli5/SKILL.md +0 -1
  89. package/.cursor/skills/evolve/SKILL.md +0 -1
  90. package/.cursor/skills/go/SKILL.md +13 -0
  91. package/.cursor/skills/grill/SKILL.md +0 -1
  92. package/.cursor/skills/harness-audit/SKILL.md +12 -0
  93. package/.cursor/skills/portfolio/SKILL.md +12 -0
  94. package/.cursor/skills/release/SKILL.md +13 -0
  95. package/.cursor/skills/repo-audit/SKILL.md +0 -1
  96. package/.cursor/skills/sunset-review/SKILL.md +0 -1
  97. package/.cursor/skills/test/SKILL.md +12 -0
  98. package/.cursor/skills/ux-grill/SKILL.md +0 -1
  99. package/.cursor-plugin/plugin.json +2 -2
  100. package/.orchestrator/policy/blocked-commands.json +10 -0
  101. package/AGENTS.md +1 -1
  102. package/CHANGELOG.md +80 -0
  103. package/README.md +74 -235
  104. package/commands/session.md +10 -0
  105. package/docs/USER-GUIDE.md +24 -0
  106. package/docs/ci-setup.md +53 -0
  107. package/docs/codex-setup.md +1 -1
  108. package/docs/components.md +12 -5
  109. package/docs/events-schema.md +5 -1
  110. package/docs/install.md +128 -0
  111. package/docs/persona-panel.md +1 -1
  112. package/docs/pi-setup.md +1 -1
  113. package/docs/rule-authoring.md +83 -14
  114. package/docs/scope-collision-guard.md +2 -0
  115. package/docs/session-config-reference.md +6 -4
  116. package/docs/session-config-template.md +38 -0
  117. package/docs/telemetry.md +15 -0
  118. package/hooks/_lib/hook-import-set.json +46 -6
  119. package/hooks/_lib/subagent-paths.mjs +15 -0
  120. package/hooks/_lib/vcs-create-matcher.mjs +217 -62
  121. package/hooks/enforce-scope.mjs +42 -1
  122. package/hooks/hooks-codex.json +1 -1
  123. package/hooks/hooks.json +1 -1
  124. package/hooks/on-session-end.mjs +14 -2
  125. package/hooks/on-stop.mjs +43 -1
  126. package/hooks/post-bash-write-verify.mjs +3 -0
  127. package/hooks/pre-auq-clarity.mjs +3 -0
  128. package/hooks/pre-bash-issue-budget.mjs +103 -17
  129. package/hooks/pre-task-scope-disjoint.mjs +152 -3
  130. package/hooks/skill-invocation-telemetry.mjs +2 -1
  131. package/package.json +3 -2
  132. package/pi/prompts/autopilot.md +3 -3
  133. package/pi/prompts/bootstrap.md +3 -3
  134. package/pi/prompts/brainstorm.md +3 -3
  135. package/pi/prompts/close.md +2 -2
  136. package/pi/prompts/convergence-monitoring.md +11 -0
  137. package/pi/prompts/debug.md +3 -3
  138. package/pi/prompts/discovery.md +3 -3
  139. package/pi/prompts/dispatcher.md +3 -3
  140. package/pi/prompts/eli5.md +3 -3
  141. package/pi/prompts/eval.md +3 -3
  142. package/pi/prompts/evolve.md +3 -3
  143. package/pi/prompts/go.md +2 -2
  144. package/pi/prompts/grill.md +3 -3
  145. package/pi/prompts/harness-audit.md +2 -3
  146. package/pi/prompts/memory-cleanup.md +3 -3
  147. package/pi/prompts/npm-publish.md +11 -0
  148. package/pi/prompts/persona-panel.md +3 -3
  149. package/pi/prompts/plan.md +3 -3
  150. package/pi/prompts/portfolio.md +2 -2
  151. package/pi/prompts/reconcile.md +3 -3
  152. package/pi/prompts/release.md +3 -3
  153. package/pi/prompts/repo-audit.md +3 -4
  154. package/pi/prompts/session.md +1 -1
  155. package/pi/prompts/spinout.md +3 -3
  156. package/pi/prompts/sunset-review.md +3 -3
  157. package/pi/prompts/templates-ack.md +1 -1
  158. package/pi/prompts/test.md +3 -3
  159. package/pi/prompts/ux-grill.md +3 -3
  160. package/scripts/archive-closed-prds.mjs +2 -2
  161. package/scripts/auq-audit.mjs +2 -3
  162. package/scripts/backfill-abandoned-sessions.mjs +57 -3
  163. package/scripts/backfill-evidence-digest.mjs +2 -1
  164. package/scripts/backfill-learnings-from-vault.mjs +2 -2
  165. package/scripts/check-package-manager.mjs +2 -2
  166. package/scripts/ci/assert-vitest-green.mjs +2 -1
  167. package/scripts/emit-session.mjs +2 -3
  168. package/scripts/export-hw-learnings.mjs +2 -1
  169. package/scripts/express-path.mjs +1 -1
  170. package/scripts/gc-stale-worktrees.mjs +2 -1
  171. package/scripts/generate-codex-skills.mjs +48 -4
  172. package/scripts/generate-cursor-adapter.mjs +173 -9
  173. package/scripts/generate-hook-import-set.mjs +12 -27
  174. package/scripts/generate-pi-prompts.mjs +183 -13
  175. package/scripts/github-protection-audit.mjs +2 -3
  176. package/scripts/lib/agent-frontmatter.mjs +23 -1
  177. package/scripts/lib/claude-md-budget-lint.mjs +2 -5
  178. package/scripts/lib/command-blocker.mjs +209 -9
  179. package/scripts/lib/config/drift-check.mjs +19 -0
  180. package/scripts/lib/convergence-monitor.mjs +2 -2
  181. package/scripts/lib/cursor-hook-bridge.mjs +2 -2
  182. package/scripts/lib/description-surface.mjs +2 -5
  183. package/scripts/lib/dispatcher/cli.mjs +2 -1
  184. package/scripts/lib/ecosystem-wizard.mjs +2 -1
  185. package/scripts/lib/fetch-baseline.mjs +3 -8
  186. package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +2 -1
  187. package/scripts/lib/gitlab-portfolio/cli.mjs +2 -1
  188. package/scripts/lib/instruction-budget-guard.mjs +186 -46
  189. package/scripts/lib/is-main-module.mjs +82 -0
  190. package/scripts/lib/locks/index.mjs +32 -25
  191. package/scripts/lib/maintenance-due-banner.mjs +69 -3
  192. package/scripts/lib/peer-discovery.mjs +2 -5
  193. package/scripts/lib/playwright-driver/runner.mjs +63 -2
  194. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +642 -0
  195. package/scripts/lib/rules-sync.mjs +2 -5
  196. package/scripts/lib/scope-echo.mjs +392 -7
  197. package/scripts/lib/session-close-backfill.mjs +58 -6
  198. package/scripts/lib/state-md.mjs +84 -3
  199. package/scripts/lib/sunset/walker.mjs +31 -4
  200. package/scripts/lib/tests-src-ratio.mjs +2 -6
  201. package/scripts/lib/tmux-layout/telemetry-stats.mjs +2 -1
  202. package/scripts/lib/user-invocable-skills.mjs +185 -0
  203. package/scripts/lib/validate/check-banner-parity.mjs +2 -2
  204. package/scripts/lib/validate/check-cursor-adapter.mjs +2 -2
  205. package/scripts/lib/validate/check-dead-bridge.mjs +2 -2
  206. package/scripts/lib/validate/check-doc-cli-commands.mjs +2 -2
  207. package/scripts/lib/validate/check-entry-guard.mjs +366 -0
  208. package/scripts/lib/validate/check-guard-requires-parity.mjs +2 -2
  209. package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +2 -2
  210. package/scripts/lib/validate/check-learning-provenance.mjs +2 -2
  211. package/scripts/lib/validate/check-skill-links.mjs +27 -6
  212. package/scripts/lib/validate/check-skill-script-paths.mjs +2 -2
  213. package/scripts/lib/validate/check-test-git-config-target.mjs +2 -2
  214. package/scripts/lib/validate/check-unicode-safety.mjs +2 -2
  215. package/scripts/lib/validate/check-untracked-test-deps.mjs +2 -2
  216. package/scripts/lib/validate/check-unwired-features.mjs +266 -11
  217. package/scripts/lib/validate/check-validator-registration.mjs +2 -2
  218. package/scripts/lib/validate/check-vcs-repo-flag.mjs +2 -2
  219. package/scripts/lib/validate-vendored-rules.mjs +35 -9
  220. package/scripts/lib/wave-transcript-tail.mjs +2 -2
  221. package/scripts/lock-reaper.mjs +2 -1
  222. package/scripts/materialize-wave-scope.mjs +87 -4
  223. package/scripts/migrate-sessions-jsonl.mjs +2 -1
  224. package/scripts/migrate-vault-paths.mjs +2 -3
  225. package/scripts/release.mjs +124 -35
  226. package/scripts/relocate-vault-corpus.mjs +2 -3
  227. package/scripts/repair-invalid-sessions.mjs +2 -2
  228. package/scripts/session-shape.mjs +2 -2
  229. package/scripts/site-numbers.mjs +35 -11
  230. package/scripts/sweep-expired-rules.mjs +216 -0
  231. package/scripts/validate-plugin.mjs +9 -0
  232. package/scripts/vault-consolidate.mjs +2 -2
  233. package/scripts/vault-mirror.mjs +2 -3
  234. package/scripts/wave-scope-binding.mjs +2 -3
  235. package/skills/_shared/bootstrap-gate.md +1 -1
  236. package/skills/_shared/monitor-patterns.md +1 -1
  237. package/skills/_shared/research-evidence.md +53 -0
  238. package/skills/_shared/state-ownership.md +3 -0
  239. package/skills/autopilot/SKILL.md +58 -4
  240. package/skills/bootstrap/SKILL.md +51 -1
  241. package/skills/brainstorm/SKILL.md +16 -0
  242. package/skills/claude-md-drift-check/checker.mjs +49 -11
  243. package/{commands/close.md → skills/close/SKILL.md} +9 -3
  244. package/skills/debug/SKILL.md +10 -0
  245. package/skills/discovery/SKILL.md +24 -1
  246. package/skills/discovery/probes-session.md +2 -2
  247. package/skills/dispatcher/SKILL.md +38 -7
  248. package/skills/eli5/SKILL.md +11 -0
  249. package/skills/eval/SKILL.md +14 -0
  250. package/skills/evolve/SKILL.md +8 -1
  251. package/skills/evolve/references/evolve-dialectic-mode.md +6 -2
  252. package/{commands/go.md → skills/go/SKILL.md} +9 -1
  253. package/skills/grill/SKILL.md +19 -0
  254. package/{commands/harness-audit.md → skills/harness-audit/SKILL.md} +7 -2
  255. package/skills/hook-development/SKILL.md +46 -41
  256. package/skills/memory-cleanup/SKILL.md +7 -0
  257. package/skills/npm-publish/SKILL.md +1 -1
  258. package/skills/persona-panel/SKILL.md +56 -1
  259. package/skills/persona-panel/persona-format.md +1 -1
  260. package/skills/plan/SKILL.md +28 -1
  261. package/skills/playwright-driver/SKILL.md +7 -10
  262. package/{commands/portfolio.md → skills/portfolio/SKILL.md} +8 -2
  263. package/skills/reconcile/SKILL.md +10 -0
  264. package/{commands/release.md → skills/release/SKILL.md} +16 -2
  265. package/skills/repo-audit/SKILL.md +7 -0
  266. package/skills/session-end/plan-verification.md +2 -2
  267. package/skills/session-plan/SKILL.md +1 -1
  268. package/skills/session-start/SKILL.md +5 -4
  269. package/skills/session-start/phase-8-5-express-path.md +6 -6
  270. package/skills/session-start/references/phase-1-5-session-continuity.md +1 -1
  271. package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +1 -1
  272. package/skills/session-start/references/phase-4-ssot-environment-check.md +4 -3
  273. package/skills/spinout/SKILL.md +12 -1
  274. package/skills/sunset-review/SKILL.md +13 -0
  275. package/{commands/test.md → skills/test/SKILL.md} +10 -4
  276. package/skills/ux-grill/SKILL.md +19 -1
  277. package/skills/wave-executor/SKILL.md +7 -4
  278. package/skills/wave-executor/references/wave-executor-state-init.md +13 -1
  279. package/skills/wave-executor/references/wave-loop-dispatch.md +3 -1
  280. package/skills/wave-executor/references/wave-loop-review.md +17 -1
  281. package/commands/autopilot.md +0 -80
  282. package/commands/bootstrap.md +0 -56
  283. package/commands/brainstorm.md +0 -48
  284. package/commands/debug.md +0 -36
  285. package/commands/discovery.md +0 -32
  286. package/commands/dispatcher.md +0 -59
  287. package/commands/eli5.md +0 -33
  288. package/commands/eval.md +0 -28
  289. package/commands/evolve.md +0 -10
  290. package/commands/grill.md +0 -45
  291. package/commands/memory-cleanup.md +0 -26
  292. package/commands/persona-panel.md +0 -121
  293. package/commands/plan.md +0 -15
  294. package/commands/reconcile.md +0 -23
  295. package/commands/repo-audit.md +0 -24
  296. package/commands/spinout.md +0 -15
  297. package/commands/sunset-review.md +0 -27
  298. package/commands/ux-grill.md +0 -51
@@ -255,17 +255,17 @@ export const DEFAULT_BYTE_CEILING = 121000;
255
255
  * → rulesDir extracted from `c73c094f`: { bytes: 76114, files: 8 }
256
256
  * → rulesDir extracted from `e4674109`: { bytes: 89763, files: 43 }
257
257
  *
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.
258
+ * HISTORY as of 2026-09-16 every number in this paragraph described the
259
+ * 124,000 ceiling, which no longer exists on this axis. It read, correctly for
260
+ * 2026-09-11: firing rate 0 of 3, not 0 of 2; the generated corpus then sat
261
+ * **2,427 B** below the peak known at that time (87,336 of 89,763 B), not
262
+ * 13,649 B, so one /reconcile wave of the size that session absorbed would have
263
+ * restored its worst recorded state with the ceiling still silent. Superseded
264
+ * on BOTH terms by the RE-CALIBRATED block below (ceiling 95,000, true peak
265
+ * 99,774 B, live distance to the peak 29,833 B measured 2026-09-16). Kept
266
+ * because it records the rule that produced the eventual fix: raising OR
267
+ * lowering a threshold inside a population fix is exactly the conflation this
268
+ * comment exists to end.
269
269
  *
270
270
  * FIRING-RATE AUDIT — widened from 3 states to 89 (#1308, 2026-09-11T17:26Z,
271
271
  * clean tree @ `4a49adc6`). HR-105 forbids a rule nothing records; "0 of 3"
@@ -273,35 +273,63 @@ export const DEFAULT_BYTE_CEILING = 121000;
273
273
  * guard was replayed over EVERY commit that touched `.claude/rules/`:
274
274
  * `git log --format=%h -- .claude/rules/` → 89 commits, each extracted with
275
275
  * `git archive <sha> .claude/rules | tar -x -C <tmp>` and fed to
276
- * `computeInstructionBudget({rulesDir})`:
276
+ * `computeInstructionBudget({rulesDir})`. Firing rates only — the headroom
277
+ * annotations this table used to carry are deliberately gone, because an
278
+ * absolute headroom rots between two edits of the corpus; read the live one
279
+ * from the axis itself (command in the RE-MEASURED block below):
277
280
  *
278
281
  * 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.
282
+ * byte axis (121,000) → fires 0/89 = 0.0 %
283
+ * generated axis (124,000) → fires 0/91 = 0.0 % superseded, see below
284
+ * pathScoped axis (124,000) → fires 5/91 = 5.5 % → calibrated
285
+ *
286
+ * RE-CALIBRATED 2026-09-16 (#1308). The replay was re-run over the FULL
287
+ * recorded history of this directory, not the 89-state subset:
288
+ *
289
+ * `git log --format=%h -- ./.claude/rules/` 91 states; each materialized
290
+ * with `git archive <sha> .claude/rules | tar -x -C <tmp>` and fed to
291
+ * `computeInstructionBudget({ rulesDir })`.
292
+ *
293
+ * True peak of the generated axis: **99,774 B @ `2ae08fe0`** — which CORRECTS
294
+ * the "peak ever 89,763 B" this docblock carried until now (that number came
295
+ * from a single replayed commit, not from the population). Median state:
296
+ * 4,802 B. Firing table over the same 91 states:
297
+ *
298
+ * 124,000 → 0/91 = 0.0 % (the standing, unfalsifiable value)
299
+ * 102,268 (peak+2.5%)→ 0/91 = 0.0 % (a ceiling above the peak never fires)
300
+ * 95,000 1/91 = 1.1 % ← chosen
301
+ * 88,000 → 3/91 = 3.3 %
302
+ * 80,000 → 5/91 = 5.5 %
303
+ *
304
+ * Why 95,000: it FIRES on recorded history (HR-105 a rule you cannot falsify
305
+ * is not a rule), at 1.1 % it sits in HR-101's rare band (a class above ~10 %
306
+ * is a broken instrument), and it stays silent on the corpus it lands on, so it
307
+ * does not go red before the #1367 consolidation has been committed. "Peak +
308
+ * 2.5 %" is REJECTED here for the reason the old text recommended it: above the
309
+ * peak, the axis is decorative by construction.
310
+ *
311
+ * RE-MEASURED 2026-09-16 @ `ca214376`, after the #1367 consolidation landed in
312
+ * the working tree (the state this ceiling was calibrated for is the state it
313
+ * now judges):
314
+ *
315
+ * node --input-type=module -e "const m = await import('./scripts/lib/instruction-budget-guard.mjs'); \
316
+ * console.log(JSON.stringify(m.computeInstructionBudget({ repoRoot: process.cwd() }).bySurface))"
317
+ * → generated { bytes: 69941, files: 7 } ×1.36 under 95,000
318
+ * pathScoped { bytes: 109932, files: 10 }
319
+ *
320
+ * Read the headroom off that command, never off a sentence in this block: the
321
+ * two numbers a verdict needs are the AXIS and its CEILING, and the distance
322
+ * between them changes with every rule edit. For the record at that SHA the
323
+ * generated axis sat 25,059 B under its ceiling and 29,833 B under the 99,774 B
324
+ * peak — i.e. one /reconcile wave the size of the largest recorded one would
325
+ * still not reach the peak, but a repeat of the growth that produced it would
326
+ * cross 95,000 first. That is the intended, rare signal: a cue to consolidate.
327
+ *
328
+ * BV-004 revisit trigger: re-run the 91-state replay when the generated corpus
329
+ * passes 90,000 B, or on this ceiling's FIRST fire — at which point it is
330
+ * calibrated by a real event rather than by a replay.
303
331
  */
304
- export const DEFAULT_GENERATED_BYTE_CEILING = 124000;
332
+ export const DEFAULT_GENERATED_BYTE_CEILING = 95000;
305
333
 
306
334
  /**
307
335
  * Default byte ceiling for the PATH-SCOPED rule surface — the fourth axis,
@@ -353,11 +381,14 @@ export const DEFAULT_GENERATED_BYTE_CEILING = 124000;
353
381
  * working tree (2026-09-11): 134,969 B / 11 files → FIRES
354
382
  *
355
383
  * 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.
384
+ * {@link DEFAULT_GENERATED_BYTE_CEILING} did not meet while it stood at
385
+ * 124,000 (0 of 3). Confirmed on a wider base (#1308, 2026-09-11T17:26Z @
386
+ * `4a49adc6`): replayed over all 89 commits that touched `.claude/rules/`, this
387
+ * axis fires 4/89 (4.5 %) — inside HR-101's rare band, and falsifiable.
388
+ * SUPERSEDED as a CONTRAST 2026-09-16: the sibling ceiling was re-calibrated to
389
+ * 95,000 in the same #1308 replay and now fires 1/91 (1.1 %) on recorded
390
+ * history, so "the property the sibling ceiling lacks" is no longer true of it —
391
+ * both axes are falsifiable, and this one is simply the looser of the two.
361
392
  * This is also why it is not re-derived upward off the live number: a ceiling
362
393
  * placed above 134,969 would be silent on all three states, i.e. the same
363
394
  * unfalsifiable shape, obtained by the threshold-patch move
@@ -385,9 +416,14 @@ export const DEFAULT_GENERATED_BYTE_CEILING = 124000;
385
416
  * the threshold-patch move `development.md` § Guard & Threshold Design
386
417
  * forbids.
387
418
  *
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
419
+ * Headroom moves with every rule edit, so it is measured, never quoted from
420
+ * here: `computeInstructionBudget({ repoRoot }).bySurface.pathScoped` against
421
+ * this constant. At 2026-09-16 @ `ca214376` that was 109,932 B / 10 files —
422
+ * 14,068 B under the ceiling, after the #1367 consolidation; on 2026-09-12 it
423
+ * was 1,237 B, which is how narrow this axis can get. The next `/reconcile`
424
+ * rule (~2.5 KB written standalone) trips it from a margin like that one
425
+ * unless it is absorbed into a thematic file in the SAME write step — an
426
+ * obligation written in the /reconcile-overshoot
391
427
  * learning of `.claude/rules/measurement-discipline.md` and checked by the
392
428
  * reconcile writer's budget pre-flight (`scripts/lib/reconcile/writer.mjs`).
393
429
  * A breach sets `overBudget` AND turns `tests/rules/receiving-review.test.mjs`
@@ -749,7 +785,7 @@ function sumBytes(entries) {
749
785
  * @param {Record<string, unknown>} meta
750
786
  * @returns {boolean}
751
787
  */
752
- function isMachineGeneratedRule(meta) {
788
+ export function isMachineGeneratedRule(meta) {
753
789
  if (!meta || typeof meta !== 'object') return false;
754
790
  return (
755
791
  meta['auto-generated'] === true ||
@@ -758,6 +794,110 @@ function isMachineGeneratedRule(meta) {
758
794
  );
759
795
  }
760
796
 
797
+ /** One day in milliseconds — local to this module, as in every other lib here. */
798
+ const MS_PER_DAY = 86_400_000;
799
+
800
+ /**
801
+ * Whole days from `now` until this rule's `expires-at`, or `null` when the rule
802
+ * declares no expiry or an unparseable one.
803
+ *
804
+ * Negative ⇒ already expired. The comparison basis is deliberately the SAME one
805
+ * `rule-loader.mjs` `applyGates` uses — `Date.parse(expires-at) < now`, i.e. a
806
+ * bare `YYYY-MM-DD` is UTC midnight — so "expired" here means exactly "the
807
+ * loader already filters it out of every wave's rule set", never a second
808
+ * definition of the same word. `Math.floor` therefore returns `-1` for a rule
809
+ * that expired at midnight today: past, but by less than a day.
810
+ *
811
+ * An unparseable date returns `null` (fail-OPEN, matching the loader, which
812
+ * warns and ignores the expiry rather than excluding the file) — the malformed
813
+ * value is a separate finding, not an expiry verdict.
814
+ *
815
+ * @param {{ meta?: Record<string, unknown>, now?: Date|number|string }} [opts]
816
+ * @returns {number|null}
817
+ */
818
+ export function daysUntilGeneratedRuleExpiry(opts = {}) {
819
+ const meta = opts.meta;
820
+ if (!meta || typeof meta !== 'object') return null;
821
+ if (!Object.prototype.hasOwnProperty.call(meta, 'expires-at')) return null;
822
+ const expiresAt = Date.parse(String(meta['expires-at']));
823
+ if (!Number.isFinite(expiresAt)) return null;
824
+
825
+ const raw = opts.now;
826
+ let now;
827
+ if (raw instanceof Date) now = raw.getTime();
828
+ else if (typeof raw === 'number' && Number.isFinite(raw)) now = raw;
829
+ else if (typeof raw === 'string' && Number.isFinite(Date.parse(raw))) now = Date.parse(raw);
830
+ else now = Date.now();
831
+
832
+ return Math.floor((expiresAt - now) / MS_PER_DAY);
833
+ }
834
+
835
+ /**
836
+ * Is this generated rule's `expires-at` already in the past?
837
+ *
838
+ * The defect it names (#1372): `rule-loader.mjs` filters an expired rule out at
839
+ * READ time, but nothing DELETES it — so the file keeps being counted by every
840
+ * byte ceiling above and keeps being loaded verbatim by Claude Code, which has
841
+ * no notion of `expires-at`.
842
+ *
843
+ * @param {{ meta?: Record<string, unknown>, now?: Date|number|string }} [opts]
844
+ * @returns {boolean}
845
+ */
846
+ export function isExpiredGeneratedRule(opts = {}) {
847
+ const days = daysUntilGeneratedRuleExpiry(opts);
848
+ return days !== null && days < 0;
849
+ }
850
+
851
+ /**
852
+ * Enumerate the machine-generated rule files in `rulesDir`.
853
+ *
854
+ * Same population, same filter and same frontmatter parser as
855
+ * {@link measureRuleCorpora}'s `generated` surface — that is the point of
856
+ * exporting it: a consumer that needs the FILES rather than the BYTES (the
857
+ * expiry test, the session-start maintenance probe) must not mirror the
858
+ * predicate, because a mirror agrees only until one side is edited.
859
+ *
860
+ * Three-state, like every reader in the banner family: `ok: false` means the
861
+ * directory exists but could not be enumerated, which a caller must record as
862
+ * "undeterminable" and never as "no generated rules". A MISSING directory is
863
+ * `ok: true` with an empty list — a repo without `.claude/rules/` genuinely has
864
+ * none.
865
+ *
866
+ * @param {{ rulesDir?: string, repoRoot?: string }} [opts]
867
+ * @returns {{ ok: boolean, rules: Array<{ file: string, meta: Record<string, unknown>, expiresAt: string|null }> }}
868
+ */
869
+ export function listMachineGeneratedRules(opts = {}) {
870
+ const dir =
871
+ typeof opts.rulesDir === 'string' && opts.rulesDir.length > 0
872
+ ? opts.rulesDir
873
+ : join(opts.repoRoot ?? process.cwd(), '.claude', 'rules');
874
+
875
+ /** @type {Array<{ file: string, meta: Record<string, unknown>, expiresAt: string|null }>} */
876
+ const rules = [];
877
+ let names;
878
+ try {
879
+ names = readdirSync(dir);
880
+ } catch (err) {
881
+ // ENOENT is an answer ("no generated rules"); anything else is not knowing.
882
+ return { ok: err?.code === 'ENOENT', rules };
883
+ }
884
+
885
+ for (const file of names) {
886
+ if (!file.endsWith('.md')) continue;
887
+ let meta;
888
+ try {
889
+ ({ meta } = parseGlobsFrontmatter(readFileSync(join(dir, file), 'utf8')));
890
+ } catch {
891
+ continue; // unreadable or unparseable — skip, never throw
892
+ }
893
+ if (!isMachineGeneratedRule(meta)) continue;
894
+ const raw = meta?.['expires-at'];
895
+ rules.push({ file, meta, expiresAt: typeof raw === 'string' ? raw : null });
896
+ }
897
+
898
+ return { ok: true, rules };
899
+ }
900
+
761
901
  /**
762
902
  * Single-pass scan of `.claude/rules/*.md` producing the TWO corpus measures
763
903
  * this guard's third axis needs, which #1297 proved are NOT the same set:
@@ -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
+ }
@@ -1,31 +1,38 @@
1
1
  /**
2
- * locks/index.mjs — convenience barrel for the short-lived lock protocols split
3
- * out of session-lock.mjs in #630 (A1 barrel-preserving split).
2
+ * locks/index.mjs — DEPRECATED convenience barrel for the lock protocols
3
+ * split out of session-lock.mjs in #630 (A1 barrel-preserving split).
4
4
  *
5
- * Re-exports both protocol modules so a consumer can `import { withStateMdLock,
6
- * withStagingFenceLock } from '@lib/locks/index.mjs'` in one statement. This is
7
- * a nice-to-have: session-lock.mjs remains the canonical barrel that preserves
8
- * the original 22-symbol import surface, and the two protocol modules are also
9
- * importable directly.
5
+ * @deprecated since 5.2.0, removed in 6.0.0. Import directly from
6
+ * `./state-md-lock.mjs` / `./staging-fence-lock.mjs`, or from
7
+ * `scripts/lib/session-lock.mjs` for the canonical 22-symbol barrel that
8
+ * preserves the original import surface for all its importers.
10
9
  *
11
- * Pure re-export leaf imports only the two sibling modules, never
12
- * session-lock.mjs, so there is no import cycle.
10
+ * This file shipped in 5.1.0 with zero in-repo importers and was deleted in
11
+ * dd05e0f8 (`check-unwired-features` S4: no hook, npm script, CI job or husky
12
+ * stage reached it, and it named symbols — not modules — so it did not qualify
13
+ * for the pure `export *` exemption that shape gets). Restored here as a
14
+ * one-cycle deprecation shim (`.claude/rules/development.md` § Package Lifecycle
15
+ * & Versioning) because `package.json` carries no `exports` map: every packed
16
+ * file under `scripts/lib/` is public, so a consumer that deep-imported this
17
+ * path before the deletion has no compile error to warn it, only a runtime
18
+ * "module not found" — a shim buys that consumer one minor cycle to migrate
19
+ * before 6.0.0 removes the path outright.
20
+ *
21
+ * Deliberately the `export *` form with ZERO named exports (matches the
22
+ * `scripts/lib/autopilot-telemetry.mjs` precedent): a named-export re-export
23
+ * barrel is exactly the shape `check-unwired-features.mjs` S4 flags as an
24
+ * unreachable library module (condition 4 in `collectUnreachableLibraryModules`
25
+ * requires named exports before a re-export shim over live code enters that
26
+ * census) — that flag is what got this file deleted the first time. A star
27
+ * re-export names no symbol, so the shim reads as `exports: []` and stays
28
+ * invisible to that census while its target modules stay reachable through it.
13
29
  */
14
30
 
15
- export {
16
- STATE_LOCK_PATH,
17
- DEFAULT_STATE_LOCK_TIMEOUT_MS,
18
- STATE_LOCK_POLL_MS,
19
- acquireStateLock,
20
- releaseStateLock,
21
- withStateMdLock,
22
- } from './state-md-lock.mjs';
31
+ console.warn(
32
+ '[deprecated] scripts/lib/locks/index.mjs is deprecated since 5.2.0 and will be removed in 6.0.0 — ' +
33
+ "import from './state-md-lock.mjs' / './staging-fence-lock.mjs' directly, or from " +
34
+ 'scripts/lib/session-lock.mjs for the canonical surface.',
35
+ );
23
36
 
24
- export {
25
- STAGING_FENCE_LOCK_PATH,
26
- DEFAULT_STAGING_FENCE_LOCK_TIMEOUT_MS,
27
- STAGING_FENCE_LOCK_POLL_MS,
28
- acquireStagingFenceLock,
29
- releaseStagingFenceLock,
30
- withStagingFenceLock,
31
- } from './staging-fence-lock.mjs';
37
+ export * from './state-md-lock.mjs';
38
+ export * from './staging-fence-lock.mjs';
@@ -20,7 +20,7 @@
20
20
  * ## Design constraints it is written against
21
21
  *
22
22
  * - **HR-101 (a signal may only warn if it is rare).** Nothing is due ⇒ the
23
- * probe is SILENT. Six independent signals are ANDed with a cooldown, not
23
+ * probe is SILENT. Seven independent signals are ANDed with a cooldown, not
24
24
  * ORed into a permanent warning: a repo that ran `/session housekeeping`
25
25
  * within the last {@link HOUSEKEEPING_COOLDOWN_DAYS} days says nothing at
26
26
  * all, because the operator already did the thing the banner would ask for.
@@ -33,7 +33,7 @@
33
33
  * an existing module constant. Same posture as `reconcile-nudge-banner.mjs`
34
34
  * and `loop-readiness-banner.mjs`: an advisory banner needs no switch.
35
35
  *
36
- * ## The six signals
36
+ * ## The seven signals
37
37
  *
38
38
  * | id | due when | source |
39
39
  * |-------------------|-----------------------------------------------------------------------|--------|
@@ -43,6 +43,20 @@
43
43
  * | `dialectic` | `shouldDispatchAutoDialectic().trigger === true` | `auto-dialectic.mjs` |
44
44
  * | `memory-cleanup` | `shouldDispatchAutoDream().trigger === true` | `auto-dream.mjs` |
45
45
  * | `pending-sidecar` | a pending dream/dialectic proposal younger than 14 days is unapplied | `.orchestrator/*-pending*.md` |
46
+ * | `generated-rules-expiring` | a machine-generated rule is expired or expires within {@link GENERATED_RULE_EXPIRY_HORIZON_DAYS} days | `instruction-budget-guard.mjs` |
47
+ *
48
+ * ### Why the expiry alarm is HERE and not in the test suite (#1372 follow-up)
49
+ *
50
+ * The invariant "no generated rule file sits expired in `.claude/rules/`" was
51
+ * first written as a vitest case comparing `expires-at` against TODAY. That is
52
+ * a calendar time-bomb in a BLOCKING gate: with nothing committed and nothing
53
+ * broken, `npm test` — pre-push hook and CI alike — turns red on the morning
54
+ * after the earliest date and blocks every unrelated hotfix, while the repair
55
+ * (consolidate the file, move its provenance pairs, delete it) is a human
56
+ * decision no test run can take. HR-101/HR-105: a signal must fire when
57
+ * something CHANGED, at a moment the operator can act, and be repairable by the
58
+ * thing that fires it. Session start, recommending `/session housekeeping`, is
59
+ * that moment; the predicate itself is still tested, against an INJECTED clock.
46
60
  *
47
61
  * Only SIDE-EFFECT-FREE signal functions are called — never a variant that
48
62
  * advances `.orchestrator/dialectic-last-run`, because a probe that writes the
@@ -66,13 +80,32 @@ import { shouldDispatchAutoDream } from './auto-dream.mjs';
66
80
  import { resolveMemoryDir } from './memory-paths.mjs';
67
81
  import { readCanonicalSessions } from './sessions-canonical.mjs';
68
82
  import { filterRealSessions } from './session-schema.mjs';
83
+ import {
84
+ daysUntilGeneratedRuleExpiry,
85
+ listMachineGeneratedRules,
86
+ } from './instruction-budget-guard.mjs';
69
87
 
70
88
  /**
71
89
  * How many signals this probe knows about. The banner denominator is this
72
90
  * number MINUS the signals a kill-switch skipped on this host — see the
73
91
  * `skipped` array in {@link computeMaintenanceDue}'s result.
74
92
  */
75
- export const MAINTENANCE_TOTAL_SIGNALS = 6;
93
+ export const MAINTENANCE_TOTAL_SIGNALS = 7;
94
+
95
+ /**
96
+ * How far ahead the `generated-rules-expiring` signal looks.
97
+ *
98
+ * NAMED CEILING (BV-004): 7 days is one week of sessions — long enough that the
99
+ * operator meets the warning before the loader silently starts dropping the
100
+ * rule, short enough that it is not standing noise (HR-104: a signal that is
101
+ * essentially always present is no signal). On this repo's corpus at
102
+ * 2026-09-16 the earliest `expires-at` was 14 days out, so the row is silent.
103
+ *
104
+ * REVISIT TRIGGER: if the row is due on more than ~1 session start in 10, the
105
+ * horizon is measuring the reconcile engine's 30-day default TTL rather than an
106
+ * overdue consolidation — re-aim it (HR-101), never widen it.
107
+ */
108
+ export const GENERATED_RULE_EXPIRY_HORIZON_DAYS = 7;
76
109
 
77
110
  /**
78
111
  * Active-learning floor for the `evolve` signal. Deliberately the SAME number
@@ -391,6 +424,39 @@ export async function computeMaintenanceDue(opts = {}) {
391
424
  undeterminable.push('pending-sidecar');
392
425
  }
393
426
 
427
+ // --- generated-rules-expiring (S7) ---------------------------------------
428
+ // The predicate and the population both come from `instruction-budget-guard`,
429
+ // which is where the generated corpus is DEFINED (its byte ceiling judges the
430
+ // same set). A local re-implementation would be a second definition of
431
+ // "machine-generated" that agrees only until one of the two is edited.
432
+ try {
433
+ const listed = listMachineGeneratedRules({ repoRoot });
434
+ if (!listed.ok) {
435
+ // The directory is there but unreadable — not knowing is not clean.
436
+ undeterminable.push('generated-rules-expiring');
437
+ } else {
438
+ const expiring = listed.rules
439
+ .map((r) => ({ ...r, days: daysUntilGeneratedRuleExpiry({ meta: r.meta, now }) }))
440
+ .filter((r) => r.days !== null && r.days <= GENERATED_RULE_EXPIRY_HORIZON_DAYS)
441
+ .sort((a, b) => a.days - b.days);
442
+ if (expiring.length > 0) {
443
+ // HR-106: name the files and the dates the verdict was computed from —
444
+ // the operator has to open exactly those files to repair this.
445
+ // HR-106 again, one step further: name the REPAIR, not only the
446
+ // finding. The sweep (#1377) is the mechanical half — prose out, the
447
+ // provenance pair kept as `markers only` — and an operator who is told
448
+ // only which files expired has to rediscover that the command exists.
449
+ markDue(
450
+ 'generated-rules-expiring',
451
+ `${expiring.map((r) => `${r.file} ${r.expiresAt}`).join(', ')} → ` +
452
+ 'node scripts/sweep-expired-rules.mjs (dry-run first)',
453
+ );
454
+ }
455
+ }
456
+ } catch {
457
+ undeterminable.push('generated-rules-expiring');
458
+ }
459
+
394
460
  return {
395
461
  due,
396
462
  // HR-106: the denominator counts the signals this host actually EVALUATED.
@@ -68,6 +68,7 @@ import { readLock, isLockLive, LOCK_PATH } from './session-lock.mjs';
68
68
  import { checkPeerStateMd } from './state-md-peer-guard.mjs';
69
69
  import { listWorktreesChecked } from './worktree/listing.mjs';
70
70
  import { parseSessionId } from './session-id.mjs';
71
+ import { isMainModule } from './is-main-module.mjs';
71
72
 
72
73
  /** Closed enum of provenance sources. */
73
74
  const SOURCE_DISCOVERED = 'discovered'; // lock + registry unified (irreversibly merged upstream)
@@ -901,11 +902,7 @@ async function _cliMain(argv) {
901
902
  }
902
903
 
903
904
  const __filename = fileURLToPath(import.meta.url);
904
- const _isCliMain =
905
- typeof process !== 'undefined'
906
- && process.argv[1] !== null
907
- && process.argv[1] !== undefined
908
- && path.resolve(process.argv[1]) === path.resolve(__filename);
905
+ const _isCliMain =isMainModule(import.meta.url);
909
906
 
910
907
  // Called WITHOUT `await` on purpose: a top-level await would make this module
911
908
  // async for every importer, and `_cliMain` cannot reject (all paths are caught