@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
@@ -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',
@@ -0,0 +1,291 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * A host outside `lean_projection.hosts` receives EXACTLY what `eager-all` writes.
4
+ *
5
+ * `road-to-delivery-for-every-host` step 1.4, and the gate behind its AC-2.
6
+ *
7
+ * WHY A SEPARATE GATE FROM `check_rule_projection_integrity`
8
+ * ---------------------------------------------------------
9
+ * That gate reads the trees AS THEY STAND — no regeneration, by design, because
10
+ * its job is to catch a STALE tree and regenerating erases the evidence. Its 1.3
11
+ * host axis therefore asks a content question about the entries that exist:
12
+ * "is any of these a stub in a tree that should hold bodies?"
13
+ *
14
+ * That is necessary and it is not sufficient. It cannot see a projector change
15
+ * that alters a non-delivery host's output in some way OTHER than stubbing it —
16
+ * a different frontmatter rewrite, a dropped rule, an added byte. The AC is
17
+ * byte-identity, and byte-identity can only be established by producing both
18
+ * trees and comparing them.
19
+ *
20
+ * So this gate GENERATES, twice, into a temporary root it owns:
21
+ *
22
+ * 1. `mode: eager-all` → the reference trees
23
+ * 2. `mode: delivery`, the configured `hosts:` → the trees under test
24
+ *
25
+ * and requires every tree whose host is NOT in `hosts` to be byte-identical
26
+ * across the two runs. It never writes into the repository: the fixture root is
27
+ * a `mkdtemp` directory, seeded from `dist/agent-src/rules`, and removed on exit.
28
+ *
29
+ * WHAT IT DELIBERATELY DOES NOT DO
30
+ * --------------------------------
31
+ * It does not assert that the DELIVERY host's tree changed. A flip that thins
32
+ * nothing is a different defect and `check_rule_projection_integrity` plus the
33
+ * unit corpus already cover it; asserting it here would make this gate fail on a
34
+ * legitimate `hosts: []`, which is the safe configuration.
35
+ *
36
+ * AND IT ASSERTS NOTHING WHEN EVERY THINNABLE HOST IS ENROLLED. With
37
+ * `hosts: [claude-code, cursor, cline]` there is no non-delivery tree, so the
38
+ * comparison has no subject. That case now prints a `⚠️ NOTHING COMPARED` line
39
+ * naming the gate that does carry the question, instead of the `✅ … 0 …
40
+ * byte-identical` line it printed until 2026-09-08 (R2 finding 12) — a green
41
+ * sentence for an assertion that was never made.
42
+ *
43
+ * IT ALSO DOES NOT CONSULT `lean_projection.mode`, and the divergence from
44
+ * `check_rule_projection_integrity._lean_projection_settings_for` — which
45
+ * checks the mode FIRST so "a `hosts:` list left behind after a rollback
46
+ * exempts nothing" — is deliberate. Recorded 2026-09-08 because R2 finding 13
47
+ * read it as an oversight, and running the mode-first variant here reddens the
48
+ * gate on this repository's own settings, which carry no `lean_projection`
49
+ * block at all.
50
+ *
51
+ * The two gates ask different questions. The sibling asks a question ABOUT THE
52
+ * CURRENT TREE — "is any entry on disk a stub in a tree with no delivery
53
+ * slot?" — so the live mode is part of its subject. This gate asks a
54
+ * COUNTERFACTUAL ABOUT THE PROJECTOR — "under a thinning mode with the
55
+ * configured hosts, would every other host's tree still be byte-identical?" —
56
+ * and supplies `mode: delivery` to its own test fixture to ask it. Reading the
57
+ * live mode would make the exemption set and the fixture disagree: with the
58
+ * mode off, `configured` would be empty while the fixture still thinned, and
59
+ * every stub in the delivery host's tree would be reported as a parity break.
60
+ *
61
+ * The consequence the finding names is real and is not a hole: after a rollback
62
+ * to `eager-all`, a leftover `hosts: [claude-code]` still excludes
63
+ * `.claude/rules` from THIS comparison. Nothing is hidden by that, because
64
+ * under `eager-all` nothing is thinned and the on-disk state is the sibling
65
+ * gate's subject.
66
+ *
67
+ * It also does not read the repository's own `.claude/rules` etc. Those are
68
+ * gitignored and absent on a fresh CI checkout, and a gate that silently passes
69
+ * when its subject is missing is the shape this repository's scan-scope
70
+ * discipline exists to reject — hence the fixture root, which always exists.
71
+ *
72
+ * Exit: 0 parity holds · 1 a non-delivery host's tree differs · 2 the gate could
73
+ * not run (no rule corpus to seed from).
74
+ */
75
+
76
+ import * as fs from 'node:fs';
77
+ import * as os from 'node:os';
78
+ import * as path from 'node:path';
79
+ import { fileURLToPath, pathToFileURL } from 'node:url';
80
+
81
+ import { GateLedger, UnaccountedTargetsError } from './_lib/gate_ledger.js';
82
+ import { leanProjectionHostsRaw } from './_lib/hook_settings.js';
83
+ import { resolveLeanProjectionHosts } from './_lib/lean_projection_mode.js';
84
+ import { reportScanned } from './_lib/scan_scope.js';
85
+ import {
86
+ _getStateForTest,
87
+ _resetStateForTest as _pointCondenseAt,
88
+ generate_rule_symlinks,
89
+ } from './condense.js';
90
+
91
+ const _HERE = path.resolve(fileURLToPath(import.meta.url));
92
+ export const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
93
+
94
+ /** The tool dirs this gate compares, and the host each belongs to. */
95
+ export const TREES: Readonly<Record<string, string>> = {
96
+ '.claude/rules': 'claude-code',
97
+ '.cursor/rules': 'cursor',
98
+ '.clinerules': 'cline',
99
+ };
100
+
101
+ /** Every entry in `dir` as `name -> content`, reading through symlinks. */
102
+ export function treeContents(root: string, rel: string): Record<string, string> {
103
+ const dir = path.join(root, rel);
104
+ const out: Record<string, string> = {};
105
+ if (!fs.existsSync(dir)) return out;
106
+ for (const name of fs.readdirSync(dir).sort()) {
107
+ if (name === 'README.md') continue;
108
+ try {
109
+ out[name] = fs.readFileSync(path.join(dir, name), 'utf-8');
110
+ } catch {
111
+ out[name] = '<unreadable>';
112
+ }
113
+ }
114
+ return out;
115
+ }
116
+
117
+ /** Copy the rule corpus into a fresh fixture root carrying `settings`. */
118
+ function seedFixture(repoRoot: string, tmp: string, label: string, settings: string): string {
119
+ const root = path.join(tmp, label);
120
+ const dist = path.join(root, 'dist', 'agent-src', 'rules');
121
+ fs.mkdirSync(dist, { recursive: true });
122
+ const src = path.join(repoRoot, 'dist', 'agent-src', 'rules');
123
+ for (const name of fs.readdirSync(src).filter((f) => f.endsWith('.md'))) {
124
+ fs.copyFileSync(path.join(src, name), path.join(dist, name));
125
+ }
126
+ fs.writeFileSync(path.join(root, '.agent-settings.yml'), settings, 'utf-8');
127
+ return root;
128
+ }
129
+
130
+ export interface ParityFinding {
131
+ readonly tree: string;
132
+ readonly host: string;
133
+ readonly rule: string;
134
+ readonly reason: string;
135
+ }
136
+
137
+ /** Compare two generated trees, reporting every difference in a non-delivery host's tree. */
138
+ export function compareTrees(
139
+ eagerRoot: string,
140
+ testRoot: string,
141
+ deliveryHosts: readonly string[],
142
+ ): ParityFinding[] {
143
+ const out: ParityFinding[] = [];
144
+ for (const [tree, host] of Object.entries(TREES)) {
145
+ if (deliveryHosts.includes(host)) continue;
146
+ const a = treeContents(eagerRoot, tree);
147
+ const b = treeContents(testRoot, tree);
148
+ for (const name of new Set([...Object.keys(a), ...Object.keys(b)])) {
149
+ const left = a[name];
150
+ const right = b[name];
151
+ if (left === right) continue;
152
+ const reason =
153
+ left === undefined
154
+ ? 'present under the thinning mode and absent under eager-all'
155
+ : right === undefined
156
+ ? 'written by eager-all and MISSING under the thinning mode'
157
+ : `content differs (${String(left.length)} vs ${String(right.length)} bytes)`;
158
+ out.push({ tree, host, rule: name, reason });
159
+ }
160
+ }
161
+ return out;
162
+ }
163
+
164
+ /**
165
+ * The notice for a comparison with NO SUBJECT, or `null` when there is one.
166
+ *
167
+ * R2 finding 12: with every thinnable host in `lean_projection.hosts` there is
168
+ * no non-delivery tree left, so this gate's assertion is VACUOUS — and it used
169
+ * to say so inside the same `✅ … byte-identical` sentence it prints when the
170
+ * assertion actually held. Zero compared is not a pass; it is an absence of
171
+ * subject, and the reader has to be told which gate carries the question
172
+ * instead.
173
+ *
174
+ * Deliberately NOT an exit-1: enrolling all three hosts is a legal
175
+ * configuration, and reddening a legal configuration is how a gate gets
176
+ * switched off. Pure and exported so the wording is under test without running
177
+ * the two-fixture generation.
178
+ */
179
+ export function parityScopeNotice(configured: readonly string[]): string | null {
180
+ const comparable = Object.entries(TREES).filter(([, h]) => !configured.includes(h));
181
+ if (comparable.length > 0) return null;
182
+ return (
183
+ `⚠️ check_host_tree_parity: NOTHING COMPARED — every thinnable host is in ` +
184
+ `lean_projection.hosts [${configured.join(', ') || '(none)'}], so no non-delivery tree ` +
185
+ `exists and this gate asserts nothing about this configuration. A stub in a tree with ` +
186
+ `no bound delivery slot is reported by check_rule_projection_integrity's host axis ` +
187
+ `(thinnedTreeFindings); the byte-identity question here has no subject.\n`
188
+ );
189
+ }
190
+
191
+ export function main(argv: readonly string[] = []): number {
192
+ const root = REPO_ROOT;
193
+ const dist = path.join(root, 'dist', 'agent-src', 'rules');
194
+ if (!fs.existsSync(dist) || fs.readdirSync(dist).filter((f) => f.endsWith('.md')).length === 0) {
195
+ process.stderr.write(
196
+ `❌ check_host_tree_parity: no rule corpus at dist/agent-src/rules — run \`task sync\` first.\n`,
197
+ );
198
+ return 2;
199
+ }
200
+ const configured = resolveLeanProjectionHosts(leanProjectionHostsRaw(root)).hosts;
201
+ const hostList = configured.join(', ') || '(none)';
202
+
203
+ const saved = _getStateForTest();
204
+ const savedHome = process.env['HOME'];
205
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'host-parity-'));
206
+ let findings: ParityFinding[];
207
+ try {
208
+ // An empty HOME: user-scope dedup must find no twin, or the two runs
209
+ // would differ for a reason that has nothing to do with the host axis.
210
+ const home = path.join(tmp, 'home');
211
+ fs.mkdirSync(home, { recursive: true });
212
+ process.env['HOME'] = home;
213
+
214
+ const eagerRoot = seedFixture(root, tmp, 'eager', 'lean_projection:\n mode: eager-all\n');
215
+ _pointCondenseAt(eagerRoot);
216
+ generate_rule_symlinks();
217
+
218
+ const hostsBlock = configured.length === 0 ? '' : ` hosts: [${configured.join(', ')}]\n`;
219
+ const testRoot = seedFixture(root, tmp, 'test', `lean_projection:\n mode: delivery\n${hostsBlock}`);
220
+ _pointCondenseAt(testRoot);
221
+ generate_rule_symlinks();
222
+
223
+ findings = compareTrees(eagerRoot, testRoot, configured);
224
+ } finally {
225
+ _pointCondenseAt(saved.PROJECT_ROOT);
226
+ if (savedHome === undefined) delete process.env['HOME'];
227
+ else process.env['HOME'] = savedHome;
228
+ fs.rmSync(tmp, { recursive: true, force: true });
229
+ }
230
+
231
+ const compared = Object.entries(TREES).filter(([, h]) => !configured.includes(h)).length;
232
+ reportScanned({
233
+ gate: 'check_host_tree_parity',
234
+ scanned: compared,
235
+ units: 'non-delivery host rule tree(s)',
236
+ roots: Object.keys(TREES),
237
+ });
238
+
239
+ // One ledger target per non-delivery tree, not per entry: the assertion is
240
+ // about a TREE being byte-identical, and a per-entry ledger would report
241
+ // 200-odd completed targets for what is three yes/no questions.
242
+ const ledger = new GateLedger('check_host_tree_parity');
243
+ const compareable = Object.entries(TREES).filter(([, h]) => !configured.includes(h)).map(([t]) => t);
244
+ ledger.plan(compareable);
245
+ const broken = new Set(findings.map((f) => f.tree));
246
+ for (const tree of compareable) {
247
+ if (broken.has(tree)) ledger.fail(tree, 'differs from the eager-all projection');
248
+ else ledger.complete(tree);
249
+ }
250
+ try {
251
+ if (argv.includes('--quiet')) ledger.finalize();
252
+ else ledger.report();
253
+ } catch (exc) {
254
+ if (exc instanceof UnaccountedTargetsError) {
255
+ process.stderr.write(`❌ ${exc.message}\n`);
256
+ return 1;
257
+ }
258
+ throw exc;
259
+ }
260
+
261
+ if (findings.length > 0) {
262
+ process.stderr.write(
263
+ `❌ check_host_tree_parity: ${String(findings.length)} entry(ies) differ between ` +
264
+ `eager-all and delivery on a host that is NOT in lean_projection.hosts [${hostList}]:\n`,
265
+ );
266
+ for (const f of findings) {
267
+ process.stderr.write(` · ${f.tree}/${f.rule} (${f.host}) — ${f.reason}\n`);
268
+ }
269
+ process.stderr.write(
270
+ '\nA host outside `lean_projection.hosts` must receive exactly what `eager-all` writes ' +
271
+ '(K3, and the completeness invariant). Fix the projector, never the expectation.\n',
272
+ );
273
+ return 1;
274
+ }
275
+ const notice = parityScopeNotice(configured);
276
+ if (notice !== null) {
277
+ process.stderr.write(notice);
278
+ return 0;
279
+ }
280
+ if (!argv.includes('--quiet')) {
281
+ process.stdout.write(
282
+ `✅ check_host_tree_parity: ${String(compared)} non-delivery host tree(s) byte-identical to ` +
283
+ `eager-all · delivery hosts [${hostList}]\n`,
284
+ );
285
+ }
286
+ return 0;
287
+ }
288
+
289
+ if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
290
+ process.exit(main(process.argv.slice(2)));
291
+ }