@event4u/agent-config 9.29.0 → 9.30.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 (122) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +108 -7
  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-OJM3MBJM.js → chunk-BTJNRUX3.js} +21 -34
  18. package/dist/cli-delegate/{chunk-MMXA5GY7.js → chunk-DDUNNFYX.js} +14 -0
  19. package/dist/cli-delegate/{chunk-NZ5N2XQL.js → chunk-EQY4FLJB.js} +1 -1
  20. package/dist/cli-delegate/{chunk-RCIA67XN.js → chunk-IYTGIG2Y.js} +44 -1
  21. package/dist/cli-delegate/chunk-MMJL4TIX.js +18 -0
  22. package/dist/cli-delegate/chunk-O3CO5BFB.js +161 -0
  23. package/dist/cli-delegate/{chunk-REKCMYOU.js → chunk-PFBV6XRT.js} +36 -20
  24. package/dist/cli-delegate/chunk-RVGBH4XK.js +142 -0
  25. package/dist/cli-delegate/cmd_conformance.js +2798 -95
  26. package/dist/cli-delegate/cmd_doctor.js +2 -1
  27. package/dist/cli-delegate/cmd_export.js +2 -2
  28. package/dist/cli-delegate/cmd_handoff.js +86 -6
  29. package/dist/cli-delegate/cmd_prune.js +2 -2
  30. package/dist/cli-delegate/cmd_route_audit.js +2 -1
  31. package/dist/cli-delegate/cmd_session_recycle.js +177 -0
  32. package/dist/cli-delegate/cmd_settings_get.js +1 -1
  33. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  34. package/dist/cli-delegate/cmd_settings_set.js +2 -2
  35. package/dist/cli-delegate/cmd_sync.js +2 -2
  36. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  37. package/dist/cli-delegate/cmd_validate.js +2 -2
  38. package/dist/discovery/deprecation-report.md +1 -1
  39. package/dist/discovery/discovery-manifest.json +7 -7
  40. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  41. package/dist/discovery/discovery-manifest.summary.md +1 -1
  42. package/dist/discovery/orphan-report.md +1 -1
  43. package/dist/discovery/packs.json +2 -2
  44. package/dist/discovery/trust-report.md +1 -1
  45. package/dist/discovery/workspaces.json +2 -2
  46. package/dist/hooks/dispatch.js +1904 -575
  47. package/dist/install/install.mjs +14 -0
  48. package/dist/mcp/registry-manifest.json +2 -2
  49. package/dist/mcp/server.json +2 -2
  50. package/dist/mcp/server.mjs +3760 -726
  51. package/dist/server/schemas/settings.js +6 -0
  52. package/dist/server/schemas/settings.js.map +1 -1
  53. package/dist/ui/assets/index-B5qGYOJo.js +26 -0
  54. package/dist/ui/assets/index-B5qGYOJo.js.map +1 -0
  55. package/dist/ui/index.html +1 -1
  56. package/docs/contracts/settings-classes.md +21 -5
  57. package/package.json +1 -1
  58. package/src/agent-src/scripts/roadmap_gates.ts +312 -0
  59. package/src/agent-src/scripts/update_roadmap_progress.ts +72 -4
  60. package/src/config/agent-settings.template.yml +36 -0
  61. package/src/config/dispatch-economy-metrics.json +61 -0
  62. package/src/config/evaluator-budgets.json +4 -4
  63. package/src/config/hook-token-budget.json +91 -0
  64. package/src/config/recycle-threshold-budget.json +12 -0
  65. package/src/config/rule-enforcement-baseline.json +86 -0
  66. package/src/scripts/_cli/cmd_conformance.ts +17 -2
  67. package/src/scripts/_cli/cmd_session_recycle.ts +226 -0
  68. package/src/scripts/_dispatch.bash +23 -0
  69. package/src/scripts/_lib/judgment_ladder.ts +254 -2
  70. package/src/scripts/_lib/orchestration_record.ts +41 -10
  71. package/src/scripts/_lib/recycle_envelope_paths.ts +35 -0
  72. package/src/scripts/_lib/self_repair.ts +194 -0
  73. package/src/scripts/_lib/session_eol.ts +230 -0
  74. package/src/scripts/_lib/session_role.ts +42 -0
  75. package/src/scripts/_lib/subagent_capsule.ts +159 -0
  76. package/src/scripts/_lib/subagent_response.ts +75 -0
  77. package/src/scripts/ai_council/clients.ts +50 -1
  78. package/src/scripts/ask_transport.ts +221 -0
  79. package/src/scripts/bench_hook_injection.ts +234 -0
  80. package/src/scripts/check_adr_frontmatter.ts +84 -40
  81. package/src/scripts/check_branch_freshness.ts +1 -0
  82. package/src/scripts/check_corpus_staleness.ts +18 -4
  83. package/src/scripts/check_detector_corpus.ts +17 -0
  84. package/src/scripts/check_gitignore_freshness.ts +42 -8
  85. package/src/scripts/check_hook_bundle_freshness.ts +20 -0
  86. package/src/scripts/check_kernel_prefix_stability.ts +1 -0
  87. package/src/scripts/check_standing_rule_delivery.ts +73 -32
  88. package/src/scripts/check_static_layer_stability.ts +154 -0
  89. package/src/scripts/check_suppression_hygiene.ts +48 -0
  90. package/src/scripts/dispatch_economy_report.ts +448 -0
  91. package/src/scripts/dispatch_r2_reviewer.ts +15 -2
  92. package/src/scripts/explain_run.ts +206 -4
  93. package/src/scripts/generate_settings_reference.ts +1 -0
  94. package/src/scripts/handoff_context_hook.ts +112 -4
  95. package/src/scripts/hook_manifest.yaml +117 -10
  96. package/src/scripts/hooks/concern_registry.ts +8 -0
  97. package/src/scripts/hooks/delegation_nudge_hook.ts +40 -3
  98. package/src/scripts/hooks/dispatch_hook.ts +39 -2
  99. package/src/scripts/hooks/edit_shape_hook.ts +303 -0
  100. package/src/scripts/hooks/orchestration_record_hook.ts +19 -1
  101. package/src/scripts/hooks/reread_guard_hook.ts +314 -0
  102. package/src/scripts/hooks/rtk_wrap_hook.ts +133 -14
  103. package/src/scripts/hooks/session_eol_hook.ts +268 -0
  104. package/src/scripts/hooks/turn_end_gate_hook.ts +507 -0
  105. package/src/scripts/install-hooks.sh +1 -1
  106. package/src/scripts/language_mirror_hook.ts +89 -0
  107. package/src/scripts/lint_decision_impact.ts +14 -1
  108. package/src/scripts/lint_hook_manifest.ts +82 -0
  109. package/src/scripts/lint_readme_serial_comma.ts +1 -0
  110. package/src/scripts/lint_rule_enforcement_declaration.ts +347 -0
  111. package/src/scripts/measure_turn_end_gate.ts +194 -0
  112. package/src/scripts/orchestration_record.ts +4 -1
  113. package/src/scripts/rebuild_hook_bundle.ts +1 -0
  114. package/src/scripts/report_conformance_funnel.ts +284 -0
  115. package/src/scripts/report_imperative_density.ts +1 -0
  116. package/src/scripts/routing_doctor.ts +97 -0
  117. package/src/scripts/self_repair_cli.ts +51 -3
  118. package/src/scripts/session_eol_report.ts +261 -0
  119. package/src/server/schemas/settings.ts +14 -0
  120. package/src/templates/compact-instructions.md +40 -0
  121. package/dist/ui/assets/index-JDAF00bC.js +0 -26
  122. package/dist/ui/assets/index-JDAF00bC.js.map +0 -1
