@event4u/agent-config 9.29.0 → 9.31.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 (142) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +32 -214
  3. package/README.md +1 -0
  4. package/dist/agent-src/commands/analyze/inbox.md +22 -1
  5. package/dist/agent-src/commands/explain-run.md +31 -1
  6. package/dist/agent-src/commands/team/delegate.md +1 -0
  7. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +4 -1
  8. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +17 -0
  9. package/dist/agent-src/contexts/execution/subagent-response-contract.md +11 -1
  10. package/dist/agent-src/rules/preservation-guard.md +9 -0
  11. package/dist/agent-src/scripts/roadmap_gates.ts +312 -0
  12. package/dist/agent-src/scripts/update_roadmap_progress.ts +72 -4
  13. package/dist/agent-src/skills/subagent-orchestration/SKILL.md +10 -0
  14. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  15. package/dist/cli/registry.js +2 -0
  16. package/dist/cli/registry.js.map +1 -1
  17. package/dist/cli-delegate/{chunk-RCIA67XN.js → chunk-2WCLVDWC.js} +45 -2
  18. package/dist/cli-delegate/{chunk-66OVWGB5.js → chunk-AG47LAGQ.js} +1 -1
  19. package/dist/cli-delegate/{chunk-MP3JT3IU.js → chunk-DS6BLDMG.js} +1 -1
  20. package/dist/cli-delegate/chunk-EJITJBJX.js +172 -0
  21. package/dist/cli-delegate/{chunk-NZ5N2XQL.js → chunk-ITIQSGOQ.js} +3 -3
  22. package/dist/cli-delegate/{chunk-LCFJIQ4J.js → chunk-JEJABGKB.js} +110 -6
  23. package/dist/cli-delegate/chunk-MMJL4TIX.js +18 -0
  24. package/dist/cli-delegate/{chunk-REKCMYOU.js → chunk-PFBV6XRT.js} +36 -20
  25. package/dist/cli-delegate/{chunk-OJM3MBJM.js → chunk-QEEB4LHO.js} +22 -35
  26. package/dist/cli-delegate/chunk-RVGBH4XK.js +142 -0
  27. package/dist/cli-delegate/{chunk-MMXA5GY7.js → chunk-UVKNIJH2.js} +14 -0
  28. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  29. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  30. package/dist/cli-delegate/cmd_conformance.js +2861 -96
  31. package/dist/cli-delegate/cmd_doctor.js +3 -2
  32. package/dist/cli-delegate/cmd_explain.js +2 -2
  33. package/dist/cli-delegate/cmd_export.js +4 -4
  34. package/dist/cli-delegate/cmd_handoff.js +87 -7
  35. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  36. package/dist/cli-delegate/cmd_migrate.js +2 -2
  37. package/dist/cli-delegate/cmd_packs_active.js +2 -2
  38. package/dist/cli-delegate/cmd_prune.js +4 -4
  39. package/dist/cli-delegate/cmd_route_audit.js +3 -2
  40. package/dist/cli-delegate/cmd_route_explain.js +1 -1
  41. package/dist/cli-delegate/cmd_session_recycle.js +177 -0
  42. package/dist/cli-delegate/cmd_settings_get.js +3 -56
  43. package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
  44. package/dist/cli-delegate/cmd_settings_set.js +2 -2
  45. package/dist/cli-delegate/cmd_sync.js +4 -4
  46. package/dist/cli-delegate/cmd_uninstall.js +4 -4
  47. package/dist/cli-delegate/cmd_update.js +1 -1
  48. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  49. package/dist/cli-delegate/cmd_validate.js +4 -4
  50. package/dist/cli-delegate/cmd_versions.js +1 -1
  51. package/dist/discovery/deprecation-report.md +1 -1
  52. package/dist/discovery/discovery-manifest.json +7 -7
  53. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  54. package/dist/discovery/discovery-manifest.summary.md +1 -1
  55. package/dist/discovery/orphan-report.md +1 -1
  56. package/dist/discovery/packs.json +2 -2
  57. package/dist/discovery/trust-report.md +1 -1
  58. package/dist/discovery/workspaces.json +2 -2
  59. package/dist/hooks/dispatch.js +2143 -641
  60. package/dist/install/install.mjs +133 -14
  61. package/dist/mcp/registry-manifest.json +2 -2
  62. package/dist/mcp/server.json +2 -2
  63. package/dist/mcp/server.mjs +3967 -761
  64. package/dist/scripts/_lib/agent_settings.js +111 -2
  65. package/dist/scripts/_lib/agent_settings.js.map +1 -1
  66. package/dist/server/schemas/settings.js +6 -0
  67. package/dist/server/schemas/settings.js.map +1 -1
  68. package/dist/ui/assets/index-B5qGYOJo.js +26 -0
  69. package/dist/ui/assets/index-B5qGYOJo.js.map +1 -0
  70. package/dist/ui/index.html +1 -1
  71. package/docs/contracts/settings-classes.md +21 -5
  72. package/package.json +1 -1
  73. package/src/agent-src/scripts/roadmap_gates.ts +312 -0
  74. package/src/agent-src/scripts/update_roadmap_progress.ts +72 -4
  75. package/src/config/agent-settings.template.yml +36 -0
  76. package/src/config/dispatch-economy-metrics.json +61 -0
  77. package/src/config/evaluator-budgets.json +4 -4
  78. package/src/config/hook-token-budget.json +91 -0
  79. package/src/config/pack-size-budget.json +3 -2
  80. package/src/config/recycle-threshold-budget.json +12 -0
  81. package/src/config/rule-enforcement-baseline.json +86 -0
  82. package/src/scripts/_cli/cmd_conformance.ts +17 -2
  83. package/src/scripts/_cli/cmd_session_recycle.ts +226 -0
  84. package/src/scripts/_cli/explain_last/inputs.ts +13 -0
  85. package/src/scripts/_dispatch.bash +23 -0
  86. package/src/scripts/_lib/agent_settings.ts +119 -2
  87. package/src/scripts/_lib/carrier_divergence.ts +47 -0
  88. package/src/scripts/_lib/judgment_ladder.ts +254 -2
  89. package/src/scripts/_lib/orchestration_record.ts +41 -10
  90. package/src/scripts/_lib/recycle_envelope_paths.ts +35 -0
  91. package/src/scripts/_lib/self_repair.ts +194 -0
  92. package/src/scripts/_lib/session_eol.ts +230 -0
  93. package/src/scripts/_lib/session_role.ts +42 -0
  94. package/src/scripts/_lib/subagent_capsule.ts +159 -0
  95. package/src/scripts/_lib/subagent_response.ts +75 -0
  96. package/src/scripts/ai_council/clients.ts +50 -1
  97. package/src/scripts/ask_transport.ts +221 -0
  98. package/src/scripts/bench_hook_injection.ts +234 -0
  99. package/src/scripts/check_adr_frontmatter.ts +84 -40
  100. package/src/scripts/check_branch_freshness.ts +1 -0
  101. package/src/scripts/check_corpus_staleness.ts +18 -4
  102. package/src/scripts/check_detector_corpus.ts +17 -0
  103. package/src/scripts/check_gitignore_freshness.ts +42 -8
  104. package/src/scripts/check_hook_bundle_freshness.ts +20 -0
  105. package/src/scripts/check_kernel_prefix_stability.ts +1 -0
  106. package/src/scripts/check_standing_rule_delivery.ts +73 -32
  107. package/src/scripts/check_static_layer_stability.ts +154 -0
  108. package/src/scripts/check_suppression_hygiene.ts +48 -0
  109. package/src/scripts/dispatch_economy_report.ts +448 -0
  110. package/src/scripts/dispatch_r2_reviewer.ts +15 -2
  111. package/src/scripts/explain_run.ts +206 -4
  112. package/src/scripts/generate_settings_reference.ts +1 -0
  113. package/src/scripts/handoff_context_hook.ts +112 -4
  114. package/src/scripts/hook_manifest.yaml +117 -10
  115. package/src/scripts/hooks/concern_registry.ts +8 -0
  116. package/src/scripts/hooks/delegation_nudge_hook.ts +40 -3
  117. package/src/scripts/hooks/dispatch_hook.ts +39 -2
  118. package/src/scripts/hooks/edit_shape_hook.ts +303 -0
  119. package/src/scripts/hooks/orchestration_record_hook.ts +19 -1
  120. package/src/scripts/hooks/reread_guard_hook.ts +314 -0
  121. package/src/scripts/hooks/rtk_wrap_hook.ts +133 -14
  122. package/src/scripts/hooks/session_eol_hook.ts +268 -0
  123. package/src/scripts/hooks/turn_end_gate_hook.ts +715 -0
  124. package/src/scripts/install-hooks.sh +1 -1
  125. package/src/scripts/language_mirror_hook.ts +143 -1
  126. package/src/scripts/lint_decision_impact.ts +14 -1
  127. package/src/scripts/lint_hook_manifest.ts +82 -0
  128. package/src/scripts/lint_readme_serial_comma.ts +1 -0
  129. package/src/scripts/lint_rule_enforcement_declaration.ts +347 -0
  130. package/src/scripts/measure_turn_end_gate.ts +272 -0
  131. package/src/scripts/orchestration_record.ts +4 -1
  132. package/src/scripts/rebuild_hook_bundle.ts +1 -0
  133. package/src/scripts/report_carrier_divergence.ts +72 -24
  134. package/src/scripts/report_conformance_funnel.ts +291 -0
  135. package/src/scripts/report_imperative_density.ts +1 -0
  136. package/src/scripts/routing_doctor.ts +97 -0
  137. package/src/scripts/self_repair_cli.ts +51 -3
  138. package/src/scripts/session_eol_report.ts +261 -0
  139. package/src/server/schemas/settings.ts +14 -0
  140. package/src/templates/compact-instructions.md +40 -0
  141. package/dist/ui/assets/index-JDAF00bC.js +0 -26
  142. package/dist/ui/assets/index-JDAF00bC.js.map +0 -1
