@event4u/agent-config 15.0.0 → 16.1.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 (283) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +301 -347
  5. package/CONTRIBUTING.md +9 -0
  6. package/README.md +2 -2
  7. package/dist/agent-src/commands/analyze/inbox.md +26 -0
  8. package/dist/agent-src/commands/analyze/roadmap-repos.md +27 -0
  9. package/dist/agent-src/commands/challenge-me/closure.md +178 -0
  10. package/dist/agent-src/commands/challenge-me.md +9 -2
  11. package/dist/agent-src/commands/council/default.md +25 -5
  12. package/dist/agent-src/commands/feature/plan.md +28 -1
  13. package/dist/agent-src/commands/feature/roadmap.md +28 -1
  14. package/dist/agent-src/commands/implement-ticket.md +27 -0
  15. package/dist/agent-src/commands/jira-ticket.md +27 -0
  16. package/dist/agent-src/commands/pr/create.md +29 -0
  17. package/dist/agent-src/commands/pr/merge.md +60 -9
  18. package/dist/agent-src/commands/roadmap/ai-council.md +7 -1
  19. package/dist/agent-src/commands/roadmap/create.md +77 -29
  20. package/dist/agent-src/commands/roadmap/materialize.md +27 -0
  21. package/dist/agent-src/commands/roadmap/process-full.md +123 -18
  22. package/dist/agent-src/commands/team.md +22 -0
  23. package/dist/agent-src/contexts/authority/scope-growth-ownership.md +80 -0
  24. package/dist/agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +97 -0
  25. package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +44 -3
  26. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +15 -15
  27. package/dist/agent-src/contexts/execution/decision-closure-execution.md +93 -0
  28. package/dist/agent-src/contexts/execution/fix-loop-ladder.md +108 -0
  29. package/dist/agent-src/contexts/execution/plan-confidence-gate.md +4 -2
  30. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +75 -6
  31. package/dist/agent-src/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
  32. package/dist/agent-src/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
  33. package/dist/agent-src/guidelines/design-fidelity-routing.md +29 -0
  34. package/dist/agent-src/rules/autonomous-execution.md +15 -8
  35. package/dist/agent-src/rules/delegation-policy.md +23 -45
  36. package/dist/agent-src/rules/design-fidelity.md +32 -37
  37. package/dist/agent-src/rules/evaluator-independence.md +43 -49
  38. package/dist/agent-src/rules/test-first.md +41 -0
  39. package/dist/agent-src/rules/user-interaction.md +31 -30
  40. package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +3 -3
  41. package/dist/agent-src/skills/ai-council/SKILL.md +11 -3
  42. package/dist/agent-src/skills/api-design/data/manifest.json +3 -3
  43. package/dist/agent-src/skills/database/data/manifest.json +4 -4
  44. package/dist/agent-src/skills/database/data/query-tuning.csv +3 -3
  45. package/dist/agent-src/skills/design-review/SKILL.md +50 -0
  46. package/dist/agent-src/skills/error-handling-patterns/SKILL.md +9 -0
  47. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +14 -4
  48. package/dist/agent-src/skills/experiment-loop/SKILL.md +31 -1
  49. package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
  50. package/dist/agent-src/skills/storybook-workshop/SKILL.md +42 -3
  51. package/dist/agent-src/skills/threat-modeling/SKILL.md +9 -4
  52. package/dist/agent-src/skills/threat-modeling/data/manifest.json +4 -4
  53. package/dist/agent-src/skills/verify-repair-loop/SKILL.md +37 -3
  54. package/dist/agent-src/templates/agent-settings.md +1 -1
  55. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  56. package/dist/agent-src/templates/roadmaps.md +188 -0
  57. package/dist/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
  58. package/dist/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
  59. package/dist/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
  60. package/dist/cli-delegate/check_update_banner.js +40 -2
  61. package/dist/cli-delegate/chunk-4E2W63DA.js +335 -0
  62. package/dist/cli-delegate/chunk-5R7HOLAF.js +99 -0
  63. package/dist/cli-delegate/{chunk-IVBPRCQO.js → chunk-6KFEC3G7.js} +1 -1
  64. package/dist/cli-delegate/{chunk-2TAQ2K4V.js → chunk-6KM6ORRY.js} +55 -386
  65. package/dist/cli-delegate/{chunk-2XY7ILBT.js → chunk-FC75PGJE.js} +42 -2
  66. package/dist/cli-delegate/{chunk-GYC3VRYN.js → chunk-FXYUYU4N.js} +57 -13
  67. package/dist/cli-delegate/{chunk-55ACLDUE.js → chunk-GJCBB2UG.js} +168 -10
  68. package/dist/cli-delegate/{chunk-AK6HODNQ.js → chunk-HDVVGPQW.js} +873 -539
  69. package/dist/cli-delegate/chunk-NUHLI2RX.js +80 -0
  70. package/dist/cli-delegate/{chunk-HZ52BRGG.js → chunk-P6ZHP5W2.js} +15 -90
  71. package/dist/cli-delegate/{chunk-TYSY33MD.js → chunk-QJWAUMJW.js} +12 -2
  72. package/dist/cli-delegate/{chunk-57PN4GYJ.js → chunk-RGNIWTEA.js} +1 -1
  73. package/dist/cli-delegate/chunk-ROYYTEZU.js +39 -0
  74. package/dist/cli-delegate/{chunk-2KKXJW25.js → chunk-S3VFXMTM.js} +1 -1
  75. package/dist/cli-delegate/{chunk-3FKR64R5.js → chunk-UNCRJBT3.js} +31 -7
  76. package/dist/cli-delegate/{chunk-2DK2VJWG.js → chunk-WGXTVKCQ.js} +20 -6
  77. package/dist/cli-delegate/chunk-YDPILCOL.js +152 -0
  78. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  79. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  80. package/dist/cli-delegate/cmd_conformance.js +918 -325
  81. package/dist/cli-delegate/cmd_doctor.js +7 -5
  82. package/dist/cli-delegate/cmd_explain.js +5 -3
  83. package/dist/cli-delegate/cmd_export.js +9 -7
  84. package/dist/cli-delegate/cmd_fleet.js +4 -2
  85. package/dist/cli-delegate/cmd_handoff.js +6 -6
  86. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  87. package/dist/cli-delegate/cmd_migrate.js +2 -2
  88. package/dist/cli-delegate/cmd_packs_active.js +3 -2
  89. package/dist/cli-delegate/cmd_preflight.js +4 -2
  90. package/dist/cli-delegate/cmd_prune.js +10 -8
  91. package/dist/cli-delegate/cmd_route_audit.js +2 -2
  92. package/dist/cli-delegate/cmd_route_explain.js +1 -1
  93. package/dist/cli-delegate/cmd_session_recycle.js +4 -4
  94. package/dist/cli-delegate/cmd_settings_get.js +28 -2
  95. package/dist/cli-delegate/cmd_settings_migrate.js +9 -7
  96. package/dist/cli-delegate/cmd_settings_set.js +14 -1
  97. package/dist/cli-delegate/cmd_sync.js +10 -8
  98. package/dist/cli-delegate/cmd_uninstall.js +16 -13
  99. package/dist/cli-delegate/cmd_update.js +1 -1
  100. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  101. package/dist/cli-delegate/cmd_validate.js +12 -10
  102. package/dist/cli-delegate/cmd_versions.js +1 -1
  103. package/dist/discovery/deprecation-report.md +1 -1
  104. package/dist/discovery/discovery-manifest.json +87 -40
  105. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  106. package/dist/discovery/discovery-manifest.summary.md +4 -4
  107. package/dist/discovery/dormancy-report.md +1 -1
  108. package/dist/discovery/orphan-report.md +1 -1
  109. package/dist/discovery/packs.json +10 -8
  110. package/dist/discovery/trust-report.md +3 -3
  111. package/dist/discovery/workspaces.json +8 -6
  112. package/dist/hooks/dispatch.js +2601 -1073
  113. package/dist/install/claudeRuleRewrite.js +14 -3
  114. package/dist/install/conflict.js +60 -10
  115. package/dist/install/conflictTracker.js +218 -0
  116. package/dist/install/fsPrimitives.js +54 -0
  117. package/dist/install/install.mjs +2008 -1481
  118. package/dist/install/manifestFiles.js +259 -0
  119. package/dist/install/preflight.js +14 -2
  120. package/dist/install/preserve.js +157 -0
  121. package/dist/install/recordedOwnership.js +132 -0
  122. package/dist/install/txlog.js +11 -2
  123. package/dist/mcp/registry-manifest.json +3 -3
  124. package/dist/mcp/server.json +2 -2
  125. package/dist/mcp/server.mjs +1612 -667
  126. package/dist/router.json +1 -1
  127. package/dist/scripts/_lib/agent_settings.js +3 -2
  128. package/dist/scripts/_lib/fs_atomic.js +167 -0
  129. package/dist/scripts/_lib/installed_lock.js +1 -1
  130. package/dist/scripts/_lib/installed_tools.js +460 -0
  131. package/dist/scripts/_lib/lean_projection_mode.js +3 -2
  132. package/dist/scripts/_lib/settings_renamed_keys.js +78 -0
  133. package/dist/server/io/yamlIO.js +50 -0
  134. package/dist/server/routes/install.js +8 -0
  135. package/dist/server/routes/wizard.js +9 -3
  136. package/dist/server/schemas/settings.js +17 -5
  137. package/dist/shared/missionExecution.js +97 -0
  138. package/dist/ui/assets/{index-D0jaCRPM.js → index-DH1xc62v.js} +17 -17
  139. package/dist/ui/index.html +1 -1
  140. package/docs/MIGRATION.md +103 -0
  141. package/docs/contracts/settings-classes.md +26 -7
  142. package/docs/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
  143. package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
  144. package/docs/guidelines/design-fidelity-routing.md +29 -0
  145. package/package.json +1 -1
  146. package/src/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
  147. package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
  148. package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
  149. package/src/config/agent-settings.template.yml +139 -27
  150. package/src/config/auto-rule-budget.json +16 -0
  151. package/src/config/ci-local-parity.yml +18 -5
  152. package/src/config/continuity-surface.json +28 -0
  153. package/src/config/gate-coverage.yml +146 -13
  154. package/src/config/gate-violation-baselines.json +6 -6
  155. package/src/config/hook-token-budget.json +2 -0
  156. package/src/config/host-injection-effect.json +1 -1
  157. package/src/config/loop-surfaces.yaml +215 -0
  158. package/src/config/platform-anchor.json +3 -2
  159. package/src/config/preamble-payload-budget.json +12 -7
  160. package/src/config/preamble-payload-exceptions.json +28 -0
  161. package/src/config/release-gate-locality.yml +11 -0
  162. package/src/config/routing-coverage-seed.json +18 -4
  163. package/src/config/rule-enforcement-baseline.json +0 -1
  164. package/src/install/claudeRuleRewrite.ts +16 -2
  165. package/src/install/conflict.ts +65 -9
  166. package/src/install/conflictTracker.ts +299 -0
  167. package/src/install/fsPrimitives.ts +54 -0
  168. package/src/install/manifestFiles.ts +312 -0
  169. package/src/install/preflight.ts +16 -2
  170. package/src/install/preserve.ts +205 -0
  171. package/src/install/recordedOwnership.ts +149 -0
  172. package/src/install/txlog.ts +11 -2
  173. package/src/install/types.ts +19 -4
  174. package/src/scripts/_cli/cmd_conformance.ts +88 -15
  175. package/src/scripts/_cli/cmd_doctor.ts +4 -3
  176. package/src/scripts/_cli/cmd_route_explain.ts +9 -2
  177. package/src/scripts/_cli/cmd_settings_get.ts +44 -0
  178. package/src/scripts/_cli/doctor_execution.ts +90 -0
  179. package/src/scripts/_dispatch.bash +3 -1
  180. package/src/scripts/_lib/activation_payload.ts +247 -0
  181. package/src/scripts/_lib/agent_settings.ts +4 -2
  182. package/src/scripts/_lib/authority_path.ts +102 -0
  183. package/src/scripts/_lib/base_ref_payload.ts +315 -0
  184. package/src/scripts/_lib/cascade_base.ts +94 -0
  185. package/src/scripts/_lib/changelog_eras.ts +7 -2
  186. package/src/scripts/_lib/continuation_ladder.ts +149 -2
  187. package/src/scripts/_lib/council_transport.ts +129 -0
  188. package/src/scripts/_lib/delivery_ready.ts +112 -0
  189. package/src/scripts/_lib/dropped_decision.ts +142 -0
  190. package/src/scripts/_lib/forge_protection.ts +267 -0
  191. package/src/scripts/_lib/hook_platform_keys.ts +27 -0
  192. package/src/scripts/_lib/installed_lock.ts +1 -1
  193. package/src/scripts/_lib/json_python_parity.ts +127 -0
  194. package/src/scripts/_lib/lean_projection_mode.ts +14 -3
  195. package/src/scripts/_lib/loop_surfaces.ts +382 -0
  196. package/src/scripts/_lib/measured_payload_ceiling.ts +424 -0
  197. package/src/scripts/_lib/mission_record.ts +190 -0
  198. package/src/scripts/_lib/obligation_frequency.ts +217 -0
  199. package/src/scripts/_lib/obligations.ts +402 -0
  200. package/src/scripts/_lib/orchestration_record.ts +11 -20
  201. package/src/scripts/_lib/payload_catalogue_completeness.ts +211 -0
  202. package/src/scripts/_lib/release_findings_ingest.ts +324 -0
  203. package/src/scripts/_lib/release_highlights.ts +199 -10
  204. package/src/scripts/_lib/release_holds.ts +493 -0
  205. package/src/scripts/_lib/release_migration_gate.ts +184 -0
  206. package/src/scripts/_lib/release_tag_dispatch.ts +63 -0
  207. package/src/scripts/_lib/review_baseline.ts +254 -0
  208. package/src/scripts/_lib/review_skipped_record.ts +61 -1
  209. package/src/scripts/_lib/rides_along.ts +102 -0
  210. package/src/scripts/_lib/settings_renamed_keys.ts +82 -0
  211. package/src/scripts/_lib/standing_bound_ratchet.ts +216 -103
  212. package/src/scripts/_lib/test_delta.ts +166 -0
  213. package/src/scripts/_lib/test_provenance.ts +171 -0
  214. package/src/scripts/_lib/turn_end_refusals.ts +51 -4
  215. package/src/scripts/_lib/typed_op_grant.ts +246 -0
  216. package/src/scripts/_lib/typed_op_watch.ts +185 -0
  217. package/src/scripts/ai_council/config.ts +8 -11
  218. package/src/scripts/ai_council/fallback_config.ts +5 -3
  219. package/src/scripts/ai_council/ownership_classes.ts +118 -0
  220. package/src/scripts/ask_block_census.ts +285 -15
  221. package/src/scripts/check_beta_review_markers.ts +35 -4
  222. package/src/scripts/check_enforcement_matrix.ts +624 -0
  223. package/src/scripts/check_finding_dispositions.ts +167 -16
  224. package/src/scripts/check_gate_reachability.ts +173 -10
  225. package/src/scripts/check_generator_sync.ts +954 -0
  226. package/src/scripts/check_held_object_arrivals.ts +381 -0
  227. package/src/scripts/check_preamble_payload_budget.ts +474 -124
  228. package/src/scripts/check_release_highlights.ts +33 -1
  229. package/src/scripts/check_release_holds.ts +567 -0
  230. package/src/scripts/check_release_published.ts +1 -1
  231. package/src/scripts/check_reply_consistency.ts +86 -17
  232. package/src/scripts/check_rule_projection_integrity.ts +22 -12
  233. package/src/scripts/check_test_delta.ts +294 -0
  234. package/src/scripts/check_test_weakening.ts +299 -0
  235. package/src/scripts/closure_scan.ts +333 -0
  236. package/src/scripts/compile_hook_manifest.ts +43 -3
  237. package/src/scripts/council_record_shape.ts +209 -0
  238. package/src/scripts/hook_manifest.json +1 -1
  239. package/src/scripts/hook_manifest.yaml +127 -12
  240. package/src/scripts/hooks/block_kernel_rule_writes.ts +12 -2
  241. package/src/scripts/hooks/concern_registry.ts +6 -0
  242. package/src/scripts/hooks/design_pass_hook.ts +128 -3
  243. package/src/scripts/hooks/end_review_nudge_hook.ts +101 -21
  244. package/src/scripts/hooks/evidence_independence.ts +96 -0
  245. package/src/scripts/hooks/memory_recall_hook.ts +540 -0
  246. package/src/scripts/hooks/obligation_settle_hook.ts +182 -0
  247. package/src/scripts/hooks/review_baseline_hook.ts +130 -0
  248. package/src/scripts/hooks/rule_inject_hook.ts +49 -1
  249. package/src/scripts/hooks/run_continuation_hook.ts +17 -20
  250. package/src/scripts/hooks/turn_end_gate_hook.ts +232 -14
  251. package/src/scripts/hooks_doctor.ts +89 -1
  252. package/src/scripts/hooks_status.ts +19 -0
  253. package/src/scripts/install.ts +107 -207
  254. package/src/scripts/interruption_report.ts +8 -2
  255. package/src/scripts/janitor.ts +15 -1
  256. package/src/scripts/lint_decision_classes.ts +464 -0
  257. package/src/scripts/lint_hook_manifest.ts +11 -0
  258. package/src/scripts/lint_major_migration_sections.ts +450 -0
  259. package/src/scripts/lint_plan_risk_register.ts +47 -3
  260. package/src/scripts/lint_roadmap_blockers.ts +61 -0
  261. package/src/scripts/lint_roadmap_producers.ts +202 -0
  262. package/src/scripts/measure_rule_budget.ts +101 -5
  263. package/src/scripts/measure_turn_end_gate.ts +302 -5
  264. package/src/scripts/new_roadmap.ts +37 -0
  265. package/src/scripts/orchestration_record.ts +0 -5
  266. package/src/scripts/release.ts +66 -162
  267. package/src/scripts/release_drill.ts +460 -15
  268. package/src/scripts/release_env.ts +1 -1
  269. package/src/scripts/release_publication.ts +52 -1
  270. package/src/scripts/report_enforcement_drift.ts +566 -0
  271. package/src/scripts/report_held_object_arrivals.ts +456 -0
  272. package/src/scripts/report_host_injection_effect.ts +3 -11
  273. package/src/scripts/report_standing_payload_by_host.ts +26 -152
  274. package/src/scripts/schemas/agent-settings.schema.json +23 -0
  275. package/src/scripts/schemas/command.schema.json +4 -0
  276. package/src/scripts/scope_growth.ts +219 -0
  277. package/src/scripts/sync_agent_settings.ts +140 -18
  278. package/src/scripts/trigger_eval_grandfather.json +0 -1
  279. package/src/scripts/ui_conformance_probe.ts +622 -0
  280. package/src/server/io/yamlIO.ts +48 -0
  281. package/src/server/schemas/settings.ts +31 -7
  282. package/src/shared/missionExecution.ts +126 -0
  283. package/dist/cli-delegate/chunk-523HCFYO.js +0 -90
