@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +32 -214
- package/README.md +1 -0
- package/dist/agent-src/commands/analyze/inbox.md +22 -1
- package/dist/agent-src/commands/explain-run.md +31 -1
- package/dist/agent-src/commands/team/delegate.md +1 -0
- package/dist/agent-src/contexts/execution/orchestration-telemetry.md +4 -1
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +17 -0
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +11 -1
- package/dist/agent-src/rules/preservation-guard.md +9 -0
- package/dist/agent-src/scripts/roadmap_gates.ts +312 -0
- package/dist/agent-src/scripts/update_roadmap_progress.ts +72 -4
- package/dist/agent-src/skills/subagent-orchestration/SKILL.md +10 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/registry.js +2 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-RCIA67XN.js → chunk-2WCLVDWC.js} +45 -2
- package/dist/cli-delegate/{chunk-66OVWGB5.js → chunk-AG47LAGQ.js} +1 -1
- package/dist/cli-delegate/{chunk-MP3JT3IU.js → chunk-DS6BLDMG.js} +1 -1
- package/dist/cli-delegate/chunk-EJITJBJX.js +172 -0
- package/dist/cli-delegate/{chunk-NZ5N2XQL.js → chunk-ITIQSGOQ.js} +3 -3
- package/dist/cli-delegate/{chunk-LCFJIQ4J.js → chunk-JEJABGKB.js} +110 -6
- package/dist/cli-delegate/chunk-MMJL4TIX.js +18 -0
- package/dist/cli-delegate/{chunk-REKCMYOU.js → chunk-PFBV6XRT.js} +36 -20
- package/dist/cli-delegate/{chunk-OJM3MBJM.js → chunk-QEEB4LHO.js} +22 -35
- package/dist/cli-delegate/chunk-RVGBH4XK.js +142 -0
- package/dist/cli-delegate/{chunk-MMXA5GY7.js → chunk-UVKNIJH2.js} +14 -0
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +1 -1
- package/dist/cli-delegate/cmd_conformance.js +2861 -96
- package/dist/cli-delegate/cmd_doctor.js +3 -2
- package/dist/cli-delegate/cmd_explain.js +2 -2
- package/dist/cli-delegate/cmd_export.js +4 -4
- package/dist/cli-delegate/cmd_handoff.js +87 -7
- package/dist/cli-delegate/cmd_mcp_available.js +1 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +4 -4
- package/dist/cli-delegate/cmd_route_audit.js +3 -2
- package/dist/cli-delegate/cmd_route_explain.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +177 -0
- package/dist/cli-delegate/cmd_settings_get.js +3 -56
- package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
- package/dist/cli-delegate/cmd_settings_set.js +2 -2
- package/dist/cli-delegate/cmd_sync.js +4 -4
- package/dist/cli-delegate/cmd_uninstall.js +4 -4
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +4 -4
- package/dist/cli-delegate/cmd_versions.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +7 -7
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +2143 -641
- package/dist/install/install.mjs +133 -14
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +3967 -761
- package/dist/scripts/_lib/agent_settings.js +111 -2
- package/dist/scripts/_lib/agent_settings.js.map +1 -1
- package/dist/server/schemas/settings.js +6 -0
- package/dist/server/schemas/settings.js.map +1 -1
- package/dist/ui/assets/index-B5qGYOJo.js +26 -0
- package/dist/ui/assets/index-B5qGYOJo.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/docs/contracts/settings-classes.md +21 -5
- package/package.json +1 -1
- package/src/agent-src/scripts/roadmap_gates.ts +312 -0
- package/src/agent-src/scripts/update_roadmap_progress.ts +72 -4
- package/src/config/agent-settings.template.yml +36 -0
- package/src/config/dispatch-economy-metrics.json +61 -0
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/hook-token-budget.json +91 -0
- package/src/config/pack-size-budget.json +3 -2
- package/src/config/recycle-threshold-budget.json +12 -0
- package/src/config/rule-enforcement-baseline.json +86 -0
- package/src/scripts/_cli/cmd_conformance.ts +17 -2
- package/src/scripts/_cli/cmd_session_recycle.ts +226 -0
- package/src/scripts/_cli/explain_last/inputs.ts +13 -0
- package/src/scripts/_dispatch.bash +23 -0
- package/src/scripts/_lib/agent_settings.ts +119 -2
- package/src/scripts/_lib/carrier_divergence.ts +47 -0
- package/src/scripts/_lib/judgment_ladder.ts +254 -2
- package/src/scripts/_lib/orchestration_record.ts +41 -10
- package/src/scripts/_lib/recycle_envelope_paths.ts +35 -0
- package/src/scripts/_lib/self_repair.ts +194 -0
- package/src/scripts/_lib/session_eol.ts +230 -0
- package/src/scripts/_lib/session_role.ts +42 -0
- package/src/scripts/_lib/subagent_capsule.ts +159 -0
- package/src/scripts/_lib/subagent_response.ts +75 -0
- package/src/scripts/ai_council/clients.ts +50 -1
- package/src/scripts/ask_transport.ts +221 -0
- package/src/scripts/bench_hook_injection.ts +234 -0
- package/src/scripts/check_adr_frontmatter.ts +84 -40
- package/src/scripts/check_branch_freshness.ts +1 -0
- package/src/scripts/check_corpus_staleness.ts +18 -4
- package/src/scripts/check_detector_corpus.ts +17 -0
- package/src/scripts/check_gitignore_freshness.ts +42 -8
- package/src/scripts/check_hook_bundle_freshness.ts +20 -0
- package/src/scripts/check_kernel_prefix_stability.ts +1 -0
- package/src/scripts/check_standing_rule_delivery.ts +73 -32
- package/src/scripts/check_static_layer_stability.ts +154 -0
- package/src/scripts/check_suppression_hygiene.ts +48 -0
- package/src/scripts/dispatch_economy_report.ts +448 -0
- package/src/scripts/dispatch_r2_reviewer.ts +15 -2
- package/src/scripts/explain_run.ts +206 -4
- package/src/scripts/generate_settings_reference.ts +1 -0
- package/src/scripts/handoff_context_hook.ts +112 -4
- package/src/scripts/hook_manifest.yaml +117 -10
- package/src/scripts/hooks/concern_registry.ts +8 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +40 -3
- package/src/scripts/hooks/dispatch_hook.ts +39 -2
- package/src/scripts/hooks/edit_shape_hook.ts +303 -0
- package/src/scripts/hooks/orchestration_record_hook.ts +19 -1
- package/src/scripts/hooks/reread_guard_hook.ts +314 -0
- package/src/scripts/hooks/rtk_wrap_hook.ts +133 -14
- package/src/scripts/hooks/session_eol_hook.ts +268 -0
- package/src/scripts/hooks/turn_end_gate_hook.ts +715 -0
- package/src/scripts/install-hooks.sh +1 -1
- package/src/scripts/language_mirror_hook.ts +143 -1
- package/src/scripts/lint_decision_impact.ts +14 -1
- package/src/scripts/lint_hook_manifest.ts +82 -0
- package/src/scripts/lint_readme_serial_comma.ts +1 -0
- package/src/scripts/lint_rule_enforcement_declaration.ts +347 -0
- package/src/scripts/measure_turn_end_gate.ts +272 -0
- package/src/scripts/orchestration_record.ts +4 -1
- package/src/scripts/rebuild_hook_bundle.ts +1 -0
- package/src/scripts/report_carrier_divergence.ts +72 -24
- package/src/scripts/report_conformance_funnel.ts +291 -0
- package/src/scripts/report_imperative_density.ts +1 -0
- package/src/scripts/routing_doctor.ts +97 -0
- package/src/scripts/self_repair_cli.ts +51 -3
- package/src/scripts/session_eol_report.ts +261 -0
- package/src/server/schemas/settings.ts +14 -0
- package/src/templates/compact-instructions.md +40 -0
- package/dist/ui/assets/index-JDAF00bC.js +0 -26
- package/dist/ui/assets/index-JDAF00bC.js.map +0 -1
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Static-layer stability lint (road-to-token-economy-cache Phase 2.2 — the
|
|
4
|
+
* hygiene half the Phase-1 null left standing; spike note:
|
|
5
|
+
* agents/settings/contexts/cache-injection-anatomy.md).
|
|
6
|
+
*
|
|
7
|
+
* The always-loaded layer is re-sent on every session and every spawn; a
|
|
8
|
+
* MACHINE-VOLATILE marker inside it (an absolute local path naming a real
|
|
9
|
+
* user, a session/run-id shape) is both a cache-stability hazard and a
|
|
10
|
+
* portability leak. Scope is deliberately NARROW and honest:
|
|
11
|
+
*
|
|
12
|
+
* - scanned: dist/agent-src/rules/*.md EXCLUDING the kernel set (the
|
|
13
|
+
* kernel prefix has its own byte-drift guard,
|
|
14
|
+
* check_kernel_prefix_stability — no double coverage, roadmap Risk 8)
|
|
15
|
+
* - flagged: absolute home paths (/Users/<name>/, /home/<name>/,
|
|
16
|
+
* C:\Users\<name>\) and UUID-shaped run/session ids
|
|
17
|
+
* - NOT flagged: dates in prose (stable content, not build-volatile) —
|
|
18
|
+
* build-time volatility is already impossible for .md by the ADR-201
|
|
19
|
+
* byte-exact projection (dist == rewrite(src)); this lint catches what
|
|
20
|
+
* an AUTHOR pastes, which the projection faithfully preserves.
|
|
21
|
+
*
|
|
22
|
+
* Exit 0 green · 1 findings · 2 unusable. Dead-scope guarded via
|
|
23
|
+
* assertScanned. Self-test: tests/scripts/check_static_layer_stability.test.ts
|
|
24
|
+
* proves red on a fixture carrying a home path and a UUID.
|
|
25
|
+
*/
|
|
26
|
+
import * as fs from 'node:fs';
|
|
27
|
+
import * as path from 'node:path';
|
|
28
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
29
|
+
|
|
30
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
31
|
+
|
|
32
|
+
declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
|
|
33
|
+
const _IN_BUNDLE = typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__;
|
|
34
|
+
const REPO_ROOT = path.resolve(
|
|
35
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
36
|
+
...(_IN_BUNDLE ? ['..'] : ['..', '..']),
|
|
37
|
+
);
|
|
38
|
+
const RULES_DIR = path.join(REPO_ROOT, 'dist', 'agent-src', 'rules');
|
|
39
|
+
const ROUTER = path.join(REPO_ROOT, 'dist', 'router.json');
|
|
40
|
+
|
|
41
|
+
/** Machine-volatile shapes. Home paths anchor on the OS home prefix; the
|
|
42
|
+
* UUID shape requires all five groups so ordinary hex constants stay quiet. */
|
|
43
|
+
const HOME_PATH_RE = /(?:\/(?:Users|home)\/[A-Za-z][\w.-]*\/|[A-Za-z]:\\Users\\[A-Za-z][\w.-]*\\)/;
|
|
44
|
+
const UUID_RE = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i;
|
|
45
|
+
/** Per-line opt-out for a deliberate, justified exception. */
|
|
46
|
+
const IGNORE_MARKER = 'static-layer-allow';
|
|
47
|
+
|
|
48
|
+
export interface Finding {
|
|
49
|
+
file: string;
|
|
50
|
+
line: number;
|
|
51
|
+
kind: 'home-path' | 'uuid';
|
|
52
|
+
excerpt: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function kernelBasenames(routerPath: string): Set<string> {
|
|
56
|
+
try {
|
|
57
|
+
const router = JSON.parse(fs.readFileSync(routerPath, 'utf8')) as { kernel?: unknown };
|
|
58
|
+
if (Array.isArray(router.kernel)) {
|
|
59
|
+
return new Set(
|
|
60
|
+
router.kernel
|
|
61
|
+
.filter((k): k is string => typeof k === 'string')
|
|
62
|
+
.map((k) => (k.endsWith('.md') ? k : `${k}.md`)),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
/* fall through — an unreadable router means we scan everything (fail toward coverage) */
|
|
67
|
+
}
|
|
68
|
+
return new Set();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function scan(opts: { rulesDir?: string; routerPath?: string } = {}): { findings: Finding[]; scanned: number } {
|
|
72
|
+
const rulesDir = opts.rulesDir ?? RULES_DIR;
|
|
73
|
+
const kernel = kernelBasenames(opts.routerPath ?? ROUTER);
|
|
74
|
+
const findings: Finding[] = [];
|
|
75
|
+
let scanned = 0;
|
|
76
|
+
const files = fs.existsSync(rulesDir) ? fs.readdirSync(rulesDir).filter((f) => f.endsWith('.md')) : [];
|
|
77
|
+
for (const f of files) {
|
|
78
|
+
if (kernel.has(f)) continue; // kernel prefix has its own drift guard
|
|
79
|
+
scanned += 1;
|
|
80
|
+
const text = fs.readFileSync(path.join(rulesDir, f), 'utf8');
|
|
81
|
+
const lines = text.split('\n');
|
|
82
|
+
for (let i = 0; i < lines.length; i++) {
|
|
83
|
+
const line = lines[i] ?? '';
|
|
84
|
+
if (line.includes(IGNORE_MARKER)) continue;
|
|
85
|
+
if (HOME_PATH_RE.test(line)) {
|
|
86
|
+
findings.push({ file: f, line: i + 1, kind: 'home-path', excerpt: line.trim().slice(0, 120) });
|
|
87
|
+
} else if (UUID_RE.test(line)) {
|
|
88
|
+
findings.push({ file: f, line: i + 1, kind: 'uuid', excerpt: line.trim().slice(0, 120) });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { findings, scanned };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function main(argv: readonly string[]): number {
|
|
96
|
+
let rulesDir: string | undefined;
|
|
97
|
+
let routerPath: string | undefined;
|
|
98
|
+
for (let i = 0; i < argv.length; i++) {
|
|
99
|
+
if (argv[i] === '--rules-dir') rulesDir = argv[++i];
|
|
100
|
+
else if (argv[i] === '--router') routerPath = argv[++i];
|
|
101
|
+
}
|
|
102
|
+
const opts: Parameters<typeof scan>[0] = {
|
|
103
|
+
...(rulesDir !== undefined ? { rulesDir } : {}),
|
|
104
|
+
...(routerPath !== undefined ? { routerPath } : {}),
|
|
105
|
+
};
|
|
106
|
+
let result: ReturnType<typeof scan>;
|
|
107
|
+
try {
|
|
108
|
+
result = scan(opts);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
process.stderr.write(`check_static_layer_stability: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
111
|
+
return 2;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
assertScanned({
|
|
115
|
+
gate: 'check_static_layer_stability',
|
|
116
|
+
scanned: result.scanned,
|
|
117
|
+
units: 'non-kernel always-loaded rule file(s)',
|
|
118
|
+
roots: [rulesDir ?? RULES_DIR],
|
|
119
|
+
});
|
|
120
|
+
} catch (exc) {
|
|
121
|
+
if (exc instanceof DeadScopeError) {
|
|
122
|
+
process.stderr.write(`error: ${exc.message}\n`);
|
|
123
|
+
return 1;
|
|
124
|
+
}
|
|
125
|
+
throw exc;
|
|
126
|
+
}
|
|
127
|
+
for (const f of result.findings) {
|
|
128
|
+
process.stderr.write(
|
|
129
|
+
`❌ ${f.file}:${f.line} — ${f.kind} in the always-loaded layer (machine-volatile marker; re-sent on every spawn): ${f.excerpt}\n`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
if (result.findings.length > 0) {
|
|
133
|
+
process.stderr.write(
|
|
134
|
+
`check_static_layer_stability: ${result.findings.length} finding(s) across ${result.scanned} scanned file(s). ` +
|
|
135
|
+
`A deliberate exception carries '${IGNORE_MARKER}' on the line with its reason.\n`,
|
|
136
|
+
);
|
|
137
|
+
return 1;
|
|
138
|
+
}
|
|
139
|
+
process.stdout.write(`✅ check_static_layer_stability: ${result.scanned} non-kernel always-loaded file(s) clean\n`);
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function _isCliEntry(): boolean {
|
|
144
|
+
if (_IN_BUNDLE) return false;
|
|
145
|
+
if (process.argv[1] === undefined) return false;
|
|
146
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
147
|
+
if (import.meta.url === argvUrl) return true;
|
|
148
|
+
try {
|
|
149
|
+
return fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(process.argv[1]));
|
|
150
|
+
} catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (_isCliEntry()) process.exit(main(process.argv.slice(2)));
|
|
@@ -117,6 +117,24 @@ export interface SuppressionSpec {
|
|
|
117
117
|
* migration to content anchors visible instead of aspirational.
|
|
118
118
|
*/
|
|
119
119
|
driftKey?: { positionField: string; anchorField: string };
|
|
120
|
+
/**
|
|
121
|
+
* Accept this baseline as absent at the base ref — for the ONE change that
|
|
122
|
+
* introduces it, then removed.
|
|
123
|
+
*
|
|
124
|
+
* Without it a brand-new baseline is a permanent red: the ratchet compares
|
|
125
|
+
* against `origin/main`, where the file does not exist yet, and
|
|
126
|
+
* `ratchet_base_ref` is deliberately fail-closed there because
|
|
127
|
+
* absent-at-base and mistyped-path look identical from inside. Setting it
|
|
128
|
+
* makes the verdict `new_baseline` rather than growth, so the bootstrap
|
|
129
|
+
* entries are not each reported as a new hole.
|
|
130
|
+
*
|
|
131
|
+
* Remove the flag once the introducing commit is an ancestor of the base
|
|
132
|
+
* ref — leaving it set would silently accept a later mistyped path. That
|
|
133
|
+
* removal is **enforced, not remembered**: the scan below fails when the
|
|
134
|
+
* flag is set and the baseline already resolves at the base ref, so the
|
|
135
|
+
* flag closes itself one merge after it was needed.
|
|
136
|
+
*/
|
|
137
|
+
newInThisChange?: boolean;
|
|
120
138
|
/** Why this list exists at all — printed with the count, so absence is visible. */
|
|
121
139
|
what: string;
|
|
122
140
|
}
|
|
@@ -199,6 +217,19 @@ export const SUPPRESSION_INVENTORY: readonly SuppressionSpec[] = [
|
|
|
199
217
|
growth: 'requires_falsifier',
|
|
200
218
|
what: 'per-gate known-debt counts',
|
|
201
219
|
},
|
|
220
|
+
{
|
|
221
|
+
// The root IS the list — a bare array of rule filenames, so entries are
|
|
222
|
+
// content-keyed by construction and cannot drift on line numbers.
|
|
223
|
+
// `forbidden` growth is the whole point: this baseline exists so a NEW
|
|
224
|
+
// rule cannot join the silent set, and `lint_rule_enforcement_declaration`
|
|
225
|
+
// additionally fails when an entry stops being undeclared, so the list is
|
|
226
|
+
// shrink-only from both directions.
|
|
227
|
+
file: 'src/config/rule-enforcement-baseline.json',
|
|
228
|
+
listKey: null,
|
|
229
|
+
tier: 'string_list',
|
|
230
|
+
newInThisChange: true,
|
|
231
|
+
what: 'rules predating the enforced_by-declaration ratchet',
|
|
232
|
+
},
|
|
202
233
|
];
|
|
203
234
|
|
|
204
235
|
export interface SuppressionFinding {
|
|
@@ -367,7 +398,24 @@ export function main(): number {
|
|
|
367
398
|
baseRef,
|
|
368
399
|
repoRoot: REPO_ROOT,
|
|
369
400
|
entriesOf: (parsed) => entriesOfSpec(spec, parsed),
|
|
401
|
+
...(spec.newInThisChange === true ? { allowNewBaseline: true } : {}),
|
|
370
402
|
});
|
|
403
|
+
// Self-closing bootstrap flag. `newInThisChange` exists for the ONE
|
|
404
|
+
// change that introduces a baseline, and the comment on the field says
|
|
405
|
+
// to remove it afterwards — which is exactly the kind of instruction
|
|
406
|
+
// that gets forgotten, because nothing goes red when it is ignored and
|
|
407
|
+
// a stale flag silently accepts a later mistyped path. So the check
|
|
408
|
+
// closes itself: once the baseline resolves at the base ref, the
|
|
409
|
+
// verdict is no longer `new_baseline`, and keeping the flag is a
|
|
410
|
+
// finding rather than a note in someone's follow-up list.
|
|
411
|
+
if (spec.newInThisChange === true && comparison.verdict !== 'new_baseline') {
|
|
412
|
+
const detail =
|
|
413
|
+
`\`newInThisChange\` is still set, but the baseline now resolves at ${baseRef} — ` +
|
|
414
|
+
'the flag has served its purpose and must be removed from SUPPRESSION_INVENTORY. ' +
|
|
415
|
+
'Leaving it set makes a future mistyped path pass as a new baseline forever.';
|
|
416
|
+
ledger.fail(spec.file, detail);
|
|
417
|
+
findings.push({ file: spec.file, entry: '(whole file)', kind: 'uninventoried', detail });
|
|
418
|
+
}
|
|
371
419
|
} catch (exc) {
|
|
372
420
|
if (exc instanceof BaseRefUnavailableError) {
|
|
373
421
|
ledger.fail(spec.file, exc.message);
|
|
@@ -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)));
|