@@ -0,0 +1,272 @@
1
+ #!/usr/bin/env -S npx tsx
2
+ /**
3
+ * `measure-turn-end-gate` — the false-positive pass roadmap
4
+ * `road-to-conformance-round5.md` § 3.4 asks for, run against a REAL
5
+ * transcript corpus rather than a hand-written one.
6
+ *
7
+ * Why this exists as a script and not as a number in a commit message: the
8
+ * roadmap asks to "record measured precision per detector", and a recorded
9
+ * figure nobody can re-derive is the same class of claim this whole round
10
+ * exists to remove. One command, same store, same numbers.
11
+ *
12
+ * What it measures, and what it deliberately does NOT:
13
+ *
14
+ * · FIRE RATE of each detector PER TURN, over the last assistant text of the
15
+ * turn — the population the shipped gate inspects. R2 finding 1: the first
16
+ * version scored every assistant ENTRY and published the result as a
17
+ * per-turn rate, which inflated the population ~8.6x and scored a
18
+ * closing-paragraph detector over mid-turn prose.
19
+ * · The independent scanner's own language total, printed for CONTEXT only.
20
+ * It is not an agreement figure and no overlap is claimed from it: the two
21
+ * instruments read different spans (its first prose line; this one strips
22
+ * code/quotes/paths from the reply and then scores lead-first via the same
23
+ * classifier), and this script does no per-turn matching between them.
24
+ * R2 finding 7 removed the per-session min/max arithmetic that used to
25
+ * report disjoint findings of equal count as full agreement.
26
+ * · It does NOT compute recall against the 20 promissory occurrences the
27
+ * round-5 audit reported. Those came from subagent transcript READING;
28
+ * `conformance_scan.ts` states in its own header that promissory
29
+ * closings are "deliberately NOT scanned", so there is no machine-
30
+ * readable ground truth to score against. Claiming a recall figure here
31
+ * would invent one.
32
+ * · KNOWN RESIDUAL, R2 finding 8, not fixed here: this script re-derives the
33
+ * language pin with `classify` per user entry, while the gate reads the pin
34
+ * `language_mirror_hook` persisted — which also applies a system-locale
35
+ * fallback and a keep-previous rule. Where those diverge, a turn can move
36
+ * between "pinned de", "pinned en" and "no obligation", so detector B's
37
+ * rate here is close to but not identical with the gate's. Reaching parity
38
+ * means reusing `nextState`, which is a larger change than a measurement
39
+ * correction and would itself need re-measuring.
40
+ *
41
+ * Usage:
42
+ * ./scripts-run src/scripts/measure_turn_end_gate --store <dir> [--limit N]
43
+ */
44
+ import * as fs from 'node:fs';
45
+ import * as path from 'node:path';
46
+ import { fileURLToPath, pathToFileURL } from 'node:url';
47
+
48
+ import { classify, isSyntheticPrompt } from './language_mirror_hook.js';
49
+ import { assistantText, scanSession, userText } from './conformance_scan.js';
50
+ import { detectLanguage, detectPromissory } from './hooks/turn_end_gate_hook.js';
51
+
52
+ interface Counts {
53
+ /**
54
+ * TURNS, not entries. R2 finding 1: the first version incremented a counter
55
+ * named `assistant_turns` on every assistant entry and ran both detectors on
56
+ * each one, while the gate only ever evaluates the LAST assistant text of a
57
+ * turn. A turn with k prose entries contributed k samples to the instrument
58
+ * and 1 to the gate, so the published "18.0% of turns" was a per-ENTRY rate
59
+ * wearing a per-turn label — and detector A, a closing-paragraph detector by
60
+ * construction, was being scored over mid-turn prose.
61
+ */
62
+ turns: number;
63
+ /** Assistant entries seen — kept only to show the ratio the old bug hid. */
64
+ assistant_entries: number;
65
+ promissory_fires: number;
66
+ language_fires: number;
67
+ /** The independent scanner's own per-session language total, for context. */
68
+ scanner_language: number;
69
+ sessions: number;
70
+ }
71
+
72
+ function sessionFiles(store: string, limit: number): string[] {
73
+ let entries: string[];
74
+ try {
75
+ entries = fs.readdirSync(store).filter((f) => f.endsWith('.jsonl'));
76
+ } catch {
77
+ return [];
78
+ }
79
+ const withTime = entries.map((f) => {
80
+ const p = path.join(store, f);
81
+ let mtime = 0;
82
+ try {
83
+ mtime = fs.statSync(p).mtimeMs;
84
+ } catch {
85
+ /* unreadable — sorts last */
86
+ }
87
+ return { p, mtime };
88
+ });
89
+ withTime.sort((a, b) => b.mtime - a.mtime);
90
+ return withTime.slice(0, limit).map((e) => e.p);
91
+ }
92
+
93
+ export function measure(store: string, limit: number): Counts {
94
+ const c: Counts = {
95
+ turns: 0,
96
+ assistant_entries: 0,
97
+ promissory_fires: 0,
98
+ language_fires: 0,
99
+ scanner_language: 0,
100
+ sessions: 0,
101
+ };
102
+
103
+ for (const file of sessionFiles(store, limit)) {
104
+ let lines: string[];
105
+ try {
106
+ lines = fs.readFileSync(file, 'utf-8').split('\n').filter((l) => l.trim() !== '');
107
+ } catch {
108
+ continue;
109
+ }
110
+ c.sessions += 1;
111
+ const sessionId = path.basename(file).slice(0, 8);
112
+
113
+ // The independent scanner's own language total for this session,
114
+ // reported side by side and NOT as agreement — see the header for why
115
+ // no overlap is derivable from two totals (R2 finding 7).
116
+ //
117
+ // A throw is surfaced, not swallowed to zero: swallowing silently
118
+ // reclassified a whole session's fires as scanner-disagreements.
119
+ try {
120
+ c.scanner_language += scanSession(sessionId, lines).violations.filter(
121
+ (v) => v.check === 'language-pin',
122
+ ).length;
123
+ } catch (err) {
124
+ process.stderr.write(
125
+ `measure-turn-end-gate: scanSession failed for ${sessionId} — excluded from the scanner total: ${String(err)}\n`,
126
+ );
127
+ }
128
+
129
+ let pinned: 'de' | 'en' | 'und' = 'und';
130
+ // The LAST assistant text since the last genuine user prompt. This is
131
+ // the population the gate inspects; anything else is a different
132
+ // measurement wearing the same label (R2 finding 1).
133
+ let pendingReply: string | null = null;
134
+ let pendingPin: 'de' | 'en' | 'und' = 'und';
135
+
136
+ const scoreTurn = (): void => {
137
+ if (pendingReply === null) return;
138
+ c.turns += 1;
139
+ if (detectPromissory(pendingReply) !== null) c.promissory_fires += 1;
140
+ if (detectLanguage(pendingReply, pendingPin) !== null) c.language_fires += 1;
141
+ pendingReply = null;
142
+ };
143
+
144
+ for (const raw of lines) {
145
+ let entry: Record<string, unknown>;
146
+ try {
147
+ entry = JSON.parse(raw) as Record<string, unknown>;
148
+ } catch {
149
+ continue;
150
+ }
151
+
152
+ const u = userText(entry);
153
+ if (u !== null) {
154
+ // `isSyntheticPrompt`, the SAME filter the gate uses for the
155
+ // ordinal — not `isInjectedBody`. R2 round 2, finding 10: the
156
+ // two filters classify differently, and any entry they disagree
157
+ // about shifts this instrument's turn boundary away from the
158
+ // gate's own. Population parity is the entire point of this
159
+ // correction, so the filter has to be the same one.
160
+ if (!isSyntheticPrompt(u)) {
161
+ scoreTurn(); // the previous turn ends here
162
+ const verdict = classify(u);
163
+ if (verdict.language !== 'und') pinned = verdict.language;
164
+ }
165
+ continue;
166
+ }
167
+
168
+ const a = assistantText(entry);
169
+ if (a === null || a.trim() === '') continue;
170
+ c.assistant_entries += 1;
171
+ // Overwrite: only the last one in the turn is the gate's input.
172
+ pendingReply = a;
173
+ pendingPin = pinned;
174
+ }
175
+ scoreTurn(); // the transcript's final turn
176
+ }
177
+
178
+ return c;
179
+ }
180
+
181
+ export function render(c: Counts): string {
182
+ const pct = (n: number): string => (c.turns === 0 ? '0.0' : ((100 * n) / c.turns).toFixed(1));
183
+ return [
184
+ `measure-turn-end-gate · ${c.sessions} sessions · ${c.turns} turns · ${c.assistant_entries} assistant entries`,
185
+ '',
186
+ ` detector A (promissory) fires on ${c.promissory_fires} turns (${pct(c.promissory_fires)}%)`,
187
+ ` detector B (language) fires on ${c.language_fires} turns (${pct(c.language_fires)}%)`,
188
+ '',
189
+ ' Rates are per TURN, over the last assistant text of each turn — the same',
190
+ ' population the shipped gate inspects. The entry count is printed beside',
191
+ ' the turn count because the first version of this script scored every',
192
+ ' assistant ENTRY and labelled the result per-turn; the gap between the two',
193
+ ' numbers is the size of that error.',
194
+ '',
195
+ ` For context only, the independent scanner's own language total: ${c.scanner_language}`,
196
+ ' NOT an agreement figure. The two instruments read different spans (its',
197
+ ' first prose line vs. this stripped, lead-first-scored reply) and this',
198
+ ' script does no per-turn matching between them, so no overlap can be claimed from these',
199
+ ' two totals — the previous version claimed one from per-session min/max',
200
+ ' arithmetic, which reports disjoint findings of equal count as full',
201
+ ' agreement.',
202
+ '',
203
+ ' No recall figure for detector A: promissory closings are not machine-',
204
+ ' scanned anywhere in this tree, so there is no ground truth to score.',
205
+ ].join('\n');
206
+ }
207
+
208
+ export function main(argv: string[] = process.argv.slice(2)): number {
209
+ let store = '';
210
+ let limit = 30;
211
+ for (let i = 0; i < argv.length; i += 1) {
212
+ if (argv[i] === '--store' && argv[i + 1] !== undefined) {
213
+ store = argv[i + 1]!;
214
+ i += 1;
215
+ } else if (argv[i] === '--limit' && argv[i + 1] !== undefined) {
216
+ // R2 finding 15: an unvalidated parseInt made `--limit abc` yield
217
+ // NaN, `slice(0, NaN)` return nothing, and the script exit 2 with
218
+ // "no .jsonl sessions under <store>" — a real input error reported
219
+ // as a missing corpus.
220
+ const parsed = Number.parseInt(argv[i + 1]!, 10);
221
+ if (!Number.isFinite(parsed) || parsed < 1) {
222
+ process.stderr.write(
223
+ `measure-turn-end-gate: --limit must be a positive integer, got ${JSON.stringify(argv[i + 1])}\n`,
224
+ );
225
+ return 2;
226
+ }
227
+ limit = parsed;
228
+ i += 1;
229
+ }
230
+ }
231
+ if (!store) {
232
+ process.stderr.write('measure-turn-end-gate: --store <transcript dir> is required\n');
233
+ return 2;
234
+ }
235
+ const counts = measure(store, limit);
236
+ if (counts.sessions === 0) {
237
+ // A measurement over zero sessions that exits 0 is the repo's own
238
+ // "gates that scan nothing" failure. Refuse instead.
239
+ process.stderr.write(`measure-turn-end-gate: no .jsonl sessions under ${store}\n`);
240
+ return 2;
241
+ }
242
+ process.stdout.write(`${render(counts)}\n`);
243
+ return 0;
244
+ }
245
+
246
+ // Bundle-safety, same guard as the sibling hook. R2 finding 15: a substring
247
+ // test on argv[1] auto-runs the module when esbuild inlines it into a bundle,
248
+ // where every module shares the bundle's `import.meta.url`.
249
+ declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
250
+ function _isCliEntry(): boolean {
251
+ if (typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__) {
252
+ return false;
253
+ }
254
+ if (process.argv[1] === undefined) {
255
+ return false;
256
+ }
257
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
258
+ if (import.meta.url === argvUrl) {
259
+ return true;
260
+ }
261
+ try {
262
+ const here = fs.realpathSync(fileURLToPath(import.meta.url));
263
+ const argv = fs.realpathSync(path.resolve(process.argv[1]));
264
+ return here === argv;
265
+ } catch {
266
+ return false;
267
+ }
268
+ }
269
+
270
+ if (_isCliEntry()) {
271
+ process.exit(main());
272
+ }
@@ -13,9 +13,10 @@
13
13
  * [--tiers sonnet,opus] [--agent-combo implementer,judge] [--wall-clock-ms 18500] [--dispatch-outcome DONE] \
