@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
@@ -0,0 +1,424 @@
1
+ /**
2
+ * The standing-payload ceiling, MEASURED at the base ref rather than stored.
3
+ *
4
+ * ceiling = max(design_ceiling, effective_base + active grant)
5
+ *
6
+ * WHY MEASURED
7
+ * ------------
8
+ * An AI council (2/2 convergent, 2026-09-10, under a written owner delegation)
9
+ * fixed the formula, and the reason for the move is one sentence from that
10
+ * verdict: a measured ceiling **captures every merged reduction
11
+ * automatically**, where a stored one stays at its last hand-written number —
12
+ * so payload a merge removed can be added straight back into the space it
13
+ * freed. That is the defect `ci_delivery.grace_ceiling` had for its whole life.
14
+ *
15
+ * WHY NO PER-PR HEADROOM. Both seats, without qualification: *"Neither a
16
+ * percentage nor a fixed token allowance is safe against a moving base: both
17
+ * authorize cumulative growth."* One seat put the arithmetic on it —
18
+ * 138,413 × 1.05^10 ≈ 225,000 after ten pull requests. `headroom_pct` derives
19
+ * the FIXED design ceiling from `baseline_tokens` and is never re-applied to a
20
+ * moving base. So the ordinary path is ZERO NET GROWTH while the tree is over
21
+ * design, and the design ceiling once it is at or below. 96.8 % of the last
22
+ * 250 merged pull requests already move this payload by zero or less.
23
+ *
24
+ * THE STORED TERM IS GONE — STAGE 2 LANDED 2026-09-11
25
+ * ----------------------------------------------------
26
+ * A second council round (2/2, 2026-09-11) refused to let the stored ceiling be
27
+ * removed in the change that INTRODUCED the measured one, on a structural
28
+ * ground rather than a cautious one. Under prerequisite 3 the gate runs AS IT
29
+ * EXISTS AT THE BASE REF, so — openai, verbatim — *"the new gate cannot safely
30
+ * validate its own introduction: while the implementation PR runs, the
31
+ * authoritative measuring code at the base ref is still the old
32
+ * implementation."*
33
+ *
34
+ * Stage 1 therefore landed the whole mechanism with the stored ceiling retained
35
+ * as an additional `max` term that could only widen the bound. Stage 2 removes
36
+ * it from a base that already carries this code — verified before the removal
37
+ * by running the base-ref copy of the gate against the head tree, which is the
38
+ * precondition the sequencing was about. `ci_delivery` now stores no ceiling at
39
+ * all, so the bound cannot be widened by editing a field. ADR-275 records the
40
+ * formula and its prerequisites; ADR-276 records this removal and the required
41
+ * status check that made it meaningful.
42
+ *
43
+ * THE EXCEPTION PATH, AND WHY THE WATERMARK IS THE LOAD-BEARING FIELD
44
+ * -------------------------------------------------------------------
45
+ * A change whose offsetting reduction is genuinely unsafe takes a recorded,
46
+ * approved, dated grant. Both seats specified the same six properties, and one
47
+ * is easy to lose: *"the exceptional measurement must NEVER become the next
48
+ * base."* Under a base-measured ceiling that is a mechanism, not a note —
49
+ * without it a grant of N tokens raises the measurement, the next pull request
50
+ * measures the raised base, and the grant is permanent one merge later.
51
+ * `effectiveBase` pins the base to the WATERMARK recorded at grant time for as
52
+ * long as the grant is live.
53
+ *
54
+ * SIX CONTRACT RULES, EACH FROM A BLOCKING FINDING OF THE 2026-09-11 ROUND
55
+ * ------------------------------------------------------------------------
56
+ * 1. AT MOST ONE ACTIVE GRANT. openai: overlapping grants are undefined, and
57
+ * *"'use the highest watermark' is not safe: a later watermark may already
58
+ * contain earlier exceptional growth, causing that growth to be counted both
59
+ * in the anchor and in summed grants."* anthropic preferred defined overlap
60
+ * rules to a ban but called defined semantics blocking either way, so the
61
+ * ban satisfies both seats and the permissive variant does not.
62
+ * 2. APPROVAL MUST BE AUTHENTICATED, NOT ASSERTED. Both seats' hardest
63
+ * pushback: `approved_by` in the pull request that requests the grant is
64
+ * self-asserted data authenticating nothing. A grant therefore carries an
65
+ * `approval_event` — a reference to a platform event outside the diff — and
66
+ * a grant whose approval is unverified is REFUSED rather than honoured.
67
+ * Verification is the CI step's job (it needs the platform); this module
68
+ * refuses anything it was not told is verified.
69
+ * 3. DELETION CANNOT DISCHARGE DEBT. A removed record is not a repaid one.
70
+ * Deletion is caught by the base-ref ratchet, which sees a grant that
71
+ * existed at the base and does not exist at head.
72
+ * 4. `repaid_at` IS A RECORD OF A MEASUREMENT, NOT A CLAIM. Repayment is
73
+ * established by the payload returning to the watermark. A record claiming
74
+ * repayment while the measurement says otherwise is refused.
75
+ * 5. CONSUMPTION IS DERIVED. There is no `consumed_tokens` field to edit:
76
+ * consumption is `measured − watermark`, computed here.
77
+ * 6. EXPIRY IS EVALUATED AT GATE TIME, which is merge time on a required
78
+ * check. A grant that expires while its pull request sits in review reds
79
+ * that pull request; the fix is a fresh approved grant, never an edit to the
80
+ * old one's date, which rule 3's ratchet refuses anyway.
81
+ *
82
+ * WHAT THIS IS NOT, STATED RATHER THAN IMPLIED
83
+ * ---------------------------------------------
84
+ * The ledger is editable by the pull request it governs, like the gate code.
85
+ * The council accepted that for the gate under prerequisite 3 in these terms:
86
+ * *"the mechanism does not prevent the exploit; it makes the exploit auditable
87
+ * … a forensic control, not a preventive one."* Rules 2 and 3 above are what
88
+ * keep the ledger from being weaker than that — an unverified grant buys
89
+ * nothing, and a deleted one is still owed.
90
+ *
91
+ * NO NUMERIC CAP IS ENFORCED, AND THAT IS A FINDING RATHER THAN AN OMISSION.
92
+ * Both seats refused to derive one from the 250-PR delta distribution
93
+ * (p50 0, p90 0, p95 0/+167, max +288): the sample is CENSORED by the very
94
+ * ratchet a cap would relax, so every percentile describes what got through
95
+ * rather than what was attempted. A p95 cap would license the top 5 % of
96
+ * ordinary work to use the break-glass routinely, which makes it not
97
+ * exceptional. The 2026-09-11 round added that the cap must eventually come
98
+ * from actual CONSUMED rather than granted tokens, which rule 5 makes
99
+ * measurable. Until then the approver is the cap.
100
+ */
101
+ import * as fs from 'node:fs';
102
+
103
+ /** Repo-relative path of the break-glass ledger. */
104
+ export const EXCEPTIONS_CONFIG_PATH = 'src/config/preamble-payload-exceptions.json';
105
+
106
+ /**
107
+ * A grant's state at evaluation time. Derived, never stored — a stored status
108
+ * would be a second source of truth that drifts against `expires_at`.
109
+ */
110
+ export type ExceptionState = 'active' | 'repaid' | 'expired';
111
+
112
+ export interface PayloadException {
113
+ id: string;
114
+ granted_tokens: number;
115
+ /** The measured payload immediately BEFORE the grant. The base is pinned here. */
116
+ watermark: number;
117
+ reason: string;
118
+ /**
119
+ * A reference to a platform event that authorised this grant, OUTSIDE the
120
+ * diff requesting it — the council's blocking finding #2. Shape is the CI
121
+ * step's business; this module only requires that one is named and that
122
+ * the caller has verified it.
123
+ */
124
+ approval_event: string;
125
+ /** ISO date. */
126
+ approved_at: string;
127
+ /** ISO date. After it, an unrepaid grant is a hard refusal. */
128
+ expires_at: string;
129
+ /** ISO date once the payload has measurably returned to the watermark. */
130
+ repaid_at: string | null;
131
+ }
132
+
133
+ export interface ExceptionsFile {
134
+ exceptions: PayloadException[];
135
+ /** Schema / parse problems. Non-empty means the file may not be trusted. */
136
+ errors: string[];
137
+ }
138
+
139
+ function isIsoDate(v: unknown): v is string {
140
+ return typeof v === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(v);
141
+ }
142
+
143
+ /**
144
+ * Read and VALIDATE the ledger. A malformed entry is an error, never a skip.
145
+ *
146
+ * An unreadable or invalid ledger must not silently evaluate as "no
147
+ * exceptions": that reading is permissive in one direction — a corrupted entry
148
+ * stops pinning the base to its watermark — and the caller cannot tell it from
149
+ * an honestly empty file. Callers refuse on `errors`.
150
+ */
151
+ export function readExceptions(file: string): ExceptionsFile {
152
+ let raw: unknown;
153
+ try {
154
+ raw = JSON.parse(fs.readFileSync(file, 'utf-8'));
155
+ } catch (err) {
156
+ const e = err as NodeJS.ErrnoException;
157
+ if (e.code === 'ENOENT') return { exceptions: [], errors: [] };
158
+ return { exceptions: [], errors: [`${file} is not readable as JSON: ${(err as Error).message}`] };
159
+ }
160
+ const root = (raw ?? {}) as Record<string, unknown>;
161
+ const list = root['exceptions'];
162
+ if (list === undefined) return { exceptions: [], errors: [] };
163
+ if (!Array.isArray(list)) return { exceptions: [], errors: [`${file}: 'exceptions' must be an array`] };
164
+
165
+ const errors: string[] = [];
166
+ const out: PayloadException[] = [];
167
+ const ids = new Set<string>();
168
+ list.forEach((item, i) => {
169
+ const e = (item ?? {}) as Record<string, unknown>;
170
+ const where = `${file}: exceptions[${String(i)}]`;
171
+ const id = e['id'];
172
+ const granted = Number(e['granted_tokens']);
173
+ const watermark = Number(e['watermark']);
174
+ const repaid = e['repaid_at'] ?? null;
175
+ if (typeof id !== 'string' || id.trim() === '') errors.push(`${where}: 'id' must be a non-empty string`);
176
+ else if (ids.has(id)) errors.push(`${where}: duplicate id '${id}' — an id is the record's identity`);
177
+ else ids.add(id);
178
+ if (!Number.isInteger(granted) || granted <= 0) {
179
+ errors.push(`${where}: 'granted_tokens' must be a positive integer`);
180
+ }
181
+ if (!Number.isInteger(watermark) || watermark < 0) {
182
+ errors.push(`${where}: 'watermark' must be a non-negative integer — the payload measured at grant time`);
183
+ }
184
+ for (const k of ['reason', 'approval_event'] as const) {
185
+ if (typeof e[k] !== 'string' || (e[k] as string).trim() === '') {
186
+ errors.push(
187
+ `${where}: '${k}' must be a non-empty string` +
188
+ (k === 'approval_event'
189
+ ? ". An approval recorded only by the diff requesting it authenticates nothing — name the platform event that authorised it."
190
+ : ''),
191
+ );
192
+ }
193
+ }
194
+ for (const k of ['approved_at', 'expires_at'] as const) {
195
+ if (!isIsoDate(e[k])) errors.push(`${where}: '${k}' must be a YYYY-MM-DD date`);
196
+ }
197
+ if (repaid !== null && !isIsoDate(repaid)) {
198
+ errors.push(`${where}: 'repaid_at' must be a YYYY-MM-DD date or null`);
199
+ }
200
+ if (e['consumed_tokens'] !== undefined) {
201
+ errors.push(
202
+ `${where}: 'consumed_tokens' is not a field. Consumption is DERIVED as measured − watermark; ` +
203
+ 'a stored copy would be an editable assertion about a measurement, which is the shape ' +
204
+ 'this ledger exists to avoid.',
205
+ );
206
+ }
207
+ out.push({
208
+ id: typeof id === 'string' ? id : `<entry ${String(i)}>`,
209
+ granted_tokens: Number.isFinite(granted) ? granted : 0,
210
+ watermark: Number.isFinite(watermark) ? watermark : 0,
211
+ reason: String(e['reason'] ?? ''),
212
+ approval_event: String(e['approval_event'] ?? ''),
213
+ approved_at: String(e['approved_at'] ?? ''),
214
+ expires_at: String(e['expires_at'] ?? ''),
215
+ repaid_at: repaid === null ? null : String(repaid),
216
+ });
217
+ });
218
+ return { exceptions: out, errors };
219
+ }
220
+
221
+ /** `today` as `YYYY-MM-DD`, injectable so the expiry branch is testable. */
222
+ export function stateOf(e: PayloadException, today: string): ExceptionState {
223
+ if (e.repaid_at !== null) return 'repaid';
224
+ return e.expires_at < today ? 'expired' : 'active';
225
+ }
226
+
227
+ export interface CeilingReading {
228
+ /** The number the payload is compared against. */
229
+ ceiling: number;
230
+ /** `round(baseline_tokens * (1 + headroom_pct/100))`. The floor of the ceiling. */
231
+ designCeiling: number;
232
+ /** Payload measured at the base ref, or `null` when it could not be read. */
233
+ basePayload: number | null;
234
+ /** The base after watermark pinning, or `null` with `basePayload`. */
235
+ effectiveBase: number | null;
236
+ /** `granted_tokens` of the single active grant, else 0. */
237
+ activeGrants: number;
238
+ activeIds: string[];
239
+ /** Which term set the ceiling — the one thing a reader needs at a glance. */
240
+ boundBy: 'design' | 'base' | 'grant';
241
+ /** True when the ceiling rests on a base reading that was actually taken. */
242
+ verified: boolean;
243
+ ok: boolean;
244
+ violations: string[];
245
+ /** Printed on the passing path too, so a skip cannot pass for a check. */
246
+ note: string | null;
247
+ }
248
+
249
+ export interface CeilingOptions {
250
+ designCeiling: number;
251
+ /** `null` = the base ref could not be measured. */
252
+ basePayload: number | null;
253
+ /** Why it could not be measured. Rendered verbatim. */
254
+ baseNote?: string | null;
255
+ /** The payload measured in THIS tree — needed to corroborate `repaid_at`. */
256
+ headPayload: number;
257
+ exceptions?: readonly PayloadException[];
258
+ exceptionErrors?: readonly string[];
259
+ /**
260
+ * Ids whose `approval_event` the caller VERIFIED against the platform.
261
+ *
262
+ * Absent means nothing was verified, which is the safe default: a grant is
263
+ * honoured only when someone with access to the platform said so.
264
+ */
265
+ verifiedApprovals?: readonly string[];
266
+ /** `YYYY-MM-DD`. Injected so the expiry branch is testable. */
267
+ today: string;
268
+ /**
269
+ * Refuse instead of falling back when the base could not be measured.
270
+ *
271
+ * Advisory is the default and keeps a shallow clone, a first commit and a
272
+ * detached build usable — the same argument `standing_bound_ratchet` makes
273
+ * for its own mode gate, and the same reason the flag is the caller's
274
+ * rather than sniffed from an environment variable here.
275
+ */
276
+ requireBase?: boolean;
277
+ }
278
+
279
+ /**
280
+ * Compute the effective ceiling and say whether it may be trusted.
281
+ *
282
+ * Pure: it takes a base reading rather than producing one, so every branch —
283
+ * expiry, watermark pinning, unverified approval, fail-closed base — is
284
+ * testable without git or a platform.
285
+ */
286
+ export function computeCeiling(opts: CeilingOptions): CeilingReading {
287
+ const exceptions = opts.exceptions ?? [];
288
+ const verified = new Set(opts.verifiedApprovals ?? []);
289
+ const violations = [...(opts.exceptionErrors ?? [])];
290
+
291
+ const live = exceptions.filter((e) => stateOf(e, opts.today) !== 'repaid');
292
+ const expired = live.filter((e) => stateOf(e, opts.today) === 'expired');
293
+ const candidates = live.filter((e) => stateOf(e, opts.today) === 'active');
294
+
295
+ // Rule 1 — at most one active grant. Summing overlapping grants can count
296
+ // exceptional growth twice: in a later watermark that already contains it,
297
+ // and again in the sum.
298
+ if (candidates.length > 1) {
299
+ violations.push(
300
+ `${String(candidates.length)} standing-payload exceptions are active at once ` +
301
+ `(${candidates.map((e) => e.id).join(', ')}). At most one may be. Overlapping grants have no ` +
302
+ 'defined composition: a later watermark can already contain an earlier grant\'s growth, so ' +
303
+ 'summing counts it twice. Repay or expire one before recording the next.',
304
+ );
305
+ }
306
+
307
+ // Rule 2 — an unverified approval buys nothing.
308
+ for (const e of candidates) {
309
+ if (!verified.has(e.id)) {
310
+ violations.push(
311
+ `standing-payload exception '${e.id}' has no VERIFIED approval. It names ` +
312
+ `'${e.approval_event}', and nothing in this run confirmed that event. An approval recorded ` +
313
+ 'only by the change that requests it is self-asserted data: the same diff can claim the ' +
314
+ 'grant, the approver and the date. The grant is refused until the platform event is ' +
315
+ 'checked. This is not a payload problem — do not shrink the change to clear it.',
316
+ );
317
+ }
318
+ }
319
+
320
+ // Rule 6 — expiry is evaluated now, which on a required check is merge time.
321
+ for (const e of expired) {
322
+ violations.push(
323
+ `standing-payload exception '${e.id}' expired on ${e.expires_at} and is not repaid. ` +
324
+ `It granted ${String(e.granted_tokens)} tok over a watermark of ${String(e.watermark)}. ` +
325
+ 'Bring the payload back to the watermark and record repaid_at, or obtain a fresh approved ' +
326
+ "grant — editing this one's expiry is refused by the shrink-only ratchet, and an expiry " +
327
+ 'that lapsed quietly would renew the grant by inaction.',
328
+ );
329
+ }
330
+
331
+ // Rule 4 — `repaid_at` records a measurement; it does not assert one.
332
+ for (const e of exceptions) {
333
+ if (e.repaid_at !== null && opts.headPayload > e.watermark) {
334
+ violations.push(
335
+ `standing-payload exception '${e.id}' is marked repaid on ${e.repaid_at}, but the payload ` +
336
+ `measures ${String(opts.headPayload)} against its watermark of ${String(e.watermark)} — ` +
337
+ `${String(opts.headPayload - e.watermark)} tok still outstanding. Repayment is established ` +
338
+ 'by the measurement returning to the watermark; the field records that event and cannot ' +
339
+ 'substitute for it.',
340
+ );
341
+ }
342
+ }
343
+
344
+ const activeGrants = candidates.length === 1 ? (candidates[0]?.granted_tokens ?? 0) : 0;
345
+ const activeIds = candidates.map((e) => e.id);
346
+
347
+ if (opts.basePayload === null) {
348
+ const why = opts.baseNote ?? 'the base ref could not be measured';
349
+ const fallback = opts.designCeiling;
350
+ if (opts.requireBase === true) {
351
+ violations.push(
352
+ `the standing-payload ceiling could not be measured: ${why}. This run is ENFORCING, so an ` +
353
+ 'unmeasurable base refuses rather than falling back — a base that cannot be read would ' +
354
+ 'otherwise grant an unbounded budget on an infrastructure failure. This is NOT payload ' +
355
+ 'growth: fix the checkout (a full fetch of the base ref) rather than shrinking the change.',
356
+ );
357
+ }
358
+ return {
359
+ ceiling: fallback,
360
+ designCeiling: opts.designCeiling,
361
+ basePayload: null,
362
+ effectiveBase: null,
363
+ activeGrants,
364
+ activeIds,
365
+ boundBy: 'design',
366
+ verified: false,
367
+ ok: violations.length === 0 && opts.requireBase !== true,
368
+ note: `${why}, so the ceiling is NOT a base-measured reading`,
369
+ violations,
370
+ };
371
+ }
372
+
373
+ // The watermark pin. Without it a grant raises the measurement, the next
374
+ // pull request measures the raised base, and the grant is permanent.
375
+ const effectiveBase =
376
+ candidates.length === 1
377
+ ? Math.min(opts.basePayload, candidates[0]?.watermark ?? opts.basePayload)
378
+ : opts.basePayload;
379
+
380
+ // An unverified or over-subscribed grant contributes ZERO, so a refused
381
+ // grant can never widen the bound it was refused for.
382
+ const honoured = violations.length === 0 ? activeGrants : 0;
383
+ const measuredTerm = effectiveBase + honoured;
384
+ const ceiling = Math.max(opts.designCeiling, measuredTerm);
385
+
386
+ let boundBy: CeilingReading['boundBy'] = 'design';
387
+ if (ceiling === measuredTerm && ceiling > opts.designCeiling) boundBy = honoured > 0 ? 'grant' : 'base';
388
+
389
+ return {
390
+ ceiling,
391
+ designCeiling: opts.designCeiling,
392
+ basePayload: opts.basePayload,
393
+ effectiveBase,
394
+ activeGrants: honoured,
395
+ activeIds,
396
+ boundBy,
397
+ verified: true,
398
+ ok: violations.length === 0,
399
+ violations,
400
+ note: null,
401
+ };
402
+ }
403
+
404
+ /** The human rendering, printed on BOTH paths — a check that only speaks when
405
+ * it fails is a check nobody audits until it is already load-bearing. */
406
+ export function renderCeiling(c: CeilingReading): string {
407
+ const lines: string[] = [];
408
+ if (!c.verified) {
409
+ lines.push(` ⚠️ ceiling NOT measured — ${c.note ?? 'no reason recorded'}\n`);
410
+ lines.push(` falling back to ${String(c.ceiling)} tok (design ${String(c.designCeiling)}).\n`);
411
+ } else {
412
+ const pinned = c.effectiveBase !== c.basePayload;
413
+ const how =
414
+ c.boundBy === 'design'
415
+ ? `the DESIGN ceiling — the measured base (${String(c.basePayload ?? 0)}) is at or below it`
416
+ : `base ${String(c.effectiveBase ?? 0)}` +
417
+ (pinned ? ` (PINNED to an exception watermark; measured ${String(c.basePayload ?? 0)})` : '') +
418
+ (c.activeGrants > 0 ? ` + ${String(c.activeGrants)} granted` : '') +
419
+ ` — zero net growth, design ${String(c.designCeiling)}`;
420
+ lines.push(` ✅ ceiling ${String(c.ceiling)} tok = ${how}.\n`);
421
+ }
422
+ if (c.activeIds.length > 0) lines.push(` active exception(s): ${c.activeIds.join(', ')}\n`);
423
+ return lines.join('');
424
+ }
@@ -0,0 +1,190 @@
1
+ /**
2
+ * The long-run continuity record — `road-to-adversarial-verification-and-long-runs`
3
+ * 9.1.
4
+ *
5
+ * `continuity_writer` already writes a session record. This is the MISSION
6
+ * record: the state a twelve-hour run must survive a restart with, and the two
7
+ * rules about reading it back — never re-ask a closed question, and never resume
8
+ * on an authority snapshot that has since been revoked.
9
+ *
10
+ * **Risk 5 is the reason `restore` is not just a deserialise.** The record
11
+ * carries the grant including `expires` and `revoked_by`. A restore that trusted
12
+ * the snapshot would resume with authority the owner withdrew after it was
13
+ * written — so the snapshot is revalidated against the LEDGER, which is the
14
+ * surface a revocation actually writes to. A record and a ledger that disagree
15
+ * are resolved in favour of the ledger, always.
16
+ *
17
+ * Pure. Reading the ledger and the forge belongs to the caller.
18
+ */
19
+
20
+ /** A decision the mission already settled. Re-asking one is the failure. */
21
+ export interface DecisionRef {
22
+ readonly id: string;
23
+ /** Where the reasoning lives, so a restore can read rather than redo it. */
24
+ readonly where: string;
25
+ }
26
+
27
+ /** The authority the mission is running under. */
28
+ export interface AuthoritySnapshot {
29
+ readonly grant: string;
30
+ /** ISO instant, or `null` for a grant with no expiry. */
31
+ readonly expires: string | null;
32
+ /** Who revoked it, or `null` while it stands. */
33
+ readonly revoked_by: string | null;
34
+ }
35
+
36
+ export interface MissionRecord {
37
+ readonly mission_id: string;
38
+ readonly roadmap: string;
39
+ readonly phase: string;
40
+ readonly completed_steps: readonly string[];
41
+ readonly decisions: readonly DecisionRef[];
42
+ readonly authority: AuthoritySnapshot;
43
+ readonly target_branch: string;
44
+ readonly pr: number | null;
45
+ readonly head_sha: string;
46
+ /** The last CI verdict observed, and the head it was about. */
47
+ readonly last_ci: { readonly verdict: string; readonly head: string } | null;
48
+ readonly recovery_epoch: number;
49
+ readonly attempt_count: number;
50
+ readonly pending_reviews: readonly string[];
51
+ readonly owner_owned_residue: readonly string[];
52
+ }
53
+
54
+ /** Every field a record must carry. Absent means the record is not one. */
55
+ export const REQUIRED_FIELDS: readonly (keyof MissionRecord)[] = [
56
+ 'mission_id',
57
+ 'roadmap',
58
+ 'phase',
59
+ 'completed_steps',
60
+ 'decisions',
61
+ 'authority',
62
+ 'target_branch',
63
+ 'pr',
64
+ 'head_sha',
65
+ 'last_ci',
66
+ 'recovery_epoch',
67
+ 'attempt_count',
68
+ 'pending_reviews',
69
+ 'owner_owned_residue',
70
+ ];
71
+
72
+ /** When the record is written. A side task is NOT one of these. */
73
+ export const WRITE_BOUNDARIES = [
74
+ 'phase-boundary',
75
+ 'stop',
76
+ 'pre-compact',
77
+ 'session-end',
78
+ ] as const;
79
+ export type WriteBoundary = (typeof WRITE_BOUNDARIES)[number];
80
+
81
+ /** Field names missing from a candidate record. Empty means complete. */
82
+ export function missingFields(candidate: Partial<MissionRecord>): string[] {
83
+ return REQUIRED_FIELDS.filter((f) => candidate[f] === undefined).map(String);
84
+ }
85
+
86
+ /** What the LEDGER says about a grant right now. `null` = no ledger entry. */
87
+ export interface LedgerState {
88
+ readonly grant: string;
89
+ readonly revoked_by: string | null;
90
+ }
91
+
92
+ /**
93
+ * `grant-mismatch` covers both ways the two sides can fail to be about one
94
+ * grant: they name different ones, or the record names none at all — which
95
+ * identifies nothing for the ledger to agree with.
96
+ */
97
+ export type RestoreState =
98
+ | 'resume'
99
+ | 'authority-withdrawn'
100
+ | 'authority-expired'
101
+ | 'grant-mismatch';
102
+
103
+ export interface RestoreVerdict {
104
+ readonly state: RestoreState;
105
+ readonly reason: string;
106
+ /** Questions this record already answers. A restore re-asks none of them. */
107
+ readonly closed: readonly string[];
108
+ }
109
+
110
+ /**
111
+ * Decide whether a restored mission may continue.
112
+ *
113
+ * The LEDGER wins over the snapshot, in one direction only: it can revoke a
114
+ * grant the record still shows as live, and it cannot revive one the record
115
+ * shows as revoked. A ledger that could un-revoke would make the record the
116
+ * weaker of two authorities and the revocation advisory.
117
+ *
118
+ * **Identity comes before precedence.** Asking which of the two wins is only
119
+ * meaningful once they are about the same grant. A restore that read
120
+ * `revoked_by` from a ledger describing a DIFFERENT grant would be using the
121
+ * ledger as an oracle for a question it was never asked — a mission authorised
122
+ * under a narrow grant would resume on a later, broader grant's silence. So the
123
+ * identity check runs first and refuses in both directions, which leaves the
124
+ * one-way revoke precedence below exactly as it was.
125
+ */
126
+ export function restore(
127
+ record: MissionRecord,
128
+ ledger: LedgerState | null,
129
+ now: Date,
130
+ ): RestoreVerdict {
131
+ const closed = record.decisions.map((d) => d.id);
132
+
133
+ if (record.authority.grant.trim().length === 0) {
134
+ return {
135
+ state: 'grant-mismatch',
136
+ reason: 'the snapshot names no grant, so there is nothing for the ledger to agree with',
137
+ closed,
138
+ };
139
+ }
140
+ if (ledger !== null && ledger.grant !== record.authority.grant) {
141
+ return {
142
+ state: 'grant-mismatch',
143
+ reason: `the snapshot runs under ${record.authority.grant} and the ledger describes ${ledger.grant}; the two are not about the same grant`,
144
+ closed,
145
+ };
146
+ }
147
+ if (record.authority.revoked_by !== null) {
148
+ return {
149
+ state: 'authority-withdrawn',
150
+ reason: `the record's own snapshot is revoked by ${record.authority.revoked_by}`,
151
+ closed,
152
+ };
153
+ }
154
+ if (ledger !== null && ledger.revoked_by !== null) {
155
+ return {
156
+ state: 'authority-withdrawn',
157
+ reason: `the ledger reports the grant revoked by ${ledger.revoked_by} after the snapshot was written`,
158
+ closed,
159
+ };
160
+ }
161
+ if (record.authority.expires !== null) {
162
+ const at = Date.parse(record.authority.expires);
163
+ // An UNPARSEABLE expiry is treated as expired. A grant whose lifetime
164
+ // cannot be read is not a grant with no lifetime.
165
+ if (!Number.isFinite(at) || at <= now.getTime()) {
166
+ return {
167
+ state: 'authority-expired',
168
+ reason: `the grant's expiry (${record.authority.expires}) is not a future instant`,
169
+ closed,
170
+ };
171
+ }
172
+ }
173
+ return { state: 'resume', reason: 'the grant stands in both the record and the ledger', closed };
174
+ }
175
+
176
+ /**
177
+ * Has this question already been settled by the mission?
178
+ *
179
+ * The whole point of the record: a restore that re-asks a closed question has
180
+ * spent the owner's attention on something the mission already decided, which is
181
+ * the loop this roadmap exists to end.
182
+ */
183
+ export function alreadyAnswered(record: MissionRecord, questionId: string): boolean {
184
+ return record.decisions.some((d) => d.id === questionId);
185
+ }
186
+
187
+ /** A side task does not clear the record. */
188
+ export function clearedBy(boundary: string): boolean {
189
+ return boundary === 'mission-complete';
190
+ }