@compr/opscontext-mcp 2.4.3 → 2.5.1
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/dist/cli.d.ts +1 -0
- package/dist/cli.js +347 -3
- package/dist/default-pricing.d.ts +36 -0
- package/dist/default-pricing.js +57 -0
- package/dist/detector.d.ts +40 -1
- package/dist/detector.js +118 -0
- package/dist/hooks.d.ts +14 -0
- package/dist/hooks.js +154 -0
- package/dist/index.js +24 -1
- package/dist/policy.d.ts +114 -0
- package/dist/policy.js +90 -0
- package/dist/transcript-collector.d.ts +226 -0
- package/dist/transcript-collector.js +452 -0
- package/package.json +1 -1
package/dist/cli.d.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* contextengine save-learning Save a learning (terminal fallback for MCP)
|
|
13
13
|
* contextengine score [project|path] AI-readiness score for one project (default: cwd; --all for fleet)
|
|
14
14
|
* contextengine audit Run compliance audit across all projects
|
|
15
|
+
* contextengine cost Multi-agent token/cost/capacity report from transcripts
|
|
15
16
|
* contextengine help Show this message
|
|
16
17
|
*/
|
|
17
18
|
export {};
|
package/dist/cli.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* contextengine save-learning Save a learning (terminal fallback for MCP)
|
|
13
13
|
* contextengine score [project|path] AI-readiness score for one project (default: cwd; --all for fleet)
|
|
14
14
|
* contextengine audit Run compliance audit across all projects
|
|
15
|
+
* contextengine cost Multi-agent token/cost/capacity report from transcripts
|
|
15
16
|
* contextengine help Show this message
|
|
16
17
|
*/
|
|
17
18
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync } from "fs";
|
|
@@ -297,6 +298,62 @@ function generatePreCommitHook() {
|
|
|
297
298
|
// ---------------------------------------------------------------------------
|
|
298
299
|
// Template for post-commit hook (auto-push)
|
|
299
300
|
// ---------------------------------------------------------------------------
|
|
301
|
+
/**
|
|
302
|
+
* Generate the commit-msg hook — the ONLY place `commit_message_required`
|
|
303
|
+
* can run.
|
|
304
|
+
*
|
|
305
|
+
* 🔒 LOCKED [COMMIT-MSG-HOOK-MUST-BE-INSTALLED] — 2026-08-19
|
|
306
|
+
* ⛔ NEVER ship a `commit_message_required` policy rule without generating
|
|
307
|
+
* this hook in the same `init`.
|
|
308
|
+
* WHY: the rule type, its CLI subcommand, and its tests all existed and all
|
|
309
|
+
* passed, but `init` installed only pre-commit and post-commit. Proven by
|
|
310
|
+
* execution on 2026-08-19: in a repo created by `contextengine init` with
|
|
311
|
+
* a `severity: block` rule on `server/deploy.sh`, committing that file
|
|
312
|
+
* with a non-compliant message SUCCEEDED. The gate could not fire because
|
|
313
|
+
* nothing called it. It worked in this repo only because a commit-msg
|
|
314
|
+
* hook had been installed by hand — so the author's own machine was the
|
|
315
|
+
* one place the gap was invisible.
|
|
316
|
+
* FIX: install commit-msg alongside pre-commit. `commit_message_required`
|
|
317
|
+
* cannot run from pre-commit — git does not populate the message file
|
|
318
|
+
* until after pre-commit returns (see the subcommand's own note).
|
|
319
|
+
*/
|
|
320
|
+
function generateCommitMsgHook() {
|
|
321
|
+
const lines = [];
|
|
322
|
+
lines.push("#!/bin/zsh");
|
|
323
|
+
lines.push("# ContextEngine — commit-msg policy gate");
|
|
324
|
+
lines.push("# Runs .contextengine/policy.json `commit_message_required` rules.");
|
|
325
|
+
lines.push("# MUST live here, not pre-commit: git does not populate the commit");
|
|
326
|
+
lines.push("# message file until after pre-commit returns.");
|
|
327
|
+
lines.push("");
|
|
328
|
+
lines.push('COMMIT_MSG_FILE="$1"');
|
|
329
|
+
lines.push("");
|
|
330
|
+
lines.push("find_ce_cli() {");
|
|
331
|
+
lines.push(' if [[ -x "node_modules/.bin/contextengine" ]]; then');
|
|
332
|
+
lines.push(' echo "node_modules/.bin/contextengine"');
|
|
333
|
+
lines.push(" return");
|
|
334
|
+
lines.push(" fi");
|
|
335
|
+
lines.push(" if command -v contextengine >/dev/null 2>&1; then");
|
|
336
|
+
lines.push(" command -v contextengine");
|
|
337
|
+
lines.push(" fi");
|
|
338
|
+
lines.push("}");
|
|
339
|
+
lines.push("");
|
|
340
|
+
lines.push("CE_CLI=$(find_ce_cli)");
|
|
341
|
+
lines.push("HAS_POLICY=false");
|
|
342
|
+
lines.push("[[ -f .contextengine/policy.json ]] && HAS_POLICY=true");
|
|
343
|
+
lines.push("");
|
|
344
|
+
lines.push("# No CLI or no policy = nothing to enforce. Stay silent and pass.");
|
|
345
|
+
lines.push('if [[ -z "$CE_CLI" || "$HAS_POLICY" != "true" ]]; then');
|
|
346
|
+
lines.push(" exit 0");
|
|
347
|
+
lines.push("fi");
|
|
348
|
+
lines.push("");
|
|
349
|
+
lines.push('if ! "$CE_CLI" hook commit-message-required "$COMMIT_MSG_FILE"; then');
|
|
350
|
+
lines.push(" exit 1");
|
|
351
|
+
lines.push("fi");
|
|
352
|
+
lines.push("");
|
|
353
|
+
lines.push("exit 0");
|
|
354
|
+
lines.push("");
|
|
355
|
+
return lines.join("\n");
|
|
356
|
+
}
|
|
300
357
|
function generatePostCommitHook() {
|
|
301
358
|
const lines = [];
|
|
302
359
|
lines.push("#!/bin/zsh");
|
|
@@ -449,6 +506,7 @@ async function runInit() {
|
|
|
449
506
|
const hooksDir = join(cwd, ".git", "hooks");
|
|
450
507
|
const preCommitDest = join(hooksDir, "pre-commit");
|
|
451
508
|
const postCommitDest = join(hooksDir, "post-commit");
|
|
509
|
+
const commitMsgDest = join(hooksDir, "commit-msg");
|
|
452
510
|
if (existsSync(preCommitDest)) {
|
|
453
511
|
console.log(" ⏭ .git/hooks/pre-commit already exists — skipping");
|
|
454
512
|
skipped++;
|
|
@@ -462,6 +520,21 @@ async function runInit() {
|
|
|
462
520
|
created++;
|
|
463
521
|
}
|
|
464
522
|
}
|
|
523
|
+
// [COMMIT-MSG-HOOK-MUST-BE-INSTALLED] — without this, every
|
|
524
|
+
// commit_message_required rule is silently unenforced.
|
|
525
|
+
if (existsSync(commitMsgDest)) {
|
|
526
|
+
console.log(" ⏭ .git/hooks/commit-msg already exists — skipping");
|
|
527
|
+
skipped++;
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
const answer = isNonInteractive ? "y" : await ask(rl, " Install commit-msg hook (commit-message policy gate)? [Y/n] ");
|
|
531
|
+
if (answer.toLowerCase() !== "n") {
|
|
532
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
533
|
+
writeFileSync(commitMsgDest, generateCommitMsgHook(), { mode: 0o755 });
|
|
534
|
+
console.log(" ✅ Installed .git/hooks/commit-msg");
|
|
535
|
+
created++;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
465
538
|
if (existsSync(postCommitDest)) {
|
|
466
539
|
console.log(" ⏭ .git/hooks/post-commit already exists — skipping");
|
|
467
540
|
skipped++;
|
|
@@ -529,7 +602,10 @@ import { activate, deactivate, getActivationStatus, gateCheck, } from "./activat
|
|
|
529
602
|
import { syncTierA, syncTierB, loadCommunityStore, communityRulesToChunks, mergeWithDedup, STORE_PATH as COMMUNITY_STORE_PATH, } from "./community-sync.js";
|
|
530
603
|
import { readAuditLog, verifyChain, filterByRange, toCsv, } from "./audit.js";
|
|
531
604
|
import { loadRepoPolicy, parsePolicy, formatPolicySummary, formatValidationErrors, repoPolicyPath, } from "./policy.js";
|
|
532
|
-
import {
|
|
605
|
+
import { collectRuns, metricsFor, transcriptRoot, emptyTally, addTally, totalTokens, pricingStatus, pricingFor, } from "./transcript-collector.js";
|
|
606
|
+
import { DEFAULT_PRICING, DEFAULT_PRICING_ASOF } from "./default-pricing.js";
|
|
607
|
+
import { runTranscriptHeuristics, DEFAULT_COST_THRESHOLDS, } from "./detector.js";
|
|
608
|
+
import { getStagedFiles, runSecretScan, runDocCoverage, runCommitMessageRequired, runRuleParity, formatSecretViolations, formatDocCoverageViolations, formatSecretViolationsJson, formatDocCoverageViolationsJson, formatCommitMessageViolations, formatCommitMessageViolationsJson, formatRuleParityViolations, formatRuleParityViolationsJson, } from "./hooks.js";
|
|
533
609
|
import { safeAppend } from "./audit.js";
|
|
534
610
|
import { installSkill, locateBundledSkill, buildManagedBlock, syncClaudeMd, } from "./claude-integration.js";
|
|
535
611
|
import { fileURLToPath } from "url";
|
|
@@ -1314,6 +1390,13 @@ Subcommands:
|
|
|
1314
1390
|
doc-coverage For each policy.doc_coverage rule, check whether the
|
|
1315
1391
|
commit touches matching source paths AND the required
|
|
1316
1392
|
doc section is staged. Exit 1 on blocking violations.
|
|
1393
|
+
rule-parity [--all]
|
|
1394
|
+
For each policy.rule_parity rule, check that the marker
|
|
1395
|
+
is present in EVERY listed doc, not just some. Fires only
|
|
1396
|
+
when the commit touches one of those docs; --all audits
|
|
1397
|
+
the whole repo regardless of the diff. Exit 1 on blocking
|
|
1398
|
+
violations.
|
|
1399
|
+
|
|
1317
1400
|
commit-message-required [MSG_FILE]
|
|
1318
1401
|
For each policy.commit_message_required rule, check
|
|
1319
1402
|
whether the commit touches matching source paths AND
|
|
@@ -1377,7 +1460,17 @@ tamper-evident audit log at ~/.contextengine/audit.log.`);
|
|
|
1377
1460
|
console.error(`Error reading staged diff: ${e instanceof Error ? e.message : String(e)}`);
|
|
1378
1461
|
process.exit(1);
|
|
1379
1462
|
}
|
|
1380
|
-
|
|
1463
|
+
// 🔒 LOCKED [RULE-PARITY-ALL-IGNORES-DIFF] — 2026-08-19
|
|
1464
|
+
// ⛔ NEVER let the empty-staged-diff short-circuit swallow an --all audit.
|
|
1465
|
+
// WHY: `hook rule-parity --all` is a whole-repo audit for CI and deliberate sweeps —
|
|
1466
|
+
// it does not depend on the diff at all. This early return ran first, so on a
|
|
1467
|
+
// clean tree the command printed NOTHING and exited 0. A compliance check that
|
|
1468
|
+
// reports success precisely when it did not run is the same failure shape as the
|
|
1469
|
+
// `Secrets exposure` 6/6 pass in [EXEC-FAILURE-IS-NOT-EMPTY]: silence read as a
|
|
1470
|
+
// clean bill of health. Caught by running it, not by reading it.
|
|
1471
|
+
// FIX: exempt the diff-independent audits from the short-circuit.
|
|
1472
|
+
const auditsWholeRepo = sub === "rule-parity" && args.includes("--all");
|
|
1473
|
+
if (stagedFiles.length === 0 && !auditsWholeRepo)
|
|
1381
1474
|
return; // nothing to scan
|
|
1382
1475
|
if (sub === "secret-scan") {
|
|
1383
1476
|
const violations = runSecretScan(policy, stagedFiles);
|
|
@@ -1422,6 +1515,30 @@ tamper-evident audit log at ~/.contextengine/audit.log.`);
|
|
|
1422
1515
|
process.exit(1);
|
|
1423
1516
|
return;
|
|
1424
1517
|
}
|
|
1518
|
+
if (sub === "rule-parity") {
|
|
1519
|
+
// --all audits every rule regardless of what this commit touches (CI / sweeps).
|
|
1520
|
+
const all = args.includes("--all");
|
|
1521
|
+
const violations = runRuleParity(policy, stagedFiles, repoRoot, { all });
|
|
1522
|
+
if (jsonMode) {
|
|
1523
|
+
process.stdout.write(formatRuleParityViolationsJson(violations) + "\n");
|
|
1524
|
+
}
|
|
1525
|
+
else {
|
|
1526
|
+
console.log(formatRuleParityViolations(violations));
|
|
1527
|
+
}
|
|
1528
|
+
const blocking = violations.filter((v) => v.severity === "block");
|
|
1529
|
+
for (const v of blocking) {
|
|
1530
|
+
safeAppend("hook.block", {
|
|
1531
|
+
check: "rule-parity",
|
|
1532
|
+
rule_id: v.ruleId,
|
|
1533
|
+
reason: v.reason,
|
|
1534
|
+
present_in: v.presentIn,
|
|
1535
|
+
missing_from: v.missingFrom,
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
if (blocking.length > 0)
|
|
1539
|
+
process.exit(1);
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1425
1542
|
if (sub === "commit-message-required") {
|
|
1426
1543
|
// Locate the commit-message file. Order:
|
|
1427
1544
|
// 1. CLI positional arg (`contextengine hook commit-message-required
|
|
@@ -2148,6 +2265,220 @@ function cliStats() {
|
|
|
2148
2265
|
}
|
|
2149
2266
|
}
|
|
2150
2267
|
// ---------------------------------------------------------------------------
|
|
2268
|
+
// cost — multi-agent spend, read from Claude Code's own transcripts
|
|
2269
|
+
// ---------------------------------------------------------------------------
|
|
2270
|
+
function fmtTok(n) {
|
|
2271
|
+
if (n >= 1e6)
|
|
2272
|
+
return `${(n / 1e6).toFixed(1)}M`;
|
|
2273
|
+
if (n >= 1e3)
|
|
2274
|
+
return `${(n / 1e3).toFixed(0)}k`;
|
|
2275
|
+
return String(n);
|
|
2276
|
+
}
|
|
2277
|
+
function fmtDur(ms) {
|
|
2278
|
+
if (ms === null || !Number.isFinite(ms))
|
|
2279
|
+
return "—";
|
|
2280
|
+
const s = Math.round(ms / 1000);
|
|
2281
|
+
if (s < 60)
|
|
2282
|
+
return `${s}s`;
|
|
2283
|
+
const m = Math.floor(s / 60);
|
|
2284
|
+
if (m < 60)
|
|
2285
|
+
return `${m}m${String(s % 60).padStart(2, "0")}s`;
|
|
2286
|
+
return `${Math.floor(m / 60)}h${String(m % 60).padStart(2, "0")}m`;
|
|
2287
|
+
}
|
|
2288
|
+
/** Resolve cost thresholds + pricing from policy, falling back to defaults. */
|
|
2289
|
+
function loadCostThresholds(cwd) {
|
|
2290
|
+
const res = loadRepoPolicy(cwd);
|
|
2291
|
+
if (res && res.ok && res.policy.agent_cost) {
|
|
2292
|
+
const a = res.policy.agent_cost;
|
|
2293
|
+
// [DEFAULT-RATES-SHIP-WITH-THE-PACKAGE] — an agent_cost block that omits
|
|
2294
|
+
// `pricing` must not silently price nothing.
|
|
2295
|
+
const hasOwnRates = a.pricing.length > 0;
|
|
2296
|
+
return {
|
|
2297
|
+
t: {
|
|
2298
|
+
billing_mode: a.billing_mode,
|
|
2299
|
+
pricing: hasOwnRates ? a.pricing : DEFAULT_PRICING,
|
|
2300
|
+
min_cache_efficiency: a.min_cache_efficiency,
|
|
2301
|
+
max_tool_calls_per_agent: a.max_tool_calls_per_agent,
|
|
2302
|
+
max_cost_per_agent_usd: a.max_cost_per_agent_usd,
|
|
2303
|
+
min_fanout_for_canary: a.min_fanout_for_canary,
|
|
2304
|
+
max_failed_share: a.max_failed_share,
|
|
2305
|
+
},
|
|
2306
|
+
source: ".contextengine/policy.json" +
|
|
2307
|
+
(hasOwnRates ? "" : ` (rates: built-in, as of ${DEFAULT_PRICING_ASOF})`),
|
|
2308
|
+
};
|
|
2309
|
+
}
|
|
2310
|
+
return {
|
|
2311
|
+
t: DEFAULT_COST_THRESHOLDS,
|
|
2312
|
+
source: `built-in defaults, rates as of ${DEFAULT_PRICING_ASOF} (no agent_cost in policy.json)`,
|
|
2313
|
+
};
|
|
2314
|
+
}
|
|
2315
|
+
async function cliCost(argv) {
|
|
2316
|
+
const flag = (name) => {
|
|
2317
|
+
const i = argv.indexOf(`--${name}`);
|
|
2318
|
+
return i >= 0 ? argv[i + 1] : undefined;
|
|
2319
|
+
};
|
|
2320
|
+
const json = argv.includes("--json");
|
|
2321
|
+
const topRaw = flag("top");
|
|
2322
|
+
const top = topRaw ? Math.max(1, parseInt(topRaw, 10) || 10) : 10;
|
|
2323
|
+
const daysRaw = flag("days");
|
|
2324
|
+
const since = daysRaw ? Date.now() - parseInt(daysRaw, 10) * 86_400_000 : undefined;
|
|
2325
|
+
const cwd = process.cwd();
|
|
2326
|
+
const { t, source } = loadCostThresholds(cwd);
|
|
2327
|
+
const runs = collectRuns({
|
|
2328
|
+
session: flag("session"),
|
|
2329
|
+
project: flag("project"),
|
|
2330
|
+
run: flag("run"),
|
|
2331
|
+
since,
|
|
2332
|
+
});
|
|
2333
|
+
if (!runs.length) {
|
|
2334
|
+
console.log("No multi-agent runs found in " + transcriptRoot());
|
|
2335
|
+
console.log("(fan-outs only: parent sessions are not counted — this measures delegation)");
|
|
2336
|
+
return;
|
|
2337
|
+
}
|
|
2338
|
+
const scored = runs
|
|
2339
|
+
.map((r) => ({ run: r, m: metricsFor(r, t.pricing) }))
|
|
2340
|
+
.sort((a, b) => b.m.cost.total - a.m.cost.total);
|
|
2341
|
+
const signals = runTranscriptHeuristics(runs, t);
|
|
2342
|
+
if (json) {
|
|
2343
|
+
console.log(JSON.stringify({
|
|
2344
|
+
billing_mode: t.billing_mode,
|
|
2345
|
+
cost_is_notional: t.billing_mode === "subscription",
|
|
2346
|
+
thresholds_source: source,
|
|
2347
|
+
runs: scored.map(({ run, m }) => ({
|
|
2348
|
+
runId: run.runId, kind: run.kind, project: run.project, sessionId: run.sessionId,
|
|
2349
|
+
volume: run.totals, intensity: {
|
|
2350
|
+
agents: m.agents, reported: m.reported, failed: m.failed,
|
|
2351
|
+
capacityExhausted: m.capacityExhausted, toolCalls: m.toolCalls,
|
|
2352
|
+
medianToolCalls: m.medianToolCalls, durationMs: run.durationMs,
|
|
2353
|
+
launchedBeforeFirstReport: m.launchedBeforeFirstReport,
|
|
2354
|
+
},
|
|
2355
|
+
cost: m.cost, cacheEfficiency: Number.isFinite(m.cacheEfficiency) ? m.cacheEfficiency : null,
|
|
2356
|
+
outputShare: m.outputShare,
|
|
2357
|
+
})),
|
|
2358
|
+
signals,
|
|
2359
|
+
}, null, 2));
|
|
2360
|
+
return;
|
|
2361
|
+
}
|
|
2362
|
+
// Aggregate across everything in scope.
|
|
2363
|
+
let vol = emptyTally();
|
|
2364
|
+
let agents = 0, toolCalls = 0, failed = 0, capacity = 0, reported = 0;
|
|
2365
|
+
let cost = 0, withoutCache = 0, unpriced = 0;
|
|
2366
|
+
// Which models carried tokens but matched no rate — named in the output so
|
|
2367
|
+
// the fix is actionable instead of "something was unpriced".
|
|
2368
|
+
const unpricedModels = new Set();
|
|
2369
|
+
for (const { run, m } of scored) {
|
|
2370
|
+
for (const a of run.agents) {
|
|
2371
|
+
for (const [model, tally] of a.tokensByModel) {
|
|
2372
|
+
if (totalTokens(tally) > 0 && !pricingFor(model, t.pricing)) {
|
|
2373
|
+
unpricedModels.add(model ?? "(no model recorded)");
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
vol = addTally(vol, run.totals);
|
|
2378
|
+
agents += m.agents;
|
|
2379
|
+
toolCalls += m.toolCalls;
|
|
2380
|
+
failed += m.failed;
|
|
2381
|
+
capacity += m.capacityExhausted;
|
|
2382
|
+
reported += m.reported;
|
|
2383
|
+
cost += m.cost.total;
|
|
2384
|
+
withoutCache += m.cost.withoutCache;
|
|
2385
|
+
unpriced += m.cost.unpricedTokens;
|
|
2386
|
+
}
|
|
2387
|
+
const allTok = totalTokens(vol);
|
|
2388
|
+
const cw = vol.cacheWrite5m + vol.cacheWrite1h;
|
|
2389
|
+
console.log("");
|
|
2390
|
+
console.log(`MULTI-AGENT COST — ${scored.length} run(s), ${agents} subagents`);
|
|
2391
|
+
console.log(`thresholds: ${source}`);
|
|
2392
|
+
console.log("");
|
|
2393
|
+
// ── 1. VOLUME ───────────────────────────────────────────────────────────
|
|
2394
|
+
console.log("VOLUME (tokens moved)");
|
|
2395
|
+
const volRow = (label, n) => console.log(` ${label.padEnd(16)} ${fmtTok(n).padStart(8)} ${allTok ? ((100 * n) / allTok).toFixed(1).padStart(5) : " 0.0"}%`);
|
|
2396
|
+
volRow("cache read", vol.cacheRead);
|
|
2397
|
+
volRow("cache write", cw);
|
|
2398
|
+
volRow("input (fresh)", vol.input);
|
|
2399
|
+
volRow("output", vol.output);
|
|
2400
|
+
console.log(` ${"total".padEnd(16)} ${fmtTok(allTok).padStart(8)}`);
|
|
2401
|
+
console.log("");
|
|
2402
|
+
// ── 2. VALUED COST ──────────────────────────────────────────────────────
|
|
2403
|
+
const notional = t.billing_mode === "subscription";
|
|
2404
|
+
let ci = 0, ccw = 0, ccr = 0, co = 0;
|
|
2405
|
+
for (const { m } of scored) {
|
|
2406
|
+
ci += m.cost.input;
|
|
2407
|
+
ccw += m.cost.cacheWrite;
|
|
2408
|
+
ccr += m.cost.cacheRead;
|
|
2409
|
+
co += m.cost.output;
|
|
2410
|
+
}
|
|
2411
|
+
const agg = {
|
|
2412
|
+
input: ci, cacheWrite: ccw, cacheRead: ccr, output: co,
|
|
2413
|
+
total: cost, withoutCache, unpricedTokens: unpriced,
|
|
2414
|
+
};
|
|
2415
|
+
const status = pricingStatus(agg);
|
|
2416
|
+
console.log(`VALUED COST (API list prices)${notional && status !== "unpriced" ? " — NOTIONAL, NOT BILLED" : ""}`);
|
|
2417
|
+
// [NEVER-RENDER-AN-UNKNOWN-AS-A-NUMBER] — with nothing priced there is no
|
|
2418
|
+
// cost to show. Printing a $0.00 table here reads as "this run was free"
|
|
2419
|
+
// and "caching saved 0%", both false.
|
|
2420
|
+
if (status === "unpriced") {
|
|
2421
|
+
console.log(` UNPRICED — no rate matched any model in this data, so no cost can be`);
|
|
2422
|
+
console.log(` stated. ${fmtTok(unpriced)} tokens were moved. This is an unknown, not $0.`);
|
|
2423
|
+
console.log("");
|
|
2424
|
+
console.log(` Models seen without a rate: ${[...unpricedModels].sort().join(", ") || "(unknown)"}`);
|
|
2425
|
+
console.log(` Add them to .contextengine/policy.json → agent_cost.pricing.`);
|
|
2426
|
+
console.log("");
|
|
2427
|
+
}
|
|
2428
|
+
else {
|
|
2429
|
+
if (notional) {
|
|
2430
|
+
console.log(" This machine runs Claude Code on a subscription: no dollar below is");
|
|
2431
|
+
console.log(" debited. Use these figures to compare approaches, not as spend.");
|
|
2432
|
+
}
|
|
2433
|
+
const costRow = (label, n) => console.log(` ${label.padEnd(16)} ${("$" + n.toFixed(2)).padStart(8)} ${cost ? ((100 * n) / cost).toFixed(1).padStart(5) : " 0.0"}%`);
|
|
2434
|
+
costRow("cache read", ccr);
|
|
2435
|
+
costRow("cache write", ccw);
|
|
2436
|
+
costRow("input (fresh)", ci);
|
|
2437
|
+
costRow("output", co);
|
|
2438
|
+
console.log(` ${"total".padEnd(16)} ${("$" + cost.toFixed(2)).padStart(8)}`);
|
|
2439
|
+
console.log(` ${"without cache".padEnd(16)} ${("$" + withoutCache.toFixed(2)).padStart(8)} ` +
|
|
2440
|
+
`caching saved $${(withoutCache - cost).toFixed(2)} (${withoutCache ? (100 * (1 - cost / withoutCache)).toFixed(0) : "0"}%)`);
|
|
2441
|
+
if (status === "partial") {
|
|
2442
|
+
console.log(` ⚠ ${fmtTok(unpriced)} tokens UNPRICED and NOT in the figures above` +
|
|
2443
|
+
` (${[...unpricedModels].sort().join(", ") || "unknown model"}) — the total is a floor, not the cost`);
|
|
2444
|
+
}
|
|
2445
|
+
console.log("");
|
|
2446
|
+
}
|
|
2447
|
+
// ── 3. INTENSITY (the capacity proxy) ───────────────────────────────────
|
|
2448
|
+
console.log(`INTENSITY (capacity proxy${notional ? " — the scarce resource here" : ""})`);
|
|
2449
|
+
console.log(` subagents ${String(agents).padStart(8)}`);
|
|
2450
|
+
console.log(` reported ${String(reported).padStart(8)}`);
|
|
2451
|
+
console.log(` returned nothing ${String(failed).padStart(8)}${failed ? ` (${((100 * failed) / agents).toFixed(0)}% of the fleet)` : ""}`);
|
|
2452
|
+
console.log(` died at window ${String(capacity).padStart(8)}${capacity ? " ← capacity spent for no result" : ""}`);
|
|
2453
|
+
console.log(` tool calls ${String(toolCalls).padStart(8)} (${(toolCalls / Math.max(1, agents)).toFixed(1)}/agent)`);
|
|
2454
|
+
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)"}`);
|
|
2455
|
+
console.log("");
|
|
2456
|
+
// ── Top runs ────────────────────────────────────────────────────────────
|
|
2457
|
+
console.log(`TOP RUNS BY VALUED COST (${Math.min(top, scored.length)} of ${scored.length})`);
|
|
2458
|
+
console.log(` ${"cost".padStart(8)} ${"agents".padStart(6)} ${"dead".padStart(4)} ${"tools".padStart(5)} ${"reuse".padStart(6)} ${"dur".padStart(7)} run`);
|
|
2459
|
+
for (const { run, m } of scored.slice(0, top)) {
|
|
2460
|
+
const reuse = Number.isFinite(m.cacheEfficiency) ? m.cacheEfficiency.toFixed(1) + "x" : "—";
|
|
2461
|
+
console.log(` ${("$" + m.cost.total.toFixed(2)).padStart(8)} ${String(m.agents).padStart(6)} ` +
|
|
2462
|
+
`${String(m.failed).padStart(4)} ${String(m.medianToolCalls).padStart(5)} ${reuse.padStart(6)} ` +
|
|
2463
|
+
`${fmtDur(run.durationMs).padStart(7)} ${run.runId} ${run.project.replace(/^-Users-yan-/, "")}`);
|
|
2464
|
+
}
|
|
2465
|
+
console.log("");
|
|
2466
|
+
// ── Signals ─────────────────────────────────────────────────────────────
|
|
2467
|
+
if (!signals.length) {
|
|
2468
|
+
console.log("✅ No context_burn or fanout_without_canary signals.");
|
|
2469
|
+
}
|
|
2470
|
+
else {
|
|
2471
|
+
const crit = signals.filter((s) => s.severity === "critical");
|
|
2472
|
+
console.log(`SIGNALS — ${signals.length} (${crit.length} critical)`);
|
|
2473
|
+
for (const s of signals.slice(0, 20)) {
|
|
2474
|
+
console.log(` ${s.severity === "critical" ? "🔴" : "⚠️ "} [${s.kind}] ${s.reason}`);
|
|
2475
|
+
}
|
|
2476
|
+
if (signals.length > 20)
|
|
2477
|
+
console.log(` … ${signals.length - 20} more (use --json)`);
|
|
2478
|
+
}
|
|
2479
|
+
console.log("");
|
|
2480
|
+
}
|
|
2481
|
+
// ---------------------------------------------------------------------------
|
|
2151
2482
|
// Main — route to init, CLI subcommand, or MCP server
|
|
2152
2483
|
// ---------------------------------------------------------------------------
|
|
2153
2484
|
const command = process.argv[2];
|
|
@@ -2184,6 +2515,12 @@ Usage:
|
|
|
2184
2515
|
Export hash-chained audit log (evidence aligned with
|
|
2185
2516
|
SOC 2 CC7.2 + ISO 27001 A.12.4.1 — not a certification)
|
|
2186
2517
|
contextengine audit-verify Verify audit log chain integrity (tamper detection)
|
|
2518
|
+
contextengine cost [--session ID] [--project NAME] [--run wf_ID] [--days N] [--top N] [--json]
|
|
2519
|
+
Multi-agent spend from Claude Code transcripts. Always prints
|
|
2520
|
+
VOLUME (tokens), VALUED COST (API list prices — notional on a
|
|
2521
|
+
subscription) and INTENSITY (agents, tool calls, deaths at the
|
|
2522
|
+
usage window), because volume and cost tell opposite stories.
|
|
2523
|
+
Flags context_burn + fanout_without_canary.
|
|
2187
2524
|
contextengine policy <validate|show> [args]
|
|
2188
2525
|
Author + validate the declarative .contextengine/policy.json
|
|
2189
2526
|
contextengine init-extension-secret [--force]
|
|
@@ -2197,7 +2534,7 @@ Usage:
|
|
|
2197
2534
|
Stream drift / loop / stuck-tool / fabrication alerts from the audit log
|
|
2198
2535
|
contextengine emit-event <kind> <payload-json> [--actor NAME]
|
|
2199
2536
|
Append a single event to the audit log (for integrations / scripted tests)
|
|
2200
|
-
contextengine hook <secret-scan|doc-coverage>
|
|
2537
|
+
contextengine hook <secret-scan|doc-coverage|rule-parity|commit-message-required>
|
|
2201
2538
|
Run policy-driven pre-commit checks against staged diff
|
|
2202
2539
|
(exit 1 on blocking violation; CE_JSON=1 for CI output)
|
|
2203
2540
|
contextengine install-skill [--global | --project] [--force]
|
|
@@ -2215,6 +2552,7 @@ Usage:
|
|
|
2215
2552
|
Accepts a project name or a directory path.
|
|
2216
2553
|
--all scores every discovered project (writes to each).
|
|
2217
2554
|
contextengine audit Run compliance audit (Pro)
|
|
2555
|
+
contextengine cost Multi-agent token/cost/capacity report
|
|
2218
2556
|
contextengine activate <key> <email> Activate a Pro license
|
|
2219
2557
|
contextengine deactivate Remove license and premium modules
|
|
2220
2558
|
contextengine stats Show live MCP session stats (value meter)
|
|
@@ -2304,6 +2642,12 @@ else if (command === "score") {
|
|
|
2304
2642
|
process.exit(1);
|
|
2305
2643
|
});
|
|
2306
2644
|
}
|
|
2645
|
+
else if (command === "cost") {
|
|
2646
|
+
cliCost(process.argv.slice(3)).catch((err) => {
|
|
2647
|
+
console.error("Error:", err);
|
|
2648
|
+
process.exit(1);
|
|
2649
|
+
});
|
|
2650
|
+
}
|
|
2307
2651
|
else if (command === "audit") {
|
|
2308
2652
|
cliAudit().catch((err) => {
|
|
2309
2653
|
console.error("Error:", err);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in model rates, in dollars per million tokens.
|
|
3
|
+
*
|
|
4
|
+
* 🔒 LOCKED [DEFAULT-RATES-SHIP-WITH-THE-PACKAGE] — 2026-08-20
|
|
5
|
+
* ⛔ NEVER ship an empty default pricing table again.
|
|
6
|
+
* WHY: `[PRICING-LIVES-IN-POLICY]` was read as "ship no rates at all", so
|
|
7
|
+
* 2.5.0 shipped `pricing: []` as the default. Every user without an
|
|
8
|
+
* `agent_cost` block in their own policy.json got a VALUED COST panel
|
|
9
|
+
* reading `total $0.00` and `caching saved $0.00 (0%)` over 1.08 BILLION
|
|
10
|
+
* real tokens — a confident, wrong-looking verdict on the headline feature
|
|
11
|
+
* of the release. The LOCK's intent was "rates must be correctable without
|
|
12
|
+
* a release", not "the product ships priced at nothing".
|
|
13
|
+
* FIX: ship rates here, as DATA in their own module, never inline in the
|
|
14
|
+
* collector / detector / CLI. `.contextengine/policy.json` →
|
|
15
|
+
* `agent_cost.pricing` still wins outright when present, and this file
|
|
16
|
+
* compiles to plain readable JS in `dist/`, so a rate can be corrected in
|
|
17
|
+
* place without waiting for a release.
|
|
18
|
+
*
|
|
19
|
+
* Rates are Anthropic API list prices. Cache read is 0.1x input, cache write
|
|
20
|
+
* 5m is 1.25x input, cache write 1h is 2x input.
|
|
21
|
+
*/
|
|
22
|
+
import type { ModelPricing } from "./transcript-collector.js";
|
|
23
|
+
/**
|
|
24
|
+
* When these rates were last checked against published pricing. Surfaced in
|
|
25
|
+
* `contextengine cost` output: a rate table with no date is a rate table
|
|
26
|
+
* nobody knows to distrust.
|
|
27
|
+
*/
|
|
28
|
+
export declare const DEFAULT_PRICING_ASOF = "2026-08-20";
|
|
29
|
+
/**
|
|
30
|
+
* Longest-prefix matched, so dated ids (`claude-haiku-4-5-20251001`) resolve
|
|
31
|
+
* to their family. Deliberately NO `*` catch-all: a model absent from this
|
|
32
|
+
* table must report as UNPRICED, never be valued at a guessed rate
|
|
33
|
+
* (`[ABSENCE-IS-NOT-A-VERDICT]`).
|
|
34
|
+
*/
|
|
35
|
+
export declare const DEFAULT_PRICING: ModelPricing[];
|
|
36
|
+
//# sourceMappingURL=default-pricing.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in model rates, in dollars per million tokens.
|
|
3
|
+
*
|
|
4
|
+
* 🔒 LOCKED [DEFAULT-RATES-SHIP-WITH-THE-PACKAGE] — 2026-08-20
|
|
5
|
+
* ⛔ NEVER ship an empty default pricing table again.
|
|
6
|
+
* WHY: `[PRICING-LIVES-IN-POLICY]` was read as "ship no rates at all", so
|
|
7
|
+
* 2.5.0 shipped `pricing: []` as the default. Every user without an
|
|
8
|
+
* `agent_cost` block in their own policy.json got a VALUED COST panel
|
|
9
|
+
* reading `total $0.00` and `caching saved $0.00 (0%)` over 1.08 BILLION
|
|
10
|
+
* real tokens — a confident, wrong-looking verdict on the headline feature
|
|
11
|
+
* of the release. The LOCK's intent was "rates must be correctable without
|
|
12
|
+
* a release", not "the product ships priced at nothing".
|
|
13
|
+
* FIX: ship rates here, as DATA in their own module, never inline in the
|
|
14
|
+
* collector / detector / CLI. `.contextengine/policy.json` →
|
|
15
|
+
* `agent_cost.pricing` still wins outright when present, and this file
|
|
16
|
+
* compiles to plain readable JS in `dist/`, so a rate can be corrected in
|
|
17
|
+
* place without waiting for a release.
|
|
18
|
+
*
|
|
19
|
+
* Rates are Anthropic API list prices. Cache read is 0.1x input, cache write
|
|
20
|
+
* 5m is 1.25x input, cache write 1h is 2x input.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* When these rates were last checked against published pricing. Surfaced in
|
|
24
|
+
* `contextengine cost` output: a rate table with no date is a rate table
|
|
25
|
+
* nobody knows to distrust.
|
|
26
|
+
*/
|
|
27
|
+
export const DEFAULT_PRICING_ASOF = "2026-08-20";
|
|
28
|
+
function rate(model, input, output) {
|
|
29
|
+
return {
|
|
30
|
+
model,
|
|
31
|
+
input_per_mtok: input,
|
|
32
|
+
output_per_mtok: output,
|
|
33
|
+
cache_read_per_mtok: Number((input * 0.1).toFixed(4)),
|
|
34
|
+
cache_write_5m_per_mtok: Number((input * 1.25).toFixed(4)),
|
|
35
|
+
cache_write_1h_per_mtok: Number((input * 2).toFixed(4)),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Longest-prefix matched, so dated ids (`claude-haiku-4-5-20251001`) resolve
|
|
40
|
+
* to their family. Deliberately NO `*` catch-all: a model absent from this
|
|
41
|
+
* table must report as UNPRICED, never be valued at a guessed rate
|
|
42
|
+
* (`[ABSENCE-IS-NOT-A-VERDICT]`).
|
|
43
|
+
*/
|
|
44
|
+
export const DEFAULT_PRICING = [
|
|
45
|
+
rate("claude-opus-5", 5, 25),
|
|
46
|
+
rate("claude-opus-4-8", 5, 25),
|
|
47
|
+
rate("claude-opus-4-7", 5, 25),
|
|
48
|
+
rate("claude-opus-4-6", 5, 25),
|
|
49
|
+
rate("claude-opus-4-5", 5, 25),
|
|
50
|
+
rate("claude-fable-5", 10, 50),
|
|
51
|
+
rate("claude-mythos-5", 10, 50),
|
|
52
|
+
rate("claude-sonnet-5", 3, 15),
|
|
53
|
+
rate("claude-sonnet-4-6", 3, 15),
|
|
54
|
+
rate("claude-sonnet-4-5", 3, 15),
|
|
55
|
+
rate("claude-haiku-4-5", 1, 5),
|
|
56
|
+
];
|
|
57
|
+
//# sourceMappingURL=default-pricing.js.map
|
package/dist/detector.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type AuditRecord, type AuditEvent } from "./audit.js";
|
|
2
|
-
export type DriftKind = "loop" | "stuck" | "context_bloat" | "fabrication_suspect" | "drift" | "no_insight" | "stale_doc_signal" | "silent_failure";
|
|
2
|
+
export type DriftKind = "loop" | "stuck" | "context_bloat" | "fabrication_suspect" | "drift" | "no_insight" | "stale_doc_signal" | "silent_failure" | "context_burn" | "fanout_without_canary";
|
|
3
3
|
export type Severity = "info" | "warn" | "critical";
|
|
4
4
|
export interface DriftSignal {
|
|
5
5
|
kind: DriftKind;
|
|
@@ -61,4 +61,43 @@ export declare const _internal: {
|
|
|
61
61
|
detectStaleDocSignal: typeof detectStaleDocSignal;
|
|
62
62
|
};
|
|
63
63
|
export type { AuditRecord, AuditEvent };
|
|
64
|
+
import type { RunUsage, RunMetrics, ModelPricing } from "./transcript-collector.js";
|
|
65
|
+
export interface CostThresholds {
|
|
66
|
+
billing_mode: "subscription" | "api";
|
|
67
|
+
pricing: ModelPricing[];
|
|
68
|
+
min_cache_efficiency: number;
|
|
69
|
+
max_tool_calls_per_agent: number;
|
|
70
|
+
max_cost_per_agent_usd: number;
|
|
71
|
+
min_fanout_for_canary: number;
|
|
72
|
+
max_failed_share: number;
|
|
73
|
+
}
|
|
74
|
+
export declare const DEFAULT_COST_THRESHOLDS: CostThresholds;
|
|
75
|
+
/**
|
|
76
|
+
* context_burn — the run is paying for context it is not reusing.
|
|
77
|
+
*
|
|
78
|
+
* Fires on cache INEFFICIENCY, tool-call inflation, or per-agent valued cost.
|
|
79
|
+
* Deliberately does NOT fire on a low output/volume ratio: see
|
|
80
|
+
* [BURN-IS-COST-WEIGHTED-NOT-VOLUME] in policy.ts.
|
|
81
|
+
*/
|
|
82
|
+
export declare function detectContextBurn(run: RunUsage, t: CostThresholds, m?: RunMetrics): DriftSignal | null;
|
|
83
|
+
/**
|
|
84
|
+
* fanout_without_canary — the fleet was launched before any single unit had
|
|
85
|
+
* reported, so nothing was known about per-agent consumption when the spend
|
|
86
|
+
* was committed.
|
|
87
|
+
*
|
|
88
|
+
* 🔒 LOCKED [CANARY-IS-A-TIME-ORDERING] — 2026-08-19
|
|
89
|
+
* ⛔ NEVER implement this as "no agent reported". A completed 300-agent run
|
|
90
|
+
* has 300 reports and was still un-canaried.
|
|
91
|
+
* WHY: the rule being enforced is "run ONE unit and read its consumption
|
|
92
|
+
* BEFORE scaling". That is a statement about ordering, not about outcomes,
|
|
93
|
+
* and it is only checkable by comparing each agent's start time against the
|
|
94
|
+
* earliest sibling completion.
|
|
95
|
+
* FIX: count agents that started before the first report landed. Severity
|
|
96
|
+
* rises when agents then died at the usage window — on a subscription that
|
|
97
|
+
* is the failure that actually costs something (real case: 15 of 51 agents
|
|
98
|
+
* lost in wf_41771d7b, 0 completed).
|
|
99
|
+
*/
|
|
100
|
+
export declare function detectFanoutWithoutCanary(run: RunUsage, t: CostThresholds, m?: RunMetrics): DriftSignal | null;
|
|
101
|
+
/** Run both transcript heuristics over a set of runs. */
|
|
102
|
+
export declare function runTranscriptHeuristics(runs: RunUsage[], t?: CostThresholds): DriftSignal[];
|
|
64
103
|
//# sourceMappingURL=detector.d.ts.map
|