@event4u/agent-config 14.21.0 → 14.23.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 (235) 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 +164 -187
  5. package/CREDITS.md +37 -0
  6. package/README.md +1 -1
  7. package/dist/agent-src/commands/analyze/inbox.md +380 -5
  8. package/dist/agent-src/commands/analyze/repo.md +4 -2
  9. package/dist/agent-src/commands/pr/merge.md +47 -38
  10. package/dist/agent-src/commands/roadmap/next.md +8 -0
  11. package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +69 -0
  12. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +5 -5
  13. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +12 -0
  14. package/dist/agent-src/rules/external-code-graph-interop.md +19 -14
  15. package/dist/agent-src/rules/notes-first-reasoning.md +36 -25
  16. package/dist/agent-src/rules/token-efficiency.md +26 -3
  17. package/dist/agent-src/scripts/_cli_entry.ts +51 -0
  18. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +120 -27
  19. package/dist/agent-src/scripts/roadmap_gates.ts +4 -20
  20. package/dist/agent-src/scripts/roadmap_set_step.ts +5 -2
  21. package/dist/agent-src/scripts/stubs_due.ts +4 -18
  22. package/dist/agent-src/scripts/update_roadmap_progress.ts +7 -24
  23. package/dist/agent-src/skills/code-intelligence/SKILL.md +82 -1
  24. package/dist/agent-src/skills/git-workflow/references/push-closes-its-loop.md +9 -0
  25. package/dist/agent-src/skills/roadmap-writing/SKILL.md +17 -0
  26. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +52 -2
  27. package/dist/agent-src/templates/agent-settings.md +0 -1
  28. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  29. package/dist/cli/registry.js +1 -1
  30. package/dist/cli-delegate/_cli_entry.js +8 -0
  31. package/dist/cli-delegate/_lib/pin_resolver.js +170 -0
  32. package/dist/cli-delegate/archival_sweep.js +8 -0
  33. package/dist/cli-delegate/archive_completed_roadmaps.js +542 -0
  34. package/dist/cli-delegate/check_update_banner.js +923 -0
  35. package/dist/cli-delegate/{chunk-ZVBOHSVF.js → chunk-2KKXJW25.js} +23 -9
  36. package/dist/cli-delegate/chunk-5EK5WQN6.js +28 -0
  37. package/dist/cli-delegate/chunk-7XGD2OEU.js +96 -0
  38. package/dist/cli-delegate/chunk-DBH4AHDK.js +155 -0
  39. package/dist/cli-delegate/{chunk-4FS4WRXP.js → chunk-IAI3XL43.js} +15 -3
  40. package/dist/cli-delegate/chunk-JO25G5H7.js +20 -0
  41. package/dist/cli-delegate/{chunk-643Z7FIC.js → chunk-KCNTUSPI.js} +7 -1
  42. package/dist/cli-delegate/chunk-LTYVIY76.js +7961 -0
  43. package/dist/cli-delegate/chunk-QAJS5YWY.js +999 -0
  44. package/dist/cli-delegate/{chunk-YS6XTLEK.js → chunk-TYSY33MD.js} +56 -6
  45. package/dist/cli-delegate/chunk-ULET4BYS.js +328 -0
  46. package/dist/cli-delegate/chunk-YDQSHTD7.js +187 -0
  47. package/dist/cli-delegate/chunk-YWVXLR44.js +48 -0
  48. package/dist/cli-delegate/chunk-ZZQBUHFK.js +273 -0
  49. package/dist/cli-delegate/cmd_conformance.js +2 -2
  50. package/dist/cli-delegate/cmd_export.js +2 -2
  51. package/dist/cli-delegate/cmd_handoff.js +45 -13
  52. package/dist/cli-delegate/cmd_prune.js +2 -2
  53. package/dist/cli-delegate/cmd_session_recycle.js +4 -14
  54. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  55. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  56. package/dist/cli-delegate/cmd_sync.js +2 -2
  57. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  58. package/dist/cli-delegate/cmd_validate.js +2 -2
  59. package/dist/cli-delegate/dashboard_mode.js +16 -0
  60. package/dist/cli-delegate/gate_budget.js +24 -0
  61. package/dist/cli-delegate/gate_execute.js +25 -0
  62. package/dist/cli-delegate/guarded_baseline.js +18 -0
  63. package/dist/cli-delegate/resume_probe.js +22 -0
  64. package/dist/cli-delegate/roadmap_gates.js +638 -0
  65. package/dist/cli-delegate/roadmap_set_step.js +173 -0
  66. package/dist/cli-delegate/stub_queue.js +47 -0
  67. package/dist/cli-delegate/stubs_due.js +38 -0
  68. package/dist/cli-delegate/ticket_bundles.js +59 -0
  69. package/dist/cli-delegate/update_roadmap_progress.js +60 -0
  70. package/dist/discovery/deprecation-report.md +1 -1
  71. package/dist/discovery/discovery-manifest.json +13 -13
  72. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  73. package/dist/discovery/discovery-manifest.summary.md +1 -1
  74. package/dist/discovery/dormancy-report.md +1 -1
  75. package/dist/discovery/orphan-report.md +1 -1
  76. package/dist/discovery/packs.json +2 -2
  77. package/dist/discovery/trust-report.md +1 -1
  78. package/dist/discovery/workspaces.json +2 -2
  79. package/dist/hooks/dispatch.js +1813 -946
  80. package/dist/install/claudeLayerCarriage.js +237 -0
  81. package/dist/install/claudePathsPlan.js +79 -0
  82. package/dist/install/hostLayerFingerprint.js +1 -1
  83. package/dist/install/install.mjs +21 -3
  84. package/dist/install/partitionEligibility.js +231 -117
  85. package/dist/install/ruleLayerPartition.js +10 -7
  86. package/dist/mcp/registry-manifest.json +2 -2
  87. package/dist/mcp/server.json +2 -2
  88. package/dist/mcp/server.mjs +10870 -7641
  89. package/dist/scripts/_lib/lean_projection_mode.js +108 -0
  90. package/dist/scripts/_lib/router_match.js +243 -0
  91. package/dist/scripts/_lib/rule_injection.js +216 -0
  92. package/dist/scripts/condense.js +52 -45
  93. package/dist/scripts/project_thin_rules.js +139 -19
  94. package/dist/server/schemas/settings.js +16 -3
  95. package/docs/MIGRATION.md +105 -1
  96. package/docs/contracts/settings-classes.md +40 -8
  97. package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +73 -0
  98. package/docs/guidelines/agent-infra/notes-horizon-mechanics.md +90 -0
  99. package/llms.txt +1 -1
  100. package/package.json +7 -4
  101. package/src/agent-src/scripts/_cli_entry.ts +51 -0
  102. package/src/agent-src/scripts/archive_completed_roadmaps.ts +120 -27
  103. package/src/agent-src/scripts/roadmap_gates.ts +4 -20
  104. package/src/agent-src/scripts/roadmap_set_step.ts +5 -2
  105. package/src/agent-src/scripts/stubs_due.ts +4 -18
  106. package/src/agent-src/scripts/update_roadmap_progress.ts +7 -24
  107. package/src/config/agent-settings.template.yml +47 -1
  108. package/src/config/continuity-surface.json +516 -0
  109. package/src/config/estate-count-budget.json +1 -1
  110. package/src/config/evaluator-budgets.json +4 -4
  111. package/src/config/gate-coverage.yml +29 -8
  112. package/src/config/gate-violation-baselines.json +8 -8
  113. package/src/config/gitignore-block.txt +19 -0
  114. package/src/config/hook-token-budget.json +7 -6
  115. package/src/config/host-injection-effect.json +73 -0
  116. package/src/config/packed-binary-manifest.json +33 -0
  117. package/src/config/preamble-payload-budget.json +112 -105
  118. package/src/config/publish-surface.json +4 -2
  119. package/src/config/routing-coverage-seed.json +9 -2
  120. package/src/install/claudeLayerCarriage.ts +273 -0
  121. package/src/install/claudePathsPlan.ts +76 -0
  122. package/src/install/hostLayerFingerprint.ts +1 -1
  123. package/src/install/partitionEligibility.ts +261 -127
  124. package/src/install/ruleLayerPartition.ts +19 -23
  125. package/src/scripts/_cli/cmd_session_recycle.ts +2 -25
  126. package/src/scripts/_cli/handoff_sessions.ts +14 -44
  127. package/src/scripts/_dispatch.bash +77 -27
  128. package/src/scripts/_lib/auto_dispatch.ts +2 -2
  129. package/src/scripts/_lib/catalogue_layer_parity.ts +146 -0
  130. package/src/scripts/_lib/continuity_slot.ts +295 -0
  131. package/src/scripts/_lib/continuity_surface.ts +296 -0
  132. package/src/scripts/_lib/continuity_writer.ts +253 -0
  133. package/src/scripts/_lib/corpus_manifest.ts +26 -9
  134. package/src/scripts/_lib/hook_settings.ts +86 -1
  135. package/src/scripts/_lib/injection_effect.ts +151 -0
  136. package/src/scripts/_lib/layer_overlap_notice.ts +50 -6
  137. package/src/scripts/_lib/lean_projection_mode.ts +127 -0
  138. package/src/scripts/_lib/packed_binary_predicate.ts +262 -0
  139. package/src/scripts/_lib/pin_resolver.ts +13 -0
  140. package/src/scripts/_lib/recycle_envelope_paths.ts +109 -21
  141. package/src/scripts/_lib/regression_neighbourhood.ts +159 -22
  142. package/src/scripts/_lib/retired_status.ts +74 -0
  143. package/src/scripts/_lib/session_eol.ts +44 -0
  144. package/src/scripts/_lib/session_index_trust.ts +346 -0
  145. package/src/scripts/_lib/skill_catalogue.ts +156 -21
  146. package/src/scripts/_lib/standing_bound_ratchet.ts +154 -0
  147. package/src/scripts/_lib/subagent_capsule.ts +117 -8
  148. package/src/scripts/_lib/vendored_grammar_upstream.ts +280 -0
  149. package/src/scripts/ai_council/api_transport.ts +5 -0
  150. package/src/scripts/ai_council/deliberation_metrics.ts +353 -0
  151. package/src/scripts/ai_council/prompts.ts +9 -0
  152. package/src/scripts/ai_council/synthesis_disagreement.ts +206 -0
  153. package/src/scripts/audit_mcp_tools.ts +69 -25
  154. package/src/scripts/build_mcp_catalog.ts +24 -1
  155. package/src/scripts/capture_skill_catalogue.ts +24 -11
  156. package/src/scripts/check_candidate_lines.ts +499 -0
  157. package/src/scripts/check_continuity_surface.ts +475 -0
  158. package/src/scripts/check_dependency_floors.ts +13 -6
  159. package/src/scripts/check_estate_count.ts +40 -79
  160. package/src/scripts/check_host_tree_parity.ts +291 -0
  161. package/src/scripts/check_pack_size.ts +262 -23
  162. package/src/scripts/check_preamble_payload_budget.ts +136 -7
  163. package/src/scripts/check_roadmap_trackable.ts +19 -16
  164. package/src/scripts/check_rule_layer_partition.ts +54 -34
  165. package/src/scripts/check_rule_projection_integrity.ts +102 -2
  166. package/src/scripts/check_single_delivery.ts +97 -31
  167. package/src/scripts/check_update_banner.ts +13 -0
  168. package/src/scripts/ci_settle.ts +205 -5
  169. package/src/scripts/code_graph/build.ts +217 -15
  170. package/src/scripts/code_graph/cli.ts +334 -15
  171. package/src/scripts/code_graph/detect.ts +38 -0
  172. package/src/scripts/code_graph/extract.ts +11 -0
  173. package/src/scripts/code_graph/grammar_probe.ts +43 -0
  174. package/src/scripts/code_graph/loader.ts +88 -14
  175. package/src/scripts/code_graph/query.ts +144 -11
  176. package/src/scripts/code_graph/resolution_tiers.ts +238 -0
  177. package/src/scripts/code_graph/sqlite_store.ts +262 -6
  178. package/src/scripts/code_graph/types.ts +117 -3
  179. package/src/scripts/code_graph/validate.ts +26 -1
  180. package/src/scripts/code_graph/verbs.ts +590 -0
  181. package/src/scripts/condense.ts +54 -42
  182. package/src/scripts/corpus_manifest.ts +5 -5
  183. package/src/scripts/council_cli.ts +11 -12
  184. package/src/scripts/count_intent_disagreement.ts +426 -0
  185. package/src/scripts/handoff_context_hook.ts +118 -1
  186. package/src/scripts/hook_effect_doctor.ts +82 -1
  187. package/src/scripts/hook_manifest.json +1 -1
  188. package/src/scripts/hook_manifest.yaml +87 -12
  189. package/src/scripts/hooks/block_speaking_inbox_dir.ts +182 -16
  190. package/src/scripts/hooks/chain_nudge_hook.ts +282 -0
  191. package/src/scripts/hooks/code_graph_context_hook.ts +201 -0
  192. package/src/scripts/hooks/concern_registry.ts +4 -2
  193. package/src/scripts/hooks/host_semantics.ts +1 -1
  194. package/src/scripts/hooks/orchestration_record_hook.ts +2 -2
  195. package/src/scripts/hooks/push_settle_hook.ts +3 -2
  196. package/src/scripts/hooks/rule_inject_hook.ts +108 -21
  197. package/src/scripts/hooks/session_eol_hook.ts +120 -2
  198. package/src/scripts/hooks/skill_route_hook.ts +11 -4
  199. package/src/scripts/hot_context_hook.ts +62 -7
  200. package/src/scripts/inbox_source_census.ts +322 -0
  201. package/src/scripts/install-hooks.sh +82 -0
  202. package/src/scripts/{lint_carrier_integrity.ts → lint_deferral_integrity.ts} +60 -214
  203. package/src/scripts/lint_plan_risk_register.ts +13 -17
  204. package/src/scripts/lint_roadmap_blockers.ts +164 -1
  205. package/src/scripts/mcp_server/consumer_tool_catalog.json +152 -2
  206. package/src/scripts/mcp_server/graph_tools.ts +294 -0
  207. package/src/scripts/mcp_server/path_util.ts +31 -0
  208. package/src/scripts/mcp_server/suggest_skill.ts +72 -0
  209. package/src/scripts/mcp_server/tool_catalog_source.ts +7 -1
  210. package/src/scripts/mcp_server/tools.ts +5 -69
  211. package/src/scripts/media/lib/adapter-contract.md +7 -2
  212. package/src/scripts/media/lib/fixtures/flux/asset-0001.png +0 -0
  213. package/src/scripts/media/lib/fixtures/gemini-image/asset-0001.png +0 -0
  214. package/src/scripts/media/lib/fixtures/ideogram/asset-0001.png +0 -0
  215. package/src/scripts/model_rule_injection.ts +61 -4
  216. package/src/scripts/project_thin_rules.ts +149 -16
  217. package/src/scripts/rdp_candidates_delta.ts +276 -0
  218. package/src/scripts/rdp_quality_eval.ts +217 -19
  219. package/src/scripts/release_mix_taxonomy.json +3 -2
  220. package/src/scripts/report_host_injection_effect.ts +189 -0
  221. package/src/scripts/report_layer_overlap.ts +41 -0
  222. package/src/scripts/report_skill_menu_census.ts +341 -0
  223. package/src/scripts/report_standing_payload_by_host.ts +650 -0
  224. package/src/scripts/schemas/agent-settings.schema.json +7 -1
  225. package/src/scripts/session_memory_index.ts +81 -8
  226. package/src/scripts/skill_tools/score_skill_relevance.ts +70 -6
  227. package/src/scripts/skill_tools/suggest_skill_for_task.ts +37 -3
  228. package/src/server/schemas/settings.ts +23 -4
  229. package/src/vendor/grammars/README.md +86 -0
  230. package/src/vendor/grammars/tree-sitter-javascript.wasm +0 -0
  231. package/src/vendor/grammars/tree-sitter-php.wasm +0 -0
  232. package/src/vendor/grammars/tree-sitter-typescript.wasm +0 -0
  233. package/src/scripts/_lib/carrier_status.ts +0 -118
  234. package/src/scripts/_lib/estate_carrier_cases.ts +0 -72
  235. package/src/scripts/hooks/code_graph_nudge_hook.ts +0 -191
