@event4u/agent-config 14.21.0 → 14.22.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 (181) 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 +198 -0
  5. package/CREDITS.md +37 -0
  6. package/dist/agent-src/commands/analyze/inbox.md +380 -5
  7. package/dist/agent-src/commands/analyze/repo.md +4 -2
  8. package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +69 -0
  9. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +5 -5
  10. package/dist/agent-src/rules/external-code-graph-interop.md +19 -14
  11. package/dist/agent-src/rules/token-efficiency.md +26 -3
  12. package/dist/agent-src/scripts/_cli_entry.ts +51 -0
  13. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +14 -23
  14. package/dist/agent-src/scripts/roadmap_gates.ts +4 -20
  15. package/dist/agent-src/scripts/roadmap_set_step.ts +5 -2
  16. package/dist/agent-src/scripts/stubs_due.ts +4 -18
  17. package/dist/agent-src/scripts/update_roadmap_progress.ts +7 -24
  18. package/dist/agent-src/skills/code-intelligence/SKILL.md +25 -0
  19. package/dist/agent-src/skills/roadmap-writing/SKILL.md +17 -0
  20. package/dist/agent-src/templates/agent-settings.md +0 -1
  21. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  22. package/dist/cli-delegate/_cli_entry.js +8 -0
  23. package/dist/cli-delegate/_lib/pin_resolver.js +170 -0
  24. package/dist/cli-delegate/archival_sweep.js +8 -0
  25. package/dist/cli-delegate/archive_completed_roadmaps.js +494 -0
  26. package/dist/cli-delegate/check_update_banner.js +923 -0
  27. package/dist/cli-delegate/chunk-5EK5WQN6.js +28 -0
  28. package/dist/cli-delegate/chunk-7XGD2OEU.js +96 -0
  29. package/dist/cli-delegate/{chunk-643Z7FIC.js → chunk-BIXEDQPZ.js} +6 -0
  30. package/dist/cli-delegate/chunk-DBH4AHDK.js +155 -0
  31. package/dist/cli-delegate/chunk-JO25G5H7.js +20 -0
  32. package/dist/cli-delegate/chunk-LTYVIY76.js +7961 -0
  33. package/dist/cli-delegate/chunk-QAJS5YWY.js +999 -0
  34. package/dist/cli-delegate/{chunk-ZVBOHSVF.js → chunk-TGIPZBCW.js} +1 -1
  35. package/dist/cli-delegate/{chunk-YS6XTLEK.js → chunk-TYSY33MD.js} +56 -6
  36. package/dist/cli-delegate/chunk-ULET4BYS.js +328 -0
  37. package/dist/cli-delegate/chunk-YDQSHTD7.js +187 -0
  38. package/dist/cli-delegate/chunk-YWVXLR44.js +48 -0
  39. package/dist/cli-delegate/chunk-ZZQBUHFK.js +273 -0
  40. package/dist/cli-delegate/cmd_conformance.js +2 -2
  41. package/dist/cli-delegate/cmd_export.js +1 -1
  42. package/dist/cli-delegate/cmd_handoff.js +26 -2
  43. package/dist/cli-delegate/cmd_prune.js +1 -1
  44. package/dist/cli-delegate/cmd_session_recycle.js +2 -2
  45. package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
  46. package/dist/cli-delegate/cmd_sync.js +1 -1
  47. package/dist/cli-delegate/cmd_uninstall.js +1 -1
  48. package/dist/cli-delegate/cmd_validate.js +1 -1
  49. package/dist/cli-delegate/dashboard_mode.js +16 -0
  50. package/dist/cli-delegate/gate_budget.js +24 -0
  51. package/dist/cli-delegate/gate_execute.js +25 -0
  52. package/dist/cli-delegate/guarded_baseline.js +18 -0
  53. package/dist/cli-delegate/resume_probe.js +22 -0
  54. package/dist/cli-delegate/roadmap_gates.js +638 -0
  55. package/dist/cli-delegate/roadmap_set_step.js +173 -0
  56. package/dist/cli-delegate/stub_queue.js +47 -0
  57. package/dist/cli-delegate/stubs_due.js +38 -0
  58. package/dist/cli-delegate/ticket_bundles.js +59 -0
  59. package/dist/cli-delegate/update_roadmap_progress.js +60 -0
  60. package/dist/discovery/deprecation-report.md +1 -1
  61. package/dist/discovery/discovery-manifest.json +8 -8
  62. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  63. package/dist/discovery/discovery-manifest.summary.md +1 -1
  64. package/dist/discovery/dormancy-report.md +1 -1
  65. package/dist/discovery/orphan-report.md +1 -1
  66. package/dist/discovery/packs.json +2 -2
  67. package/dist/discovery/trust-report.md +1 -1
  68. package/dist/discovery/workspaces.json +2 -2
  69. package/dist/hooks/dispatch.js +1193 -917
  70. package/dist/install/claudeLayerCarriage.js +237 -0
  71. package/dist/install/claudePathsPlan.js +79 -0
  72. package/dist/install/hostLayerFingerprint.js +1 -1
  73. package/dist/install/install.mjs +6 -0
  74. package/dist/install/partitionEligibility.js +231 -117
  75. package/dist/install/ruleLayerPartition.js +10 -7
  76. package/dist/mcp/registry-manifest.json +2 -2
  77. package/dist/mcp/server.json +2 -2
  78. package/dist/mcp/server.mjs +39219 -38995
  79. package/dist/scripts/condense.js +32 -25
  80. package/docs/MIGRATION.md +49 -1
  81. package/docs/contracts/settings-classes.md +1 -1
  82. package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +73 -0
  83. package/package.json +7 -4
  84. package/src/agent-src/scripts/_cli_entry.ts +51 -0
  85. package/src/agent-src/scripts/archive_completed_roadmaps.ts +14 -23
  86. package/src/agent-src/scripts/roadmap_gates.ts +4 -20
  87. package/src/agent-src/scripts/roadmap_set_step.ts +5 -2
  88. package/src/agent-src/scripts/stubs_due.ts +4 -18
  89. package/src/agent-src/scripts/update_roadmap_progress.ts +7 -24
  90. package/src/config/continuity-surface.json +509 -0
  91. package/src/config/estate-count-budget.json +1 -1
  92. package/src/config/gate-coverage.yml +29 -8
  93. package/src/config/gate-violation-baselines.json +8 -8
  94. package/src/config/gitignore-block.txt +19 -0
  95. package/src/config/host-injection-effect.json +73 -0
  96. package/src/config/packed-binary-manifest.json +33 -0
  97. package/src/config/preamble-payload-budget.json +112 -105
  98. package/src/config/publish-surface.json +4 -2
  99. package/src/install/claudeLayerCarriage.ts +273 -0
  100. package/src/install/claudePathsPlan.ts +76 -0
  101. package/src/install/hostLayerFingerprint.ts +1 -1
  102. package/src/install/partitionEligibility.ts +261 -127
  103. package/src/install/ruleLayerPartition.ts +19 -23
  104. package/src/scripts/_dispatch.bash +77 -27
  105. package/src/scripts/_lib/auto_dispatch.ts +2 -2
  106. package/src/scripts/_lib/catalogue_layer_parity.ts +146 -0
  107. package/src/scripts/_lib/continuity_surface.ts +296 -0
  108. package/src/scripts/_lib/corpus_manifest.ts +26 -9
  109. package/src/scripts/_lib/hook_settings.ts +1 -1
  110. package/src/scripts/_lib/injection_effect.ts +151 -0
  111. package/src/scripts/_lib/layer_overlap_notice.ts +50 -6
  112. package/src/scripts/_lib/packed_binary_predicate.ts +225 -0
  113. package/src/scripts/_lib/pin_resolver.ts +13 -0
  114. package/src/scripts/_lib/regression_neighbourhood.ts +1 -1
  115. package/src/scripts/_lib/retired_status.ts +74 -0
  116. package/src/scripts/_lib/skill_catalogue.ts +156 -21
  117. package/src/scripts/_lib/subagent_capsule.ts +117 -8
  118. package/src/scripts/ai_council/api_transport.ts +5 -0
  119. package/src/scripts/ai_council/deliberation_metrics.ts +353 -0
  120. package/src/scripts/ai_council/prompts.ts +9 -0
  121. package/src/scripts/ai_council/synthesis_disagreement.ts +206 -0
  122. package/src/scripts/capture_skill_catalogue.ts +24 -11
  123. package/src/scripts/check_candidate_lines.ts +499 -0
  124. package/src/scripts/check_continuity_surface.ts +475 -0
  125. package/src/scripts/check_dependency_floors.ts +13 -6
  126. package/src/scripts/check_estate_count.ts +40 -79
  127. package/src/scripts/check_pack_size.ts +262 -23
  128. package/src/scripts/check_roadmap_trackable.ts +19 -16
  129. package/src/scripts/check_rule_layer_partition.ts +54 -34
  130. package/src/scripts/check_single_delivery.ts +97 -31
  131. package/src/scripts/check_update_banner.ts +13 -0
  132. package/src/scripts/code_graph/build.ts +138 -15
  133. package/src/scripts/code_graph/cli.ts +76 -8
  134. package/src/scripts/code_graph/extract.ts +11 -0
  135. package/src/scripts/code_graph/grammar_probe.ts +43 -0
  136. package/src/scripts/code_graph/loader.ts +88 -14
  137. package/src/scripts/code_graph/query.ts +121 -11
  138. package/src/scripts/code_graph/resolution_tiers.ts +238 -0
  139. package/src/scripts/code_graph/sqlite_store.ts +247 -6
  140. package/src/scripts/code_graph/types.ts +66 -1
  141. package/src/scripts/code_graph/validate.ts +23 -0
  142. package/src/scripts/condense.ts +34 -22
  143. package/src/scripts/corpus_manifest.ts +5 -5
  144. package/src/scripts/council_cli.ts +11 -12
  145. package/src/scripts/count_intent_disagreement.ts +426 -0
  146. package/src/scripts/handoff_context_hook.ts +79 -0
  147. package/src/scripts/hook_effect_doctor.ts +82 -1
  148. package/src/scripts/hook_manifest.json +1 -1
  149. package/src/scripts/hook_manifest.yaml +64 -12
  150. package/src/scripts/hooks/chain_nudge_hook.ts +282 -0
  151. package/src/scripts/hooks/code_graph_context_hook.ts +212 -0
  152. package/src/scripts/hooks/concern_registry.ts +4 -2
  153. package/src/scripts/hooks/host_semantics.ts +1 -1
  154. package/src/scripts/hooks/orchestration_record_hook.ts +2 -2
  155. package/src/scripts/hooks/rule_inject_hook.ts +1 -1
  156. package/src/scripts/hooks/skill_route_hook.ts +11 -4
  157. package/src/scripts/inbox_source_census.ts +322 -0
  158. package/src/scripts/install-hooks.sh +82 -0
  159. package/src/scripts/{lint_carrier_integrity.ts → lint_deferral_integrity.ts} +60 -214
  160. package/src/scripts/lint_plan_risk_register.ts +13 -17
  161. package/src/scripts/mcp_server/suggest_skill.ts +72 -0
  162. package/src/scripts/mcp_server/tools.ts +2 -56
  163. package/src/scripts/media/lib/adapter-contract.md +7 -2
  164. package/src/scripts/media/lib/fixtures/flux/asset-0001.png +0 -0
  165. package/src/scripts/media/lib/fixtures/gemini-image/asset-0001.png +0 -0
  166. package/src/scripts/media/lib/fixtures/ideogram/asset-0001.png +0 -0
  167. package/src/scripts/rdp_candidates_delta.ts +276 -0
  168. package/src/scripts/rdp_quality_eval.ts +217 -19
  169. package/src/scripts/release_mix_taxonomy.json +3 -2
  170. package/src/scripts/report_host_injection_effect.ts +189 -0
  171. package/src/scripts/report_layer_overlap.ts +41 -0
  172. package/src/scripts/report_skill_menu_census.ts +341 -0
  173. package/src/scripts/skill_tools/score_skill_relevance.ts +70 -6
  174. package/src/scripts/skill_tools/suggest_skill_for_task.ts +37 -3
  175. package/src/vendor/grammars/README.md +61 -0
  176. package/src/vendor/grammars/tree-sitter-javascript.wasm +0 -0
  177. package/src/vendor/grammars/tree-sitter-php.wasm +0 -0
  178. package/src/vendor/grammars/tree-sitter-typescript.wasm +0 -0
  179. package/src/scripts/_lib/carrier_status.ts +0 -118
  180. package/src/scripts/_lib/estate_carrier_cases.ts +0 -72
  181. package/src/scripts/hooks/code_graph_nudge_hook.ts +0 -191
