@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
|
@@ -21,12 +21,38 @@ export interface Finding {
|
|
|
21
21
|
mutating?: boolean;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
// ── Committed envelope size caps (road-to-token-economy-dispatch Phase 6.1) ──
|
|
25
|
+
// The envelope is the ONLY return channel: a worker's full result lands on
|
|
26
|
+
// disk (runtime artifact dir, gitignored) and the envelope carries paths +
|
|
27
|
+
// verdict + a BOUNDED summary. Without caps, transcript-shaped output flows
|
|
28
|
+
// back into the orchestrator context and refunds the isolation win — the
|
|
29
|
+
// measured spawn floor is ~251k tokens; a capped envelope is ≤ ~3k. Caps are
|
|
30
|
+
// validator ERRORS, never silent truncation (never-silent discipline): an
|
|
31
|
+
// oversized envelope is rejected loudly and the author shortens it.
|
|
32
|
+
/** Max chars for the summary — bounded prose, not a transcript. */
|
|
33
|
+
export const MAX_SUMMARY_CHARS = 2000;
|
|
34
|
+
/** Max chars for one line-shaped field (handoff, a risk, a finding title). */
|
|
35
|
+
export const MAX_RESPONSE_LINE_CHARS = 240;
|
|
36
|
+
/** Max entries per array (findings, risks, artifact_paths) — mirrors the capsule. */
|
|
37
|
+
export const MAX_RESPONSE_ENTRIES = 40;
|
|
38
|
+
/** Max chars for one artifact path ref. Mirrors the capsule's ref cap. */
|
|
39
|
+
export const MAX_ARTIFACT_REF_CHARS = 200;
|
|
40
|
+
/** Committed max for the WHOLE serialized envelope (~3k tokens ceiling). */
|
|
41
|
+
export const MAX_ENVELOPE_CHARS = 12000;
|
|
42
|
+
|
|
24
43
|
export interface SubagentResponse {
|
|
25
44
|
summary: string;
|
|
26
45
|
findings: Finding[];
|
|
27
46
|
risks: string[];
|
|
28
47
|
confidence: Confidence;
|
|
29
48
|
handoff: string;
|
|
49
|
+
/**
|
|
50
|
+
* Where the worker's FULL results live on disk (runtime artifact dir,
|
|
51
|
+
* gitignored) — the orchestrator consumes from these paths on demand,
|
|
52
|
+
* never via wholesale transcript ingestion (Phase 6.1/6.2). Ref tokens
|
|
53
|
+
* only, capped like every other array.
|
|
54
|
+
*/
|
|
55
|
+
artifact_paths?: string[];
|
|
30
56
|
/**
|
|
31
57
|
* Premises the worker acted on, `{statement, basis, epistemic_state}` —
|
|
32
58
|
* the same shape and the same validator as the CHECKPOINT capsule
|
|
@@ -74,6 +100,55 @@ export function validateResponse(input: unknown): ValidationResult {
|
|
|
74
100
|
}
|
|
75
101
|
}
|
|
76
102
|
}
|
|
103
|
+
// ── Committed size caps (Phase 6.1) — errors, never silent truncation ──
|
|
104
|
+
if (typeof r.summary === 'string' && r.summary.length > MAX_SUMMARY_CHARS) {
|
|
105
|
+
errors.push(`summary is ${r.summary.length} chars (max ${MAX_SUMMARY_CHARS}) — the envelope carries a bounded summary; the full result belongs on disk (artifact_paths)`);
|
|
106
|
+
}
|
|
107
|
+
if (typeof r.handoff === 'string' && r.handoff.length > MAX_RESPONSE_LINE_CHARS) {
|
|
108
|
+
errors.push(`handoff is ${r.handoff.length} chars (max ${MAX_RESPONSE_LINE_CHARS})`);
|
|
109
|
+
}
|
|
110
|
+
for (const [key, arr] of [
|
|
111
|
+
['findings', r.findings],
|
|
112
|
+
['risks', r.risks],
|
|
113
|
+
['artifact_paths', r.artifact_paths],
|
|
114
|
+
] as const) {
|
|
115
|
+
if (Array.isArray(arr) && arr.length > MAX_RESPONSE_ENTRIES) {
|
|
116
|
+
errors.push(`${key} carries ${arr.length} entries (max ${MAX_RESPONSE_ENTRIES}) — an envelope is a handoff, not a transcript`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (Array.isArray(r.risks)) {
|
|
120
|
+
for (const [i, risk] of (r.risks as unknown[]).entries()) {
|
|
121
|
+
if (typeof risk === 'string' && risk.length > MAX_RESPONSE_LINE_CHARS) {
|
|
122
|
+
errors.push(`risks[${i}] is ${risk.length} chars (max ${MAX_RESPONSE_LINE_CHARS})`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (Array.isArray(r.findings)) {
|
|
127
|
+
for (const [i, f] of (r.findings as Finding[]).entries()) {
|
|
128
|
+
if (f && typeof f.title === 'string' && f.title.length > MAX_RESPONSE_LINE_CHARS) {
|
|
129
|
+
errors.push(`finding[${i}] title is ${f.title.length} chars (max ${MAX_RESPONSE_LINE_CHARS})`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (r.artifact_paths !== undefined) {
|
|
134
|
+
if (!Array.isArray(r.artifact_paths)) {
|
|
135
|
+
errors.push('artifact_paths must be an array of path ref tokens');
|
|
136
|
+
} else {
|
|
137
|
+
for (const [i, p] of (r.artifact_paths as unknown[]).entries()) {
|
|
138
|
+
if (typeof p !== 'string' || p.includes('\n') || p.length > MAX_ARTIFACT_REF_CHARS) {
|
|
139
|
+
errors.push(`artifact_paths[${i}] must be a single-line path ref ≤ ${MAX_ARTIFACT_REF_CHARS} chars`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const serialized = JSON.stringify(input);
|
|
146
|
+
if (serialized.length > MAX_ENVELOPE_CHARS) {
|
|
147
|
+
errors.push(`envelope serializes to ${serialized.length} chars (max ${MAX_ENVELOPE_CHARS}) — move content to disk and reference it via artifact_paths`);
|
|
148
|
+
}
|
|
149
|
+
} catch {
|
|
150
|
+
errors.push('envelope is not JSON-serializable');
|
|
151
|
+
}
|
|
77
152
|
return { valid: errors.length === 0, errors };
|
|
78
153
|
}
|
|
79
154
|
|
|
@@ -45,6 +45,37 @@ import * as fs from 'node:fs';
|
|
|
45
45
|
import * as path from 'node:path';
|
|
46
46
|
|
|
47
47
|
import { hardenedSpawnEnv } from '../_lib/spawn_env.js';
|
|
48
|
+
import { SESSION_ROLE_ENV } from '../_lib/session_role.js';
|
|
49
|
+
import { load_agent_settings } from '../_lib/agent_settings.js';
|
|
50
|
+
|
|
51
|
+
// Session-wide subagent model ceiling (token-economy-dispatch Phase 5.2):
|
|
52
|
+
// class-C setting, default absent, HUMAN-set only. When present, the CLI
|
|
53
|
+
// spawn below exports CLAUDE_CODE_SUBAGENT_MODEL so the spawned vendor
|
|
54
|
+
// session caps its own subagents' models. Memoised PER CWD, not per
|
|
55
|
+
// process (review finding 2026-08-10): a long-lived server process may
|
|
56
|
+
// serve several projects, and a single-slot memo would apply project A's
|
|
57
|
+
// spend cap to project B's spawns. A human edit still needs a process
|
|
58
|
+
// restart to be seen — acceptable for a per-install cap, stated here.
|
|
59
|
+
const _modelCeilingMemo = new Map<string, string | null>();
|
|
60
|
+
export function _subagentModelCeiling(cwd: string = process.cwd()): string | null {
|
|
61
|
+
const hit = _modelCeilingMemo.get(cwd);
|
|
62
|
+
if (hit !== undefined) return hit;
|
|
63
|
+
let ceiling: string | null;
|
|
64
|
+
try {
|
|
65
|
+
const settings = load_agent_settings({ cwd }) as Record<string, unknown>;
|
|
66
|
+
const sub = (settings['subagents'] ?? {}) as Record<string, unknown>;
|
|
67
|
+
const v = typeof sub['model_ceiling'] === 'string' ? sub['model_ceiling'].trim() : '';
|
|
68
|
+
ceiling = v.length > 0 ? v : null;
|
|
69
|
+
} catch {
|
|
70
|
+
ceiling = null; // unreadable settings → no ceiling (fail-open)
|
|
71
|
+
}
|
|
72
|
+
_modelCeilingMemo.set(cwd, ceiling);
|
|
73
|
+
return ceiling;
|
|
74
|
+
}
|
|
75
|
+
/** Test seam: reset the per-cwd ceiling memo. */
|
|
76
|
+
export function _resetModelCeilingMemo(): void {
|
|
77
|
+
_modelCeilingMemo.clear();
|
|
78
|
+
}
|
|
48
79
|
import * as user_global_paths from '../_lib/user_global_paths.js';
|
|
49
80
|
import { appendEvent } from './events_log.js';
|
|
50
81
|
|
|
@@ -1179,7 +1210,25 @@ export abstract class CliClient extends ExternalAIClient {
|
|
|
1179
1210
|
// (loader preload, git *_COMMAND, NODE_OPTIONS, …) so an
|
|
1180
1211
|
// attacker-influenced parent env cannot RCE via the spawned CLI
|
|
1181
1212
|
// or a `git` it invokes internally.
|
|
1182
|
-
|
|
1213
|
+
// Role marking (token-economy-dispatch Phase 2.2): a council
|
|
1214
|
+
// member CLI session is worker-class — it never talks to OUR
|
|
1215
|
+
// user, never dispatches sub-workers, never runs a council.
|
|
1216
|
+
// The spawned vendor CLI loads this repo's hook chains; the
|
|
1217
|
+
// marker lets the dispatcher run the thinner worker chain
|
|
1218
|
+
// (manifest `roles.worker.drop`; pre_tool_use guards are
|
|
1219
|
+
// structurally exempt). This is the ONE in-tree spawn point
|
|
1220
|
+
// that launches a separate CLI session today — Agent-tool
|
|
1221
|
+
// subagents share the host process env and cannot be marked
|
|
1222
|
+
// (probed live 2026-08-10; see _lib/session_role.ts).
|
|
1223
|
+
// The optional model ceiling (subagents.model_ceiling, class C)
|
|
1224
|
+
// rides the same spawn: a spend cap the human set per install.
|
|
1225
|
+
env: hardenedSpawnEnv({
|
|
1226
|
+
[SESSION_ROLE_ENV]: 'worker',
|
|
1227
|
+
...((): Record<string, string> => {
|
|
1228
|
+
const ceiling = _subagentModelCeiling();
|
|
1229
|
+
return ceiling !== null ? { CLAUDE_CODE_SUBAGENT_MODEL: ceiling } : {};
|
|
1230
|
+
})(),
|
|
1231
|
+
}),
|
|
1183
1232
|
};
|
|
1184
1233
|
if (stdinPayload !== null) {
|
|
1185
1234
|
spawnOpts.input = stdinPayload;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Rung-0.5 ask transport — one prompt, one answer, no harness, no session
|
|
4
|
+
* (road-to-token-economy-dispatch Phase 4.1).
|
|
5
|
+
*
|
|
6
|
+
* The judgment ladder's gap this closes: rung 0 routes mechanical slices to
|
|
7
|
+
* deterministic scripts; rung 1 jumps straight to a full subagent spawn
|
|
8
|
+
* whose measured median cold start is ~251k tokens (dispatch_floor,
|
|
9
|
+
* 2026-08-10). A single bounded QUESTION — a classification, a small
|
|
10
|
+
* verification, a one-file semantic lookup — needs neither a session nor
|
|
11
|
+
* tools: one completion over the reconciled council transport chain
|
|
12
|
+
* (cli → api → ∅) answers it at roughly two orders of magnitude below the
|
|
13
|
+
* spawn floor.
|
|
14
|
+
*
|
|
15
|
+
* Hard caps, per the roadmap's anti-loop discipline (Risk 2 — "rung 0.5
|
|
16
|
+
* becomes a hidden agent loop"):
|
|
17
|
+
* - ONE completion. No retry beyond the transport chain's own cli→api
|
|
18
|
+
* fallback semantics inside the client.
|
|
19
|
+
* - NO tool use, NO session, NO follow-up turn.
|
|
20
|
+
* - Honest ∅: council disabled / no member / transport failure → null
|
|
21
|
+
* (CLI exit 3), never a fabricated answer and never an escalation this
|
|
22
|
+
* module performs itself. Escalating to a spawn is the CALLER's
|
|
23
|
+
* decision and is recorded as such (`escalated` on the caller's line).
|
|
24
|
+
*
|
|
25
|
+
* Telemetry: every completed ask appends ONE orchestration_record line
|
|
26
|
+
* (route_taken: "ask", spawn_count: 0, dispatch_tokens measured from the
|
|
27
|
+
* provider response) — the substitution/adoption metrics registered in
|
|
28
|
+
* `src/config/dispatch-economy-metrics.json` read these lines.
|
|
29
|
+
*
|
|
30
|
+
* Reuses, never re-implements: `council_cli.load_settings` +
|
|
31
|
+
* `build_members` (member construction, auth, transport resolution,
|
|
32
|
+
* cli-first billing classification — all unchanged), `CouncilResponse`
|
|
33
|
+
* accounting, `_lib/orchestration_record` validation.
|
|
34
|
+
*
|
|
35
|
+
* Usage:
|
|
36
|
+
* ./scripts-run src/scripts/ask_transport "is X true in file Y?" \
|
|
37
|
+
* [--system "<system prompt>"] [--member <name>] [--format text|json] \
|
|
38
|
+
* [--no-record] [--dir <audit-dir>]
|
|
39
|
+
*
|
|
40
|
+
* Exit codes: 0 answer produced · 2 usage error · 3 honest-∅ (no transport).
|
|
41
|
+
*/
|
|
42
|
+
import * as crypto from 'node:crypto';
|
|
43
|
+
import * as fs from 'node:fs';
|
|
44
|
+
import * as path from 'node:path';
|
|
45
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
46
|
+
|
|
47
|
+
import type { ExternalAIClient } from './ai_council/clients.js';
|
|
48
|
+
import { buildOrchestrationLine } from './_lib/orchestration_record.js';
|
|
49
|
+
import { DEFAULT_DIR } from './orchestration_record.js';
|
|
50
|
+
|
|
51
|
+
export interface AskResult {
|
|
52
|
+
answer: string;
|
|
53
|
+
member: string;
|
|
54
|
+
model: string;
|
|
55
|
+
input_tokens: number;
|
|
56
|
+
output_tokens: number;
|
|
57
|
+
latency_ms: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface AskOptions {
|
|
61
|
+
/** Injected members (tests). Default: council_cli.load_settings + build_members. */
|
|
62
|
+
members?: ExternalAIClient[];
|
|
63
|
+
/** Pick a specific member by provider/name; default: first configured. */
|
|
64
|
+
member?: string;
|
|
65
|
+
systemPrompt?: string;
|
|
66
|
+
/** Telemetry append target; null disables recording (tests / --no-record). */
|
|
67
|
+
auditDir?: string | null;
|
|
68
|
+
/** Injected clock/id (tests). */
|
|
69
|
+
now?: () => Date;
|
|
70
|
+
id?: () => string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const DEFAULT_SYSTEM =
|
|
74
|
+
'You are a single-completion answer service. Answer the question directly, ' +
|
|
75
|
+
'concisely, and honestly. If the question cannot be answered without tools, ' +
|
|
76
|
+
'files, or more context, reply exactly: INSUFFICIENT_CONTEXT — one line on ' +
|
|
77
|
+
'what is missing. Never invent facts.';
|
|
78
|
+
|
|
79
|
+
/** Lazily import the council CLI surface so tests with injected members never load it. */
|
|
80
|
+
async function defaultMembers(): Promise<ExternalAIClient[]> {
|
|
81
|
+
const mod = await import('./council_cli.js');
|
|
82
|
+
const settings = mod.load_settings();
|
|
83
|
+
return mod.build_members(settings);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function recordAskLine(auditDir: string, result: AskResult | null, member: string, ts: string, id: string): void {
|
|
87
|
+
const { line, errors } = buildOrchestrationLine({
|
|
88
|
+
spawn_count: 0,
|
|
89
|
+
token_delta: 0,
|
|
90
|
+
token_delta_provenance: 'estimated',
|
|
91
|
+
route_taken: 'ask',
|
|
92
|
+
verify_mode: 'none',
|
|
93
|
+
dispatch_outcome: result === null ? 'BLOCKED' : 'DONE',
|
|
94
|
+
dispatch_tokens: result === null ? null : result.input_tokens + result.output_tokens,
|
|
95
|
+
wall_clock_ms: result === null ? 0 : Math.max(0, Math.round(result.latency_ms)),
|
|
96
|
+
agent_combo: [member || 'none'],
|
|
97
|
+
origin: 'dispatch-economy-2026',
|
|
98
|
+
ts,
|
|
99
|
+
id,
|
|
100
|
+
});
|
|
101
|
+
if (errors.length || line === null) return; // telemetry must never fail the ask
|
|
102
|
+
try {
|
|
103
|
+
const file = path.join(auditDir, `${ts.slice(0, 7)}.jsonl`);
|
|
104
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
105
|
+
fs.appendFileSync(file, `${JSON.stringify(line)}\n`, 'utf8');
|
|
106
|
+
} catch {
|
|
107
|
+
/* fail-open — accounting never blocks the answer */
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* One bounded ask. Returns the answer or `null` (honest ∅ — council
|
|
113
|
+
* disabled, no member resolves, transport failure, or provider error).
|
|
114
|
+
*/
|
|
115
|
+
export async function askOnce(prompt: string, opts: AskOptions = {}): Promise<AskResult | null> {
|
|
116
|
+
const trimmed = prompt.trim();
|
|
117
|
+
if (!trimmed) return null;
|
|
118
|
+
|
|
119
|
+
let members: ExternalAIClient[];
|
|
120
|
+
try {
|
|
121
|
+
members = opts.members ?? (await defaultMembers());
|
|
122
|
+
} catch {
|
|
123
|
+
return _finish(null, 'none', opts); // disabled / unreadable config → ∅
|
|
124
|
+
}
|
|
125
|
+
const memberName = (m: ExternalAIClient): string => {
|
|
126
|
+
const c = m as { name?: string; provider?: string };
|
|
127
|
+
return c.name || c.provider || 'unknown';
|
|
128
|
+
};
|
|
129
|
+
const pick =
|
|
130
|
+
opts.member !== undefined ? members.find((m) => memberName(m) === opts.member) : members[0];
|
|
131
|
+
if (pick === undefined) {
|
|
132
|
+
return _finish(null, opts.member ?? 'none', opts);
|
|
133
|
+
}
|
|
134
|
+
const provider = memberName(pick);
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const response = pick.ask(opts.systemPrompt ?? DEFAULT_SYSTEM, trimmed);
|
|
138
|
+
const r = response instanceof Promise ? await response : response;
|
|
139
|
+
if (r === null || r === undefined || (r.error !== null && r.error !== undefined && r.error !== '')) {
|
|
140
|
+
return _finish(null, provider, opts);
|
|
141
|
+
}
|
|
142
|
+
const text = (r.text ?? '').trim();
|
|
143
|
+
if (!text) return _finish(null, provider, opts);
|
|
144
|
+
return _finish(
|
|
145
|
+
{
|
|
146
|
+
answer: text,
|
|
147
|
+
member: provider,
|
|
148
|
+
model: r.model ?? 'unknown',
|
|
149
|
+
input_tokens: r.input_tokens ?? 0,
|
|
150
|
+
output_tokens: r.output_tokens ?? 0,
|
|
151
|
+
latency_ms: r.latency_ms ?? 0,
|
|
152
|
+
},
|
|
153
|
+
provider,
|
|
154
|
+
opts,
|
|
155
|
+
);
|
|
156
|
+
} catch {
|
|
157
|
+
return _finish(null, provider, opts); // ONE completion — no retry here
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function _finish(result: AskResult | null, member: string, opts: AskOptions): AskResult | null {
|
|
162
|
+
const auditDir = opts.auditDir === undefined ? path.join(process.cwd(), DEFAULT_DIR) : opts.auditDir;
|
|
163
|
+
if (auditDir !== null) {
|
|
164
|
+
const ts = (opts.now ? opts.now() : new Date()).toISOString();
|
|
165
|
+
const id = opts.id ? opts.id() : crypto.randomUUID();
|
|
166
|
+
recordAskLine(auditDir, result, member, ts, id);
|
|
167
|
+
}
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function main(argv: readonly string[]): Promise<number> {
|
|
172
|
+
let prompt = '';
|
|
173
|
+
let system: string | undefined;
|
|
174
|
+
let member: string | undefined;
|
|
175
|
+
let format: 'text' | 'json' = 'text';
|
|
176
|
+
let record = true;
|
|
177
|
+
let dir: string | undefined;
|
|
178
|
+
for (let i = 0; i < argv.length; i++) {
|
|
179
|
+
const a = argv[i] ?? '';
|
|
180
|
+
if (a === '--system') system = argv[++i];
|
|
181
|
+
else if (a === '--member') member = argv[++i];
|
|
182
|
+
else if (a === '--format') format = argv[++i] === 'json' ? 'json' : 'text';
|
|
183
|
+
else if (a === '--no-record') record = false;
|
|
184
|
+
else if (a === '--dir') dir = argv[++i];
|
|
185
|
+
else if (!a.startsWith('--')) prompt = prompt ? `${prompt} ${a}` : a;
|
|
186
|
+
}
|
|
187
|
+
if (!prompt.trim()) {
|
|
188
|
+
process.stderr.write('ask_transport: a question argument is required\n');
|
|
189
|
+
return 2;
|
|
190
|
+
}
|
|
191
|
+
const opts: AskOptions = {
|
|
192
|
+
...(system !== undefined ? { systemPrompt: system } : {}),
|
|
193
|
+
...(member !== undefined ? { member } : {}),
|
|
194
|
+
auditDir: record ? (dir ?? path.join(process.cwd(), DEFAULT_DIR)) : null,
|
|
195
|
+
};
|
|
196
|
+
const result = await askOnce(prompt, opts);
|
|
197
|
+
if (result === null) {
|
|
198
|
+
process.stderr.write('ask_transport: ∅ — no transport resolved or the single completion failed (no retry by design)\n');
|
|
199
|
+
return 3;
|
|
200
|
+
}
|
|
201
|
+
process.stdout.write(
|
|
202
|
+
format === 'json' ? JSON.stringify(result, null, 2) + '\n' : result.answer + '\n',
|
|
203
|
+
);
|
|
204
|
+
return 0;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
|
|
208
|
+
function _isCliEntry(): boolean {
|
|
209
|
+
if (typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__) return false;
|
|
210
|
+
if (process.argv[1] === undefined) return false;
|
|
211
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
212
|
+
if (import.meta.url === argvUrl) return true;
|
|
213
|
+
try {
|
|
214
|
+
return fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(process.argv[1]));
|
|
215
|
+
} catch {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (_isCliEntry()) {
|
|
220
|
+
main(process.argv.slice(2)).then((rc) => process.exit(rc));
|
|
221
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Hook-injection byte bench + CI gate (road-to-token-economy-cache Phase 3.2)
|
|
4
|
+
* — the latency harness's twin for the INJECTION side.
|
|
5
|
+
*
|
|
6
|
+
* Runs every concern bound on the `claude` platform against the committed
|
|
7
|
+
* fixture envelope of each slot it is bound to (tests/fixtures/hooks/
|
|
8
|
+
* <event>.json), in-process via CONCERN_REGISTRY with the hook-stdin
|
|
9
|
+
* override, under AGENT_CONFIG_REPLAY=1 (state writes skipped). Measures the
|
|
10
|
+
* bytes of the stdout payload fields a concern injects — `reason` +
|
|
11
|
+
* `additional_context` + `context` — and gates them against
|
|
12
|
+
* `src/config/hook-token-budget.json`:
|
|
13
|
+
*
|
|
14
|
+
* - a concern whose emission exceeds its row (or the default cap) → RED
|
|
15
|
+
* - an EMITTING concern with no explicit row above the default is fine;
|
|
16
|
+
* the default cap IS its row — but a breach names the missing row so
|
|
17
|
+
* the fix is a registered decision, not a silent default bump
|
|
18
|
+
* - per-slot SUM over all bound concerns > the slot cap → RED
|
|
19
|
+
*
|
|
20
|
+
* HONEST SCOPE, stated up front: concerns are conditional-silence by design —
|
|
21
|
+
* a generic fixture triggers few of them, so most measure 0 bytes here. The
|
|
22
|
+
* gate therefore enforces the cap on whatever DOES fire under the committed
|
|
23
|
+
* fixtures (deterministic, CI-reproducible), while the census mode
|
|
24
|
+
* (`--record`) measures REAL sessions: it appends counts-only lines
|
|
25
|
+
* ({ts, slot, concern, bytes}) to agents/runtime/state/injection-census.jsonl
|
|
26
|
+
* — the repeat-injection-census blocker's instrument. Claiming the fixture
|
|
27
|
+
* numbers are the live distribution would be the coverage inflation this
|
|
28
|
+
* repo's gate discipline refuses; the two modes exist because neither
|
|
29
|
+
* substitutes for the other.
|
|
30
|
+
*
|
|
31
|
+
* Class A: in-process, no daemon, no network. Exit 0 green · 1 breach ·
|
|
32
|
+
* 2 unusable invocation. Dead-scope guarded: zero concerns run = RED.
|
|
33
|
+
*
|
|
34
|
+
* Usage:
|
|
35
|
+
* ./scripts-run src/scripts/bench_hook_injection [--format text|json]
|
|
36
|
+
* [--record] [--platform claude]
|
|
37
|
+
*/
|
|
38
|
+
import * as fs from 'node:fs';
|
|
39
|
+
import * as path from 'node:path';
|
|
40
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
41
|
+
|
|
42
|
+
import { CONCERN_REGISTRY } from './hooks/concern_registry.js';
|
|
43
|
+
import { setHookStdinOverride, clearHookStdinOverride } from './hooks/hook_stdin.js';
|
|
44
|
+
import { _load_yaml, _resolve_concerns, EVENT_VOCABULARY, type JsonObject } from './hooks/dispatch_hook.js';
|
|
45
|
+
|
|
46
|
+
declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
|
|
47
|
+
const _IN_BUNDLE = typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__;
|
|
48
|
+
const REPO_ROOT = path.resolve(
|
|
49
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
50
|
+
...(_IN_BUNDLE ? ['..'] : ['..', '..']),
|
|
51
|
+
);
|
|
52
|
+
const MANIFEST = path.join(REPO_ROOT, 'src', 'scripts', 'hook_manifest.yaml');
|
|
53
|
+
const BUDGET = path.join(REPO_ROOT, 'src', 'config', 'hook-token-budget.json');
|
|
54
|
+
const FIXTURES = path.join(REPO_ROOT, 'tests', 'fixtures', 'hooks');
|
|
55
|
+
const CENSUS_FILE = path.join('agents', 'runtime', 'state', 'injection-census.jsonl');
|
|
56
|
+
|
|
57
|
+
export interface Measurement {
|
|
58
|
+
slot: string;
|
|
59
|
+
concern: string;
|
|
60
|
+
bytes: number;
|
|
61
|
+
cap: number;
|
|
62
|
+
breach: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface Budget {
|
|
66
|
+
default_cap_bytes: number;
|
|
67
|
+
per_concern_caps_bytes: Record<string, number | string>;
|
|
68
|
+
per_slot_sum_caps_bytes: Record<string, number | string>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function capFor(budget: Budget, concern: string): number {
|
|
72
|
+
const v = budget.per_concern_caps_bytes[concern];
|
|
73
|
+
return typeof v === 'number' ? v : budget.default_cap_bytes;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Run one registered concern in-process with the fixture envelope; return payload bytes. */
|
|
77
|
+
export function runConcernOnce(
|
|
78
|
+
script: string,
|
|
79
|
+
args: readonly string[],
|
|
80
|
+
envelope: JsonObject,
|
|
81
|
+
): number {
|
|
82
|
+
const main = CONCERN_REGISTRY[script];
|
|
83
|
+
if (main === undefined) return -1; // not in registry — parity tests own that failure
|
|
84
|
+
let out = '';
|
|
85
|
+
const prevWrite = process.stdout.write;
|
|
86
|
+
const prevErr = process.stderr.write;
|
|
87
|
+
const prevArgv = process.argv;
|
|
88
|
+
const prevReplay = process.env['AGENT_CONFIG_REPLAY'];
|
|
89
|
+
setHookStdinOverride(JSON.stringify(envelope));
|
|
90
|
+
process.env['AGENT_CONFIG_REPLAY'] = '1';
|
|
91
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
92
|
+
process.stdout.write = ((chunk: any, enc?: any, cb?: any): boolean => {
|
|
93
|
+
out += typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf-8');
|
|
94
|
+
const callback = typeof enc === 'function' ? enc : cb;
|
|
95
|
+
if (typeof callback === 'function') callback();
|
|
96
|
+
return true;
|
|
97
|
+
}) as typeof process.stdout.write;
|
|
98
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
99
|
+
process.stderr.write = ((chunk: any, enc?: any, cb?: any): boolean => {
|
|
100
|
+
const callback = typeof enc === 'function' ? enc : cb;
|
|
101
|
+
if (typeof callback === 'function') callback();
|
|
102
|
+
return true;
|
|
103
|
+
}) as typeof process.stderr.write;
|
|
104
|
+
try {
|
|
105
|
+
process.argv = [prevArgv[0] as string, script, ...args, '--platform', 'claude'];
|
|
106
|
+
main([...args, '--platform', 'claude']);
|
|
107
|
+
} catch {
|
|
108
|
+
/* a crashing concern injects nothing — latency/parity suites own crashes */
|
|
109
|
+
} finally {
|
|
110
|
+
process.stdout.write = prevWrite;
|
|
111
|
+
process.stderr.write = prevErr;
|
|
112
|
+
process.argv = prevArgv;
|
|
113
|
+
if (prevReplay === undefined) delete process.env['AGENT_CONFIG_REPLAY'];
|
|
114
|
+
else process.env['AGENT_CONFIG_REPLAY'] = prevReplay;
|
|
115
|
+
clearHookStdinOverride();
|
|
116
|
+
}
|
|
117
|
+
const text = out.trim();
|
|
118
|
+
if (!text) return 0;
|
|
119
|
+
try {
|
|
120
|
+
const parsed = JSON.parse(text) as Record<string, unknown>;
|
|
121
|
+
let bytes = 0;
|
|
122
|
+
for (const key of ['reason', 'additional_context', 'context']) {
|
|
123
|
+
const v = parsed[key];
|
|
124
|
+
if (typeof v === 'string') bytes += Buffer.byteLength(v, 'utf-8');
|
|
125
|
+
}
|
|
126
|
+
return bytes;
|
|
127
|
+
} catch {
|
|
128
|
+
return Buffer.byteLength(text, 'utf-8'); // non-JSON stdout counts whole
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function bench(opts: { platform?: string; budgetPath?: string } = {}): { measurements: Measurement[]; slotSums: Record<string, { bytes: number; cap: number; breach: boolean }>; errors: string[] } {
|
|
133
|
+
const platform = opts.platform ?? 'claude';
|
|
134
|
+
const manifest = _load_yaml(MANIFEST);
|
|
135
|
+
const budget = JSON.parse(fs.readFileSync(opts.budgetPath ?? BUDGET, 'utf8')) as Budget;
|
|
136
|
+
const measurements: Measurement[] = [];
|
|
137
|
+
const slotSums: Record<string, { bytes: number; cap: number; breach: boolean }> = {};
|
|
138
|
+
const errors: string[] = [];
|
|
139
|
+
|
|
140
|
+
for (const event of EVENT_VOCABULARY) {
|
|
141
|
+
const fixtureFile = path.join(FIXTURES, `${event}.json`);
|
|
142
|
+
if (!fs.existsSync(fixtureFile)) continue;
|
|
143
|
+
const envelope = JSON.parse(fs.readFileSync(fixtureFile, 'utf8')) as JsonObject;
|
|
144
|
+
const concerns = _resolve_concerns(manifest, platform, event);
|
|
145
|
+
let sum = 0;
|
|
146
|
+
for (const concern of concerns) {
|
|
147
|
+
const bytes = runConcernOnce(String(concern['script']), Array.isArray(concern['args']) ? concern['args'].map(String) : [], envelope);
|
|
148
|
+
if (bytes < 0) {
|
|
149
|
+
errors.push(`${event}/${String(concern['name'])}: not in CONCERN_REGISTRY`);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const cap = capFor(budget, String(concern['name']));
|
|
153
|
+
const breach = bytes > cap;
|
|
154
|
+
measurements.push({ slot: event, concern: String(concern['name']), bytes, cap, breach });
|
|
155
|
+
sum += bytes;
|
|
156
|
+
}
|
|
157
|
+
const slotCapRaw = budget.per_slot_sum_caps_bytes[event];
|
|
158
|
+
const slotCap = typeof slotCapRaw === 'number' ? slotCapRaw : Number.MAX_SAFE_INTEGER;
|
|
159
|
+
slotSums[event] = { bytes: sum, cap: slotCap, breach: sum > slotCap };
|
|
160
|
+
}
|
|
161
|
+
return { measurements, slotSums, errors };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function main(argv: readonly string[]): number {
|
|
165
|
+
let format: 'text' | 'json' = 'text';
|
|
166
|
+
let record = false;
|
|
167
|
+
let platform = 'claude';
|
|
168
|
+
for (let i = 0; i < argv.length; i++) {
|
|
169
|
+
const a = argv[i];
|
|
170
|
+
if (a === '--format') format = argv[++i] === 'json' ? 'json' : 'text';
|
|
171
|
+
else if (a === '--record') record = true;
|
|
172
|
+
else if (a === '--platform') platform = argv[++i] ?? platform;
|
|
173
|
+
}
|
|
174
|
+
let result: ReturnType<typeof bench>;
|
|
175
|
+
try {
|
|
176
|
+
result = bench({ platform });
|
|
177
|
+
} catch (err) {
|
|
178
|
+
process.stderr.write(`bench_hook_injection: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
179
|
+
return 2;
|
|
180
|
+
}
|
|
181
|
+
// Dead-scope guard: a bench that ran zero concerns proves nothing (a
|
|
182
|
+
// gate that scans nothing exits green is the recorded failure class).
|
|
183
|
+
if (result.measurements.length === 0) {
|
|
184
|
+
process.stderr.write('bench_hook_injection: DEAD SCOPE — zero concerns ran; fixtures or manifest unreadable\n');
|
|
185
|
+
return 1;
|
|
186
|
+
}
|
|
187
|
+
if (record) {
|
|
188
|
+
try {
|
|
189
|
+
const file = path.join(process.cwd(), CENSUS_FILE);
|
|
190
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
191
|
+
const ts = new Date().toISOString();
|
|
192
|
+
for (const m of result.measurements) {
|
|
193
|
+
if (m.bytes > 0) {
|
|
194
|
+
fs.appendFileSync(file, JSON.stringify({ ts, slot: m.slot, concern: m.concern, bytes: m.bytes }) + '\n');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
} catch {
|
|
198
|
+
/* census is observability — never fails the bench */
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const breaches = result.measurements.filter((m) => m.breach);
|
|
202
|
+
const slotBreaches = Object.entries(result.slotSums).filter(([, s]) => s.breach);
|
|
203
|
+
if (format === 'json') {
|
|
204
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
205
|
+
} else {
|
|
206
|
+
const emitting = result.measurements.filter((m) => m.bytes > 0);
|
|
207
|
+
process.stdout.write(`bench_hook_injection: ${result.measurements.length} concern-slot pairs · ${emitting.length} emitted under the committed fixtures\n`);
|
|
208
|
+
for (const m of emitting) {
|
|
209
|
+
process.stdout.write(` ${m.slot.padEnd(20)} ${m.concern.padEnd(24)} ${String(m.bytes).padStart(6)} B (cap ${m.cap})${m.breach ? ' ❌ BREACH' : ''}\n`);
|
|
210
|
+
}
|
|
211
|
+
for (const [slot, s] of Object.entries(result.slotSums)) {
|
|
212
|
+
if (s.bytes > 0) process.stdout.write(` slot-sum ${slot.padEnd(20)} ${String(s.bytes).padStart(6)} B (cap ${s.cap === Number.MAX_SAFE_INTEGER ? '—' : s.cap})${s.breach ? ' ❌ BREACH' : ''}\n`);
|
|
213
|
+
}
|
|
214
|
+
for (const e of result.errors) process.stdout.write(` warn: ${e}\n`);
|
|
215
|
+
}
|
|
216
|
+
if (breaches.length > 0 || slotBreaches.length > 0) {
|
|
217
|
+
process.stderr.write(`bench_hook_injection: ${breaches.length} concern breach(es), ${slotBreaches.length} slot-sum breach(es) — the budget row is the decision surface (src/config/hook-token-budget.json)\n`);
|
|
218
|
+
return 1;
|
|
219
|
+
}
|
|
220
|
+
return 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function _isCliEntry(): boolean {
|
|
224
|
+
if (_IN_BUNDLE) return false;
|
|
225
|
+
if (process.argv[1] === undefined) return false;
|
|
226
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
227
|
+
if (import.meta.url === argvUrl) return true;
|
|
228
|
+
try {
|
|
229
|
+
return fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(process.argv[1]));
|
|
230
|
+
} catch {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (_isCliEntry()) process.exit(main(process.argv.slice(2)));
|