@@ -22,7 +22,7 @@
22
22
  * Exit codes: 0 green · 1 over budget · 2 misuse / unreadable input.
23
23
  */
24
24
 
25
- import { execFileSync } from 'node:child_process';
25
+ import { execFileSync, spawnSync } from 'node:child_process';
26
26
  import { createHash } from 'node:crypto';
27
27
  import * as fs from 'node:fs';
28
28
  import * as path from 'node:path';
@@ -30,6 +30,12 @@ import { fileURLToPath } from 'node:url';
30
30
 
31
31
  import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
32
32
  import { runSelfTest } from './_lib/gate_self_test.js';
33
+ import {
34
+ type EligibilityVerdict,
35
+ evaluateEntry,
36
+ type GrammarProbe,
37
+ loadManifest,
38
+ } from './_lib/packed_binary_predicate.js';
33
39
 
34
40
  const REPO_ROOT = path.resolve(fileURLToPath(import.meta.url), '..', '..', '..');
35
41
  const BUDGET_PATH = path.join(REPO_ROOT, 'src', 'config', 'pack-size-budget.json');
@@ -306,19 +312,26 @@ export function payloadFingerprint(entryPath: string, size: number): string {
306
312
  }
307
313
 
308
314
  /**
309
- * Measured on the BUILT payload, 2026-09-07: 3041 entries, of which 3026 are
310
- * text and these 15 are not. `archive` is empty and stays a hard zero.
315
+ * Bound exceptions exist for `dotfile` and `no-extension` ONLY.
311
316
  *
312
- * The roadmap step that added this expected `binary` to be zero too. It is not:
313
- * three 69-byte PNG placeholders ship as media-adapter fixtures. Recorded as a
314
- * correction rather than resolved by redefining the class — they are genuinely
315
- * binary, they genuinely ship, and dropping them from the published surface is
316
- * a consumer-visible change that belongs to whoever owns those adapters.
317
+ * `binary` and `archive` are not exceptable HERE and never will be: this table
318
+ * is prose-bound, and prose is exactly what a binary must not be admitted on.
319
+ * `archive` remains a hard zero with no admission path of any kind. `binary`
320
+ * has one since 2026-09-08 — `src/config/packed-binary-manifest.json`, gated by
321
+ * the mechanically verified predicate in `_lib/packed_binary_predicate.ts`,
322
+ * which re-derives every claim from the packed bytes. Adding a `binary` row to
323
+ * the table below still does nothing. That is AC-6 of
324
+ * `road-to-scan-that-fails-closed` read literally, after an AI council
325
+ * (2026-09-07, 2 seats, unanimous over two rounds) resolved the split between
326
+ * reading "at zero" as the ratchet and as the observed count. It chose the
327
+ * observed count, and chose disposition 3 — ship a textual representation —
328
+ * over generating fixtures at dry-run time, on the measurement that 12 of the
329
+ * 15 asset-bearing media fixtures ALREADY ship an ASCII placeholder at their
330
+ * documented path (`FIXTURE-<adapter-id>-<ext>`). The three 69-byte PNGs that
331
+ * used to sit here were the outliers, not the norm; they now carry the sibling
332
+ * convention and the class is empty by observation.
317
333
  */