@@ -0,0 +1,448 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Dispatch-economy report — computes the two registered metrics from
4
+ * `src/config/dispatch-economy-metrics.json` (road-to-token-economy-dispatch
5
+ * Phase 1):
6
+ *
7
+ * dispatch_floor median init_tokens per role + init/work ratio per leg
8
+ * rules_efficiency median rules_used / rules_carried per worker envelope
9
+ *
10
+ * Metric semantics (restated from the registration file, load-bearing):
11
+ *
12
+ * init_tokens = billable_input of the leg's FIRST assistant record
13
+ * work_tokens = leg total billable_input − init_tokens
14
+ * billable_input = input_tokens + cache_read_input_tokens
15
+ * + cache_creation_input_tokens
16
+ *
17
+ * Sources, in the registration file's priority order:
18
+ * 1. Transcript ledger via `_lib/cc_transcript.ts` (per-agentId legs,
19
+ * message.id+requestId dedupe) — provenance `measured`.
20
+ * 2. Audit-log orchestration lines carrying `init_tokens`/`work_tokens`
21
+ * with `floor_provenance: "measured"`, and the `rules_carried`/
22
+ * `rules_used` pairs for rules_efficiency.
23
+ *
24
+ * ROLE ATTRIBUTION — honest limitation until the Phase 2 role axis ships:
25
+ * transcripts carry no session role, so every transcript leg reports as
26
+ * role `worker`; reviewer legs are attributed ONLY from audit-line
27
+ * `agent_combo` entries matching /review|judge|validator/i. The output
28
+ * names the attribution source per number.
29
+ *
30
+ * Class A (ADR-124 no-runtime-boundary): in-process, per-invocation, no
31
+ * socket, no daemon, no network, no writes — stdout report only. This is a
32
+ * REPORT, not a gate: exit code is 0 on every computed path (2 only on
33
+ * unusable invocation, e.g. unreadable registration file).
34
+ *
35
+ * Usage:
36
+ * ./scripts-run src/scripts/dispatch_economy_report \
37
+ * [--format text|json] [--root <transcripts-root>] [--audit-dir <dir>]
38
+ * [--max-age-days 14]
39
+ */
40
+ import * as fs from 'node:fs';
41
+ import * as path from 'node:path';
42
+ import { fileURLToPath, pathToFileURL } from 'node:url';
43
+
44
+ import {
45
+ billableInputTokens,
46
+ DEFAULT_PROJECTS_ROOT,
47
+ scanTranscripts,
48
+ weightedInputUnits,
49
+ type TranscriptRecord,
50
+ } from './_lib/cc_transcript.js';
51
+
52
+ const _HERE = fileURLToPath(import.meta.url);
53
+ // src/scripts/dispatch_economy_report.ts → parents[1] is the repo root (same
54
+ // 2-up resolution cache_realization_report.ts uses at the same depth).
55
+ const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
56
+ const REGISTRATION_FILE = path.join(REPO_ROOT, 'src', 'config', 'dispatch-economy-metrics.json');
57
+ const DEFAULT_AUDIT_DIR = path.join('agents', 'runtime', 'state', 'audit');
58
+
59
+ /** Reviewer attribution off audit-line agent_combo — id-shaped types only. */
60
+ const REVIEWER_TYPE_RE = /review|judge|validator/i;
61
+
62
+ export interface LegStat {
63
+ agentId: string;
64
+ calls: number;
65
+ /** Raw billable_input of the first record — comparable to the cache-economy 235.5k baseline. */
66
+ init_tokens: number;
67
+ /** Raw billable_input of the rest of the leg. */
68
+ work_tokens: number;
69
+ /** COST-shaped ratio: weightedInputUnits(init) / weightedInputUnits(rest)
70
+ * (read 0.1× · 5m write 1.25× · 1h write 2×). Raw billable_input would
71
+ * count every turn's cache-READ re-transmission at full weight and make
72
+ * the "more spent loading context than working" threshold unmeetable by
73
+ * construction — measured live 2026-08-10: raw ratio 0.02 on legs whose
74
+ * weighted ratio is the number the threshold means. null when the leg
75
+ * has a single call (floor-only, no work to ratio against). */
76
+ ratio: number | null;
77
+ }
78
+
79
+ export interface RoleFloor {
80
+ role: 'worker' | 'reviewer';
81
+ attribution: 'transcript-leg (role axis not shipped — all legs report as worker)' | 'audit-line agent_combo';
82
+ legs: number;
83
+ single_call_legs: number;
84
+ median_init_tokens: number | null;
85
+ median_ratio_finite: number | null;
86
+ }
87
+
88
+ export interface RulesEfficiency {
89
+ envelopes_with_pair: number;
90
+ median_quota: number | null;
91
+ }
92
+
93
+ export interface Report {
94
+ schema: 'dispatch-economy-report/v1';
95
+ registration: { registered_at: string; review_by: string };
96
+ window: { max_age_days: number; transcript_records: number; legs: number };
97
+ dispatch_floor: {
98
+ roles: RoleFloor[];
99
+ thresholds: { projection_mandatory_ratio: number; success_ratio_after_projection: number };
100
+ /** Single-run reading — the registered verdict is a sustained-window call at review date. */
101
+ projection_mandatory_signal: boolean | null;
102
+ };
103
+ rules_efficiency: RulesEfficiency & { threshold_low_quota: number; low_quota_signal: boolean | null };
104
+ ask_economy: {
105
+ ask_lines: number;
106
+ adopted: number;
107
+ escalated: number;
108
+ threshold_escalation_kill: number;
109
+ /** null until asks exist; the registered verdict is a review-window call. */
110
+ escalation_kill_signal: boolean | null;
111
+ };
112
+ return_channel: {
113
+ lines_with_field: number;
114
+ median_chars: number | null;
115
+ envelope_cap_chars: number;
116
+ /** median above the committed envelope cap = transcript-shaped returns flowing back. */
117
+ cap_breach_signal: boolean | null;
118
+ };
119
+ notes: string[];
120
+ }
121
+
122
+ export function median(values: readonly number[]): number | null {
123
+ if (values.length === 0) return null;
124
+ const s = [...values].sort((a, b) => a - b);
125
+ const mid = Math.floor(s.length / 2);
126
+ const lo = s[mid - 1];
127
+ const hi = s[mid];
128
+ if (s.length % 2 === 1) return hi ?? null;
129
+ return lo !== undefined && hi !== undefined ? (lo + hi) / 2 : null;
130
+ }
131
+
132
+ /** Group subagent records into per-agentId legs and compute init/work per leg. */
133
+ export function computeLegs(records: readonly TranscriptRecord[]): LegStat[] {
134
+ const byAgent = new Map<string, TranscriptRecord[]>();
135
+ for (const r of records) {
136
+ if (r.bucket !== 'subagent' || r.agentId === null) continue;
137
+ const list = byAgent.get(r.agentId) ?? [];
138
+ list.push(r);
139
+ byAgent.set(r.agentId, list);
140
+ }
141
+ const legs: LegStat[] = [];
142
+ for (const [agentId, recs] of byAgent) {
143
+ // Timestamp sort; records without timestamps keep file order (stable sort).
144
+ const sorted = [...recs].sort((a, b) => {
145
+ if (a.timestamp === null || b.timestamp === null) return 0;
146
+ return a.timestamp < b.timestamp ? -1 : a.timestamp > b.timestamp ? 1 : 0;
147
+ });
148
+ const first = sorted[0];
149
+ if (first === undefined) continue;
150
+ const init = billableInputTokens(first.usage);
151
+ const total = sorted.reduce((acc, r) => acc + billableInputTokens(r.usage), 0);
152
+ const work = total - init;
153
+ const initUnits = weightedInputUnits(first.usage);
154
+ const workUnits = sorted.slice(1).reduce((acc, r) => acc + weightedInputUnits(r.usage), 0);
155
+ legs.push({
156
+ agentId,
157
+ calls: sorted.length,
158
+ init_tokens: init,
159
+ work_tokens: work,
160
+ ratio: workUnits > 0 ? initUnits / workUnits : null,
161
+ });
162
+ }
163
+ return legs.sort((a, b) => (a.agentId < b.agentId ? -1 : 1));
164
+ }
165
+
166
+ interface AuditOrchestration {
167
+ rules_carried?: unknown;
168
+ rules_used?: unknown;
169
+ init_tokens?: unknown;
170
+ work_tokens?: unknown;
171
+ floor_provenance?: unknown;
172
+ agent_combo?: unknown;
173
+ route_taken?: unknown;
174
+ outcome?: unknown;
175
+ escalated?: unknown;
176
+ return_channel_chars?: unknown;
177
+ origin?: unknown;
178
+ }
179
+
180
+ function isFiniteNonNegInt(v: unknown): v is number {
181
+ return typeof v === 'number' && Number.isInteger(v) && v >= 0;
182
+ }
183
+
184
+ /**
185
+ * Read every orchestration object out of the audit dir's monthly JSONL files,
186
+ * honouring the same age window the transcript scan uses. Files are monthly,
187
+ * so the mtime filter (mtime updates on append) is the available granularity —
188
+ * a file whose LAST append is inside the window is read whole; per-line `ts`
189
+ * filtering then trims to the window exactly.
190
+ */
191
+ export function readAuditOrchestrations(
192
+ auditDir: string,
193
+ opts: { maxAgeDays?: number; now?: Date } = {},
194
+ ): AuditOrchestration[] {
195
+ if (!fs.existsSync(auditDir)) return [];
196
+ const out: AuditOrchestration[] = [];
197
+ const cutoffMs =
198
+ opts.maxAgeDays !== undefined
199
+ ? (opts.now ?? new Date()).getTime() - opts.maxAgeDays * 24 * 60 * 60 * 1000
200
+ : null;
201
+ let entries: string[];
202
+ try {
203
+ entries = fs.readdirSync(auditDir).filter((f) => f.endsWith('.jsonl')).sort();
204
+ } catch {
205
+ return [];
206
+ }
207
+ if (cutoffMs !== null) {
208
+ entries = entries.filter((f) => {
209
+ try {
210
+ return fs.statSync(path.join(auditDir, f)).mtimeMs >= cutoffMs;
211
+ } catch {
212
+ return false;
213
+ }
214
+ });
215
+ }
216
+ for (const f of entries) {
217
+ let text: string;
218
+ try {
219
+ text = fs.readFileSync(path.join(auditDir, f), 'utf8');
220
+ } catch {
221
+ continue;
222
+ }
223
+ for (const line of text.split('\n')) {
224
+ if (line.trim().length === 0) continue;
225
+ try {
226
+ const parsed = JSON.parse(line) as { orchestration?: AuditOrchestration; ts?: unknown };
227
+ if (!parsed.orchestration || typeof parsed.orchestration !== 'object') continue;
228
+ if (cutoffMs !== null && typeof parsed.ts === 'string') {
229
+ const t = Date.parse(parsed.ts);
230
+ if (Number.isFinite(t) && t < cutoffMs) continue; // outside the window
231
+ }
232
+ out.push(parsed.orchestration);
233
+ } catch {
234
+ continue;
235
+ }
236
+ }
237
+ }
238
+ return out;
239
+ }
240
+
241
+ export function buildReport(opts: {
242
+ root: string;
243
+ auditDir: string;
244
+ maxAgeDays: number;
245
+ now?: Date;
246
+ }): Report {
247
+ const registrationRaw = fs.readFileSync(REGISTRATION_FILE, 'utf8');
248
+ const registration = JSON.parse(registrationRaw) as {
249
+ registered_at: string;
250
+ review_by: string;
251
+ metrics: {
252
+ dispatch_floor: { thresholds: { projection_mandatory_ratio: number; success_ratio_after_projection: number } };
253
+ rules_efficiency: { thresholds: { low_quota_signal: number } };
254
+ ask_economy: { thresholds: { escalation_kill: number } };
255
+ return_channel: { thresholds: { envelope_cap_chars: number } };
256
+ };
257
+ };
258
+
259
+ const scanOpts: Parameters<typeof scanTranscripts>[0] = {
260
+ root: opts.root,
261
+ maxAgeDays: opts.maxAgeDays,
262
+ ...(opts.now !== undefined ? { now: opts.now } : {}),
263
+ };
264
+ const scan = scanTranscripts(scanOpts);
265
+ const legs = computeLegs(scan.records);
266
+ const finiteRatios = legs.map((l) => l.ratio).filter((r): r is number => r !== null);
267
+ const singleCall = legs.filter((l) => l.ratio === null).length;
268
+
269
+ const orchestrations = readAuditOrchestrations(opts.auditDir, {
270
+ maxAgeDays: opts.maxAgeDays,
271
+ ...(opts.now !== undefined ? { now: opts.now } : {}),
272
+ });
273
+ const reviewerMeasured: { init: number; work: number }[] = [];
274
+ const quotas: number[] = [];
275
+ let askLines = 0;
276
+ let askAdopted = 0;
277
+ let askEscalated = 0;
278
+ const returnChars: number[] = [];
279
+ for (const o of orchestrations) {
280
+ if (o.route_taken === 'ask') {
281
+ askLines += 1;
282
+ if (o.outcome === 'DONE') askAdopted += 1;
283
+ }
284
+ // Ask->spawn escalation is recorded on the CALLER's follow-up SPAWN
285
+ // line (route_taken=subagent, escalated=true, origin-tagged) — the
286
+ // ask line itself cannot know it was insufficient. Counting
287
+ // escalated on ask lines would make the registered kill criterion
288
+ // unmeasurable by construction (review finding, 2026-08-10).
289
+ if (o.route_taken === 'subagent' && o.escalated === true && o.origin === 'dispatch-economy-2026') {
290
+ askEscalated += 1;
291
+ }
292
+ if (isFiniteNonNegInt(o.return_channel_chars)) {
293
+ returnChars.push(o.return_channel_chars);
294
+ }
295
+ if (isFiniteNonNegInt(o.rules_carried) && isFiniteNonNegInt(o.rules_used) && o.rules_carried > 0) {
296
+ quotas.push(o.rules_used / o.rules_carried);
297
+ }
298
+ const isReviewer = Array.isArray(o.agent_combo) && o.agent_combo.some((t) => typeof t === 'string' && REVIEWER_TYPE_RE.test(t));
299
+ if (isReviewer && o.floor_provenance === 'measured' && isFiniteNonNegInt(o.init_tokens) && isFiniteNonNegInt(o.work_tokens)) {
300
+ reviewerMeasured.push({ init: o.init_tokens, work: o.work_tokens });
301
+ }
302
+ }
303
+
304
+ const workerFloor: RoleFloor = {
305
+ role: 'worker',
306
+ attribution: 'transcript-leg (role axis not shipped — all legs report as worker)',
307
+ legs: legs.length,
308
+ single_call_legs: singleCall,
309
+ median_init_tokens: median(legs.map((l) => l.init_tokens)),
310
+ median_ratio_finite: median(finiteRatios),
311
+ };
312
+ // Audit lines carry raw token counts only (no cache split), so the
313
+ // reviewer ratio is raw init/work — flagged via the attribution string.
314
+ const reviewerRatios = reviewerMeasured.filter((p) => p.work > 0).map((p) => p.init / p.work);
315
+ const reviewerFloor: RoleFloor = {
316
+ role: 'reviewer',
317
+ attribution: 'audit-line agent_combo',
318
+ legs: reviewerMeasured.length,
319
+ single_call_legs: reviewerMeasured.filter((p) => p.work === 0).length,
320
+ median_init_tokens: median(reviewerMeasured.map((p) => p.init)),
321
+ median_ratio_finite: median(reviewerRatios),
322
+ };
323
+
324
+ const t = registration.metrics.dispatch_floor.thresholds;
325
+ const lowQuota = registration.metrics.rules_efficiency.thresholds.low_quota_signal;
326
+ const medianQuota = median(quotas);
327
+ const medianWorkerRatio = workerFloor.median_ratio_finite;
328
+
329
+ const notes: string[] = [
330
+ `dedup: ${scan.totalSeen} raw assistant records → ${scan.dedupedCount} (replay inflation removed)`,
331
+ 'single_call_legs are floor-only legs (all init, zero work) — they are excluded from the finite-ratio median and support, never dilute, the projection-mandatory reading',
332
+ 'projection_mandatory_signal is a single-run reading; the registered verdict is a sustained-window call at review date (dispatch-economy-metrics.json)',
333
+ ];
334
+
335
+ return {
336
+ schema: 'dispatch-economy-report/v1',
337
+ registration: { registered_at: registration.registered_at, review_by: registration.review_by },
338
+ window: { max_age_days: opts.maxAgeDays, transcript_records: scan.dedupedCount, legs: legs.length },
339
+ dispatch_floor: {
340
+ roles: [workerFloor, reviewerFloor],
341
+ thresholds: t,
342
+ projection_mandatory_signal: medianWorkerRatio === null ? null : medianWorkerRatio > t.projection_mandatory_ratio,
343
+ },
344
+ rules_efficiency: {
345
+ envelopes_with_pair: quotas.length,
346
+ median_quota: medianQuota,
347
+ threshold_low_quota: lowQuota,
348
+ low_quota_signal: medianQuota === null ? null : medianQuota < lowQuota,
349
+ },
350
+ ask_economy: {
351
+ ask_lines: askLines,
352
+ adopted: askAdopted,
353
+ escalated: askEscalated,
354
+ threshold_escalation_kill: registration.metrics.ask_economy.thresholds.escalation_kill,
355
+ escalation_kill_signal:
356
+ askLines === 0
357
+ ? null
358
+ : askEscalated / askLines > registration.metrics.ask_economy.thresholds.escalation_kill,
359
+ },
360
+ return_channel: {
361
+ lines_with_field: returnChars.length,
362
+ median_chars: median(returnChars),
363
+ envelope_cap_chars: registration.metrics.return_channel.thresholds.envelope_cap_chars,
364
+ cap_breach_signal:
365
+ returnChars.length === 0
366
+ ? null
367
+ : (median(returnChars) ?? 0) > registration.metrics.return_channel.thresholds.envelope_cap_chars,
368
+ },
369
+ notes,
370
+ };
371
+ }
372
+
373
+ function fmtTokens(n: number | null): string {
374
+ if (n === null) return '—';
375
+ return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(Math.round(n));
376
+ }
377
+
378
+ function renderText(r: Report): string {
379
+ const lines: string[] = [];
380
+ lines.push('dispatch-economy report (registered: ' + r.registration.registered_at + ' · review by: ' + r.registration.review_by + ')');
381
+ lines.push(`window: ${r.window.max_age_days}d · ${r.window.transcript_records} deduped records · ${r.window.legs} legs`);
382
+ lines.push('');
383
+ lines.push('dispatch_floor:');
384
+ for (const role of r.dispatch_floor.roles) {
385
+ lines.push(
386
+ ` ${role.role.padEnd(8)} legs=${role.legs} (single-call: ${role.single_call_legs}) · median init=${fmtTokens(role.median_init_tokens)} · median init/work=${role.median_ratio_finite === null ? '—' : role.median_ratio_finite.toFixed(2)}`,
387
+ );
388
+ lines.push(` attribution: ${role.attribution}`);
389
+ }
390
+ lines.push(
391
+ ` projection-mandatory signal (ratio > ${r.dispatch_floor.thresholds.projection_mandatory_ratio}): ${r.dispatch_floor.projection_mandatory_signal === null ? 'no data' : String(r.dispatch_floor.projection_mandatory_signal)}`,
392
+ );
393
+ lines.push('');
394
+ lines.push('rules_efficiency:');
395
+ lines.push(
396
+ ` envelopes with pair=${r.rules_efficiency.envelopes_with_pair} · median quota=${r.rules_efficiency.median_quota === null ? '—' : r.rules_efficiency.median_quota.toFixed(2)} · low-quota signal (< ${r.rules_efficiency.threshold_low_quota}): ${r.rules_efficiency.low_quota_signal === null ? 'no data' : String(r.rules_efficiency.low_quota_signal)}`,
397
+ );
398
+ lines.push('');
399
+ lines.push('return_channel:');
400
+ lines.push(
401
+ ` lines=${r.return_channel.lines_with_field} · median chars=${r.return_channel.median_chars === null ? '—' : Math.round(r.return_channel.median_chars)} · cap-breach signal (> ${r.return_channel.envelope_cap_chars}): ${r.return_channel.cap_breach_signal === null ? 'no data' : String(r.return_channel.cap_breach_signal)}`,
402
+ );
403
+ lines.push('');
404
+ lines.push('ask_economy:');
405
+ lines.push(
406
+ ` ask lines=${r.ask_economy.ask_lines} · adopted=${r.ask_economy.adopted} · escalated=${r.ask_economy.escalated} · escalation-kill signal (> ${r.ask_economy.threshold_escalation_kill}): ${r.ask_economy.escalation_kill_signal === null ? 'no data' : String(r.ask_economy.escalation_kill_signal)}`,
407
+ );
408
+ lines.push('');
409
+ for (const n of r.notes) lines.push(`note: ${n}`);
410
+ return lines.join('\n') + '\n';
411
+ }
412
+
413
+ export function main(argv: readonly string[]): number {
414
+ let format: 'text' | 'json' = 'text';
415
+ let root = DEFAULT_PROJECTS_ROOT;
416
+ let auditDir = path.join(process.cwd(), DEFAULT_AUDIT_DIR);
417
+ let maxAgeDays = 14;
418
+ for (let i = 0; i < argv.length; i++) {
419
+ const a = argv[i];
420
+ if (a === '--format') format = argv[++i] === 'json' ? 'json' : 'text';
421
+ else if (a === '--root') root = argv[++i] ?? root;
422
+ else if (a === '--audit-dir') auditDir = argv[++i] ?? auditDir;
423
+ else if (a === '--max-age-days') maxAgeDays = Number(argv[++i] ?? maxAgeDays) || maxAgeDays;
424
+ }
425
+ let report: Report;
426
+ try {
427
+ report = buildReport({ root, auditDir, maxAgeDays });
428
+ } catch (err) {
429
+ process.stderr.write(`dispatch_economy_report: ${err instanceof Error ? err.message : String(err)}\n`);
430
+ return 2;
431
+ }
432
+ process.stdout.write(format === 'json' ? JSON.stringify(report, null, 2) + '\n' : renderText(report));
433
+ return 0;
434
+ }
435
+
436
+ declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
437
+ function _isCliEntry(): boolean {
438
+ if (typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__) return false;
439
+ if (process.argv[1] === undefined) return false;
440
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
441
+ if (import.meta.url === argvUrl) return true;
442
+ try {
443
+ return fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(process.argv[1]));
444
+ } catch {
445
+ return false;
446
+ }
447
+ }
448
+ if (_isCliEntry()) process.exit(main(process.argv.slice(2)));
@@ -350,7 +350,10 @@ export function extractAcceptanceCriteria(roadmapText: string): string {
350
350
  const lines = roadmapText.split('\n');
351
351
  let start = -1;
352
352
  for (let i = 0; i < lines.length; i++) {
353
- if (/^## Acceptance Criteria\s*$/.test(lines[i] as string)) {
353
+ // Case-insensitive: the tree carries both `## Acceptance Criteria` and
354
+ // `## Acceptance criteria`; a case-sensitive match silently extracted
355
+ // nothing for the latter (found by the zcs-close R2 review, 2026-08-09).
356
+ if (/^## acceptance criteria\s*$/i.test(lines[i] as string)) {
354
357
  start = i;
355
358
  break;
356
359
  }
@@ -924,7 +927,17 @@ function runDispatch(args: Args): number {
924
927
  fs.mkdirSync(inputDirAbs, { recursive: true });
925
928
  fs.writeFileSync(path.join(inputDirAbs, 'diff.patch'), scopeDiffText, 'utf-8');
926
929
  if (roadmapText !== null) {
927
- fs.writeFileSync(path.join(inputDirAbs, 'roadmap.md'), roadmapText, 'utf-8');
930
+ // The snapshot lands under agents/evidence/, which check_references
931
+ // walks, while the live roadmap layer is deliberately excluded from
932
+ // that gate — so a roadmap that legitimately quotes a nonexistent
933
+ // path (e.g. documenting a hallucinated council citation) would red
934
+ // CI through its own review snapshot. The header exemption keeps the
935
+ // snapshot verbatim below the marker; roadmap_hash binds the LIVE
936
+ // file and is unaffected. Found by the zcs-close CI run, 2026-08-09.
937
+ const snapshotHeader =
938
+ '<!-- check-refs: skip -->\n' +
939
+ '<!-- verbatim roadmap snapshot for the R2 reviewer; the live roadmap layer is excluded from check_references, and a snapshot must not fail a gate its source is exempt from -->\n';
940
+ fs.writeFileSync(path.join(inputDirAbs, 'roadmap.md'), snapshotHeader + roadmapText, 'utf-8');
928
941
  }
929
942
  fs.writeFileSync(path.join(inputDirAbs, 'acceptance-criteria.md'), acText ?? '', 'utf-8');
930
943
  fs.writeFileSync(path.join(inputDirAbs, 'prompt.md'), promptText, 'utf-8');