14
14
  * [--first-pass-success true|false] [--escalated true|false] \
15
15
  * [--init-tokens 1200] [--payload-hash <hex8-64>] [--lookup-class definition|references|string-existence|report-run] \
16
- * [--route-taken primitive|subagent] [--budget-hit true|false] [--correctness-match true|false] \
16
+ * [--route-taken primitive|subagent|ask] [--budget-hit true|false] [--correctness-match true|false] \
17
17
  * [--cache-hit true|false] [--origin lean-init-2026] \
18
18
  * [--rules-carried 32] [--rules-used 5] \
19
+ * [--work-tokens 41000] [--floor-provenance measured|estimated] \
19
20
  * [--capsule-emitted true|false] [--capsule-entries 9] \
20
21
  * [--watermark-step 8] [--saturation-step 4] [--trigger-arm-earlier watermark|saturation|tie] \
21
22
  * [--dir <audit-dir>] [--dry-run]
@@ -144,6 +145,8 @@ export function main(argv: string[] = process.argv.slice(2)): number {
144
145
  origin: str(flags, 'origin'),
145
146
  rules_carried: int(flags, 'rules-carried'),
146
147
  rules_used: int(flags, 'rules-used'),
148
+ work_tokens: int(flags, 'work-tokens'),
149
+ floor_provenance: str(flags, 'floor-provenance') as Provenance | undefined,
147
150
  capsule_emitted: bool(flags, 'capsule-emitted'),
148
151
  capsule_entries: int(flags, 'capsule-entries'),
149
152
  watermark_step: int(flags, 'watermark-step'),
@@ -47,6 +47,7 @@ import { fileURLToPath } from "node:url";
47
47
 
48
48
  import { check } from "./check_hook_bundle_freshness.js";
49
49
 
50
+ // ledger-exempt: self-heal builder, not a checker — one build → probe → atomic-rename transaction whose outcome is a single aggregate heal verdict; the per-source staleness accounting lives in check_hook_bundle_freshness, which carries the ledger.
50
51
  const HERE = path.dirname(fileURLToPath(import.meta.url));
51
52
  const REPO_ROOT = path.resolve(HERE, "..", "..");
52
53
  const LIVE_REL = path.join("dist", "hooks", "dispatch.js");
@@ -35,16 +35,30 @@
35
35
  * ignored, which is the failure `check-rule-invariants` already paid for once.
36
36
  * The point is that the condition becomes VISIBLE, not that it becomes illegal.
37
37
  *
38
- * THE THREE CLASSES, AND WHICH ONE A READER MUST ACT ON
39
- * ----------------------------------------------------
40
- * body-diff the two carriers deliver different TEXT. This is the class
38
+ * THE FOUR CLASSES, AND WHICH ONE A READER MUST ACT ON
39
+ * ---------------------------------------------------
40
+ * body-diff the two carriers deliver different PROSE. This is the class
41
41
  * round 5's contradiction lived in, and the only one that
42
- * needs a decision. Precedence when it appears: the project
43
- * projection is generated from `src/` at the current commit,
44
- * so it is the newer copy and it wins; the global install is
45
- * a release snapshot. Printed with the report so the
46
- * precedence is read at the moment it matters rather than
47
- * looked up.
42
+ * needs a decision. What the host does about it: NOTHING —
43
+ * both copies load at launch with the same priority and no
44
+ * precedence marker between them, so the divergence is
45
+ * binding-undefined rather than resolved in either
46
+ * direction. Cited, not inferred:
47
+ * `agents/evidence/analysis/claude-code-rules-dir-contract.md`
48
+ * (host 2.1.226, first-party observation plus the host's own
49
+ * documentation). The project copy is the NEWER text, which
50
+ * is a fact about recency and not a precedence rule — the
51
+ * earlier wording of this header called it a win, and a
52
+ * reader acting on that would have believed the host resolves
53
+ * something it does not.
54
+ * frontmatter-only the prose is byte-identical and only the metadata block
55
+ * differs. The host delivers the prose without that block, so
56
+ * nothing a reader must act on reaches the model differently.
57
+ * Split out because it was, measured, the ENTIRE content of
58
+ * the `body-diff` class: 109 of 109 on 2026-08-10. Left inside
59
+ * body-diff it manufactured the report's only actionable
60
+ * class out of a metadata difference — the same mistake the
61
+ * `unreadable` branch below already refuses to make.
48
62
  * provenance-only identical after removing the two installer ownership keys.
49
63
  * Structural, expected, and decided against closing —
50
64
  * `agents/settings/contexts/dedup-reachability-refusal.md`.
@@ -69,7 +83,7 @@ import * as path from 'node:path';
69
83
  import process from 'node:process';
70
84
  import { fileURLToPath, pathToFileURL } from 'node:url';
71
85
 
72
- import { comparePair, type PairVerdict } from './_lib/carrier_divergence.js';
86
+ import { comparePair, type PairVerdict, proseEqual } from './_lib/carrier_divergence.js';
73
87
 
74
88
  const _HERE = fileURLToPath(import.meta.url);
75
89
  const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
@@ -103,6 +117,13 @@ export interface CarrierDivergence {
103
117
  shared: number;
104
118
  identical: string[];
105
119
  provenanceOnly: string[];
120
+ /**
121
+ * Prose byte-identical, metadata block different. A real byte difference — so
122
+ * `comparePair` still calls it `body-diff` and the dedup predicate is
123
+ * unchanged — but not a difference in what the host delivers, so it is not
124
+ * something a reader has to act on.
125
+ */
126
+ frontmatterOnly: string[];
106
127
  bodyDiff: string[];
107
128
  projectOnly: string[];
108
129
  globalOnly: string[];
@@ -171,6 +192,7 @@ export function compareCarriers(
171
192
  shared: 0,
172
193
  identical: [],
173
194
  provenanceOnly: [],
195
+ frontmatterOnly: [],
174
196
  bodyDiff: [],
175
197
  projectOnly: [],
176
198
  globalOnly: [],
@@ -196,11 +218,12 @@ export function compareCarriers(
196
218
  }
197
219
  out.shared += 1;
198
220
  let verdict: PairVerdict;
221
+ let proseSame: boolean;
199
222
  try {
200
- verdict = comparePair(
201
- fs.readFileSync(path.join(projectDir, rule)),
202
- fs.readFileSync(path.join(globalDir, rule)),
203
- );
223
+ const projectCopy = fs.readFileSync(path.join(projectDir, rule));
224
+ const globalCopy = fs.readFileSync(path.join(globalDir, rule));
225
+ verdict = comparePair(projectCopy, globalCopy);
226
+ proseSame = proseEqual(projectCopy.toString('utf-8'), globalCopy.toString('utf-8'));
204
227
  } catch {
205
228
  // An unreadable copy is not a body difference and must not be
206
229
  // reported as one — a permission error would otherwise manufacture
@@ -213,6 +236,7 @@ export function compareCarriers(
213
236
  }
214
237
  if (verdict === 'identical') out.identical.push(rule);
215
238
  else if (verdict === 'provenance-only') out.provenanceOnly.push(rule);
239
+ else if (proseSame) out.frontmatterOnly.push(rule);
216
240
  else out.bodyDiff.push(rule);
217
241
  }
218
242
  return out;
@@ -244,7 +268,8 @@ export function render(d: CarrierDivergence): string {
244
268
  lines.push(` shared rule names ${d.shared}`);
245
269
  lines.push(` byte-identical ${d.identical.length}`);
246
270
  lines.push(` differ ONLY in the install stamp ${d.provenanceOnly.length}`);
247
- lines.push(` differ in BODY ${d.bodyDiff.length}`);
271
+ lines.push(` differ ONLY in frontmatter ${d.frontmatterOnly.length}`);
272
+ lines.push(` differ in PROSE ${d.bodyDiff.length}`);
248
273
  if (d.unreadable.length > 0) {
249
274
  lines.push(` unreadable on one side ${d.unreadable.length} (${d.unreadable.join(', ')})`);
250
275
  lines.push(' Not counted as shared and NOT a body difference — a dangling entry is');
@@ -255,16 +280,34 @@ export function render(d: CarrierDivergence): string {
255
280
  lines.push('');
256
281
 
257
282
  if (d.bodyDiff.length > 0) {
258
- lines.push(' BODY DIVERGENCE — the two carriers deliver different text for these rules.');
259
- lines.push(' Both copies reach the model, with no precedence marker between them, so a');
260
- lines.push(' claim one copy retracts can be re-asserted by the other. PRECEDENCE: the');
261
- lines.push(' project projection is generated from src/ at this commit and wins; the');
262
- lines.push(' global install is a release snapshot. Close it by reinstalling the global');
263
- lines.push(' copy, or by reading the project copy as authoritative.');
283
+ lines.push(' PROSE DIVERGENCE — the two carriers deliver different governed text for');
284
+ lines.push(' these rules. Both copies reach the model, and the host resolves NOTHING:');
285
+ lines.push(' rules without a `paths` key load at launch with the same priority as');
286
+ lines.push(' CLAUDE.md, with no precedence marker between the layers, so a claim one');
287
+ lines.push(' copy retracts can be re-asserted by the other and which text binds is');
288
+ lines.push(' UNDEFINED. Cited, not inferred — agents/evidence/analysis/');
289
+ lines.push(' claude-code-rules-dir-contract.md (host 2.1.226, the host\'s own docs plus');
290
+ lines.push(' a first-party observation). The project copy is the NEWER text, which is a');
291
+ lines.push(' fact about recency and NOT a host precedence rule: close the divergence by');
292
+ lines.push(' reinstalling the global copy, or decide which text binds — do not assume');
293
+ lines.push(' the newer one already won.');
264
294
  for (const r of d.bodyDiff) lines.push(` - ${r}`);
265
295
  lines.push('');
266
296
  }
267
297
 
298
+ if (d.frontmatterOnly.length > 0) {
299
+ lines.push(` ${String(d.frontmatterOnly.length)} pair(s) differ ONLY in the frontmatter block — prose byte-identical.`);
300
+ lines.push(' Not a governed-text difference and nothing to act on: the host delivers the');
301
+ lines.push(' prose without that block, and none of the keys in it is one this host reads');
302
+ lines.push(' (claude-code-rules-dir-contract.md: it reads `paths`, and agent-config\'s own');
303
+ lines.push(' vocabulary is not read at all). Two writers, two frontmatter policies —');
304
+ lines.push(' `generate-tools` emits `paths` where a rule is path-scoped and nothing');
305
+ lines.push(' otherwise; `install.ts` writes the full vocabulary plus its ownership stamp.');
306
+ lines.push(' Reported as a count on purpose: naming every pair would bury the class');
307
+ lines.push(' above, which is the one a reader must act on.');
308
+ lines.push('');
309
+ }
310
+
268
311
  if (d.provenanceOnly.length > 0) {
269
312
  lines.push(` ${String(d.provenanceOnly.length)} pair(s) differ only by the installer's package:/source_path: stamp.`);
270
313
  lines.push(' Not a content difference and NOT a defect: install.ts stamps every installed');
@@ -302,9 +345,14 @@ export function render(d: CarrierDivergence): string {
302
345
 
303
346
  if (d.bodyDiff.length === 0) {
304
347
  lines.push('');
305
- lines.push(' No body divergence. The condition is transient — it returns whenever the');
306
- lines.push(' checkout moves ahead of the installed release — so this is a reading of');
307
- lines.push(' right now, not a property of the repo. Re-run it, do not cite it.');
348
+ lines.push(' No prose divergence — no rule\'s governed text differs between the carriers.');
349
+ lines.push(' The condition is transient — it returns whenever the checkout moves ahead of');
350
+ lines.push(' the installed release — so this is a reading of right now, not a property of');
351
+ lines.push(' the repo. Re-run it, do not cite it. That holds doubly here: the reading also');
352
+ lines.push(' depends on WHICH GENERATION of the emitter produced the project tree (a');
353
+ lines.push(' frontmatter-carrying symlink tree and a frontmatter-less real-file tree');
354
+ lines.push(' classify the same commit differently), so a number from another checkout is');
355
+ lines.push(' not comparable to this one.');
308
356
  }
309
357
  return lines.join('\n');
310
358
  }