@compr/opscontext-mcp 2.5.2 โ 2.5.4
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/README.md +1 -1
- package/dist/activation.d.ts +0 -28
- package/dist/activation.js +27 -148
- package/dist/audit.d.ts +59 -1
- package/dist/audit.js +194 -4
- package/dist/cli-commands.js +1 -0
- package/dist/cli.js +99 -206
- package/dist/cost-report.d.ts +22 -0
- package/dist/cost-report.js +220 -0
- package/dist/index.js +45 -1
- package/dist/tools-manifest.d.ts +2 -2
- package/dist/tools-manifest.js +1 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -226,6 +226,52 @@ function generateMcpJson() {
|
|
|
226
226
|
// ---------------------------------------------------------------------------
|
|
227
227
|
// Template for pre-commit hook (CE doc freshness + secret scanner)
|
|
228
228
|
// ---------------------------------------------------------------------------
|
|
229
|
+
/**
|
|
230
|
+
* ๐ LOCKED [SKIPPING-A-HOOK-MUST-NAME-WHAT-IS-UNENFORCED] โ 2026-08-20
|
|
231
|
+
* โ NEVER print a bare "already exists, skipping" for a hook slot. NEVER overwrite or
|
|
232
|
+
* append to a foreign hook either.
|
|
233
|
+
* WHY: `init` refuses to clobber an existing hook, which is right, but it said so with one
|
|
234
|
+
* grey line among a column of green ticks. The user reads "init done" and believes the
|
|
235
|
+
* policy gates are live; in that repo they were never installed and enforce nothing.
|
|
236
|
+
* This is exactly ยงE1 arriving through a different door: there, the rule existed and no
|
|
237
|
+
* hook called it; here, a hook exists and does not call the rule. Both produce a gate
|
|
238
|
+
* that is present in every document and absent at runtime, and neither produces an error.
|
|
239
|
+
* A real instance is on this machine: invocme-odoo-connector has a hand-written
|
|
240
|
+
* pre-commit, so `init` skipped it, and none of that repo's CE gates have ever run.
|
|
241
|
+
* Appending instead is not the fix, and is how that repo ended up with a whole secret
|
|
242
|
+
* scanner sitting unreachable behind an earlier `exit 0`.
|
|
243
|
+
* FIX: on skip, read the existing hook. If it does not invoke the CE CLI, say which gates are
|
|
244
|
+
* therefore unenforced and print the one line that wires them in. Silence here reads as
|
|
245
|
+
* success.
|
|
246
|
+
*/
|
|
247
|
+
function existingHookInvokesCE(path) {
|
|
248
|
+
try {
|
|
249
|
+
const body = readFileSync(path, "utf-8");
|
|
250
|
+
return /\b(contextengine|opscontext)\b/.test(body);
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
// Unreadable is not proof of absence; say nothing rather than claim a verdict.
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function warnSkippedHook(path, slot) {
|
|
258
|
+
if (existingHookInvokesCE(path))
|
|
259
|
+
return;
|
|
260
|
+
const gates = {
|
|
261
|
+
"pre-commit": "secret-scan, doc-coverage, rule-parity",
|
|
262
|
+
"commit-msg": "commit-message-required",
|
|
263
|
+
"post-commit": "audit trail of the push",
|
|
264
|
+
};
|
|
265
|
+
const wire = {
|
|
266
|
+
"pre-commit": 'contextengine hook secret-scan && contextengine hook doc-coverage && contextengine hook rule-parity',
|
|
267
|
+
"commit-msg": 'contextengine hook commit-message-required "$1"',
|
|
268
|
+
"post-commit": "contextengine end-session",
|
|
269
|
+
};
|
|
270
|
+
console.log(` โ ๏ธ that hook never calls ContextEngine, so these enforce NOTHING here:`);
|
|
271
|
+
console.log(` ${gates[slot]}`);
|
|
272
|
+
console.log(` Wire them by adding this line to ${path}:`);
|
|
273
|
+
console.log(` ${wire[slot]}`);
|
|
274
|
+
}
|
|
229
275
|
function generatePreCommitHook() {
|
|
230
276
|
const lines = [];
|
|
231
277
|
lines.push("#!/bin/zsh");
|
|
@@ -508,7 +554,8 @@ async function runInit() {
|
|
|
508
554
|
const postCommitDest = join(hooksDir, "post-commit");
|
|
509
555
|
const commitMsgDest = join(hooksDir, "commit-msg");
|
|
510
556
|
if (existsSync(preCommitDest)) {
|
|
511
|
-
console.log(" โญ .git/hooks/pre-commit already exists โ skipping");
|
|
557
|
+
console.log(" โญ .git/hooks/pre-commit already exists โ skipping (never overwritten)");
|
|
558
|
+
warnSkippedHook(preCommitDest, "pre-commit");
|
|
512
559
|
skipped++;
|
|
513
560
|
}
|
|
514
561
|
else {
|
|
@@ -523,7 +570,8 @@ async function runInit() {
|
|
|
523
570
|
// [COMMIT-MSG-HOOK-MUST-BE-INSTALLED] โ without this, every
|
|
524
571
|
// commit_message_required rule is silently unenforced.
|
|
525
572
|
if (existsSync(commitMsgDest)) {
|
|
526
|
-
console.log(" โญ .git/hooks/commit-msg already exists โ skipping");
|
|
573
|
+
console.log(" โญ .git/hooks/commit-msg already exists โ skipping (never overwritten)");
|
|
574
|
+
warnSkippedHook(commitMsgDest, "commit-msg");
|
|
527
575
|
skipped++;
|
|
528
576
|
}
|
|
529
577
|
else {
|
|
@@ -536,7 +584,8 @@ async function runInit() {
|
|
|
536
584
|
}
|
|
537
585
|
}
|
|
538
586
|
if (existsSync(postCommitDest)) {
|
|
539
|
-
console.log(" โญ .git/hooks/post-commit already exists โ skipping");
|
|
587
|
+
console.log(" โญ .git/hooks/post-commit already exists โ skipping (never overwritten)");
|
|
588
|
+
warnSkippedHook(postCommitDest, "post-commit");
|
|
540
589
|
skipped++;
|
|
541
590
|
}
|
|
542
591
|
else {
|
|
@@ -601,11 +650,9 @@ import { listLearnings, learningsToChunks, learningsStats, formatLearnings, save
|
|
|
601
650
|
import { saveSession, loadSession, listSessions, deleteSession, formatSession, formatSessionList, } from "./sessions.js";
|
|
602
651
|
import { activate, deactivate, getActivationStatus, gateCheck, } from "./activation.js";
|
|
603
652
|
import { syncTierA, syncTierB, loadCommunityStore, communityRulesToChunks, mergeWithDedup, STORE_PATH as COMMUNITY_STORE_PATH, } from "./community-sync.js";
|
|
604
|
-
import { readAuditLog, verifyChain, filterByRange, toCsv, rotateAuditLog, planRotation, listSegments, } from "./audit.js";
|
|
653
|
+
import { readAuditLog, verifyChain, filterByRange, toCsv, rotateAuditLog, planRotation, listSegments, acknowledgeRedaction, } from "./audit.js";
|
|
605
654
|
import { loadRepoPolicy, parsePolicy, formatPolicySummary, formatValidationErrors, repoPolicyPath, } from "./policy.js";
|
|
606
|
-
import {
|
|
607
|
-
import { DEFAULT_PRICING, DEFAULT_PRICING_ASOF } from "./default-pricing.js";
|
|
608
|
-
import { runTranscriptHeuristics, DEFAULT_COST_THRESHOLDS, } from "./detector.js";
|
|
655
|
+
import { buildCostReport } from "./cost-report.js";
|
|
609
656
|
import { getStagedFiles, runSecretScan, runDocCoverage, runCommitMessageRequired, runRuleParity, formatSecretViolations, formatDocCoverageViolations, formatSecretViolationsJson, formatDocCoverageViolationsJson, formatCommitMessageViolations, formatCommitMessageViolationsJson, formatRuleParityViolations, formatRuleParityViolationsJson, } from "./hooks.js";
|
|
610
657
|
import { safeAppend } from "./audit.js";
|
|
611
658
|
import { installSkill, locateBundledSkill, buildManagedBlock, syncClaudeMd, } from "./claude-integration.js";
|
|
@@ -1917,12 +1964,39 @@ function cliAuditRotate(args) {
|
|
|
1917
1964
|
process.exit(2);
|
|
1918
1965
|
}
|
|
1919
1966
|
}
|
|
1967
|
+
/** Acknowledge deliberately redacted audit records on the chain. [LOCK] [REDACTION-IS-A-CHAINED-RECORD] */
|
|
1968
|
+
function cliAuditRedactAck(args) {
|
|
1969
|
+
const idxAt = args.indexOf("--index");
|
|
1970
|
+
const reasonAt = args.indexOf("--reason");
|
|
1971
|
+
const raw = idxAt >= 0 ? args[idxAt + 1] ?? "" : "";
|
|
1972
|
+
const reason = reasonAt >= 0 ? args[reasonAt + 1] ?? "" : "";
|
|
1973
|
+
const indices = raw.split(",").map((x) => Number(x.trim())).filter((n) => Number.isInteger(n) && n >= 0);
|
|
1974
|
+
if (indices.length === 0 || !reason.trim()) {
|
|
1975
|
+
console.error(`usage: contextengine audit-redact-ack --index <i,j,k> --reason "<what was removed and why>"`);
|
|
1976
|
+
console.error(` Indices are the ones 'audit-verify' lists as altered. Only altered records can be acknowledged.`);
|
|
1977
|
+
process.exit(1);
|
|
1978
|
+
}
|
|
1979
|
+
const r = acknowledgeRedaction(indices, reason, "cli");
|
|
1980
|
+
for (const x of r.rejected)
|
|
1981
|
+
console.error(` โ ${x.index}: ${x.why}`);
|
|
1982
|
+
if (!r.record) {
|
|
1983
|
+
console.error(`\nNothing acknowledged.`);
|
|
1984
|
+
process.exit(1);
|
|
1985
|
+
}
|
|
1986
|
+
console.log(`\nโ
Acknowledged ${r.acknowledged.length} redacted record(s): ${r.acknowledged.join(", ")}`);
|
|
1987
|
+
console.log(` Chained as audit.redact, hash ${r.record.hash.slice(0, 16)}โฆ`);
|
|
1988
|
+
const after = verifyChain();
|
|
1989
|
+
console.log(` audit-verify now: ${after.ok ? "OK" : "FAILED"}, ${(after.redactedIndices ?? []).length} redacted, ${(after.tamperedIndices ?? []).length} altered.`);
|
|
1990
|
+
}
|
|
1920
1991
|
async function cliAuditVerify() {
|
|
1921
1992
|
const report = verifyChain();
|
|
1922
1993
|
const forks = report.forkIndices ?? [];
|
|
1994
|
+
const redacted = report.redactedIndices ?? [];
|
|
1923
1995
|
if (report.ok) {
|
|
1924
1996
|
console.log(`โ
Audit chain verified โ ${report.total} record(s).`);
|
|
1925
|
-
console.log(
|
|
1997
|
+
console.log(redacted.length === 0
|
|
1998
|
+
? ` No record was altered, and no history is missing.`
|
|
1999
|
+
: ` No history is missing. ${redacted.length} record(s) redacted and acknowledged on the chain (indices ${redacted.slice(0, 8).join(", ")}${redacted.length > 8 ? ", โฆ" : ""}), 0 altered.`);
|
|
1926
2000
|
if (forks.length > 0) {
|
|
1927
2001
|
// [VERIFY-FORK-IS-NOT-TAMPER] โ surface this, but do not call it tampering.
|
|
1928
2002
|
console.log(`\nโ ๏ธ ${forks.length} concurrent-append fork(s) detected (not tampering).`);
|
|
@@ -1941,6 +2015,11 @@ async function cliAuditVerify() {
|
|
|
1941
2015
|
console.error(`\n Altered records (content does not match its own hash):`);
|
|
1942
2016
|
console.error(` ${t.slice(0, 10).join(", ")}${t.length > 10 ? `, โฆ (+${t.length - 10} more)` : ""}`);
|
|
1943
2017
|
console.error(` This is tampering: the record's bytes were changed after it was written.`);
|
|
2018
|
+
console.error(` If this was a deliberate redaction of a secret, acknowledge it on the chain:`);
|
|
2019
|
+
console.error(` contextengine audit-redact-ack --index ${t.slice(0, 3).join(",")} --reason "<what was removed and why>"`);
|
|
2020
|
+
}
|
|
2021
|
+
if (redacted.length > 0) {
|
|
2022
|
+
console.error(`\n Also ${redacted.length} redacted record(s), acknowledged on the chain, not counted above.`);
|
|
1944
2023
|
}
|
|
1945
2024
|
if ((report.orphanIndices ?? []).length > 0) {
|
|
1946
2025
|
const o = report.orphanIndices;
|
|
@@ -2325,218 +2404,28 @@ function cliStats() {
|
|
|
2325
2404
|
}
|
|
2326
2405
|
}
|
|
2327
2406
|
// ---------------------------------------------------------------------------
|
|
2328
|
-
// cost โ multi-agent spend, read from Claude Code's own transcripts
|
|
2407
|
+
// cost โ multi-agent spend, read from Claude Code's own transcripts.
|
|
2408
|
+
// Rendered by src/cost-report.ts, shared with the MCP tool. [LOCK] [COST-REPORT-ONE-RENDERER]
|
|
2329
2409
|
// ---------------------------------------------------------------------------
|
|
2330
|
-
function fmtTok(n) {
|
|
2331
|
-
if (n >= 1e6)
|
|
2332
|
-
return `${(n / 1e6).toFixed(1)}M`;
|
|
2333
|
-
if (n >= 1e3)
|
|
2334
|
-
return `${(n / 1e3).toFixed(0)}k`;
|
|
2335
|
-
return String(n);
|
|
2336
|
-
}
|
|
2337
|
-
function fmtDur(ms) {
|
|
2338
|
-
if (ms === null || !Number.isFinite(ms))
|
|
2339
|
-
return "โ";
|
|
2340
|
-
const s = Math.round(ms / 1000);
|
|
2341
|
-
if (s < 60)
|
|
2342
|
-
return `${s}s`;
|
|
2343
|
-
const m = Math.floor(s / 60);
|
|
2344
|
-
if (m < 60)
|
|
2345
|
-
return `${m}m${String(s % 60).padStart(2, "0")}s`;
|
|
2346
|
-
return `${Math.floor(m / 60)}h${String(m % 60).padStart(2, "0")}m`;
|
|
2347
|
-
}
|
|
2348
|
-
/** Resolve cost thresholds + pricing from policy, falling back to defaults. */
|
|
2349
|
-
function loadCostThresholds(cwd) {
|
|
2350
|
-
const res = loadRepoPolicy(cwd);
|
|
2351
|
-
if (res && res.ok && res.policy.agent_cost) {
|
|
2352
|
-
const a = res.policy.agent_cost;
|
|
2353
|
-
// [DEFAULT-RATES-SHIP-WITH-THE-PACKAGE] โ an agent_cost block that omits
|
|
2354
|
-
// `pricing` must not silently price nothing.
|
|
2355
|
-
const hasOwnRates = a.pricing.length > 0;
|
|
2356
|
-
return {
|
|
2357
|
-
t: {
|
|
2358
|
-
billing_mode: a.billing_mode,
|
|
2359
|
-
pricing: hasOwnRates ? a.pricing : DEFAULT_PRICING,
|
|
2360
|
-
min_cache_efficiency: a.min_cache_efficiency,
|
|
2361
|
-
max_tool_calls_per_agent: a.max_tool_calls_per_agent,
|
|
2362
|
-
max_cost_per_agent_usd: a.max_cost_per_agent_usd,
|
|
2363
|
-
min_fanout_for_canary: a.min_fanout_for_canary,
|
|
2364
|
-
max_failed_share: a.max_failed_share,
|
|
2365
|
-
},
|
|
2366
|
-
source: ".contextengine/policy.json" +
|
|
2367
|
-
(hasOwnRates ? "" : ` (rates: built-in, as of ${DEFAULT_PRICING_ASOF})`),
|
|
2368
|
-
};
|
|
2369
|
-
}
|
|
2370
|
-
return {
|
|
2371
|
-
t: DEFAULT_COST_THRESHOLDS,
|
|
2372
|
-
source: `built-in defaults, rates as of ${DEFAULT_PRICING_ASOF} (no agent_cost in policy.json)`,
|
|
2373
|
-
};
|
|
2374
|
-
}
|
|
2375
2410
|
async function cliCost(argv) {
|
|
2376
2411
|
const flag = (name) => {
|
|
2377
2412
|
const i = argv.indexOf(`--${name}`);
|
|
2378
2413
|
return i >= 0 ? argv[i + 1] : undefined;
|
|
2379
2414
|
};
|
|
2380
|
-
const json = argv.includes("--json");
|
|
2381
2415
|
const topRaw = flag("top");
|
|
2382
|
-
const top = topRaw ? Math.max(1, parseInt(topRaw, 10) || 10) : 10;
|
|
2383
2416
|
const daysRaw = flag("days");
|
|
2384
|
-
const
|
|
2385
|
-
const cwd = process.cwd();
|
|
2386
|
-
const { t, source } = loadCostThresholds(cwd);
|
|
2387
|
-
const runs = collectRuns({
|
|
2417
|
+
const report = buildCostReport({
|
|
2388
2418
|
session: flag("session"),
|
|
2389
2419
|
project: flag("project"),
|
|
2390
2420
|
run: flag("run"),
|
|
2391
|
-
|
|
2421
|
+
top: topRaw ? parseInt(topRaw, 10) || 10 : 10,
|
|
2422
|
+
days: daysRaw ? parseInt(daysRaw, 10) || undefined : undefined,
|
|
2392
2423
|
});
|
|
2393
|
-
if (
|
|
2394
|
-
console.log(
|
|
2395
|
-
console.log("(fan-outs only: parent sessions are not counted โ this measures delegation)");
|
|
2424
|
+
if (argv.includes("--json") && report.json) {
|
|
2425
|
+
console.log(JSON.stringify(report.json, null, 2));
|
|
2396
2426
|
return;
|
|
2397
2427
|
}
|
|
2398
|
-
|
|
2399
|
-
.map((r) => ({ run: r, m: metricsFor(r, t.pricing) }))
|
|
2400
|
-
.sort((a, b) => b.m.cost.total - a.m.cost.total);
|
|
2401
|
-
const signals = runTranscriptHeuristics(runs, t);
|
|
2402
|
-
if (json) {
|
|
2403
|
-
console.log(JSON.stringify({
|
|
2404
|
-
billing_mode: t.billing_mode,
|
|
2405
|
-
cost_is_notional: t.billing_mode === "subscription",
|
|
2406
|
-
thresholds_source: source,
|
|
2407
|
-
runs: scored.map(({ run, m }) => ({
|
|
2408
|
-
runId: run.runId, kind: run.kind, project: run.project, sessionId: run.sessionId,
|
|
2409
|
-
volume: run.totals, intensity: {
|
|
2410
|
-
agents: m.agents, reported: m.reported, failed: m.failed,
|
|
2411
|
-
capacityExhausted: m.capacityExhausted, toolCalls: m.toolCalls,
|
|
2412
|
-
medianToolCalls: m.medianToolCalls, durationMs: run.durationMs,
|
|
2413
|
-
launchedBeforeFirstReport: m.launchedBeforeFirstReport,
|
|
2414
|
-
},
|
|
2415
|
-
cost: m.cost, cacheEfficiency: Number.isFinite(m.cacheEfficiency) ? m.cacheEfficiency : null,
|
|
2416
|
-
outputShare: m.outputShare,
|
|
2417
|
-
})),
|
|
2418
|
-
signals,
|
|
2419
|
-
}, null, 2));
|
|
2420
|
-
return;
|
|
2421
|
-
}
|
|
2422
|
-
// Aggregate across everything in scope.
|
|
2423
|
-
let vol = emptyTally();
|
|
2424
|
-
let agents = 0, toolCalls = 0, failed = 0, capacity = 0, reported = 0;
|
|
2425
|
-
let cost = 0, withoutCache = 0, unpriced = 0;
|
|
2426
|
-
// Which models carried tokens but matched no rate โ named in the output so
|
|
2427
|
-
// the fix is actionable instead of "something was unpriced".
|
|
2428
|
-
const unpricedModels = new Set();
|
|
2429
|
-
for (const { run, m } of scored) {
|
|
2430
|
-
for (const a of run.agents) {
|
|
2431
|
-
for (const [model, tally] of a.tokensByModel) {
|
|
2432
|
-
if (totalTokens(tally) > 0 && !pricingFor(model, t.pricing)) {
|
|
2433
|
-
unpricedModels.add(model ?? "(no model recorded)");
|
|
2434
|
-
}
|
|
2435
|
-
}
|
|
2436
|
-
}
|
|
2437
|
-
vol = addTally(vol, run.totals);
|
|
2438
|
-
agents += m.agents;
|
|
2439
|
-
toolCalls += m.toolCalls;
|
|
2440
|
-
failed += m.failed;
|
|
2441
|
-
capacity += m.capacityExhausted;
|
|
2442
|
-
reported += m.reported;
|
|
2443
|
-
cost += m.cost.total;
|
|
2444
|
-
withoutCache += m.cost.withoutCache;
|
|
2445
|
-
unpriced += m.cost.unpricedTokens;
|
|
2446
|
-
}
|
|
2447
|
-
const allTok = totalTokens(vol);
|
|
2448
|
-
const cw = vol.cacheWrite5m + vol.cacheWrite1h;
|
|
2449
|
-
console.log("");
|
|
2450
|
-
console.log(`MULTI-AGENT COST โ ${scored.length} run(s), ${agents} subagents`);
|
|
2451
|
-
console.log(`thresholds: ${source}`);
|
|
2452
|
-
console.log("");
|
|
2453
|
-
// โโ 1. VOLUME โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
2454
|
-
console.log("VOLUME (tokens moved)");
|
|
2455
|
-
const volRow = (label, n) => console.log(` ${label.padEnd(16)} ${fmtTok(n).padStart(8)} ${allTok ? ((100 * n) / allTok).toFixed(1).padStart(5) : " 0.0"}%`);
|
|
2456
|
-
volRow("cache read", vol.cacheRead);
|
|
2457
|
-
volRow("cache write", cw);
|
|
2458
|
-
volRow("input (fresh)", vol.input);
|
|
2459
|
-
volRow("output", vol.output);
|
|
2460
|
-
console.log(` ${"total".padEnd(16)} ${fmtTok(allTok).padStart(8)}`);
|
|
2461
|
-
console.log("");
|
|
2462
|
-
// โโ 2. VALUED COST โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
2463
|
-
const notional = t.billing_mode === "subscription";
|
|
2464
|
-
let ci = 0, ccw = 0, ccr = 0, co = 0;
|
|
2465
|
-
for (const { m } of scored) {
|
|
2466
|
-
ci += m.cost.input;
|
|
2467
|
-
ccw += m.cost.cacheWrite;
|
|
2468
|
-
ccr += m.cost.cacheRead;
|
|
2469
|
-
co += m.cost.output;
|
|
2470
|
-
}
|
|
2471
|
-
const agg = {
|
|
2472
|
-
input: ci, cacheWrite: ccw, cacheRead: ccr, output: co,
|
|
2473
|
-
total: cost, withoutCache, unpricedTokens: unpriced,
|
|
2474
|
-
};
|
|
2475
|
-
const status = pricingStatus(agg);
|
|
2476
|
-
console.log(`VALUED COST (API list prices)${notional && status !== "unpriced" ? " โ NOTIONAL, NOT BILLED" : ""}`);
|
|
2477
|
-
// [NEVER-RENDER-AN-UNKNOWN-AS-A-NUMBER] โ with nothing priced there is no
|
|
2478
|
-
// cost to show. Printing a $0.00 table here reads as "this run was free"
|
|
2479
|
-
// and "caching saved 0%", both false.
|
|
2480
|
-
if (status === "unpriced") {
|
|
2481
|
-
console.log(` UNPRICED โ no rate matched any model in this data, so no cost can be`);
|
|
2482
|
-
console.log(` stated. ${fmtTok(unpriced)} tokens were moved. This is an unknown, not $0.`);
|
|
2483
|
-
console.log("");
|
|
2484
|
-
console.log(` Models seen without a rate: ${[...unpricedModels].sort().join(", ") || "(unknown)"}`);
|
|
2485
|
-
console.log(` Add them to .contextengine/policy.json โ agent_cost.pricing.`);
|
|
2486
|
-
console.log("");
|
|
2487
|
-
}
|
|
2488
|
-
else {
|
|
2489
|
-
if (notional) {
|
|
2490
|
-
console.log(" This machine runs Claude Code on a subscription: no dollar below is");
|
|
2491
|
-
console.log(" debited. Use these figures to compare approaches, not as spend.");
|
|
2492
|
-
}
|
|
2493
|
-
const costRow = (label, n) => console.log(` ${label.padEnd(16)} ${("$" + n.toFixed(2)).padStart(8)} ${cost ? ((100 * n) / cost).toFixed(1).padStart(5) : " 0.0"}%`);
|
|
2494
|
-
costRow("cache read", ccr);
|
|
2495
|
-
costRow("cache write", ccw);
|
|
2496
|
-
costRow("input (fresh)", ci);
|
|
2497
|
-
costRow("output", co);
|
|
2498
|
-
console.log(` ${"total".padEnd(16)} ${("$" + cost.toFixed(2)).padStart(8)}`);
|
|
2499
|
-
console.log(` ${"without cache".padEnd(16)} ${("$" + withoutCache.toFixed(2)).padStart(8)} ` +
|
|
2500
|
-
`caching saved $${(withoutCache - cost).toFixed(2)} (${withoutCache ? (100 * (1 - cost / withoutCache)).toFixed(0) : "0"}%)`);
|
|
2501
|
-
if (status === "partial") {
|
|
2502
|
-
console.log(` โ ${fmtTok(unpriced)} tokens UNPRICED and NOT in the figures above` +
|
|
2503
|
-
` (${[...unpricedModels].sort().join(", ") || "unknown model"}) โ the total is a floor, not the cost`);
|
|
2504
|
-
}
|
|
2505
|
-
console.log("");
|
|
2506
|
-
}
|
|
2507
|
-
// โโ 3. INTENSITY (the capacity proxy) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
2508
|
-
console.log(`INTENSITY (capacity proxy${notional ? " โ the scarce resource here" : ""})`);
|
|
2509
|
-
console.log(` subagents ${String(agents).padStart(8)}`);
|
|
2510
|
-
console.log(` reported ${String(reported).padStart(8)}`);
|
|
2511
|
-
console.log(` returned nothing ${String(failed).padStart(8)}${failed ? ` (${((100 * failed) / agents).toFixed(0)}% of the fleet)` : ""}`);
|
|
2512
|
-
console.log(` died at window ${String(capacity).padStart(8)}${capacity ? " โ capacity spent for no result" : ""}`);
|
|
2513
|
-
console.log(` tool calls ${String(toolCalls).padStart(8)} (${(toolCalls / Math.max(1, agents)).toFixed(1)}/agent)`);
|
|
2514
|
-
console.log(` cache reuse ${(cw ? (vol.cacheRead / cw).toFixed(1) + "x" : "โ").padStart(8)} ${cw && vol.cacheRead / cw < t.min_cache_efficiency ? "โ below floor, prefix is being rebuilt" : "(higher is better)"}`);
|
|
2515
|
-
console.log("");
|
|
2516
|
-
// โโ Top runs โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
2517
|
-
console.log(`TOP RUNS BY VALUED COST (${Math.min(top, scored.length)} of ${scored.length})`);
|
|
2518
|
-
console.log(` ${"cost".padStart(8)} ${"agents".padStart(6)} ${"dead".padStart(4)} ${"tools".padStart(5)} ${"reuse".padStart(6)} ${"dur".padStart(7)} run`);
|
|
2519
|
-
for (const { run, m } of scored.slice(0, top)) {
|
|
2520
|
-
const reuse = Number.isFinite(m.cacheEfficiency) ? m.cacheEfficiency.toFixed(1) + "x" : "โ";
|
|
2521
|
-
console.log(` ${("$" + m.cost.total.toFixed(2)).padStart(8)} ${String(m.agents).padStart(6)} ` +
|
|
2522
|
-
`${String(m.failed).padStart(4)} ${String(m.medianToolCalls).padStart(5)} ${reuse.padStart(6)} ` +
|
|
2523
|
-
`${fmtDur(run.durationMs).padStart(7)} ${run.runId} ${run.project.replace(/^-Users-yan-/, "")}`);
|
|
2524
|
-
}
|
|
2525
|
-
console.log("");
|
|
2526
|
-
// โโ Signals โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
2527
|
-
if (!signals.length) {
|
|
2528
|
-
console.log("โ
No context_burn or fanout_without_canary signals.");
|
|
2529
|
-
}
|
|
2530
|
-
else {
|
|
2531
|
-
const crit = signals.filter((s) => s.severity === "critical");
|
|
2532
|
-
console.log(`SIGNALS โ ${signals.length} (${crit.length} critical)`);
|
|
2533
|
-
for (const s of signals.slice(0, 20)) {
|
|
2534
|
-
console.log(` ${s.severity === "critical" ? "๐ด" : "โ ๏ธ "} [${s.kind}] ${s.reason}`);
|
|
2535
|
-
}
|
|
2536
|
-
if (signals.length > 20)
|
|
2537
|
-
console.log(` โฆ ${signals.length - 20} more (use --json)`);
|
|
2538
|
-
}
|
|
2539
|
-
console.log("");
|
|
2428
|
+
console.log(report.text);
|
|
2540
2429
|
}
|
|
2541
2430
|
/** Package version, read from the installed package.json rather than hardcoded. */
|
|
2542
2431
|
function readPackageVersion() {
|
|
@@ -2586,6 +2475,7 @@ Usage:
|
|
|
2586
2475
|
Export hash-chained audit log (evidence aligned with
|
|
2587
2476
|
SOC 2 CC7.2 + ISO 27001 A.12.4.1 โ not a certification)
|
|
2588
2477
|
contextengine audit-verify Verify audit log chain integrity (tamper detection)
|
|
2478
|
+
contextengine audit-redact-ack Acknowledge deliberately redacted records on the chain (--index i,j --reason "...")
|
|
2589
2479
|
contextengine audit-rotate [--keep-days N] [--max-records N] [--dry-run]
|
|
2590
2480
|
Move old history into an archive segment. Archives
|
|
2591
2481
|
whatever is older than N days (default 30) OR beyond
|
|
@@ -2791,6 +2681,9 @@ else if (command === "sync-claude-md") {
|
|
|
2791
2681
|
process.exit(1);
|
|
2792
2682
|
});
|
|
2793
2683
|
}
|
|
2684
|
+
else if (command === "audit-redact-ack") {
|
|
2685
|
+
cliAuditRedactAck(process.argv.slice(3));
|
|
2686
|
+
}
|
|
2794
2687
|
else if (command === "audit-rotate") {
|
|
2795
2688
|
cliAuditRotate(process.argv.slice(3));
|
|
2796
2689
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type CostThresholds } from "./detector.js";
|
|
2
|
+
export interface CostReportOptions {
|
|
3
|
+
session?: string;
|
|
4
|
+
project?: string;
|
|
5
|
+
run?: string;
|
|
6
|
+
days?: number;
|
|
7
|
+
top?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface CostReport {
|
|
10
|
+
/** Human-readable report, what the CLI prints. */
|
|
11
|
+
text: string;
|
|
12
|
+
/** Structured report, what `--json` prints. null when no runs were found. */
|
|
13
|
+
json: Record<string, unknown> | null;
|
|
14
|
+
runs: number;
|
|
15
|
+
}
|
|
16
|
+
/** Resolve cost thresholds + pricing from policy, falling back to defaults. */
|
|
17
|
+
export declare function loadCostThresholds(cwd: string): {
|
|
18
|
+
t: CostThresholds;
|
|
19
|
+
source: string;
|
|
20
|
+
};
|
|
21
|
+
export declare function buildCostReport(opts?: CostReportOptions, cwd?: string): CostReport;
|
|
22
|
+
//# sourceMappingURL=cost-report.d.ts.map
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-agent cost report, shared by the CLI (`contextengine cost`) and the MCP tool
|
|
3
|
+
* (`agent_cost`). One renderer, two surfaces.
|
|
4
|
+
*
|
|
5
|
+
* [LOCKED] [COST-REPORT-ONE-RENDERER] โ 2026-08-21
|
|
6
|
+
* [NEVER] render the cost report in cli.ts or index.ts directly.
|
|
7
|
+
* WHY: the CLI shipped on 2026-08-20 as 170 lines of console.log; an MCP tool written the same
|
|
8
|
+
* way would have been a second copy of every threshold, label and guard (NOTIONAL, UNPRICED,
|
|
9
|
+
* floor-not-cost) that drifts the first time one of them is edited.
|
|
10
|
+
* FIX: buildCostReport() returns { text, json }; cli.ts prints, index.ts responds. Both surfaces
|
|
11
|
+
* read the same thresholds from .contextengine/policy.json via loadCostThresholds().
|
|
12
|
+
*/
|
|
13
|
+
import { collectRuns, metricsFor, transcriptRoot, emptyTally, addTally, totalTokens, pricingStatus, pricingFor, } from "./transcript-collector.js";
|
|
14
|
+
import { DEFAULT_PRICING, DEFAULT_PRICING_ASOF } from "./default-pricing.js";
|
|
15
|
+
import { runTranscriptHeuristics, DEFAULT_COST_THRESHOLDS, } from "./detector.js";
|
|
16
|
+
import { loadRepoPolicy } from "./policy.js";
|
|
17
|
+
function fmtTok(n) {
|
|
18
|
+
if (n >= 1e6)
|
|
19
|
+
return `${(n / 1e6).toFixed(1)}M`;
|
|
20
|
+
if (n >= 1e3)
|
|
21
|
+
return `${(n / 1e3).toFixed(0)}k`;
|
|
22
|
+
return String(n);
|
|
23
|
+
}
|
|
24
|
+
function fmtDur(ms) {
|
|
25
|
+
if (ms === null || !Number.isFinite(ms))
|
|
26
|
+
return "โ";
|
|
27
|
+
const s = Math.round(ms / 1000);
|
|
28
|
+
if (s < 60)
|
|
29
|
+
return `${s}s`;
|
|
30
|
+
const m = Math.floor(s / 60);
|
|
31
|
+
if (m < 60)
|
|
32
|
+
return `${m}m${String(s % 60).padStart(2, "0")}s`;
|
|
33
|
+
return `${Math.floor(m / 60)}h${String(m % 60).padStart(2, "0")}m`;
|
|
34
|
+
}
|
|
35
|
+
/** Resolve cost thresholds + pricing from policy, falling back to defaults. */
|
|
36
|
+
export function loadCostThresholds(cwd) {
|
|
37
|
+
const res = loadRepoPolicy(cwd);
|
|
38
|
+
if (res && res.ok && res.policy.agent_cost) {
|
|
39
|
+
const a = res.policy.agent_cost;
|
|
40
|
+
// [DEFAULT-RATES-SHIP-WITH-THE-PACKAGE] โ an agent_cost block that omits
|
|
41
|
+
// `pricing` must not silently price nothing.
|
|
42
|
+
const hasOwnRates = a.pricing.length > 0;
|
|
43
|
+
return {
|
|
44
|
+
t: {
|
|
45
|
+
billing_mode: a.billing_mode,
|
|
46
|
+
pricing: hasOwnRates ? a.pricing : DEFAULT_PRICING,
|
|
47
|
+
min_cache_efficiency: a.min_cache_efficiency,
|
|
48
|
+
max_tool_calls_per_agent: a.max_tool_calls_per_agent,
|
|
49
|
+
max_cost_per_agent_usd: a.max_cost_per_agent_usd,
|
|
50
|
+
min_fanout_for_canary: a.min_fanout_for_canary,
|
|
51
|
+
max_failed_share: a.max_failed_share,
|
|
52
|
+
},
|
|
53
|
+
source: ".contextengine/policy.json" +
|
|
54
|
+
(hasOwnRates ? "" : ` (rates: built-in, as of ${DEFAULT_PRICING_ASOF})`),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
t: DEFAULT_COST_THRESHOLDS,
|
|
59
|
+
source: `built-in defaults, rates as of ${DEFAULT_PRICING_ASOF} (no agent_cost in policy.json)`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export function buildCostReport(opts = {}, cwd = process.cwd()) {
|
|
63
|
+
const out = [];
|
|
64
|
+
const line = (s = "") => { out.push(s); };
|
|
65
|
+
const top = Math.max(1, opts.top ?? 10);
|
|
66
|
+
const since = opts.days ? Date.now() - opts.days * 86_400_000 : undefined;
|
|
67
|
+
const { t, source } = loadCostThresholds(cwd);
|
|
68
|
+
const runs = collectRuns({
|
|
69
|
+
session: opts.session,
|
|
70
|
+
project: opts.project,
|
|
71
|
+
run: opts.run,
|
|
72
|
+
since,
|
|
73
|
+
});
|
|
74
|
+
if (!runs.length) {
|
|
75
|
+
line("No multi-agent runs found in " + transcriptRoot());
|
|
76
|
+
line("(fan-outs only: parent sessions are not counted โ this measures delegation)");
|
|
77
|
+
return { text: out.join("\n"), json: null, runs: 0 };
|
|
78
|
+
}
|
|
79
|
+
const scored = runs
|
|
80
|
+
.map((r) => ({ run: r, m: metricsFor(r, t.pricing) }))
|
|
81
|
+
.sort((a, b) => b.m.cost.total - a.m.cost.total);
|
|
82
|
+
const signals = runTranscriptHeuristics(runs, t);
|
|
83
|
+
const json = {
|
|
84
|
+
billing_mode: t.billing_mode,
|
|
85
|
+
cost_is_notional: t.billing_mode === "subscription",
|
|
86
|
+
thresholds_source: source,
|
|
87
|
+
runs: scored.map(({ run, m }) => ({
|
|
88
|
+
runId: run.runId, kind: run.kind, project: run.project, sessionId: run.sessionId,
|
|
89
|
+
volume: run.totals, intensity: {
|
|
90
|
+
agents: m.agents, reported: m.reported, failed: m.failed,
|
|
91
|
+
capacityExhausted: m.capacityExhausted, toolCalls: m.toolCalls,
|
|
92
|
+
medianToolCalls: m.medianToolCalls, durationMs: run.durationMs,
|
|
93
|
+
launchedBeforeFirstReport: m.launchedBeforeFirstReport,
|
|
94
|
+
},
|
|
95
|
+
cost: m.cost, cacheEfficiency: Number.isFinite(m.cacheEfficiency) ? m.cacheEfficiency : null,
|
|
96
|
+
outputShare: m.outputShare,
|
|
97
|
+
})),
|
|
98
|
+
signals,
|
|
99
|
+
};
|
|
100
|
+
// Aggregate across everything in scope.
|
|
101
|
+
let vol = emptyTally();
|
|
102
|
+
let agents = 0, toolCalls = 0, failed = 0, capacity = 0, reported = 0;
|
|
103
|
+
let cost = 0, withoutCache = 0, unpriced = 0;
|
|
104
|
+
// Which models carried tokens but matched no rate โ named in the output so
|
|
105
|
+
// the fix is actionable instead of "something was unpriced".
|
|
106
|
+
const unpricedModels = new Set();
|
|
107
|
+
for (const { run, m } of scored) {
|
|
108
|
+
for (const a of run.agents) {
|
|
109
|
+
for (const [model, tally] of a.tokensByModel) {
|
|
110
|
+
if (totalTokens(tally) > 0 && !pricingFor(model, t.pricing)) {
|
|
111
|
+
unpricedModels.add(model ?? "(no model recorded)");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
vol = addTally(vol, run.totals);
|
|
116
|
+
agents += m.agents;
|
|
117
|
+
toolCalls += m.toolCalls;
|
|
118
|
+
failed += m.failed;
|
|
119
|
+
capacity += m.capacityExhausted;
|
|
120
|
+
reported += m.reported;
|
|
121
|
+
cost += m.cost.total;
|
|
122
|
+
withoutCache += m.cost.withoutCache;
|
|
123
|
+
unpriced += m.cost.unpricedTokens;
|
|
124
|
+
}
|
|
125
|
+
const allTok = totalTokens(vol);
|
|
126
|
+
const cw = vol.cacheWrite5m + vol.cacheWrite1h;
|
|
127
|
+
line();
|
|
128
|
+
line(`MULTI-AGENT COST โ ${scored.length} run(s), ${agents} subagents`);
|
|
129
|
+
line(`thresholds: ${source}`);
|
|
130
|
+
line();
|
|
131
|
+
// โโ 1. VOLUME โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
132
|
+
line("VOLUME (tokens moved)");
|
|
133
|
+
const volRow = (label, n) => line(` ${label.padEnd(16)} ${fmtTok(n).padStart(8)} ${allTok ? ((100 * n) / allTok).toFixed(1).padStart(5) : " 0.0"}%`);
|
|
134
|
+
volRow("cache read", vol.cacheRead);
|
|
135
|
+
volRow("cache write", cw);
|
|
136
|
+
volRow("input (fresh)", vol.input);
|
|
137
|
+
volRow("output", vol.output);
|
|
138
|
+
line(` ${"total".padEnd(16)} ${fmtTok(allTok).padStart(8)}`);
|
|
139
|
+
line();
|
|
140
|
+
// โโ 2. VALUED COST โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
141
|
+
const notional = t.billing_mode === "subscription";
|
|
142
|
+
let ci = 0, ccw = 0, ccr = 0, co = 0;
|
|
143
|
+
for (const { m } of scored) {
|
|
144
|
+
ci += m.cost.input;
|
|
145
|
+
ccw += m.cost.cacheWrite;
|
|
146
|
+
ccr += m.cost.cacheRead;
|
|
147
|
+
co += m.cost.output;
|
|
148
|
+
}
|
|
149
|
+
const agg = {
|
|
150
|
+
input: ci, cacheWrite: ccw, cacheRead: ccr, output: co,
|
|
151
|
+
total: cost, withoutCache, unpricedTokens: unpriced,
|
|
152
|
+
};
|
|
153
|
+
const status = pricingStatus(agg);
|
|
154
|
+
line(`VALUED COST (API list prices)${notional && status !== "unpriced" ? " โ NOTIONAL, NOT BILLED" : ""}`);
|
|
155
|
+
// [NEVER-RENDER-AN-UNKNOWN-AS-A-NUMBER] โ with nothing priced there is no
|
|
156
|
+
// cost to show. Printing a $0.00 table here reads as "this run was free"
|
|
157
|
+
// and "caching saved 0%", both false.
|
|
158
|
+
if (status === "unpriced") {
|
|
159
|
+
line(` UNPRICED โ no rate matched any model in this data, so no cost can be`);
|
|
160
|
+
line(` stated. ${fmtTok(unpriced)} tokens were moved. This is an unknown, not $0.`);
|
|
161
|
+
line();
|
|
162
|
+
line(` Models seen without a rate: ${[...unpricedModels].sort().join(", ") || "(unknown)"}`);
|
|
163
|
+
line(` Add them to .contextengine/policy.json โ agent_cost.pricing.`);
|
|
164
|
+
line();
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
if (notional) {
|
|
168
|
+
line(" This machine runs Claude Code on a subscription: no dollar below is");
|
|
169
|
+
line(" debited. Use these figures to compare approaches, not as spend.");
|
|
170
|
+
}
|
|
171
|
+
const costRow = (label, n) => line(` ${label.padEnd(16)} ${("$" + n.toFixed(2)).padStart(8)} ${cost ? ((100 * n) / cost).toFixed(1).padStart(5) : " 0.0"}%`);
|
|
172
|
+
costRow("cache read", ccr);
|
|
173
|
+
costRow("cache write", ccw);
|
|
174
|
+
costRow("input (fresh)", ci);
|
|
175
|
+
costRow("output", co);
|
|
176
|
+
line(` ${"total".padEnd(16)} ${("$" + cost.toFixed(2)).padStart(8)}`);
|
|
177
|
+
line(` ${"without cache".padEnd(16)} ${("$" + withoutCache.toFixed(2)).padStart(8)} ` +
|
|
178
|
+
`caching saved $${(withoutCache - cost).toFixed(2)} (${withoutCache ? (100 * (1 - cost / withoutCache)).toFixed(0) : "0"}%)`);
|
|
179
|
+
if (status === "partial") {
|
|
180
|
+
line(` โ ${fmtTok(unpriced)} tokens UNPRICED and NOT in the figures above` +
|
|
181
|
+
` (${[...unpricedModels].sort().join(", ") || "unknown model"}) โ the total is a floor, not the cost`);
|
|
182
|
+
}
|
|
183
|
+
line();
|
|
184
|
+
}
|
|
185
|
+
// โโ 3. INTENSITY (the capacity proxy) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
186
|
+
line(`INTENSITY (capacity proxy${notional ? " โ the scarce resource here" : ""})`);
|
|
187
|
+
line(` subagents ${String(agents).padStart(8)}`);
|
|
188
|
+
line(` reported ${String(reported).padStart(8)}`);
|
|
189
|
+
line(` returned nothing ${String(failed).padStart(8)}${failed ? ` (${((100 * failed) / agents).toFixed(0)}% of the fleet)` : ""}`);
|
|
190
|
+
line(` died at window ${String(capacity).padStart(8)}${capacity ? " โ capacity spent for no result" : ""}`);
|
|
191
|
+
line(` tool calls ${String(toolCalls).padStart(8)} (${(toolCalls / Math.max(1, agents)).toFixed(1)}/agent)`);
|
|
192
|
+
line(` cache reuse ${(cw ? (vol.cacheRead / cw).toFixed(1) + "x" : "โ").padStart(8)} ${cw && vol.cacheRead / cw < t.min_cache_efficiency ? "โ below floor, prefix is being rebuilt" : "(higher is better)"}`);
|
|
193
|
+
line();
|
|
194
|
+
// โโ Top runs โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
195
|
+
line(`TOP RUNS BY VALUED COST (${Math.min(top, scored.length)} of ${scored.length})`);
|
|
196
|
+
line(` ${"cost".padStart(8)} ${"agents".padStart(6)} ${"dead".padStart(4)} ${"tools".padStart(5)} ${"reuse".padStart(6)} ${"dur".padStart(7)} run`);
|
|
197
|
+
for (const { run, m } of scored.slice(0, top)) {
|
|
198
|
+
const reuse = Number.isFinite(m.cacheEfficiency) ? m.cacheEfficiency.toFixed(1) + "x" : "โ";
|
|
199
|
+
line(` ${("$" + m.cost.total.toFixed(2)).padStart(8)} ${String(m.agents).padStart(6)} ` +
|
|
200
|
+
`${String(m.failed).padStart(4)} ${String(m.medianToolCalls).padStart(5)} ${reuse.padStart(6)} ` +
|
|
201
|
+
`${fmtDur(run.durationMs).padStart(7)} ${run.runId} ${run.project.replace(/^-Users-yan-/, "")}`);
|
|
202
|
+
}
|
|
203
|
+
line();
|
|
204
|
+
// โโ Signals โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
205
|
+
if (!signals.length) {
|
|
206
|
+
line("โ
No context_burn or fanout_without_canary signals.");
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
const crit = signals.filter((s) => s.severity === "critical");
|
|
210
|
+
line(`SIGNALS โ ${signals.length} (${crit.length} critical)`);
|
|
211
|
+
for (const s of signals.slice(0, 20)) {
|
|
212
|
+
line(` ${s.severity === "critical" ? "๐ด" : "โ ๏ธ "} [${s.kind}] ${s.reason}`);
|
|
213
|
+
}
|
|
214
|
+
if (signals.length > 20)
|
|
215
|
+
line(` โฆ ${signals.length - 20} more (use --json)`);
|
|
216
|
+
}
|
|
217
|
+
line();
|
|
218
|
+
return { text: out.join("\n"), json, runs: scored.length };
|
|
219
|
+
}
|
|
220
|
+
//# sourceMappingURL=cost-report.js.map
|