@@ -155,8 +155,6 @@ import { materialiseSubtree } from './_lib/base_tree.js';
155
155
  import { SKILLS_POSIX, measureSkillEstate } from './_lib/skill_estate.js';
156
156
  import { CONCERN_MANIFEST_POSIX, countConcerns } from './_lib/concern_estate.js';
157
157
  import { runGateCli, runSelfTest, type SelfTestCase } from './_lib/gate_self_test.js';
158
- import { countDeclaredCarriers, declaresCarrier } from './_lib/carrier_status.js';
159
- import { carrierEstateCases } from './_lib/estate_carrier_cases.js';
160
158
 
161
159
  const GATE = 'check_estate_count';
162
160
  const BUDGET_REL = path.join('src', 'config', 'estate-count-budget.json');
@@ -438,38 +436,12 @@ function countIn(roadmapRoot: string, sub: string): number {
438
436
  }
439
437
  }
440
438
 
441
- /**
442
- * Top-level roadmaps `collect()` drops for declaring `status: carrier`, added
443
- * back so that FLIPPING a status is count-neutral.
444
- *
445
- * Measured before this: flipping one file to `carrier` moved `active_roadmaps
446
- * 3 → 2` and the gate printed "estate within its ratchet", because a count
447
- * below the floor is a drawdown and free. The floor is measured at the base
448
- * ref, so once such a flip merges the lowered number becomes the next floor —
449
- * the file cannot be flipped back without paying, and any roadmap can be
450
- * laundered out of the count by adding one word.
451
- *
452
- * A reclassification is not a disposal, so the metric counts the file on both
453
- * sides. Same argument `open_blockers` makes below for spanning `later/`.
454
- * Deleting a carrier is still a shrink and still earns no offset
455
- * (`classifyDiff`); adding one is growth that takes the claim path.
456
- *
457
- * This is therefore the one roadmap metric that does NOT equal the dashboard
458
- * header, stated here rather than left to be tripped over.
459
- */
460
- function countActiveCarriers(roadmapRoot: string): number {
461
- return countDeclaredCarriers(roadmapRoot, isRoadmapCandidate, fs, path.join);
462
- }
463
-
464
439
  /**
465
440
  * Roadmaps parked in `later/` whose blockers count, parsed for them.
466
441
  *
467
- * Skips a draft AND a carrier, which is the same pair `collect()` skips on the
468
- * active side. It used to skip only the draft, so parking a carrier moved its
469
- * blockers INTO the gated count while the identical file at the top level kept
470
- * them out — a ratchet verdict that moved on a relocation, for no stated
471
- * reason. Three of the four status readers learned `carrier` when the status
472
- * shipped; this is the fourth.
442
+ * Skips exactly what `collect()` skips on the active side, through the same
443
+ * `isUnscheduled` predicate. Skipping a different set here would make a ratchet
444
+ * verdict move on a relocation, for no stated reason.
473
445
  */