@@ -18,60 +18,99 @@
18
18
  * bucket has no local source at all — the census reports that one as an explicit
19
19
  * residual rather than pretending to measure it.
20
20
  *
21
- * THE BOUND IS SHRINK-ONLY, AND NOW CHECKED
22
- * ------------------------------------------
23
- * `ci_delivery.why_a_grace_ceiling` says of the grace ceiling *"It may never
24
- * move UP"*, and `grace_ceiling_history` in the same file records it moving up
25
- * twice. ADR-264 resolved that against the practice — the sentence stands — and
26
- * left the sentence unenforced, which is exactly the state that produced both
27
- * raises. `_lib/standing_bound_ratchet.ts` is the enforcement: the effective
28
- * ceiling is compared against the one at the base ref and a rise refuses the run,
29
- * whether it arrived by editing the config or by passing a bigger `--ceiling`.
21
+ * THE CEILING IS MEASURED AT THE BASE REF, NOT STORED
22
+ * ----------------------------------------------------
23
+ * It used to be a stored `ci_delivery.grace_ceiling`, whose own text said *"It
24
+ * may never move UP"* while `grace_ceiling_history` recorded it moving up
25
+ * twice. ADR-264 resolved that against the practice and left the sentence
26
+ * unenforced; `_lib/standing_bound_ratchet.ts` became the enforcement.
30
27
  *
