@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
@@ -0,0 +1,650 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * What each host actually loads at rest, per host, in bytes and tokens.
4
+ *
5
+ * `road-to-delivery-for-every-host` step 0.1 — the baseline the flip is
6
+ * measured against. The roadmap's headline figure (138,200 chars/4 tokens) is a
7
+ * SINGLE number over one bucket set; this splits it by host, because the whole
8
+ * point of `lean_projection.hosts` is that the flip must move one host's number
9
+ * and leave every other host's byte-identical.
10
+ *
11
+ * WHY THE PROJECTION SOURCE AND NOT THE LOCAL TREE
12
+ * -----------------------------------------------
13
+ * The obvious census — walk `.claude/rules`, `.cursor/rules`, `.clinerules` on
14
+ * disk — measures this MACHINE, not the package. Two filters shrink a
15
+ * maintainer checkout below what a consumer install receives:
16
+ *
17
+ * · user-scope dedup: a rule byte-identical to a twin already installed at
18
+ * user scope is skipped (`condense` prints "N rule(s) skipped — byte-identical
19
+ * twin already installed at user scope"), so `.claude/rules` holds 13 files
20
+ * here against 114 projectable ones;
21
+ * · workspace/pack scope: `_scoped_rule_basenames` filters by the consumer's
22
+ * own `workspaces:`/`packs:` settings, which differ per install.
23
+ *
24
+ * Both are real and both are install-local, so a number read off this disk is
25
+ * not reproducible anywhere else — and step 0.1's verify demands a re-run at the
26
+ * same pin be byte-identical. So the unit here is the PROJECTION SOURCE
27
+ * (`dist/agent-src/rules`) minus the ADR-004 `type: manual` rules no per-tool
28
+ * tree ever receives, which is the UPPER BOUND a host loads: an unscoped,
29
+ * un-deduplicated consumer install. Every scope narrowing moves a host DOWN from
30
+ * this figure, never up.
31
+ *
32
+ * WHAT IT DOES NOT ESTABLISH
33
+ * --------------------------
34
+ * Nothing about whether a host READS what it is given. `docs/enforcement-by-host.md`
35
+ * owns that axis, and a host with a rule tree it never loads would look identical
36
+ * here. This measures delivery, not consumption.
37
+ *
38
+ * Usage:
39
+ * ./scripts-run src/scripts/report_standing_payload_by_host
40
+ * ./scripts-run src/scripts/report_standing_payload_by_host --emit --pin <sha>
41
+ *
42
+ * Deterministic: hosts sort by id, no timestamp, the pin is an argument, and no
43
+ * value is read from `$HOME`.
44
+ */
45
+
46
+ import { execFileSync } from 'node:child_process';
47
+ import * as fs from 'node:fs';
48
+ import * as path from 'node:path';
49
+ import { fileURLToPath, pathToFileURL } from 'node:url';
50
+
51
+ import {
52
+ loadRouter,
53
+ matchTierRules,
54
+ pathOnlyRuleIds,
55
+ selectForInjection,
56
+ } from './_lib/rule_injection.js';
57
+ import { CAP_BYTES } from './hooks/rule_inject_hook.js';
58
+
59
+ const _HERE = fileURLToPath(import.meta.url);
60
+ export const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
61
+ export const ARTIFACT_REL = path.join(
62
+ 'agents', 'evidence', 'analysis', 'standing-payload-by-host-2026-09.md',
63
+ );
64
+ export const RULES_DIST_REL = path.join('dist', 'agent-src', 'rules');
65
+
66
+ /**
67
+ * One host's rule surface, and the code that writes it.
68
+ *
69
+ * `writer` is a `file:line` into the tree, checked by `assertWritersResolve`
70
+ * below rather than asserted in prose — a citation nothing verifies is the
71
+ * failure mode this whole roadmap is written against.
72
+ */
73
+ export interface HostSurface {
74
+ readonly host: string;
75
+ /** Repo-relative dir or file the host loads, or `null` when this tree writes none. */
76
+ readonly surface: string | null;
77
+ /** `true` when `surface` is a directory receiving one file per projected rule. */
78
+ readonly perRuleTree: boolean;
79
+ readonly writer: string;
80
+ /**
81
+ * A literal substring the cited LINE must contain (R2 finding 8).
82
+ *
83
+ * Without it `assertWritersResolve` was a bounds test wearing a provenance
84
+ * test's name: it checked that the line NUMBER was inside the file and
85
+ * never read the line, so a citation that drifted by one commit — the
86
+ * exact failure this table's docstring says the check replaces prose
87
+ * assertion to prevent — passed unchanged, and so would a wholesale
88
+ * renumbering as long as the file stayed long enough.
89
+ *
90
+ * Kept short and structural (a call, a path literal, a const name) so it
91
+ * survives reformatting but not a move.
92
+ */
93
+ readonly anchor: string;
94
+ readonly note: string;
95
+ }
96
+
97
+ /**
98
+ * The host axis, taken from `docs/enforcement-by-host.md:18-28` rather than
99
+ * invented here, plus the writer each surface has in this tree.
100
+ */
101
+ export const HOST_SURFACES: readonly HostSurface[] = [
102
+ {
103
+ host: 'augment', surface: '.augment/rules', perRuleTree: true,
104
+ writer: 'src/scripts/condense.ts:2499',
105
+ anchor: '\'.augment/rules\',',
106
+ note: 'copies by default; symlinks under `augment.rules_use_symlinks`',
107
+ },
108
+ {
109
+ host: 'claude-code', surface: '.claude/rules', perRuleTree: true,
110
+ writer: 'src/scripts/condense.ts:1192',
111
+ anchor: '_emit_claude_rule(',
112
+ note: '`_emit_claude_rule` rewrites frontmatter to the host\'s own `paths:` key',
113
+ },
114
+ {
115
+ host: 'cline', surface: '.clinerules', perRuleTree: true,
116
+ writer: 'src/scripts/condense.ts:1194',
117
+ anchor: 'fs.symlinkSync(',
118
+ note: 'symlink per rule into the projection',
119
+ },
120
+ {
121
+ host: 'copilot', surface: '.github/copilot-instructions.md', perRuleTree: false,
122
+ writer: 'src/scripts/generate_capability_matrix.ts:138',
123
+ anchor: '_INSTALL_TIME_CELLS',
124
+ note: 'NOT written by `condense`: the installer aggregates it from `src/agent-src/templates/copilot-instructions.md`, which is why the capability matrix marks this cell `adapter` and footnotes it as install-time',
125
+ },
126
+ {
127
+ host: 'cowork', surface: null, perRuleTree: false,
128
+ writer: '—',
129
+ anchor: '',
130
+ note: 'no rule-tree writer in this tree; carries hook bindings only',
131
+ },
132
+ {
133
+ host: 'cursor', surface: '.cursor/rules', perRuleTree: true,
134
+ writer: 'src/scripts/condense.ts:1194',
135
+ anchor: 'fs.symlinkSync(',
136
+ note: 'symlink per rule, plus `.mdc` companions at `condense.ts:1422`',
137
+ },
138
+ {
139
+ host: 'gemini', surface: 'GEMINI.md', perRuleTree: false,
140
+ writer: 'src/scripts/condense.ts:1498',
141
+ anchor: '\'GEMINI.md\'',
142
+ note: 'single file',
143
+ },
144
+ {
145
+ host: 'windsurf', surface: '.windsurfrules', perRuleTree: false,
146
+ writer: 'src/scripts/condense.ts:1242',
147
+ anchor: '\'.windsurfrules\'',
148
+ note: 'single concatenated file',
149
+ },
150
+ {
151
+ host: 'codex', surface: '.codex/agent-config.md', perRuleTree: false,
152
+ writer: 'src/scripts/install.ts:1541',
153
+ anchor: '\'agent-config.md\'',
154
+ note: 'written by the installer, not by `condense`; absent in this checkout',
155
+ },
156
+ ];
157
+
158
+ /** chars/4, the unit `check_preamble_payload_budget` reports. */
159
+ export function tokensChars4(chars: number): number {
160
+ return Math.round(chars / 4);
161
+ }
162
+
163
+ /** A rule is `manual` when its frontmatter says so, quoted or bare (ADR-004). */
164
+ export function isManualRule(text: string): boolean {
165
+ return /^type:\s*"?manual"?\s*$/mu.test(text);
166
+ }
167
+
168
+ export interface RuleCorpus {
169
+ readonly files: number;
170
+ readonly manual: number;
171
+ readonly projected: number;
172
+ readonly projectedChars: number;
173
+ }
174
+
175
+ /** The projection source, split into what a per-rule host tree receives and what it never does. */
176
+ export function readRuleCorpus(root: string): RuleCorpus {
177
+ const dir = path.join(root, RULES_DIST_REL);
178
+ const names = fs.readdirSync(dir).filter((f) => f.endsWith('.md')).sort();
179
+ let manual = 0;
180
+ let projected = 0;
181
+ let projectedChars = 0;
182
+ for (const name of names) {
183
+ const text = fs.readFileSync(path.join(dir, name), 'utf-8');
184
+ if (isManualRule(text)) {
185
+ manual += 1;
186
+ continue;
187
+ }
188
+ projected += 1;
189
+ projectedChars += Buffer.byteLength(text, 'utf-8');
190
+ }
191
+ return { files: names.length, manual, projected, projectedChars };
192
+ }
193
+
194
+ /** Bytes of the `type: manual` rules — the difference between the two rule figures. */
195
+ export function manualCorpusChars(root: string): number {
196
+ const dir = path.join(root, RULES_DIST_REL);
197
+ let chars = 0;
198
+ for (const name of fs.readdirSync(dir).filter((f) => f.endsWith('.md')).sort()) {
199
+ const text = fs.readFileSync(path.join(dir, name), 'utf-8');
200
+ if (isManualRule(text)) chars += Buffer.byteLength(text, 'utf-8');
201
+ }
202
+ return chars;
203
+ }
204
+
205
+ /** Bytes of a single-file surface, or `null` when it is absent from this checkout. */
206
+ export function singleFileChars(root: string, rel: string): number | null {
207
+ try {
208
+ return Buffer.byteLength(fs.readFileSync(path.join(root, rel), 'utf-8'), 'utf-8');
209
+ } catch {
210
+ return null;
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Every `writer` citation resolves to a real line.
216
+ *
217
+ * Run on every invocation, not only under a flag: an artifact whose whole value
218
+ * is `file:line` provenance must not be publishable with a citation that has
219
+ * silently drifted by one commit.
220
+ */
221
+ export function assertWritersResolve(root: string): string[] {
222
+ const problems: string[] = [];
223
+ for (const h of HOST_SURFACES) {
224
+ if (h.writer === '—') continue;
225
+ const [rel, lineRaw] = h.writer.split(':');
226
+ const abs = path.join(root, rel as string);
227
+ if (!fs.existsSync(abs)) {
228
+ problems.push(`${h.host}: writer file ${rel as string} does not exist`);
229
+ continue;
230
+ }
231
+ const lines = fs.readFileSync(abs, 'utf-8').split('\n');
232
+ const n = Number(lineRaw);
233
+ if (!Number.isInteger(n) || n < 1 || n > lines.length) {
234
+ problems.push(`${h.host}: writer ${h.writer} is outside the file (${String(lines.length)} lines)`);
235
+ continue;
236
+ }
237
+ // The line itself, not merely its existence (R2 finding 8).
238
+ const line = lines[n - 1] as string;
239
+ if (h.anchor === '') {
240
+ problems.push(`${h.host}: writer ${h.writer} carries no anchor to check the line against`);
241
+ continue;
242
+ }
243
+ if (!line.includes(h.anchor)) {
244
+ problems.push(
245
+ `${h.host}: writer ${h.writer} no longer carries its anchor ` +
246
+ `${JSON.stringify(h.anchor)} — the line now reads ${JSON.stringify(line.trim())}`,
247
+ );
248
+ }
249
+ }
250
+ return problems;
251
+ }
252
+
253
+ /** How many surfaces carry a citation at all — counted, never `length - 1` (R2 finding 14). */
254
+ export function citedWriterCount(): number {
255
+ return HOST_SURFACES.filter((h) => h.writer !== '—').length;
256
+ }
257
+
258
+ /**
259
+ * Why `pin` is not usable, or `null` (R2 finding 9).
260
+ *
261
+ * `--pin` was free text with no comparison against HEAD, while the artifact it
262
+ * stamps opens with "Pinned to commit <sha> … a re-run at the same pin is
263
+ * byte-identical". Every figure in it is read from the WORKING TREE, so a pin
264
+ * that is not the current commit documents nothing — and the committed artifact
265
+ * carried a pin from an earlier commit while the branch had since edited
266
+ * `condense.ts`, whose line numbers the artifact cites. The pin is now an
267
+ * ASSERTION about HEAD rather than a label, and a dirty tree is reported for
268
+ * the same reason: the byte-identity sentence is false the moment the tree the
269
+ * figures were read from is not the commit they are stamped with.
270
+ */
271
+ export function pinProblem(root: string, pin: string): string | null {
272
+ let head: string;
273
+ try {
274
+ head = execFileSync('git', ['-C', root, 'rev-parse', 'HEAD'], { encoding: 'utf-8' }).trim();
275
+ } catch {
276
+ return 'git rev-parse HEAD failed — the pin cannot be asserted';
277
+ }
278
+ if (pin !== head) {
279
+ return (
280
+ `--pin ${pin} is not HEAD (${head}). Every figure in the artifact is read from the ` +
281
+ 'working tree, so a pin that is not the current commit stamps the report with a ' +
282
+ 'commit its numbers were never taken at.'
283
+ );
284
+ }
285
+ return null;
286
+ }
287
+
288
+ /** `true` when the working tree carries uncommitted changes to a path the report reads. */
289
+ export function treeIsDirty(root: string): boolean {
290
+ try {
291
+ const out = execFileSync(
292
+ 'git',
293
+ ['-C', root, 'status', '--porcelain', '--', RULES_DIST_REL, 'src/scripts', 'tests/eval/routing-matrix'],
294
+ { encoding: 'utf-8' },
295
+ );
296
+ return out.trim() !== '';
297
+ } catch {
298
+ return false;
299
+ }
300
+ }
301
+
302
+ export function renderArtifact(root: string, pin: string): string {
303
+ const corpus = readRuleCorpus(root);
304
+ const perTreeChars = corpus.projectedChars;
305
+ const L: string[] = [];
306
+ L.push('<!-- evidence-type: analysis -->');
307
+ L.push('# Standing rule payload, per host');
308
+ L.push('');
309
+ L.push(`Pinned to commit \`${pin}\`. Generated by`);
310
+ L.push(`\`./scripts-run src/scripts/report_standing_payload_by_host --emit --pin ${pin}\`;`);
311
+ L.push('a re-run at the same pin is byte-identical — no value is read from `$HOME` and no');
312
+ L.push('timestamp is written.');
313
+ L.push('');
314
+ L.push('## The unit, and why it is not this disk');
315
+ L.push('');
316
+ L.push('The census is taken over the **projection source** `dist/agent-src/rules`, minus the');
317
+ L.push('ADR-004 `type: manual` rules no per-tool tree ever receives. That is the UPPER BOUND a');
318
+ L.push('host loads: an unscoped, un-deduplicated consumer install.');
319
+ L.push('');
320
+ L.push('Reading it off the local trees instead would measure this machine. Two install-local');
321
+ L.push('filters shrink a maintainer checkout below what a consumer receives — user-scope dedup');
322
+ L.push('(a rule byte-identical to a user-scope twin is skipped) and workspace/pack scope. Both');
323
+ L.push('are real, both differ per install, and step 0.1 requires a re-run at the same pin to be');
324
+ L.push('byte-identical. Every scope narrowing moves a host DOWN from the figure below, never up.');
325
+ L.push('');
326
+ L.push('**This measures delivery, not consumption.** Whether a host READS what it is handed is');
327
+ L.push('the axis `docs/enforcement-by-host.md:18-28` owns; a host with a tree it never loads');
328
+ L.push('would look identical here.');
329
+ L.push('');
330
+ L.push('## The rule corpus');
331
+ L.push('');
332
+ L.push('| Quantity | Value |');
333
+ L.push('|---|---:|');
334
+ L.push(`| \`.md\` files in \`${RULES_DIST_REL}\` | ${String(corpus.files)} |`);
335
+ L.push(`| of those, \`type: manual\` (never projected, ADR-004) | ${String(corpus.manual)} |`);
336
+ L.push(`| projected to a per-rule host tree | ${String(corpus.projected)} |`);
337
+ L.push(`| bytes of the projected set | ${String(perTreeChars)} |`);
338
+ L.push(`| chars/4 tokens of the projected set | ${String(tokensChars4(perTreeChars))} |`);
339
+ L.push('');
340
+ L.push('## Per host');
341
+ L.push('');
342
+ L.push('| Host | Surface | Shape | Bytes | chars/4 tok | Writer |');
343
+ L.push('|---|---|---|---:|---:|---|');
344
+ for (const h of HOST_SURFACES) {
345
+ if (h.surface === null) {
346
+ L.push(`| \`${h.host}\` | — | none | — | — | ${h.writer} |`);
347
+ continue;
348
+ }
349
+ if (h.perRuleTree) {
350
+ L.push(
351
+ `| \`${h.host}\` | \`${h.surface}\` | per-rule tree | ${String(perTreeChars)} | ` +
352
+ `${String(tokensChars4(perTreeChars))} | \`${h.writer}\` |`,
353
+ );
354
+ continue;
355
+ }
356
+ const chars = singleFileChars(root, h.surface);
357
+ const bytes = chars === null ? 'absent' : String(chars);
358
+ const toks = chars === null ? 'absent' : String(tokensChars4(chars));
359
+ L.push(`| \`${h.host}\` | \`${h.surface}\` | single file | ${bytes} | ${toks} | \`${h.writer}\` |`);
360
+ }
361
+ L.push('');
362
+ L.push('Notes per host:');
363
+ L.push('');
364
+ for (const h of HOST_SURFACES) {
365
+ L.push(`- \`${h.host}\` — ${h.note}`);
366
+ }
367
+ L.push('');
368
+ L.push('## The two units, recorded once');
369
+ L.push('');
370
+ // The rules bucket is DERIVED here, never a literal. The two figures below
371
+ // it are readings of another gate and are dated rather than recomputed —
372
+ // this script does not own them, and a stale literal presented as live is
373
+ // the defect class R2 findings 8/9/14 are about (spotted while fixing them:
374
+ // `122,608` had gone stale and contradicted the computed sum three
375
+ // paragraphs down, inside the same emitted artifact).
376
+ const censusTok = tokensChars4(perTreeChars) + tokensChars4(manualCorpusChars(root));
377
+ const SKILLS_CATALOG_TOK = 14846;
378
+ const CLAUDE_MD_TOK = 746;
379
+ L.push('- **chars/4** is what `check_preamble_payload_budget` reports. Its rules bucket is');
380
+ L.push(` derived here as **${String(censusTok)} tok** (every \`.md\` in the projection directory).`);
381
+ L.push(' The other two buckets are that gate\'s own readings, last taken 2026-09-08 and NOT');
382
+ L.push(` recomputed by this script: preloaded skills catalog ${String(SKILLS_CATALOG_TOK)} ·`);
383
+ L.push(` CLAUDE.md hierarchy ${String(CLAUDE_MD_TOK)}, for a measured total of`);
384
+ L.push(` **${String(censusTok + SKILLS_CATALOG_TOK + CLAUDE_MD_TOK)} tok**. Re-read it with`);
385
+ L.push(' `./scripts-run src/scripts/check_preamble_payload_budget` rather than quoting this line.');
386
+ L.push('- **Exact BPE** is the unit `docs/CLAIMS.md:365` uses for the delivery experiment:');
387
+ L.push(' 120,582 → 18,573 exact-BPE standing tokens for the rule corpus under');
388
+ L.push(' `lean_projection.mode: delivery`. The two units are NOT interchangeable and no delta');
389
+ L.push(' is computed between them here.');
390
+ L.push('');
391
+ L.push('**Reconciling the two rule figures, because they differ and both are right.** The');
392
+ L.push(`census bucket reads ${String(censusTok)} chars/4 tok over the projection directory; the projected-set`);
393
+ const manualChars = manualCorpusChars(root);
394
+ L.push(`row above reads ${String(tokensChars4(perTreeChars))}. The gap is the ${String(corpus.manual)} \`type: manual\` rules: the census counts every`);
395
+ L.push(`\`.md\` in the directory, this table counts only what a per-tool tree receives. ${String(manualChars)} bytes`);
396
+ L.push(`= ${String(tokensChars4(manualChars))} tok, and ${String(tokensChars4(perTreeChars))} + ${String(tokensChars4(manualChars))} = ${String(tokensChars4(perTreeChars) + tokensChars4(manualChars))}. A manual rule reaches no host tree`);
397
+ L.push('by ADR-004, so excluding it is correct here and including it is correct there.');
398
+ L.push('');
399
+ L.push('## Per-slot delivery fire sizes — the activation charge (3.1)');
400
+ L.push('');
401
+ L.push('Gate-open per-fire emission of the `rule-inject` concern over the frozen');
402
+ L.push('`tests/eval/routing-matrix` corpus, measured through the SAME selection the runtime');
403
+ L.push('concern uses (`_lib/rule_injection.ts::matchTierRules` + `selectForInjection` at the');
404
+ L.push('concern\'s own `CAP_BYTES`), never through a second model of it.');
405
+ L.push('');
406
+ L.push('| Slot | Fires | p50 B | p90 B | max B |');
407
+ L.push('|---|---:|---:|---:|---:|');
408
+ for (const r of slotFireSizes(root)) {
409
+ L.push(
410
+ `| \`${r.slot}\` | ${String(r.fires)} | ${String(r.p50)} | ${String(r.p90)} | ${String(r.max)} |`,
411
+ );
412
+ }
413
+ L.push('');
414
+ L.push('`pre_compact` is zero **by construction, not by measurement**: that branch of');
415
+ L.push('`rule_inject_hook` clears the seen-set and returns allow without writing to stdout, so');
416
+ L.push('there is no emission to sample.');
417
+ L.push('');
418
+ const pathOnly = [...pathOnlyRuleIds(loadRouter(root))].sort();
419
+ L.push('**Rules reachable ONLY on `pre_tool_use`**, which is the condition owner ruling E2');
420
+ L.push(`makes the \`pre_tool_use\` binding conditional on — ${String(pathOnly.length)} of them, all labelled:`);
421
+ L.push('');
422
+ for (const id of pathOnly) L.push(`- \`${id}\``);
423
+ L.push('');
424
+ L.push('## What the flip must move, and what it must not');
425
+ L.push('');
426
+ L.push('`claude-code` is the only host in the default `lean_projection.hosts`. Its row above is');
427
+ L.push('the number the flip reduces. **Every other per-rule-tree row must stay byte-identical**');
428
+ L.push('— that is the D1 repair and the Phase 1.4 gate, and it is why this table exists per host');
429
+ L.push('rather than as one total.');
430
+ L.push('');
431
+ return L.join('\n');
432
+ }
433
+
434
+ /** One positive prompt of the frozen corpus, with the open files it declares. */
435
+ interface CorpusPrompt {
436
+ prompt: string;
437
+ openFiles: string[] | null;
438
+ }
439
+
440
+ /**
441
+ * Every YAML shape inside `positives:` this hand parser cannot read (R2 finding 10).
442
+ *
443
+ * `readCorpusPositives` accepts only `- prompt: "…"` with a double-quoted
444
+ * same-line scalar and an inline `open_files: [...]`. Any other legal shape —
445
+ * single quotes, an unquoted scalar, a folded block, a block sequence for
446
+ * `open_files` — was SILENTLY SKIPPED, which drops fires out of the
447
+ * distribution the 16,384-byte `user_prompt_submit` row is derived from. A
448
+ * measurement that governs a budget must fail loudly on a shape it cannot read
449
+ * rather than under-count, so the CLI refuses on a non-empty result here.
450
+ *
451
+ * Reported as a shape problem rather than fixed by widening the parser: this
452
+ * script must not grow a second YAML implementation, and the corpus is frozen,
453
+ * so the honest answer to an unreadable shape is to say which file carries it.
454
+ */
455
+ export function corpusShapeProblems(root: string): string[] {
456
+ const dir = path.join(root, 'tests', 'eval', 'routing-matrix');
457
+ const problems: string[] = [];
458
+ if (!fs.existsSync(dir)) return problems;
459
+ for (const f of fs.readdirSync(dir).filter((n) => n.endsWith('.yaml')).sort()) {
460
+ let inPositives = false;
461
+ let lineNo = 0;
462
+ for (const raw of fs.readFileSync(path.join(dir, f), 'utf-8').split('\n')) {
463
+ lineNo += 1;
464
+ if (/^positives:/u.test(raw)) { inPositives = true; continue; }
465
+ if (/^near_misses:/u.test(raw)) { inPositives = false; continue; }
466
+ if (!inPositives) continue;
467
+ if (/^\s*-\s*prompt:/u.test(raw) && !/^\s*-\s*prompt:\s*"(.*)"\s*$/u.test(raw)) {
468
+ problems.push(
469
+ `${f}:${String(lineNo)}: a \`- prompt:\` this parser cannot read — only a ` +
470
+ `double-quoted same-line scalar is accepted: ${JSON.stringify(raw.trim())}`,
471
+ );
472
+ continue;
473
+ }
474
+ if (/^\s*-\s*/u.test(raw) && !/^\s*-\s*(prompt|command|open_files):/u.test(raw)
475
+ && raw.trim() !== '-' && raw.trim() !== '') {
476
+ // A bare sequence item under `positives:` is a shape with no
477
+ // `prompt:` key on its first line — folded or nested.
478
+ if (!/^\s{4,}/u.test(raw)) {
479
+ problems.push(
480
+ `${f}:${String(lineNo)}: a positives entry whose first line carries no ` +
481
+ `\`prompt:\` key: ${JSON.stringify(raw.trim())}`,
482
+ );
483
+ }
484
+ }
485
+ if (/^\s*open_files:\s*$/u.test(raw)) {
486
+ problems.push(
487
+ `${f}:${String(lineNo)}: a BLOCK-sequence \`open_files:\` — only the inline ` +
488
+ '`[...]` form is read, so this entry\'s files would be dropped',
489
+ );
490
+ }
491
+ }
492
+ }
493
+ return problems;
494
+ }
495
+
496
+ /** Every positive prompt in the frozen routing-matrix corpus. Near-misses are not fires. */
497
+ export function readCorpusPositives(root: string): CorpusPrompt[] {
498
+ const dir = path.join(root, 'tests', 'eval', 'routing-matrix');
499
+ const out: CorpusPrompt[] = [];
500
+ if (!fs.existsSync(dir)) return out;
501
+ for (const f of fs.readdirSync(dir).filter((n) => n.endsWith('.yaml')).sort()) {
502
+ let inPositives = false;
503
+ let cur: CorpusPrompt | null = null;
504
+ for (const raw of fs.readFileSync(path.join(dir, f), 'utf-8').split('\n')) {
505
+ if (/^positives:/u.test(raw)) { inPositives = true; continue; }
506
+ if (/^near_misses:/u.test(raw)) { inPositives = false; continue; }
507
+ if (!inPositives) continue;
508
+ const p = /^\s*-\s*prompt:\s*"(.*)"\s*$/u.exec(raw);
509
+ if (p !== null) {
510
+ cur = { prompt: p[1] as string, openFiles: null };
511
+ out.push(cur);
512
+ continue;
513
+ }
514
+ const of = /^\s*open_files:\s*\[(.*)\]\s*$/u.exec(raw);
515
+ if (of !== null && cur !== null) {
516
+ cur.openFiles = (of[1] as string)
517
+ .split(',')
518
+ .map((x) => x.trim().replace(/^["']|["']$/gu, ''))
519
+ .filter((x) => x !== '');
520
+ }
521
+ }
522
+ }
523
+ return out;
524
+ }
525
+
526
+ /** Nearest-rank percentile over an unsorted sample. Empty sample → 0. */
527
+ export function percentile(xs: readonly number[], q: number): number {
528
+ if (xs.length === 0) return 0;
529
+ const s = [...xs].sort((a, b) => a - b);
530
+ return s[Math.min(s.length - 1, Math.floor(q * (s.length - 1)))] as number;
531
+ }
532
+
533
+ export interface SlotFireSizes {
534
+ readonly slot: string;
535
+ readonly fires: number;
536
+ readonly p50: number;
537
+ readonly p90: number;
538
+ readonly max: number;
539
+ }
540
+
541
+ /**
542
+ * Gate-open per-fire emission sizes for the delivery concern, per slot
543
+ * (road-to-delivery-for-every-host 3.1).
544
+ *
545
+ * Measured through the SAME selection the runtime concern uses —
546
+ * `matchTierRules` + `selectForInjection` at the concern's own `CAP_BYTES` —
547
+ * rather than through a second model of it. An offline figure computed by a
548
+ * different matcher would price a set the concern does not deliver.
549
+ *
550
+ * `pre_compact` is 0 by construction rather than by measurement: that branch of
551
+ * `rule_inject_hook` clears the seen-set and returns allow without writing to
552
+ * stdout, so there is no emission to sample.
553
+ */
554
+ export function slotFireSizes(root: string): SlotFireSizes[] {
555
+ const router = loadRouter(root);
556
+ const prompts = readCorpusPositives(root);
557
+ const sample = (mode: 'prompt' | 'file'): number[] => {
558
+ const out: number[] = [];
559
+ for (const r of prompts) {
560
+ if (mode === 'file' && (r.openFiles === null || r.openFiles.length === 0)) continue;
561
+ const matches =
562
+ mode === 'prompt'
563
+ ? matchTierRules(router, r.prompt, null, null)
564
+ : matchTierRules(router, '', r.openFiles, null);
565
+ if (matches.length === 0) continue;
566
+ const sel = selectForInjection(root, matches, CAP_BYTES);
567
+ if (sel.selected.length > 0) out.push(sel.bytes);
568
+ }
569
+ return out;
570
+ };
571
+ const row = (slot: string, xs: number[]): SlotFireSizes => ({
572
+ slot,
573
+ fires: xs.length,
574
+ p50: percentile(xs, 0.5),
575
+ p90: percentile(xs, 0.9),
576
+ max: xs.length === 0 ? 0 : Math.max(...xs),
577
+ });
578
+ return [
579
+ row('user_prompt_submit', sample('prompt')),
580
+ row('pre_tool_use', sample('file')),
581
+ row('pre_compact', []),
582
+ ];
583
+ }
584
+
585
+ function headSha(root: string): string {
586
+ return execFileSync('git', ['-C', root, 'rev-parse', 'HEAD'], { encoding: 'utf-8' }).trim();
587
+ }
588
+
589
+ export function main(argv: readonly string[]): number {
590
+ const root = REPO_ROOT;
591
+ const problems = assertWritersResolve(root);
592
+ if (problems.length > 0) {
593
+ process.stderr.write('❌ a writer citation no longer resolves:\n');
594
+ for (const p of problems) process.stderr.write(` · ${p}\n`);
595
+ return 1;
596
+ }
597
+ // R2 finding 10: an unreadable corpus shape is a refusal, never a silent
598
+ // under-count of the distribution a budget row is derived from.
599
+ const shape = corpusShapeProblems(root);
600
+ if (shape.length > 0) {
601
+ process.stderr.write(
602
+ `❌ ${String(shape.length)} corpus entry/entries in a shape this reader cannot parse ` +
603
+ '— the fire distribution would be under-counted:\n',
604
+ );
605
+ for (const p of shape) process.stderr.write(` · ${p}\n`);
606
+ return 1;
607
+ }
608
+ const pinIdx = argv.indexOf('--pin');
609
+ const pin = pinIdx !== -1 && pinIdx + 1 < argv.length ? (argv[pinIdx + 1] as string) : headSha(root);
610
+ // R2 finding 9: the pin is an assertion about HEAD, not a label.
611
+ const badPin = pinProblem(root, pin);
612
+ if (badPin !== null) {
613
+ process.stderr.write(`❌ ${badPin}\n`);
614
+ return 1;
615
+ }
616
+ if (argv.includes('--emit') && treeIsDirty(root) && !argv.includes('--allow-dirty')) {
617
+ process.stderr.write(
618
+ '❌ the working tree carries uncommitted changes under dist/agent-src/rules, ' +
619
+ 'src/scripts or tests/eval/routing-matrix, so the figures would not be reproducible ' +
620
+ `at pin ${pin}. Commit first, or pass --allow-dirty to stamp the artifact as ` +
621
+ 'provisional.\n',
622
+ );
623
+ return 1;
624
+ }
625
+ if (argv.includes('--emit')) {
626
+ const out = path.join(root, ARTIFACT_REL);
627
+ fs.mkdirSync(path.dirname(out), { recursive: true });
628
+ fs.writeFileSync(out, renderArtifact(root, pin), 'utf-8');
629
+ process.stdout.write(`wrote ${ARTIFACT_REL} · pin ${pin}\n`);
630
+ }
631
+ const corpus = readRuleCorpus(root);
632
+ process.stdout.write(`scanned: ${String(HOST_SURFACES.length)} host(s)\n`);
633
+ process.stdout.write(
634
+ ` rule corpus ${String(corpus.files)} files · ${String(corpus.manual)} manual · ` +
635
+ `${String(corpus.projected)} projected · ${String(corpus.projectedChars)} bytes · ` +
636
+ `${String(tokensChars4(corpus.projectedChars))} chars/4 tok\n`,
637
+ );
638
+ // R2 finding 14: counted, not `length - 1`. The old form derived the `-1`
639
+ // from the single current `writer: '—'` row, so a second surface-less host
640
+ // reported a wrong count, and the ratio could never read anything but N/N.
641
+ const cited = citedWriterCount();
642
+ process.stdout.write(
643
+ ` writer citations resolved: ${String(cited - problems.length)}/${String(cited)}\n`,
644
+ );
645
+ return 0;
646
+ }
647
+
648
+ if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
649
+ process.exit(main(process.argv.slice(2)));
650
+ }
@@ -39,7 +39,13 @@
39
39
  "properties": {
40
40
  "mode": {
41
41
  "type": "string",
42
- "enum": ["eager-all", "thin"]
42
+ "enum": ["eager-all", "thin", "delivery"],
43
+ "description": "eager-all = every rule body projected into every host tree. thin = pointer stubs with no delivery mechanism. delivery = thin stubs PLUS the rule-inject hook concern, which delivers a body back on trigger match; shipped default for Claude Code since ADR-267. `delivery` was MISSING from this enum until 2026-09-07 while _lib/lean_projection_mode.ts had accepted it for months, so a consumer who set the documented value failed validation."
44
+ },
45
+ "hosts": {
46
+ "type": "array",
47
+ "items": { "type": "string", "enum": ["claude-code", "cursor", "cline"] },
48
+ "description": "Which hosts a thinning mode may thin (ADR-267). Absent resolves to [claude-code]. Only the three hosts with a per-rule rule tree are accepted. TWO LAYERS, and they answer differently on purpose (R2 finding 6, 2026-09-08): this enum is the AUTHORING layer, so an id outside it is a hard validation FAILURE under validate_agent_settings and under the wizard's settings schema \u2014 not a warning. resolveLeanProjectionHosts is the READ-TIME backstop for a hand-edited file that never passed either, because condense._lean_projection_settings and the rule-inject hook both read .agent-settings.yml directly with no validation; there such an id is dropped with a warning worded by describeDroppedHosts, and the set never widens implicitly. Every host outside this list receives exactly what eager-all writes, asserted byte-for-byte by check_host_tree_parity."
43
49
  }
44
50
  }
45
51
  },