318
334
  const BOUND_PAYLOAD_EXCEPTIONS: readonly BoundPayloadException[] = [
319
- { fingerprint: '955b5da399e14fb39cfe1f3279044061043e64e2bd1576871514db927e56d701', path: 'src/scripts/media/lib/fixtures/flux/asset-0001.png', size: 69, why: 'media-adapter response fixture — a 69-byte placeholder PNG the flux adapter test decodes; binary by construction' },
320
- { fingerprint: 'c2dd9743367ece8ffe0deef5e798d39967c22245749fd7f798edd8aa78b70f59', path: 'src/scripts/media/lib/fixtures/gemini-image/asset-0001.png', size: 69, why: 'media-adapter response fixture — same class as the flux placeholder' },
321
- { fingerprint: 'cbac44d021a1dc41b938e798eb9be4147fc9d35194c348dfe9acaa0596bc83a5', path: 'src/scripts/media/lib/fixtures/ideogram/asset-0001.png', size: 69, why: 'media-adapter response fixture — same class as the flux placeholder' },
322
335
  { fingerprint: '47e4328e71652a0375ade7eb4dac8a3e4b6df1c6e4e74caa75287266676cc9a6', path: 'agents/templates/.ai-council.yml.example', size: 30561, why: 'shipped council config TEMPLATE — a dotfile because the file it is copied to is one. RE-PINNED 2026-09-07, 29,872 -> 30,561 bytes: the exception is path-AND-size-bound by design, so this branch adding a `disabled_reason` honest-null block to the gemini seat in the template invalidated it and the gate refused the entry as unaccounted. The re-pin excepts the SAME path for the SAME reason at its measured new size — it does not widen the class, raise a cap, or except anything the gate was not already carrying.' },
323
336
  { fingerprint: 'd36a53951c184db7481139235056e90b53a853714b3a68b31ba1349ecb95ac1d', path: 'agents/templates/.ai-video.xml.example', size: 9209, why: 'shipped video-provider config template — same reason as the council template' },
324
337
  { fingerprint: 'f83229412cf8cceaadd0cb1934140a0c7c8277df43edc590f370572f6df6da98', path: 'dist/agent-src/templates/agents/.gitattributes.fragment', size: 2140, why: 'gitattributes fragment the installer appends to a consumer repo' },
@@ -360,16 +373,25 @@ export interface TypeClassification {
360
373
  /**
361
374
  * Classify the whole payload by type and report what is not accounted for.
362
375
  *
363
- * Every entry outside `text` needs a bound exception. `archive` has none and is
364
- * meant to stay that way; `binary` has three, named; the dotfile and
365
- * extensionless entries are each bound to a path AND a size, so replacing one
366
- * with different content re-fires the check on that entry rather than passing
367
- * inside a count.
376
+ * `archive` stays a closed class: no admission path, so an entry in it is
377
+ * always a violation. `binary` is admitted only through `eligible` — the
378
+ * caller supplies the verdicts, this function never decides eligibility itself
379
+ * — and an INELIGIBLE binary is unaccounted exactly as before.
380
+ *
381
+ * The counts are the OBSERVED payload and are never reduced by eligibility. A
382
+ * caller compares `observed` against `allowed` (the number of entries that
383
+ * passed the predicate); nothing here subtracts an excused file from a count,
384
+ * because a measurement an exception can move is not a measurement.
385
+ *
386
+ * Dotfile and extensionless entries are each bound to a path AND a size, so
387
+ * replacing one with different content re-fires the check on that entry rather
388
+ * than passing inside a count.
368
389
  */
369
390
  export function classifyPayloadTypes(
370
391
  files: readonly PackFile[],
371
392
  root = REPO_ROOT,
372
393
  head: (p: string) => Uint8Array | null = (p) => readHead(p, root),
394
+ eligible: ReadonlySet<string> = new Set<string>(),
373
395
  ): TypeClassification {
374
396
  const counts: Record<PayloadType | 'unreadable', number> = {
375
397
  archive: 0,
@@ -387,7 +409,11 @@ export function classifyPayloadTypes(
387
409
  counts[type] += 1;
388
410
  if (type === 'text') continue;
389
411
  const fingerprint = payloadFingerprint(f.path, f.size);
390
- if (byFingerprint.has(fingerprint)) {
412
+ // A binary is accounted for ONLY by the mechanical predicate. It is
413
+ // never matched against the prose table, so a `binary` row added there
414
+ // would still do nothing.
415
+ if (type === 'binary' && eligible.has(f.path)) continue;
416
+ if (type !== 'binary' && type !== 'archive' && byFingerprint.has(fingerprint)) {
391
417
  used.add(fingerprint);
392
418
  continue;
393
419
  }
@@ -400,6 +426,127 @@ export function classifyPayloadTypes(
400
426
  };
401
427
  }
402
428
 
429
+ /**
430
+ * Binary eligibility over the packed payload.
431
+ *
432
+ * The probe is a CHILD PROCESS running the engine's own loader
433
+ * (`code_graph/grammar_probe.ts`), not a reimplementation here: a second
434
+ * loader inside the gate could accept bytes the engine rejects, which is the
435
+ * one failure mode that would let a broken grammar ship while the gate called
436
+ * it valid.
437
+ *
438
+ * A probe that cannot run at all yields `null` for every file, which FAILS
439
+ * conditions 8 and 9 rather than passing them — an unverifiable binary is not
440
+ * an eligible one.
441
+ */
442
+ export interface BinaryEligibilityReport {
443
+ /** Paths that passed every condition. */
444
+ readonly eligible: ReadonlySet<string>;
445
+ /** Every packed binary, eligible or not, with its failing conditions. */
446
+ readonly verdicts: readonly EligibilityVerdict[];
447
+ /** True count of binaries in the payload — never reduced by eligibility. */
448
+ readonly observedCount: number;
449
+ /** True byte total of those binaries. */
450
+ readonly observedBytes: number;
451
+ /** Entries that passed the predicate. Compliance is observed <= allowed. */
452
+ readonly allowedCount: number;
453
+ /** Byte total of the allowed entries. */
454
+ readonly allowedBytes: number;
455
+ }
456
+
457
+ export function evaluateBinaryEligibility(
458
+ files: readonly PackFile[],
459
+ root = REPO_ROOT,
460
+ probeAll: (paths: readonly string[]) => Map<string, GrammarProbe | null> = probeGrammars,
461
+ ): BinaryEligibilityReport {
462
+ const manifest = loadManifest(root);
463
+ const packedPaths = new Set(files.map((f) => f.path));
464
+ const binaries = files.filter((f) => classifyEntry(f.path, readHead(f.path, root)) === 'binary');
465
+ const probes = probeAll(binaries.map((b) => b.path));
466
+
467
+ const verdicts: EligibilityVerdict[] = [];
468
+ for (const b of binaries) {
469
+ let bytes: Uint8Array | null = null;
470
+ try {
471
+ bytes = fs.readFileSync(path.join(root, b.path));
472
+ } catch {
473
+ bytes = null;
474
+ }
475
+ verdicts.push(
476
+ evaluateEntry({
477
+ filePath: b.path,
478
+ packedPaths,
479
+ manifest,
480
+ bytes,
481
+ probe: probes.get(b.path) ?? null,
482
+ }),
483
+ );
484
+ }
485
+ const eligible = new Set(verdicts.filter((v) => v.eligible).map((v) => v.path));
486
+ const sizeOf = new Map(files.map((f) => [f.path, f.size]));
487
+ return {
488
+ eligible,
489
+ verdicts,
490
+ observedCount: binaries.length,
491
+ observedBytes: binaries.reduce((n, b) => n + b.size, 0),
492
+ allowedCount: eligible.size,
493
+ allowedBytes: [...eligible].reduce((n, p) => n + (sizeOf.get(p) ?? 0), 0),
494
+ };
495
+ }
496
+
497
+ /** Run the production loader over `paths` in one child process. */
498
+ function probeGrammars(paths: readonly string[]): Map<string, GrammarProbe | null> {
499
+ const out = new Map<string, GrammarProbe | null>();
500
+ for (const p of paths) out.set(p, null);
501
+ if (paths.length === 0) return out;
502
+ const script = path.join(REPO_ROOT, 'src/scripts/code_graph/grammar_probe.ts');
503
+ const res = spawnSync('npx', ['tsx', script, ...paths.map((p) => path.join(REPO_ROOT, p))], {
504
+ cwd: REPO_ROOT,
505
+ encoding: 'utf-8',
506
+ });
507
+ if (res.status !== 0 || !res.stdout.trim()) return out;
508
+ try {
509
+ const rows = JSON.parse(res.stdout.trim()) as Array<{
510
+ path: string;
511
+ ok: boolean;
512
+ abi?: number;
513
+ grammar_id?: string;
514
+ }>;
515
+ for (const r of rows) {
516
+ const rel = path.relative(REPO_ROOT, r.path);
517
+ if (r.ok && typeof r.abi === 'number' && typeof r.grammar_id === 'string') {
518
+ out.set(rel, { abi: r.abi, grammar_id: r.grammar_id });
519
+ }
520
+ }
521
+ } catch {
522
+ /* an unparseable probe leaves every entry null, which fails 8 and 9 */
523
+ }
524
+ return out;
525
+ }
526
+
527
+ /** The always-printed binary line: the measurement, then the allowance. */
528
+ export function binaryLine(r: BinaryEligibilityReport): string {
529
+ return (
530
+ `binary: observed ${String(r.observedCount)} (${r.observedBytes.toLocaleString('en-US')} B) · ` +
531
+ `allowed ${String(r.allowedCount)} (${r.allowedBytes.toLocaleString('en-US')} B)`
532
+ );
533
+ }
534
+
535
+ /**
536
+ * Compliance is `observed <= allowed`.
537
+ *
538
+ * Stated as its own check rather than inferred from the unaccounted list,
539
+ * because the two can only ever agree by construction and a gate that relies on
540
+ * that is one refactor away from silently accepting an excess.
541
+ */
542
+ export function binaryComplianceViolations(r: BinaryEligibilityReport): string[] {
543
+ if (r.observedCount <= r.allowedCount) return [];
544
+ return [
545
+ `binary payload: observed ${String(r.observedCount)} entries but only ${String(r.allowedCount)} are ` +
546
+ 'allowed by src/config/packed-binary-manifest.json under the eligibility predicate',
547
+ ];
548
+ }
549
+
403
550
  /** One line naming every type class and its count. */
404
551
  export function payloadTypeLine(c: TypeClassification): string {
405
552
  const parts = [...PAYLOAD_TYPES, 'unreadable' as const]
@@ -413,11 +560,17 @@ export function typeClassViolations(c: TypeClassification): string[] {
413
560
  return c.unbound.map(
414
561
  (u) =>
415
562
  `payload type \`${u.type}\`: ${u.path} (${String(u.size)} bytes) is not accounted for. ` +
416
- (u.type === 'archive' || u.type === 'binary'
417
- ? 'This class is meant to be empty. '
418
- : 'Every dotfile and extensionless entry is carried by a path-and-size-bound exception. ') +
419
- `If it belongs in the published surface, add a BoundPayloadException with ` +
420
- `fingerprint sha256:${u.fingerprint} and a reason; otherwise exclude it from package.json files[].`,
563
+ (u.type === 'archive'
564
+ ? 'Archive is a closed class with no admission path of any kind. ' +
565
+ 'Exclude it from package.json files[].'
566
+ : u.type === 'binary'
567
+ ? 'A binary ships only via src/config/packed-binary-manifest.json, and only when every ' +
568
+ 'condition of the eligibility predicate holds on the packed bytes ' +
569
+ '(_lib/packed_binary_predicate.ts). Add or correct its manifest entry, or exclude it ' +
570
+ 'from package.json files[]. The failing conditions are listed above.'
571
+ : 'Every dotfile and extensionless entry is carried by a path-and-size-bound exception. ' +
572
+ `If it belongs in the published surface, add a BoundPayloadException with ` +
573
+ `fingerprint sha256:${u.fingerprint} and a reason; otherwise exclude it from package.json files[].`),
421
574
  );
422
575
  }
423
576
 
@@ -664,6 +817,81 @@ function selfTest(): number {
664
817
  ? 1
665
818
  : 0,
666
819
  },
820
+ {
821
+ // THE PROSE-PATH SENSITIVITY PROOF. `LICENSE` at 1064 bytes
822
+ // carries a real bound exception, so under the pre-2026-09-07
823
+ // code this entry passed. It is fed binary head bytes here, and
824
+ // binary outranks no-extension in `classifyEntry`, so a
825
+ // prose-exceptable `binary` class would accept it. It must not.
826
+ //
827
+ // STILL LOAD-BEARING after the 2026-09-08 reopening, and more
828
+ // so: `binary` is admissible again, but ONLY through the
829
+ // mechanically verified manifest. This row is what goes red if
830
+ // anyone ever wires the prose table back into the class.
831
+ name: 'type/binary: a manifest-eligible binary IS admitted',
832
+ expect: 'accept',
833
+ // THE OPEN-PATH ACCEPTING CONTROL. The council's own argument
834
+ // against the closed class was that a control nobody saw fire
835
+ // is a claim; the same holds in reverse for a path nobody saw
836
+ // accept. Without this row every binary assertion here is a
837
+ // rejection, and a predicate that only ever refuses is
838
+ // indistinguishable from the hard zero it replaced.
839
+ run: () =>
840
+ typeClassViolations(
841
+ classifyPayloadTypes(
842
+ [{ path: 'src/vendor/grammars/tree-sitter-php.wasm', size: 812594 }],
843
+ REPO_ROOT,
844
+ () => Uint8Array.from([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]),
845
+ new Set(['src/vendor/grammars/tree-sitter-php.wasm']),
846
+ ),
847
+ ).length > 0
848
+ ? 1
849
+ : 0,
850
+ },
851
+ {
852
+ name: 'type/binary: the SAME path is refused when eligibility did not pass',
853
+ expect: 'reject',
854
+ // The paired negative. Same path, same bytes, empty eligible
855
+ // set — so the admission demonstrably comes from the predicate
856
+ // and not from the path looking familiar.
857
+ run: () =>
858
+ typeClassViolations(
859
+ classifyPayloadTypes(
860
+ [{ path: 'src/vendor/grammars/tree-sitter-php.wasm', size: 812594 }],
861
+ REPO_ROOT,
862
+ () => Uint8Array.from([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]),
863
+ new Set<string>(),
864
+ ),
865
+ ).length > 0
866
+ ? 1
867
+ : 0,
868
+ },
869
+ {
870
+ name: 'type/binary: an entry with a VALID bound exception is still refused once it reads binary',
871
+ expect: 'reject',
872
+ run: () =>
873
+ typeClassViolations(
874
+ classifyPayloadTypes([{ path: 'LICENSE', size: 1064 }], REPO_ROOT, () =>
875
+ Uint8Array.from([0x7f, 0x45, 0x4c, 0x46, 0x02, 0x00, 0x00]),
876
+ ),
877
+ ).length > 0
878
+ ? 1
879
+ : 0,
880
+ },
881
+ {
882
+ // The negative control for the row above: the SAME exceptable
883
+ // path with ordinary text still passes on its exception.
884
+ name: 'type/extensionless: the same bound entry with text content still passes',
885
+ expect: 'accept',
886
+ run: () =>
887
+ typeClassViolations(
888
+ classifyPayloadTypes([{ path: 'LICENSE', size: 1064 }], REPO_ROOT, () =>
889
+ new TextEncoder().encode('Apache License\n'),
890
+ ),
891
+ ).length > 0
892
+ ? 1
893
+ : 0,
894
+ },
667
895
  {
668
896
  name: 'type/extensionless: an unbound extensionless entry is refused',
669
897
  expect: 'reject',
@@ -781,11 +1009,15 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
781
1009
  // still knows the population it was red over.
782
1010
  process.stdout.write(`scanned: ${String(pack.files.length)}\n`);
783
1011
 
784
- const types = classifyPayloadTypes(pack.files);
1012
+ // Binary eligibility, decided on the PACKED bytes before classification so
1013
+ // the classifier is told which paths passed and never decides itself.
1014
+ const binaryReport = evaluateBinaryEligibility(pack.files);
1015
+ const types = classifyPayloadTypes(pack.files, REPO_ROOT, (p) => readHead(p, REPO_ROOT), binaryReport.eligible);
785
1016
  const errors = [
786
1017
  ...evaluate(budget, pack),
787
1018
  ...classifyPayload(pack.files),
788
1019
  ...typeClassViolations(types),
1020
+ ...binaryComplianceViolations(binaryReport),
789
1021
  ];
790
1022
  const { perSkill, total } = skillBytes(pack.files);
791
1023
  if (argv.includes('--json')) {
@@ -798,6 +1030,13 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
798
1030
  // what a reader needs to judge a violation: "one unaccounted binary" reads
799
1031
  // very differently against a payload of 3 binaries than against one of 0.
800
1032
  process.stdout.write(` payload types: ${payloadTypeLine(types)}\n`);
1033
+ // The observed binary count and byte total on EVERY run, green or red.
1034
+ // Eligibility must never move a measurement, so both totals are printed
1035
+ // beside the allowance rather than folded into it.
1036
+ process.stdout.write(` ${binaryLine(binaryReport)}\n`);
1037
+ for (const v of binaryReport.verdicts.filter((x) => !x.eligible)) {
1038
+ for (const f of v.failures) process.stdout.write(` ${v.path}: ${f}\n`);
1039
+ }
801
1040
  for (const unused of types.unusedExceptions) {
802
1041
  // Not an error: an unbuilt tree legitimately packs fewer files. Named
803
1042
  // so a stale exception is visible rather than accumulating unread.
@@ -18,7 +18,24 @@
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
- * Exit codes: 0 within budget · 1 over budget · 2 misuse / unreadable budget.
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`.
30
+ *
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.
36
+ *
37
+ * Exit codes: 0 within budget · 1 over budget or the bound rose · 2 misuse /
38
+ * unreadable budget.
22
39
  */
23
40
  import { execFileSync } from 'node:child_process';
24
41
  import * as fs from 'node:fs';
@@ -29,6 +46,13 @@ import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
29
46
  import { censusClaudeMdHierarchy, censusRuleDir, censusSkillsCatalog } from './preamble_byte_census.js';
30
47
  import { PREFIX_STABLE_SURFACES, prefixStableRoots } from './_lib/prefix_stable_surfaces.js';
31
48
  import { attributeGrowth, buildLedger, renderAttribution } from './_lib/asset_delivery_ledger.js';
49
+ import { resolveBaseRef } from './_lib/ratchet_base_ref.js';
50
+ import {
51
+ assertBoundsDidNotRise,
52
+ type BoundsRatchetVerdict,
53
+ type GitRunner,
54
+ realGit,
55
+ } from './_lib/standing_bound_ratchet.js';
32
56
 
33
57
  const HERE = path.dirname(new URL(import.meta.url).pathname);
34
58
  const REPO_ROOT = path.resolve(HERE, '..', '..');
@@ -38,6 +62,15 @@ interface Budget {
38
62
  baseline_tokens: number;
39
63
  headroom_pct: number;
40
64
  target_tokens: { median: number; p95: number };
65
+ /**
66
+ * `ci_delivery.grace_ceiling`, or `null` when the file carries none.
67
+ *
68
+ * Read here only so the shrink-only ratchet has a head-side value on a local
69
+ * run that passes no `--ceiling`. The number the payload is COMPARED against
70
+ * still arrives through the flag, so this does not become a second home for
71
+ * the ceiling.
72
+ */
73
+ grace_ceiling: number | null;
41
74
  }
42
75
 
43
76
  export interface BudgetVerdict {
@@ -60,10 +93,13 @@ export function readBudget(file: string = BUDGET_FILE): Budget {
60
93
  throw new Error(`${file}: baseline_tokens and headroom_pct must both be numbers`);
61
94
  }
62
95
  const target = (raw['target_tokens'] ?? {}) as Record<string, unknown>;
96
+ const delivery = (raw['ci_delivery'] ?? {}) as Record<string, unknown>;
97
+ const grace = Number(delivery['grace_ceiling']);
63
98
  return {
64
99
  baseline_tokens: baseline,
65
100
  headroom_pct: headroom,
66
101
  target_tokens: { median: Number(target['median']), p95: Number(target['p95']) },
102
+ grace_ceiling: Number.isFinite(grace) ? grace : null,
67
103
  };
68
104
  }
69
105
 
@@ -142,6 +178,47 @@ export function evaluate(repoRoot?: string, budgetFile?: string, overrideCeiling
142
178
  return { measured, baseline: budget.baseline_tokens, ceiling, withinBudget: measured <= ceiling, buckets };
143
179
  }
144
180
 
181
+ /**
182
+ * The measurement plus the shrink-only bound check.
183
+ *
184
+ * `evaluate` above answers "how big is the tree" and touches no ref;
185
+ * this answers "may this tree ship" and needs the base one. They stay separate
186
+ * because a census that silently depended on a remote ref would behave
187
+ * differently in a shallow clone, and the census is the half other callers use.
188
+ */
189
+ export interface Decision {
190
+ verdict: BudgetVerdict;
191
+ bounds: BoundsRatchetVerdict;
192
+ /** False when the payload is over the ceiling, or the ceiling itself rose. */
193
+ ok: boolean;
194
+ }
195
+
196
+ export interface DecideOptions {
197
+ repoRoot?: string;
198
+ budgetFile?: string;
199
+ overrideCeiling?: number;
200
+ env?: NodeJS.ProcessEnv;
201
+ git?: GitRunner;
202
+ /** Test seam: pin the base ref instead of resolving it. `null` = none resolved. */
203
+ baseRef?: string | null;
204
+ }
205
+
206
+ export function decide(opts: DecideOptions = {}): Decision {
207
+ const repoRoot = opts.repoRoot ?? REPO_ROOT;
208
+ const git = opts.git ?? realGit;
209
+ const budget = readBudget(opts.budgetFile);
210
+ const verdict = evaluate(repoRoot, opts.budgetFile, opts.overrideCeiling);
211
+ const baseRef =
212
+ opts.baseRef !== undefined ? opts.baseRef : resolveBaseRef(repoRoot, opts.env ?? process.env, git);
213
+ const bounds = assertBoundsDidNotRise({
214
+ repoRoot,
215
+ baseRef,
216
+ git,
217
+ headGraceCeiling: opts.overrideCeiling ?? budget.grace_ceiling ?? 0,
218
+ });
219
+ return { verdict, bounds, ok: bounds.ok && verdict.withinBudget };
220
+ }
221
+
145
222
  /**
146
223
  * Per-asset attribution for a refusal, against the merge-base tree.
147
224
  *
@@ -203,6 +280,26 @@ function attributeGrowthAgainstBase(): string[] | null {
203
280
  }
204
281
  }
205
282
 
283
+ /**
284
+ * The bound check, rendered for a human — on BOTH paths, green and red.
285
+ *
286
+ * A check that only speaks when it fails is a check nobody audits until it is
287
+ * already load-bearing. Printing the compared ref and the earlier bound on the
288
+ * passing path is also the only way a reader can tell "verified and unchanged"
289
+ * from "skipped because no base ref resolved", which are different facts.
290
+ */
291
+ function renderBounds(b: BoundsRatchetVerdict): string {
292
+ if (b.note !== null) {
293
+ return ` ${'grace ceiling ratchet'.padEnd(38)} ${'SKIPPED'.padStart(8)} — ${b.note}\n`;
294
+ }
295
+ const base = b.baseGraceCeiling === null ? 'n/a' : String(b.baseGraceCeiling);
296
+ const state = b.ok ? 'ok' : 'ROSE';
297
+ return (
298
+ ` ${'grace ceiling ratchet'.padEnd(38)} ${state.padStart(8)} — ` +
299
+ `${base} at ${b.baseRef ?? 'n/a'}, shrink-only (ADR-264)\n`
300
+ );
301
+ }
302
+
206
303
  export function main(argv: string[] = process.argv.slice(2)): number {
207
304
  const json = argv.includes('--format=json') || argv.includes('--json');
208
305
  // `--ceiling <n>`: the grace ceiling the CI step reads out of
@@ -211,13 +308,14 @@ export function main(argv: string[] = process.argv.slice(2)): number {
211
308
  // tighter-than-design value is IGNORED rather than honoured — see `evaluate`.
212
309
  const ci = argv.indexOf('--ceiling');
213
310
  const override = ci !== -1 && argv[ci + 1] !== undefined ? Number(argv[ci + 1]) : undefined;
214
- let verdict: BudgetVerdict;
311
+ let decision: Decision;
215
312
  try {
216
- verdict = evaluate(undefined, undefined, override);
313
+ decision = decide(override === undefined ? {} : { overrideCeiling: override });
217
314
  } catch (err) {
218
315
  process.stderr.write(`❌ preamble-payload budget: ${(err as Error).message}\n`);
219
316
  return 2;
220
317
  }
318
+ const verdict = decision.verdict;
221
319
 
222
320
  // A ratchet over a measurement of nothing always passes: move
223
321
  // `dist/agent-src/` and every census returns zero, which is trivially under
@@ -239,8 +337,24 @@ export function main(argv: string[] = process.argv.slice(2)): number {
239
337
  }
240
338
 
241
339
  if (json) {
242
- process.stdout.write(JSON.stringify(verdict, null, 2) + '\n');
243
- return verdict.withinBudget ? 0 : 1;
340
+ process.stdout.write(
341
+ JSON.stringify(
342
+ {
343
+ ...verdict,
344
+ ok: decision.ok,
345
+ bound_ratchet: {
346
+ ok: decision.bounds.ok,
347
+ base_ref: decision.bounds.baseRef,
348
+ base_grace_ceiling: decision.bounds.baseGraceCeiling,
349
+ note: decision.bounds.note,
350
+ violations: decision.bounds.violations,
351
+ },
352
+ },
353
+ null,
354
+ 2,
355
+ ) + '\n',
356
+ );
357
+ return decision.ok ? 0 : 1;
244
358
  }
245
359
 
246
360
  for (const b of verdict.buckets) {
@@ -252,6 +366,20 @@ export function main(argv: string[] = process.argv.slice(2)): number {
252
366
  ` ${'measured total'.padEnd(38)} ${String(verdict.measured).padStart(8)} tok ` +
253
367
  `(baseline ${verdict.baseline}, ${sign}${delta}; ceiling ${verdict.ceiling})\n`,
254
368
  );
369
+ process.stdout.write(renderBounds(decision.bounds));
370
+
371
+ // The bound is checked BEFORE the size question and independently of it. A
372
+ // change that lifts its own ceiling has already defeated the gate, and
373
+ // reporting that as "within budget" would be the config-weakening move
374
+ // wearing a green checkmark.
375
+ if (!decision.bounds.ok) {
376
+ process.stderr.write(
377
+ '❌ the standing-payload ceiling rose in this change:\n' +
378
+ decision.bounds.violations.map((v) => ` · ${v}\n`).join(''),
379
+ );
380
+ return 1;
381
+ }
382
+
255
383
  if (!verdict.withinBudget) {
256
384
  // road-to-delivered-cost-truth 2.2 — a gate names its own "no". The
257
385
  // ceiling message alone states a fact and leaves the reader to find the
@@ -268,8 +396,9 @@ export function main(argv: string[] = process.argv.slice(2)): number {
268
396
  process.stderr.write(
269
397
  `❌ per-spawn preamble payload grew past the ratchet: ${verdict.measured} > ${verdict.ceiling} tok.\n` +
270
398
  ` Every rule and skill description here is re-written on EVERY subagent spawn, so growth\n` +
271
- ` is paid per spawn, not once. Either shrink the addition, or raise baseline_tokens in\n` +
272
- ` src/config/preamble-payload-budget.json with the reason in the same commit.\n`,
399
+ ` is paid per spawn, not once. Shrink the addition, or migrate the prose out of the\n` +
400
+ ` standing rule. The grace ceiling may NOT be raised to fit it: ADR-264, enforced by\n` +
401
+ ` the shrink-only ratchet above.\n`,
273
402
  );
274
403
  // Three distinct states, and conflating the last two is a diagnostic
275
404
  // defect rather than a cosmetic one: "I could not look" and "I looked
@@ -19,6 +19,16 @@
19
19
  * `skipped/`, template/README/open-questions) MUST:
20
20
  * 1. Be parseable by the dashboard's `PHASE_RE` (>= one `## Phase <id>`).
21
21
  * 2. Have at least one trackable checkbox under every parsed phase.
22
+ * 3. Not declare a RETIRED `status:` value.
23
+ *
24
+ * `status: draft` is the one exemption. It used to have a second, `carrier`,
25
+ * which ADR-262 deleted: a `status:` value never again decides whether an
26
+ * autonomous run may act on a roadmap, so a receiver of deferred work is now
27
+ * ordinary estate and passes normal trackability. A surviving `status: carrier`
28
+ * is REJECTED here with the migration diagnostic rather than silently ignored —
29
+ * deleting a vocabulary term quietly would leave old files wearing a value
30
+ * nothing reads, and the next author would infer a meaning from its presence.
31
+ * The retired set and its diagnostic live in `_lib/retired_status.ts`.
22
32
  *
23
33
  * Exit codes:
24
34
  * 0 — every active roadmap has parseable phases with >= 1 checkbox per phase.
@@ -31,6 +41,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
31
41
 
32
42
  import { checkRatchet } from './_lib/gate_baseline.js';
33
43
  import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
44
+ import { declaresRetiredStatus, retiredStatusDiagnostic } from './_lib/retired_status.js';
34
45
 
35
46
  const QUIET = process.argv.slice(2).includes('--quiet');
36
47
 
@@ -49,15 +60,6 @@ const PHASE_RE =
49
60
  // FRONTMATTER_RE = re.compile(r"\A---\n(.*?)\n---\s*\n", re.DOTALL)
50
61
  const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\s*\n/;
51
62
  const DRAFT_VALUES: ReadonlySet<string> = new Set(['draft']);
52
- /**
53
- * A carrier holds obligations deferred out of an archived parent. It is not
54
- * schedulable work — every item in it has an unmet resumption trigger — and it
55
- * has no `## Phase` headings by construction, so it is skipped exactly where a
56
- * draft is. It differs from a draft in what protects it: a draft may be deleted
57
- * freely, a carrier may not, and `lint_carrier_integrity` reds when one loses
58
- * its receiver.
59
- */
60
- const CARRIER_VALUES: ReadonlySet<string> = new Set(['carrier']);
61
63
 
62
64
  const EXCLUDE_NAMES: ReadonlySet<string> = new Set([
63
65
  'template.md',
@@ -101,11 +103,6 @@ function is_draft(fm: Map<string, string>): boolean {
101
103
  return DRAFT_VALUES.has((fm.get('status') ?? '').toLowerCase());
102
104
  }
103
105
 
104
- /** True for a `status: carrier` roadmap — see CARRIER_VALUES. */
105
- function is_carrier(fm: Map<string, string>): boolean {
106
- return CARRIER_VALUES.has((fm.get('status') ?? '').toLowerCase());
107
- }
108
-
109
106
  /** Mirror update_roadmap_progress.is_roadmap_candidate (POSIX path parts). */
110
107
  function is_roadmap_candidate(p: string): boolean {
111
108
  const name = path.basename(p);
@@ -182,7 +179,7 @@ function find_active_roadmaps(root: string): string[] {
182
179
  continue;
183
180
  }
184
181
  const text = fs.readFileSync(p, 'utf-8');
185
- if (is_draft(parse_frontmatter(text)) || is_carrier(parse_frontmatter(text))) {
182
+ if (is_draft(parse_frontmatter(text))) {
186
183
  continue;
187
184
  }
188
185
  out.push(p);
@@ -193,6 +190,13 @@ function find_active_roadmaps(root: string): string[] {
193
190
  /** Return human-readable violation strings for a single roadmap. */
194
191
  function violations_for(p: string): string[] {
195
192
  const text = fs.readFileSync(p, 'utf-8');
193
+ const retired = declaresRetiredStatus(text);
194
+ if (retired !== null) {
195
+ // Returned alone: a file wearing a deleted status has a migration to do
196
+ // before its phase structure is worth reporting on, and stacking phase
197
+ // findings on top would bury the one finding that has to be read first.
198
+ return [`${p}: ${retiredStatusDiagnostic(retired)}`];
199
+ }
196
200
  PHASE_RE.lastIndex = 0;
197
201
  const matches: Array<{ start: number; end: number; id: string; name: string | undefined }> =
198
202
  [];
@@ -359,7 +363,6 @@ export {
359
363
  CHECKBOX_RE,
360
364
  PHASE_RE,
361
365
  is_draft,
362
- is_carrier,
363
366
  is_roadmap_candidate,
364
367
  parse_frontmatter,
365
368
  find_active_roadmaps,