474
446
  function laterRoadmaps(roadmapRoot: string): Array<{ open_blockers: readonly unknown[] }> {
475
447
  const dir = path.join(roadmapRoot, 'later');
@@ -496,10 +468,13 @@ function laterRoadmaps(roadmapRoot: string): Array<{ open_blockers: readonly unk
496
468
  /**
497
469
  * Count the estate, three ways.
498
470
  *
499
- * `active_roadmaps` is `collect()` PLUS the top-level carriers `collect()`
500
- * drops, so the parser is shared with the dashboard while a status flip stays
501
- * count-neutral — see `countActiveCarriers` for why that beats parity here, and
502
- * for the one number on this gate that the dashboard header does not print.
471
+ * `active_roadmaps` is `collect()`, so the parser is shared with the dashboard
472
+ * and the number equals the dashboard header. It used to carry an add-back for
473
+ * the top-level roadmaps `collect()` dropped for declaring `status: carrier`,
474
+ * which existed only to keep FLIPPING to that status count-neutral — a flip
475
+ * lowered the count, a count below the floor is a free drawdown, and the
476
+ * lowered number then became the next floor. ADR-262 deleted the status, so
477
+ * there is no flip to neutralise and no divergence from the header left.
503
478
  * `later_roadmaps` is a directory listing because parked files are
504
479
  * outside that corpus by design — filtered through the same `is_roadmap_candidate`
505
480
  * predicate as the active side, so `later/README.md` is not a roadmap here either.
@@ -524,7 +499,7 @@ export function countEstate(repoRoot: string): EstateCounts {
524
499
  rows.reduce((n, r) => n + r.open_blockers.length, 0);
525
500
  const skills = measureSkillEstate(repoRoot);
526
501
  return {
527
- active_roadmaps: stats.length + countActiveCarriers(roadmapRoot),
502
+ active_roadmaps: stats.length,
528
503
  later_roadmaps: countIn(roadmapRoot, 'later'),
529
504
  open_blockers: openOf(stats) + openOf(laterRoadmaps(roadmapRoot)),
530
505
  skill_count: skills.skill_count,
@@ -590,9 +565,6 @@ export function exemptionReason(text: string): string | null {
590
565
  return reason === '' ? null : reason;
591
566
  }
592
567
 
593
- /** Re-export; `_lib/carrier_status.ts` holds the one implementation. */
594
- export const isCarrierText = declaresCarrier;
595
-
596
568
  /**
597
569
  * Classify the change's effect on the active roadmap tree.
598
570
  *
@@ -604,13 +576,6 @@ export const isCarrierText = declaresCarrier;
604
576
  export function classifyDiff(
605
577
  nameStatus: string,
606
578
  readFile: (rel: string) => string | null,
607
- /**
608
- * The file's content at the BASE ref, for paths this change removed — a
609
- * deleted carrier cannot be read from the working tree and its status
610
- * decides whether the deletion earns an offset. Defaults to null, which
611
- * reproduces the previous scoring for any caller without a pre-image.
612
- */
613
- readBase: (rel: string) => string | null = () => null,
614
579
  ): OffsetLedger {
615
580
  const added: string[] = [];
616
581
  const offsets: string[] = [];
@@ -624,10 +589,8 @@ export function classifyDiff(
624
589
  const from = cols[1] ?? '';
625
590
  const to = cols[2] ?? '';
626
591
  if (isActiveTopLevel(from) && isDisposed(to)) {
627
- if (!isCarrierText(readBase(from))) {
628
- offsets.push(from);
629
- if (isParked(to)) parked.push(to);
630
- }
592
+ offsets.push(from);
593
+ if (isParked(to)) parked.push(to);
631
594
  } else if (isDisposed(from) && isActiveTopLevel(to)) {
632
595
  added.push(to);
633
596
  }
@@ -638,10 +601,7 @@ export function classifyDiff(
638
601
  if (status === 'A') {
639
602
  added.push(file);
640
603
  } else if (status === 'D') {
641
- // A carrier's removal is never an offset. See `readBase`.
642
- if (!isCarrierText(readBase(file))) {
643
- offsets.push(file);
644
- }
604
+ offsets.push(file);
645
605
  }
646
606
  }
647
607
  for (const file of added) {
@@ -832,20 +792,13 @@ export function evaluate(
832
792
  if (!diff.ok) {
833
793
  offsetSkipReason = `git diff against ${baseRef} failed — one-in-one-out not evaluated`;
834
794
  } else {
835
- offsets = classifyDiff(
836
- diff.stdout,
837
- (rel) => {
838
- try {
839
- return fs.readFileSync(path.join(repoRoot, rel), 'utf-8');
840
- } catch {
841
- return null;
842
- }
843
- },
844
- (rel) => {
845
- const show = git(['show', `${baseRef}:${rel}`], repoRoot);
846
- return show.ok ? show.stdout : null;
847
- },
848
- );
795
+ offsets = classifyDiff(diff.stdout, (rel) => {
796
+ try {
797
+ return fs.readFileSync(path.join(repoRoot, rel), 'utf-8');
798
+ } catch {
799
+ return null;
800
+ }
801
+ });
849
802
  }
850
803
  const patch = git(
851
804
  ['diff', '--unified=0', '--find-renames', `${baseRef}...HEAD`, '--', ROADMAPS_REL],
@@ -982,9 +935,6 @@ function selfTest(): number {
982
935
  const roadmap = (name: string, extra = ''): string =>
983
936
  `# Roadmap: ${name}\n\n## Phase 1\n\n- [ ] **1.1** s\n${extra}`;
984
937
 
985
- /** A `status: carrier` roadmap — counted as estate, and never an offset. */
986
- const CARRIER = `---\nstatus: carrier\n---\n# Roadmap: carrier\n\n## Phase 1\n\n- [~] **1.1** s\n`;
987
-
988
938
  /**
989
939
  * A `status: draft` roadmap — invisible to `collect()`, so adding one cannot
990
940
  * move `active_roadmaps`. That is what makes it the fixture for a case that
@@ -1197,14 +1147,25 @@ function selfTest(): number {
1197
1147
  after: (dir) => write(dir, 'agents/roadmaps/later/road-to-parked.md', roadmap('P')),
1198
1148
  }),
1199
1149
  },
1200
- ...carrierEstateCases({
1201
- fixture,
1202
- write,
1203
- remove: (dir, rel) => fs.rmSync(path.join(dir, rel)),
1204
- roadmap,
1205
- CARRIER,
1206
- DRAFT,
1207
- }),
1150
+ {
1151
+ // Survives the deletion of the carrier cases this used to control
1152
+ // for (ADR-262): it asserts the plain rule those cases were the
1153
+ // exception to — a removal offsets an addition — over a DRAFT
1154
+ // addition, which `collect()` cannot see, so only the offset
1155
+ // scoring can decide it.
1156
+ name: 'deleting an ordinary roadmap offsets a new draft → accept',
1157
+ expect: 'accept',
1158
+ run: () =>
1159
+ fixture({
1160
+ roadmaps: 3,
1161
+ base: 'main',
1162
+ before: (dir) => write(dir, 'agents/roadmaps/road-to-spare.md', roadmap('S')),
1163
+ after: (dir) => {
1164
+ fs.rmSync(path.join(dir, 'agents/roadmaps/road-to-spare.md'));
1165
+ write(dir, 'agents/roadmaps/road-to-new.md', DRAFT);
1166
+ },
1167
+ }),
1168
+ },
1208
1169
  {
1209
1170
  name: 'a new open blocker with no claim → reject',
1210
1171
  expect: 'reject',
@@ -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.
@@ -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,