31
- * It is also ALL that `road-to-a-standing-budget-with-headroom` step 1.3 ships.
32
- * That step designed a 128-token Iron Law reserve; an AI council refused to
33
- * activate it 2/2 on 2026-09-08 because the verifier is inside the change under
34
- * review, so a protected approval record grants nothing. ADR-265 carries the
35
- * verdict and the activation prerequisites.
28
+ * `road-to-delivery-for-every-host` 4.4 retires the stored number entirely. An
29
+ * AI council (2/2, 2026-09-10, under a written owner delegation) converged on
36
30
  *
37
- * Exit codes: 0 within budget · 1 over budget or the bound rose · 2 misuse /
38
- * unreadable budget.
31
+ * ceiling = max(design_ceiling, payload_at_base_ref + active grants)
32
+ *
33
+ * for one reason: a measured ceiling captures every merged reduction
34
+ * automatically, where a stored one stays at its last hand-written number — so
35
+ * payload a merge removed can be added straight back into the space it freed.
36
+ * The ordinary path is therefore ZERO NET GROWTH while the tree is over design,
37
+ * and the design ceiling once it is at or below. No per-PR headroom: both seats
38
+ * refused one because a percentage or a fixed allowance against a moving base
39
+ * authorises cumulative growth (138,413 × 1.05^10 ≈ 225,000).
40
+ *
41
+ * Three prerequisites came with the verdict and all three are in the pipeline
42
+ * below. The base measurement FAILS CLOSED under `--require-base`, because a
43
+ * base that cannot be read would otherwise grant an unbounded budget. The
44
+ * shrink-only ratchet moved with the ceiling: `design_ceiling` and every
45
+ * existing exception grant and watermark are now the bounded numbers. And the
46
+ * catalogue exhaustiveness audit refuses payload sitting outside every measured
47
+ * bucket, which defeats a stored ceiling exactly as it would defeat this one.
48
+ *
49
+ * A change whose offsetting reduction is genuinely unsafe takes a recorded,
50
+ * human-approved, dated grant in `src/config/preamble-payload-exceptions.json`.
51
+ * 96.8 % of the last 250 merged pull requests moved this payload by zero or
52
+ * less, so the ordinary path stays autonomous and only the tail reaches a
53
+ * person. `_lib/measured_payload_ceiling.ts` carries the mechanism and the
54
+ * reasoning, including why no numeric cap is enforced.
55
+ *
56
+ * ADR-265 still holds for what this does NOT do: the 128-token Iron Law reserve
57
+ * stays unshipped, because a mechanism that GRANTS budget cannot rest on a
58
+ * verifier the change under review can edit.
59
+ *
60
+ * Exit codes: 0 within budget · 1 over budget, a bound rose, the ceiling could
61
+ * not be measured under `--require-base`, or payload sits outside the catalogue
62
+ * · 2 misuse / unreadable budget.
39
63
  */
40
- import { execFileSync } from 'node:child_process';
41
64
  import * as fs from 'node:fs';
42
- import * as os from 'node:os';
43
65
  import * as path from 'node:path';
44
66
 
67
+ import { type ActivationPayload, measureActivationPayload } from './_lib/activation_payload.js';
68
+ import { asOf } from './_lib/as_of.js';
45
69
  import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
70
+ import { CAP_BYTES } from './hooks/rule_inject_hook.js';
46
71
  import { censusClaudeMdHierarchy, censusRuleDir, censusSkillsCatalog } from './preamble_byte_census.js';
47
72
  import { PREFIX_STABLE_SURFACES, prefixStableRoots } from './_lib/prefix_stable_surfaces.js';
48
73
  import { HOST_SURFACES } from './_lib/host_projection_reach.js';
49
74
  import { attributeGrowth, buildLedger, renderAttribution } from './_lib/asset_delivery_ledger.js';
50
75
  import { resolveBaseRef } from './_lib/ratchet_base_ref.js';
76
+ import { extractSurfacesAtRef, measurePayloadAtRef } from './_lib/base_ref_payload.js';
77
+ import {
78
+ type CeilingReading,
79
+ computeCeiling,
80
+ EXCEPTIONS_CONFIG_PATH,
81
+ readExceptions,
82
+ renderCeiling,
83
+ } from './_lib/measured_payload_ceiling.js';
84
+ import { auditCatalogue, type CatalogueAudit } from './_lib/payload_catalogue_completeness.js';
51
85
  import {
52
86
  assertBoundsDidNotRise,
53
87
  type BoundsRatchetVerdict,
88
+ boundsFrom,
54
89
  type GitRunner,
55
90
  realGit,
56
91
  } from './_lib/standing_bound_ratchet.js';
57
92
 
58
93
  const HERE = path.dirname(new URL(import.meta.url).pathname);
94
+ /**
95
+ * Where this SCRIPT lives — the default tree to measure, and nothing more.
96
+ *
97
+ * Under prerequisite 3 of the measured-ceiling verdict, CI runs this file as it
98
+ * exists at the BASE ref against the HEAD tree, so that a pull request cannot
99
+ * edit the code that measures it. There the two diverge and `--repo-root`
100
+ * carries the tree; everywhere else they are the same directory.
101
+ */
59
102
  const REPO_ROOT = path.resolve(HERE, '..', '..');
60
- const BUDGET_FILE = path.join(REPO_ROOT, 'src', 'config', 'preamble-payload-budget.json');
103
+
104
+ /** The budget config inside a given tree. */
105
+ function budgetFileIn(repoRoot: string): string {
106
+ return path.join(repoRoot, 'src', 'config', 'preamble-payload-budget.json');
107
+ }
108
+ const BUDGET_FILE = budgetFileIn(REPO_ROOT);
61
109
 
62
110
  interface Budget {
63
111
  baseline_tokens: number;
64
112
  headroom_pct: number;
65
113
  target_tokens: { median: number; p95: number };
66
- /**
67
- * `ci_delivery.grace_ceiling`, or `null` when the file carries none.
68
- *
69
- * Read here only so the shrink-only ratchet has a head-side value on a local
70
- * run that passes no `--ceiling`. The number the payload is COMPARED against
71
- * still arrives through the flag, so this does not become a second home for
72
- * the ceiling.
73
- */
74
- grace_ceiling: number | null;
75
114
  }
76
115
 
77
116
  export interface BudgetVerdict {
@@ -94,16 +133,19 @@ export function readBudget(file: string = BUDGET_FILE): Budget {
94
133
  throw new Error(`${file}: baseline_tokens and headroom_pct must both be numbers`);
95
134
  }
96
135
  const target = (raw['target_tokens'] ?? {}) as Record<string, unknown>;
97
- const delivery = (raw['ci_delivery'] ?? {}) as Record<string, unknown>;
98
- const grace = Number(delivery['grace_ceiling']);
99
136
  return {
100
137
  baseline_tokens: baseline,
101
138
  headroom_pct: headroom,
102
139
  target_tokens: { median: Number(target['median']), p95: Number(target['p95']) },
103
- grace_ceiling: Number.isFinite(grace) ? grace : null,
104
140
  };
105
141
  }
106
142
 
143
+ /** The raw budget JSON, for the bound derivation that must see head and base
144
+ * through the SAME function. `readBudget` narrows; this does not. */
145
+ function readBudgetRaw(file: string): unknown {
146
+ return JSON.parse(fs.readFileSync(file, 'utf-8'));
147
+ }
148
+
107
149
  /** Look one surface root up by id. Throws on an unknown id — a renamed surface
108
150
  * must fail loudly here rather than degrade this gate to measuring nothing. */
109
151
  function surfaceRoot(id: string): string {
@@ -358,105 +400,196 @@ export function evaluate(repoRoot?: string, budgetFile?: string, overrideCeiling
358
400
  export interface Decision {
359
401
  verdict: BudgetVerdict;
360
402
  bounds: BoundsRatchetVerdict;
361
- /** False when the payload is over the ceiling, or the ceiling itself rose. */
403
+ /** How the ceiling was arrived at, and whether it may be trusted. */
404
+ ceiling: CeilingReading;
405
+ /** Payload the census does not enumerate. Empty findings = complete. */
406
+ catalogue: CatalogueAudit;
407
+ /** False when the payload is over the ceiling, a bound rose, the ceiling
408
+ * could not be measured under `--require-base`, or payload escaped the
409
+ * catalogue. */
362
410
  ok: boolean;
363
411
  }
364
412
 
365
413
  export interface DecideOptions {
366
414
  repoRoot?: string;
367
415
  budgetFile?: string;
368
- overrideCeiling?: number;
369
416
  env?: NodeJS.ProcessEnv;
370
417
  git?: GitRunner;
371
418
  /** Test seam: pin the base ref instead of resolving it. `null` = none resolved. */
372
419
  baseRef?: string | null;
373
- /** Refuse instead of skipping when the shrink-only bound cannot be verified. */
420
+ /** Refuse instead of skipping when the base ref cannot be read. */
374
421
  requireBase?: boolean;
422
+ /** Test seam: pin today's date for the exception-expiry branch. */
423
+ today?: string;
424
+ /** Test seam: supply the base payload instead of measuring the ref. */
425
+ basePayload?: number | null;
426
+ /**
427
+ * Exception ids whose approval event was VERIFIED against the platform.
428
+ *
429
+ * Empty by default, which is the safe reading: a grant nobody confirmed is
430
+ * a grant the diff asserted about itself. Only a caller with platform
431
+ * access can populate this, which is why it is not read from the tree.
432
+ */
433
+ verifiedApprovals?: readonly string[];
375
434
  }
376
435
 
436
+ /**
437
+ * The measurement, the ceiling it is compared against, and everything that can
438
+ * refuse independently of the size question.
439
+ *
440
+ * `evaluate` above answers "how big is the tree" and touches no ref; this
441
+ * answers "may this tree ship" and needs the base one. They stay separate
442
+ * because a census that silently depended on a remote ref would behave
443
+ * differently in a shallow clone, and the census is the half other callers use.
444
+ *
445
+ * ORDER MATTERS ONLY IN ONE PLACE: the ceiling is established BEFORE the size
446
+ * comparison, because a run that could not measure its ceiling has no
447
+ * business reporting a tree as within one.
448
+ */
377
449
  export function decide(opts: DecideOptions = {}): Decision {
378
450
  const repoRoot = opts.repoRoot ?? REPO_ROOT;
379
451
  const git = opts.git ?? realGit;
380
- const budget = readBudget(opts.budgetFile);
381
- const verdict = evaluate(repoRoot, opts.budgetFile, opts.overrideCeiling);
452
+ const budgetFile = opts.budgetFile ?? budgetFileIn(repoRoot);
453
+ const budget = readBudget(budgetFile);
454
+ const design = Math.round(budget.baseline_tokens * (1 + budget.headroom_pct / 100));
455
+
382
456
  const baseRef =
383
457
  opts.baseRef !== undefined ? opts.baseRef : resolveBaseRef(repoRoot, opts.env ?? process.env, git);
458
+
459
+ // The base payload goes through THIS file's own census, so base and head
460
+ // cannot be measured by two different definitions — the property that keeps
461
+ // a measured ceiling from drifting against the number it bounds.
462
+ const sumPayload = (root: string): number =>
463
+ measureDeterministicPayload(root).reduce((n, b) => n + b.tokens, 0);
464
+ let basePayload: number | null;
465
+ let baseNote: string | null;
466
+ if (opts.basePayload !== undefined) {
467
+ basePayload = opts.basePayload;
468
+ baseNote = basePayload === null ? 'the base payload was pinned to null by the caller' : null;
469
+ } else if (baseRef === null || baseRef.trim() === '') {
470
+ basePayload = null;
471
+ baseNote = 'no base ref resolved';
472
+ } else {
473
+ const reading = measurePayloadAtRef({ repoRoot, ref: baseRef, measure: sumPayload });
474
+ basePayload = reading.tokens;
475
+ baseNote = reading.note;
476
+ }
477
+
478
+ const exceptionsFile = path.join(repoRoot, EXCEPTIONS_CONFIG_PATH);
479
+ const ex = readExceptions(exceptionsFile);
480
+ const ceiling = computeCeiling({
481
+ designCeiling: design,
482
+ basePayload,
483
+ baseNote,
484
+ headPayload: sumPayload(repoRoot),
485
+ exceptions: ex.exceptions,
486
+ exceptionErrors: ex.errors,
487
+ verifiedApprovals: opts.verifiedApprovals ?? [],
488
+ // Through the as-of seam, not the wall clock. An exception expiry is
489
+ // exactly the kind of verdict that must be reproducible from the
490
+ // commit: a grant that expires overnight would make the same tree pass
491
+ // and then fail with nothing in the diff to explain it.
492
+ today: opts.today ?? asOf().toISOString().slice(0, 10),
493
+ requireBase: opts.requireBase === true,
494
+ });
495
+
496
+ const verdict = evaluate(repoRoot, budgetFile, ceiling.ceiling);
497
+
498
+ // The head-side bounds go through `boundsFrom` exactly as the base-side
499
+ // ones do, from the raw configs rather than the narrowed ones — one
500
+ // derivation for both sides, which is what the grace ceiling never had.
501
+ let headBounds: Record<string, number> = { design_ceiling: design };
502
+ try {
503
+ const raw = readBudgetRaw(budgetFile);
504
+ let rawEx: unknown = null;
505
+ try {
506
+ rawEx = JSON.parse(fs.readFileSync(exceptionsFile, 'utf-8'));
507
+ } catch {
508
+ /* absent ledger contributes no bounds */
509
+ }
510
+ headBounds = boundsFrom(raw, rawEx) ?? headBounds;
511
+ } catch {
512
+ /* readBudget above already threw on an unusable file */
513
+ }
384
514
  const bounds = assertBoundsDidNotRise({
385
515
  repoRoot,
386
516
  baseRef,
387
517
  git,
388
- headGraceCeiling: opts.overrideCeiling ?? budget.grace_ceiling ?? 0,
518
+ headBounds,
389
519
  requireBase: opts.requireBase === true,
390
520
  });
391
- return { verdict, bounds, ok: bounds.ok && verdict.withinBudget };
521
+
522
+ const catalogue = auditCatalogue(repoRoot);
523
+
524
+ return {
525
+ verdict,
526
+ bounds,
527
+ ceiling,
528
+ catalogue,
529
+ // THE SIZE COMPARISON IS ONLY MADE AGAINST A CEILING THAT WAS
530
+ // ESTABLISHED. An advisory run that could not read the base has no
531
+ // bound to compare against, and falling back to the design ceiling
532
+ // would report a 28 %-over tree as OVER BUDGET for a reason that has
533
+ // nothing to do with the change — the exact "I could not look" /
534
+ // "I looked and it grew" conflation the enforcing refusal spells out
535
+ // in its own message. So it reports and does not gate.
536
+ //
537
+ // This is not a hole: the CI step passes `--require-base`, where an
538
+ // unestablished ceiling REFUSES. It was invisible until the stored
539
+ // ceiling was deleted, because that number was the fallback.
540
+ ok:
541
+ bounds.ok &&
542
+ ceiling.ok &&
543
+ catalogue.findings.length === 0 &&
544
+ (ceiling.verified ? verdict.withinBudget : true),
545
+ };
392
546
  }
393
547
 
394
548
  /**
395
- * Per-asset attribution for a refusal, against the merge-base tree.
549
+ * Per-asset attribution for a refusal, against the base tree.
550
+ *
551
+ * Every failure mode returns `null` — the caller treats attribution as an
552
+ * explanation, never as a precondition for refusing. A gate that failed to
553
+ * refuse because it could not explain itself would be strictly worse than one
554
+ * that refuses without the explanation.
396
555
  *
397
- * Reads the base tree through `git worktree`-free plumbing: the ledger is built
398
- * over a temporary checkout of `git merge-base HEAD origin/main`. Every failure
399
- * mode returns an empty list — the caller treats attribution as an explanation,
400
- * never as a precondition for refusing.
556
+ * It reads the base tree through `_lib/base_ref_payload`, the same
557
+ * materialisation the ceiling uses. It used to shell `git archive` directly,
558
+ * which silently drops `export-ignore` paths — harmless for the two roots this
559
+ * ledger reads, and a live 746-token defect for the ceiling. Both now go
560
+ * through the one reader rather than through two that differ in a way nobody
561
+ * would notice until the numbers disagreed.
401
562
  */
402
- function attributeGrowthAgainstBase(): string[] | null {
563
+ function attributeGrowthAgainstBase(repoRoot: string, baseRef: string | null): string[] | null {
564
+ if (baseRef === null || baseRef.trim() === '') return null;
565
+ const t = extractSurfacesAtRef({ repoRoot, ref: baseRef });
566
+ if (t.tree === null) return null;
403
567
  try {
404
- const base = execFileSync('git', ['merge-base', 'HEAD', 'origin/main'], {
405
- cwd: REPO_ROOT,
406
- encoding: 'utf-8',
407
- stdio: ['ignore', 'pipe', 'ignore'],
408
- }).trim();
409
- if (base.length === 0) return null;
410
-
411
- const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'payload-base-'));
412
- try {
413
- for (const rel of prefixStableRoots()) {
414
- // `git archive` of one subtree, extracted into the scratch root.
415
- // A root absent at the base ref simply yields nothing.
416
- try {
417
- const tar = execFileSync('git', ['archive', base, '--', rel], {
418
- cwd: REPO_ROOT,
419
- maxBuffer: 256 * 1024 * 1024,
420
- stdio: ['ignore', 'pipe', 'ignore'],
421
- });
422
- // `-f -` is not optional: without it BSD tar reads its
423
- // default device rather than stdin, and the extraction
424
- // silently produces nothing — which reads downstream as
425
- // "the base tree was unavailable" rather than as a bug.
426
- execFileSync('tar', ['-x', '-f', '-', '-C', tmp], {
427
- input: tar,
428
- stdio: ['pipe', 'ignore', 'ignore'],
429
- });
430
- } catch {
431
- /* root absent at base — nothing to extract */
432
- }
433
- }
434
- const [rulesRel, skillsRel] = prefixStableRoots();
435
- const before = buildLedger(
436
- path.join(tmp, rulesRel ?? ''),
437
- path.join(tmp, skillsRel ?? ''),
438
- tmp,
439
- );
440
- const after = buildLedger(
441
- path.join(REPO_ROOT, rulesRel ?? ''),
442
- path.join(REPO_ROOT, skillsRel ?? ''),
443
- REPO_ROOT,
444
- );
445
- if (before.rows.length === 0) return null;
446
- return renderAttribution(attributeGrowth(before.rows, after.rows));
447
- } finally {
448
- fs.rmSync(tmp, { recursive: true, force: true });
449
- }
568
+ const [rulesRel, skillsRel] = prefixStableRoots();
569
+ const before = buildLedger(
570
+ path.join(t.tree, rulesRel ?? ''),
571
+ path.join(t.tree, skillsRel ?? ''),
572
+ t.tree,
573
+ );
574
+ const after = buildLedger(
575
+ path.join(repoRoot, rulesRel ?? ''),
576
+ path.join(repoRoot, skillsRel ?? ''),
577
+ repoRoot,
578
+ );
579
+ if (before.rows.length === 0) return null;
580
+ return renderAttribution(attributeGrowth(before.rows, after.rows));
450
581
  } catch {
451
582
  return null;
583
+ } finally {
584
+ t.dispose();
452
585
  }
453
586
  }
454
587
 
455
588
  /** The stderr header for a refused bound. Exported so both refusals are testable. */
456
589
  export function boundsRefusalHeader(b: BoundsRatchetVerdict): string {
457
590
  return b.verified
458
- ? '❌ the standing-payload ceiling rose in this change:\n'
459
- : '❌ the standing-payload ceiling could not be VERIFIED, and this run requires it:\n';
591
+ ? '❌ a shrink-only standing-payload bound rose in this change:\n'
592
+ : '❌ the standing-payload bounds could not be VERIFIED, and this run requires it:\n';
460
593
  }
461
594
 
462
595
  /**
@@ -475,42 +608,139 @@ export function boundsRefusalHeader(b: BoundsRatchetVerdict): string {
475
608
  * reserved for the ADVISORY skip, where nothing was refused at all.
476
609
  */
477
610
  export function renderBounds(b: BoundsRatchetVerdict): string {
611
+ const label = 'shrink-only bound ratchet';
478
612
  if (b.note !== null && b.ok) {
479
- return ` ${'grace ceiling ratchet'.padEnd(38)} ${'SKIPPED'.padStart(8)} — ${b.note}\n`;
613
+ return ` ${label.padEnd(38)} ${'SKIPPED'.padStart(8)} — ${b.note}\n`;
480
614
  }
481
615
  if (!b.verified) {
482
616
  return (
483
- ` ${'grace ceiling ratchet'.padEnd(38)} ${'UNVERIFIED'.padStart(8)} — ` +
484
- `${b.note ?? 'the bound could not be read'} (this run requires it)\n`
617
+ ` ${label.padEnd(38)} ${'UNVERIFIED'.padStart(8)} — ` +
618
+ `${b.note ?? 'the bounds could not be read'} (this run requires it)\n`
485
619
  );
486
620
  }
487
- const base = b.baseGraceCeiling === null ? 'n/a' : String(b.baseGraceCeiling);
621
+ const n = b.baseBounds === null ? 0 : Object.keys(b.baseBounds).length;
488
622
  const state = b.ok ? 'ok' : 'ROSE';
489
623
  return (
490
- ` ${'grace ceiling ratchet'.padEnd(38)} ${state.padStart(8)} — ` +
491
- `${base} at ${b.baseRef ?? 'n/a'}, shrink-only (ADR-264)\n`
624
+ ` ${label.padEnd(38)} ${state.padStart(8)} — ` +
625
+ `${String(n)} bound(s) at ${b.baseRef ?? 'n/a'}, shrink-only (ADR-264)\n`
492
626
  );
493
627
  }
494
628
 
629
+ /**
630
+ * The activation reading, rendered with a label nothing else in this gate uses.
631
+ *
632
+ * Third of the three quantities this report keeps structurally apart
633
+ * (`road-to-the-delivery-flip-that-tells-the-truth` 3.1). The first two are
634
+ * ADR-270's `source_corpus` and `host_payload`; this one is per FIRE and in
635
+ * BYTES, while both of those are per SESSION and in chars/4 tokens. Two units
636
+ * and two denominators is precisely why it carries its own label rather than
637
+ * being folded into either total — and why no line below adds it to one.
638
+ *
639
+ * INFORMATIONAL, like the host reading beside it. It never reaches the exit
640
+ * code: this phase adds reporting and registers no second gate. The activation
641
+ * charge already HAS a registered ceiling in
642
+ * `src/config/hook-token-budget.json` (`per_slot_sum_caps_bytes.user_prompt_submit`
643
+ * and the `rule-inject` concern row); enforcing it a second time here would put
644
+ * one obligation behind two gates that can disagree.
645
+ */
646
+ export function renderActivation(a: ActivationPayload): string {
647
+ const out = ['\n activation payload — per trigger-match fire, BYTES (informational, not gated)\n'];
648
+ if (!a.measured) {
649
+ out.push(
650
+ ` ${'· UNMEASURED'.padEnd(38)} ${a.unmeasured_reason ?? 'reason not recorded'}\n` +
651
+ ' Reported as unmeasured rather than estimated: a fabricated distribution\n' +
652
+ ' published beside a standing saving is the defect this number exists to stop.\n',
653
+ );
654
+ return out.join('');
655
+ }
656
+ for (const s of a.slots) {
657
+ const label = `· ${s.slot}`;
658
+ out.push(
659
+ ` ${label.padEnd(38)} ${String(s.fires).padStart(5)} fires ` +
660
+ `p50 ${String(s.p50).padStart(6)} B p90 ${String(s.p90).padStart(6)} B ` +
661
+ `max ${String(s.max).padStart(6)} B\n`,
662
+ );
663
+ }
664
+ out.push(
665
+ ` ${'· sampled at cap'.padEnd(38)} ${String(a.cap_bytes).padStart(5)} B over ${a.corpus}\n` +
666
+ ' Not summed into either total above: this is bytes per fire, those are chars/4\n' +
667
+ ' tokens per session. `pre_compact` is 0 by construction, not by measurement.\n',
668
+ );
669
+ return out.join('');
670
+ }
671
+
495
672
  export function main(argv: string[] = process.argv.slice(2)): number {
496
673
  const json = argv.includes('--format=json') || argv.includes('--json');
497
- // `--ceiling <n>`: the grace ceiling the CI step reads out of
498
- // `ci_delivery.grace_ceiling`. Read from the budget file there, never written
499
- // in the workflow, so the number has exactly one home. A non-numeric or
500
- // tighter-than-design value is IGNORED rather than honoured — see `evaluate`.
501
- const ci = argv.indexOf('--ceiling');
502
- const override = ci !== -1 && argv[ci + 1] !== undefined ? Number(argv[ci + 1]) : undefined;
503
-
504
- // `--require-base`: refuse instead of skipping when the shrink-only bound
505
- // cannot be verified. An AI council (2/2, 2026-09-10) made this blocking
506
- // before the ceiling may be measured at the base ref rather than stored,
507
- // because an unreadable base costs a comparison today and would grant an
508
- // unbounded budget there. Opt-in rather than the default, and rather than
509
- // derived from `GITHUB_ACTIONS`: a shallow clone, a first commit and a
510
- // detached build all legitimately have no base, and a gate that reds on a
511
- // developer's machine gets switched off.
674
+
675
+ // `--ceiling <n>` is GONE. It existed to hand the gate the stored grace
676
+ // ceiling, and the ceiling is measured now. Refusing loudly rather than
677
+ // ignoring it is the point: a caller still passing a number believes it is
678
+ // setting the bound, and silently measuring something else would be the
679
+ // most expensive kind of no-op. The replacement for a deliberate, bounded
680
+ // widening is an approved entry in the exceptions ledger.
681
+ if (argv.includes('--ceiling')) {
682
+ process.stderr.write(
683
+ '❌ preamble-payload budget: --ceiling was removed. The ceiling is now MEASURED as\n' +
684
+ ' max(design_ceiling, payload at the base ref + active grants), so a caller-supplied\n' +
685
+ ` number cannot set it. To widen it deliberately, record an approved grant in\n` +
686
+ ` ${EXCEPTIONS_CONFIG_PATH} — it carries an approver, a reason, a watermark and an\n` +
687
+ ' expiry, which a flag never did.\n',
688
+ );
689
+ return 2;
690
+ }
691
+
692
+ // `--require-base`: refuse instead of skipping when the base ref cannot be
693
+ // read. An AI council (2/2, 2026-09-10) made this blocking for the move to
694
+ // a base-measured ceiling, because an unreadable base costs a comparison
695
+ // under a stored ceiling and grants an unbounded budget under a measured
696
+ // one. Opt-in rather than the default, and rather than derived from
697
+ // `GITHUB_ACTIONS`: a shallow clone, a first commit and a detached build
698
+ // all legitimately have no base, and a gate that reds on a developer's
699
+ // machine gets switched off.
512
700
  const requireBase = argv.includes('--require-base');
513
701
 
702
+ // `--approved <id>` (repeatable): this caller VERIFIED that exception's
703
+ // approval event against the platform. Nothing in the tree can establish
704
+ // that — an `approved_by` field in the diff requesting the grant is
705
+ // self-asserted data, which was the 2026-09-11 council's hardest pushback
706
+ // in both seats. So the fact arrives from outside, from a caller that
707
+ // queried the platform, and a grant nobody confirmed contributes zero
708
+ // tokens and refuses the run.
709
+ const verifiedApprovals: string[] = [];
710
+ for (let i = 0; i < argv.length; i += 1) {
711
+ if (argv[i] !== '--approved') continue;
712
+ const id = argv[i + 1];
713
+ if (id === undefined || id.startsWith('--')) {
714
+ process.stderr.write('❌ preamble-payload budget: --approved needs an exception id.\n');
715
+ return 2;
716
+ }
717
+ verifiedApprovals.push(id);
718
+ }
719
+
720
+ // `--repo-root <path>`: measure THIS tree, rather than the one this script
721
+ // happens to sit in. Prerequisite 3 of the measured-ceiling verdict — CI
722
+ // runs the gate as it exists at the BASE ref against the HEAD tree, so a
723
+ // pull request cannot edit the code that measures it. Both seats chose this
724
+ // over code-owner review on the gate, which would put the sole maintainer
725
+ // in the path of every gate-editing pull request. Its honest limit,
726
+ // anthropic's words: "the mechanism does not prevent the exploit; it makes
727
+ // the exploit auditable" — a weakening merged first and exploited second is
728
+ // visible in `git log` and is not blocked.
729
+ const ri = argv.indexOf('--repo-root');
730
+ const repoRootArg = ri !== -1 ? argv[ri + 1] : undefined;
731
+ if (ri !== -1 && (repoRootArg === undefined || repoRootArg.startsWith('--'))) {
732
+ process.stderr.write('❌ preamble-payload budget: --repo-root needs a path.\n');
733
+ return 2;
734
+ }
735
+ const repoRoot = repoRootArg === undefined ? REPO_ROOT : path.resolve(repoRootArg);
736
+ if (!fs.existsSync(budgetFileIn(repoRoot))) {
737
+ process.stderr.write(
738
+ `❌ preamble-payload budget: ${repoRoot} carries no ${'src/config/preamble-payload-budget.json'} — ` +
739
+ 'that is not a tree this gate can measure.\n',
740
+ );
741
+ return 2;
742
+ }
743
+
514
744
  // `--host <id>` / `--project-rules-dir <path>`: the additive host reading
515
745
  // (AI council 2026-09-09, option 1A). Mutually exclusive on purpose — a call
516
746
  // passing both is asking two different questions and would silently get one
@@ -541,7 +771,7 @@ export function main(argv: string[] = process.argv.slice(2)): number {
541
771
  if (hostArg !== undefined || rulesDirArg !== undefined) {
542
772
  try {
543
773
  host = measureHostPayload(
544
- REPO_ROOT,
774
+ repoRoot,
545
775
  hostArg !== undefined
546
776
  ? { host: hostArg }
547
777
  : { rulesDirOverride: rulesDirArg as string },
@@ -552,12 +782,16 @@ export function main(argv: string[] = process.argv.slice(2)): number {
552
782
  }
553
783
  }
554
784
 
785
+ // The third labelled number. Taken unconditionally and behind no flag: a
786
+ // cost that has to be asked for is a cost that stays unpublished, which is
787
+ // the failure mode `road-to-the-delivery-flip-that-tells-the-truth` names.
788
+ // `measureActivationPayload` never throws and never estimates, so this
789
+ // cannot change any verdict below — only what the report says.
790
+ const activation = measureActivationPayload(repoRoot, CAP_BYTES);
791
+
555
792
  let decision: Decision;
556
793
  try {
557
- decision = decide({
558
- ...(override === undefined ? {} : { overrideCeiling: override }),
559
- requireBase,
560
- });
794
+ decision = decide({ repoRoot, requireBase, verifiedApprovals });
561
795
  } catch (err) {
562
796
  process.stderr.write(`❌ preamble-payload budget: ${(err as Error).message}\n`);
563
797
  return 2;
@@ -593,7 +827,15 @@ export function main(argv: string[] = process.argv.slice(2)): number {
593
827
  // for exactly that: until the two are named apart, every
594
828
  // ceiling discussion risks comparing unlike quantities. The
595
829
  // legacy top-level fields stay so no existing reader breaks.
596
- measurement_scope: host === null ? 'source-corpus' : 'source-corpus+host-payload',
830
+ // Three parts now, joined from what was actually taken
831
+ // rather than from a fixed pair: the activation reading is
832
+ // the third quantity and it is absent on a tree with no
833
+ // frozen corpus, so a hardcoded label would over-claim.
834
+ measurement_scope: [
835
+ 'source-corpus',
836
+ ...(host === null ? [] : ['host-payload']),
837
+ ...(activation.measured ? ['activation-payload'] : []),
838
+ ].join('+'),
597
839
  source_corpus: {
598
840
  measured: verdict.measured,
599
841
  baseline: verdict.baseline,
@@ -601,11 +843,32 @@ export function main(argv: string[] = process.argv.slice(2)): number {
601
843
  buckets: verdict.buckets,
602
844
  },
603
845
  host_payload: host,
846
+ // Per FIRE and in BYTES — never added to either total
847
+ // above, which are per session and in chars/4 tokens.
848
+ activation_payload: activation,
604
849
  ok: decision.ok,
850
+ measured_ceiling: {
851
+ ceiling: decision.ceiling.ceiling,
852
+ design_ceiling: decision.ceiling.designCeiling,
853
+ base_payload: decision.ceiling.basePayload,
854
+ effective_base: decision.ceiling.effectiveBase,
855
+ active_grants: decision.ceiling.activeGrants,
856
+ active_exception_ids: decision.ceiling.activeIds,
857
+ verified: decision.ceiling.verified,
858
+ ok: decision.ceiling.ok,
859
+ note: decision.ceiling.note,
860
+ violations: decision.ceiling.violations,
861
+ },
862
+ catalogue_completeness: {
863
+ ok: decision.catalogue.findings.length === 0,
864
+ trees_seen: decision.catalogue.treesSeen,
865
+ files_scanned: decision.catalogue.filesScanned,
866
+ findings: decision.catalogue.findings,
867
+ },
605
868
  bound_ratchet: {
606
869
  ok: decision.bounds.ok,
607
870
  base_ref: decision.bounds.baseRef,
608
- base_grace_ceiling: decision.bounds.baseGraceCeiling,
871
+ base_bounds: decision.bounds.baseBounds,
609
872
  note: decision.bounds.note,
610
873
  violations: decision.bounds.violations,
611
874
  },
@@ -617,6 +880,11 @@ export function main(argv: string[] = process.argv.slice(2)): number {
617
880
  return decision.ok ? 0 : 1;
618
881
  }
619
882
 
883
+ // The label goes above the buckets so the gated number cannot be read as
884
+ // either of the two beneath it. This is the source tree's own size — what
885
+ // it COULD deliver — and after a per-host projection flip that is no longer
886
+ // what any one host receives.
887
+ process.stdout.write(' source corpus — what the source tree could deliver (GATED)\n');
620
888
  for (const b of verdict.buckets) {
621
889
  process.stdout.write(` ${b.name.padEnd(38)} ${String(b.tokens).padStart(8)} tok\n`);
622
890
  }
@@ -626,7 +894,14 @@ export function main(argv: string[] = process.argv.slice(2)): number {
626
894
  ` ${'measured total'.padEnd(38)} ${String(verdict.measured).padStart(8)} tok ` +
627
895
  `(baseline ${verdict.baseline}, ${sign}${delta}; ceiling ${verdict.ceiling})\n`,
628
896
  );
897
+ process.stdout.write(renderCeiling(decision.ceiling));
629
898
  process.stdout.write(renderBounds(decision.bounds));
899
+ process.stdout.write(
900
+ ` ${'catalogue completeness'.padEnd(38)} ` +
901
+ `${(decision.catalogue.findings.length === 0 ? 'ok' : 'GAPS').padStart(8)} — ` +
902
+ `${String(decision.catalogue.treesSeen)} projected tree(s), ` +
903
+ `${String(decision.catalogue.filesScanned)} file(s) scanned for stray rule payload\n`,
904
+ );
630
905
 
631
906
  // The host reading prints AFTER the source verdict and never touches the
632
907
  // exit code below. The label says which surface each number describes,
@@ -634,7 +909,10 @@ export function main(argv: string[] = process.argv.slice(2)): number {
634
909
  // sharing one name.
635
910
  if (host !== null) {
636
911
  const label = host.host === null ? host.rules_path : host.host;
637
- process.stdout.write(`\n host payload — ${label} (informational, not gated)\n`);
912
+ process.stdout.write(
913
+ `\n host payload — ${label}, what this host actually receives at rest ` +
914
+ '(informational, not gated)\n',
915
+ );
638
916
  for (const b of host.buckets) {
639
917
  process.stdout.write(` ${('· ' + b.name).padEnd(38)} ${String(b.tokens).padStart(8)} tok\n`);
640
918
  }
@@ -653,6 +931,32 @@ export function main(argv: string[] = process.argv.slice(2)): number {
653
931
  }
654
932
  }
655
933
 
934
+ process.stdout.write(renderActivation(activation));
935
+
936
+ // THE CEILING IS ESTABLISHED BEFORE THE SIZE QUESTION. A run that could not
937
+ // measure its own ceiling has no business reporting a tree as within one,
938
+ // and an expired grant is a refusal whatever the payload does.
939
+ if (!decision.ceiling.ok) {
940
+ process.stderr.write(
941
+ (decision.ceiling.verified
942
+ ? '❌ the standing-payload exception ledger refuses this run:\n'
943
+ : '❌ the standing-payload ceiling could not be MEASURED, and this run requires it:\n') +
944
+ decision.ceiling.violations.map((v) => ` · ${v}\n`).join(''),
945
+ );
946
+ return 1;
947
+ }
948
+
949
+ // Payload the census cannot see is payload no ceiling bounds. Refused
950
+ // BEFORE the size comparison for the same reason: a green "within budget"
951
+ // over an incomplete catalogue is a measurement of the wrong set.
952
+ if (decision.catalogue.findings.length > 0) {
953
+ process.stderr.write(
954
+ '❌ standing payload sits outside every measured bucket:\n' +
955
+ decision.catalogue.findings.map((v) => ` · ${v}\n`).join(''),
956
+ );
957
+ return 1;
958
+ }
959
+
656
960
  // The bound is checked BEFORE the size question and independently of it. A
657
961
  // change that lifts its own ceiling has already defeated the gate, and
658
962
  // reporting that as "within budget" would be the config-weakening move
@@ -668,6 +972,19 @@ export function main(argv: string[] = process.argv.slice(2)): number {
668
972
  return 1;
669
973
  }
670
974
 
975
+ // An advisory run that never established a ceiling reports the number and
976
+ // stops. Saying "over budget" against a bound nobody read would send the
977
+ // reader to shrink a rule over a fetch problem — the same misdiagnosis the
978
+ // `--require-base` refusal is worded to prevent, one layer up.
979
+ if (!decision.ceiling.verified) {
980
+ process.stdout.write(
981
+ '⚠️ size comparison SKIPPED — the ceiling was not established in this run, so there is\n' +
982
+ ' nothing to compare the measurement against. This is NOT a pass on the payload: pass\n' +
983
+ ' --require-base (as CI does) to make an unestablished ceiling a refusal instead.\n',
984
+ );
985
+ return 0;
986
+ }
987
+
671
988
  if (!verdict.withinBudget) {
672
989
  // road-to-delivered-cost-truth 2.2 — a gate names its own "no". The
673
990
  // ceiling message alone states a fact and leaves the reader to find the
@@ -680,13 +997,46 @@ export function main(argv: string[] = process.argv.slice(2)): number {
680
997
  // build or a first commit legitimately has none. A gate that failed to
681
998
  // refuse because it could not explain itself would be strictly worse
682
999
  // than one that refuses without the explanation.
683
- const attribution = attributeGrowthAgainstBase();
1000
+ const attribution = attributeGrowthAgainstBase(repoRoot, decision.bounds.baseRef);
1001
+
1002
+ // REJECTION INSTRUMENTATION — prerequisite 4a's converged floor.
1003
+ //
1004
+ // Both council seats refused to derive an exception cap from the 250-PR
1005
+ // delta distribution, for one reason: the sample is CENSORED by the very
1006
+ // ratchet a cap would relax, so every percentile describes what got
1007
+ // through rather than what was attempted. The thing that uncensors it is
1008
+ // recording the deltas that were BLOCKED, which nothing did.
1009
+ //
1010
+ // This line is that record, and its limits are worth stating rather than
1011
+ // implying: it is a log line in a CI job, not a database. It captures
1012
+ // attempts that reached this gate and failed; it cannot see a change
1013
+ // nobody pushed. It is deliberately machine-greppable from a workflow
1014
+ // log so the distribution can be rebuilt later without a schema anybody
1015
+ // has to maintain today.
1016
+ process.stdout.write(
1017
+ 'payload-rejection: ' +
1018
+ JSON.stringify({
1019
+ measured: verdict.measured,
1020
+ ceiling: verdict.ceiling,
1021
+ attempted_delta: decision.ceiling.effectiveBase === null
1022
+ ? null
1023
+ : verdict.measured - decision.ceiling.effectiveBase,
1024
+ design_ceiling: decision.ceiling.designCeiling,
1025
+ base_payload: decision.ceiling.basePayload,
1026
+ base_ref: decision.bounds.baseRef,
1027
+ active_grants: decision.ceiling.activeGrants,
1028
+ buckets: verdict.buckets.map((b) => ({ name: b.name, tokens: b.tokens })),
1029
+ }) +
1030
+ '\n',
1031
+ );
1032
+
684
1033
  process.stderr.write(
685
1034
  `❌ per-spawn preamble payload grew past the ratchet: ${verdict.measured} > ${verdict.ceiling} tok.\n` +
686
1035
  ` Every rule and skill description here is re-written on EVERY subagent spawn, so growth\n` +
687
1036
  ` is paid per spawn, not once. Shrink the addition, or migrate the prose out of the\n` +
688
- ` standing rule. The grace ceiling may NOT be raised to fit it: ADR-264, enforced by\n` +
689
- ` the shrink-only ratchet above.\n`,
1037
+ ` standing rule. The ceiling may NOT be widened to fit it — it is MEASURED at the base\n` +
1038
+ ` ref, so there is no number to edit. A change whose offsetting reduction is genuinely\n` +
1039
+ ` unsafe takes an approved, dated grant in ${EXCEPTIONS_CONFIG_PATH}.\n`,
690
1040
  );
691
1041
  // Three distinct states, and conflating the last two is a diagnostic
692
1042
  // defect rather than a cosmetic one: "I could not look" and "I looked