@compr/opscontext-mcp 2.4.3 → 2.5.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/dist/cli.d.ts +1 -0
- package/dist/cli.js +309 -3
- package/dist/detector.d.ts +40 -1
- package/dist/detector.js +116 -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 +208 -0
- package/dist/transcript-collector.js +447 -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,9 @@ 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, } from "./transcript-collector.js";
|
|
606
|
+
import { runTranscriptHeuristics, DEFAULT_COST_THRESHOLDS, } from "./detector.js";
|
|
607
|
+
import { getStagedFiles, runSecretScan, runDocCoverage, runCommitMessageRequired, runRuleParity, formatSecretViolations, formatDocCoverageViolations, formatSecretViolationsJson, formatDocCoverageViolationsJson, formatCommitMessageViolations, formatCommitMessageViolationsJson, formatRuleParityViolations, formatRuleParityViolationsJson, } from "./hooks.js";
|
|
533
608
|
import { safeAppend } from "./audit.js";
|
|
534
609
|
import { installSkill, locateBundledSkill, buildManagedBlock, syncClaudeMd, } from "./claude-integration.js";
|
|
535
610
|
import { fileURLToPath } from "url";
|
|
@@ -1314,6 +1389,13 @@ Subcommands:
|
|
|
1314
1389
|
doc-coverage For each policy.doc_coverage rule, check whether the
|
|
1315
1390
|
commit touches matching source paths AND the required
|
|
1316
1391
|
doc section is staged. Exit 1 on blocking violations.
|
|
1392
|
+
rule-parity [--all]
|
|
1393
|
+
For each policy.rule_parity rule, check that the marker
|
|
1394
|
+
is present in EVERY listed doc, not just some. Fires only
|
|
1395
|
+
when the commit touches one of those docs; --all audits
|
|
1396
|
+
the whole repo regardless of the diff. Exit 1 on blocking
|
|
1397
|
+
violations.
|
|
1398
|
+
|
|
1317
1399
|
commit-message-required [MSG_FILE]
|
|
1318
1400
|
For each policy.commit_message_required rule, check
|
|
1319
1401
|
whether the commit touches matching source paths AND
|
|
@@ -1377,7 +1459,17 @@ tamper-evident audit log at ~/.contextengine/audit.log.`);
|
|
|
1377
1459
|
console.error(`Error reading staged diff: ${e instanceof Error ? e.message : String(e)}`);
|
|
1378
1460
|
process.exit(1);
|
|
1379
1461
|
}
|
|
1380
|
-
|
|
1462
|
+
// 🔒 LOCKED [RULE-PARITY-ALL-IGNORES-DIFF] — 2026-08-19
|
|
1463
|
+
// ⛔ NEVER let the empty-staged-diff short-circuit swallow an --all audit.
|
|
1464
|
+
// WHY: `hook rule-parity --all` is a whole-repo audit for CI and deliberate sweeps —
|
|
1465
|
+
// it does not depend on the diff at all. This early return ran first, so on a
|
|
1466
|
+
// clean tree the command printed NOTHING and exited 0. A compliance check that
|
|
1467
|
+
// reports success precisely when it did not run is the same failure shape as the
|
|
1468
|
+
// `Secrets exposure` 6/6 pass in [EXEC-FAILURE-IS-NOT-EMPTY]: silence read as a
|
|
1469
|
+
// clean bill of health. Caught by running it, not by reading it.
|
|
1470
|
+
// FIX: exempt the diff-independent audits from the short-circuit.
|
|
1471
|
+
const auditsWholeRepo = sub === "rule-parity" && args.includes("--all");
|
|
1472
|
+
if (stagedFiles.length === 0 && !auditsWholeRepo)
|
|
1381
1473
|
return; // nothing to scan
|
|
1382
1474
|
if (sub === "secret-scan") {
|
|
1383
1475
|
const violations = runSecretScan(policy, stagedFiles);
|
|
@@ -1422,6 +1514,30 @@ tamper-evident audit log at ~/.contextengine/audit.log.`);
|
|
|
1422
1514
|
process.exit(1);
|
|
1423
1515
|
return;
|
|
1424
1516
|
}
|
|
1517
|
+
if (sub === "rule-parity") {
|
|
1518
|
+
// --all audits every rule regardless of what this commit touches (CI / sweeps).
|
|
1519
|
+
const all = args.includes("--all");
|
|
1520
|
+
const violations = runRuleParity(policy, stagedFiles, repoRoot, { all });
|
|
1521
|
+
if (jsonMode) {
|
|
1522
|
+
process.stdout.write(formatRuleParityViolationsJson(violations) + "\n");
|
|
1523
|
+
}
|
|
1524
|
+
else {
|
|
1525
|
+
console.log(formatRuleParityViolations(violations));
|
|
1526
|
+
}
|
|
1527
|
+
const blocking = violations.filter((v) => v.severity === "block");
|
|
1528
|
+
for (const v of blocking) {
|
|
1529
|
+
safeAppend("hook.block", {
|
|
1530
|
+
check: "rule-parity",
|
|
1531
|
+
rule_id: v.ruleId,
|
|
1532
|
+
reason: v.reason,
|
|
1533
|
+
present_in: v.presentIn,
|
|
1534
|
+
missing_from: v.missingFrom,
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
if (blocking.length > 0)
|
|
1538
|
+
process.exit(1);
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1425
1541
|
if (sub === "commit-message-required") {
|
|
1426
1542
|
// Locate the commit-message file. Order:
|
|
1427
1543
|
// 1. CLI positional arg (`contextengine hook commit-message-required
|
|
@@ -2148,6 +2264,183 @@ function cliStats() {
|
|
|
2148
2264
|
}
|
|
2149
2265
|
}
|
|
2150
2266
|
// ---------------------------------------------------------------------------
|
|
2267
|
+
// cost — multi-agent spend, read from Claude Code's own transcripts
|
|
2268
|
+
// ---------------------------------------------------------------------------
|
|
2269
|
+
function fmtTok(n) {
|
|
2270
|
+
if (n >= 1e6)
|
|
2271
|
+
return `${(n / 1e6).toFixed(1)}M`;
|
|
2272
|
+
if (n >= 1e3)
|
|
2273
|
+
return `${(n / 1e3).toFixed(0)}k`;
|
|
2274
|
+
return String(n);
|
|
2275
|
+
}
|
|
2276
|
+
function fmtDur(ms) {
|
|
2277
|
+
if (ms === null || !Number.isFinite(ms))
|
|
2278
|
+
return "—";
|
|
2279
|
+
const s = Math.round(ms / 1000);
|
|
2280
|
+
if (s < 60)
|
|
2281
|
+
return `${s}s`;
|
|
2282
|
+
const m = Math.floor(s / 60);
|
|
2283
|
+
if (m < 60)
|
|
2284
|
+
return `${m}m${String(s % 60).padStart(2, "0")}s`;
|
|
2285
|
+
return `${Math.floor(m / 60)}h${String(m % 60).padStart(2, "0")}m`;
|
|
2286
|
+
}
|
|
2287
|
+
/** Resolve cost thresholds + pricing from policy, falling back to defaults. */
|
|
2288
|
+
function loadCostThresholds(cwd) {
|
|
2289
|
+
const res = loadRepoPolicy(cwd);
|
|
2290
|
+
if (res && res.ok && res.policy.agent_cost) {
|
|
2291
|
+
const a = res.policy.agent_cost;
|
|
2292
|
+
return {
|
|
2293
|
+
t: {
|
|
2294
|
+
billing_mode: a.billing_mode,
|
|
2295
|
+
pricing: a.pricing,
|
|
2296
|
+
min_cache_efficiency: a.min_cache_efficiency,
|
|
2297
|
+
max_tool_calls_per_agent: a.max_tool_calls_per_agent,
|
|
2298
|
+
max_cost_per_agent_usd: a.max_cost_per_agent_usd,
|
|
2299
|
+
min_fanout_for_canary: a.min_fanout_for_canary,
|
|
2300
|
+
max_failed_share: a.max_failed_share,
|
|
2301
|
+
},
|
|
2302
|
+
source: ".contextengine/policy.json",
|
|
2303
|
+
};
|
|
2304
|
+
}
|
|
2305
|
+
return { t: DEFAULT_COST_THRESHOLDS, source: "built-in defaults (no agent_cost in policy.json)" };
|
|
2306
|
+
}
|
|
2307
|
+
async function cliCost(argv) {
|
|
2308
|
+
const flag = (name) => {
|
|
2309
|
+
const i = argv.indexOf(`--${name}`);
|
|
2310
|
+
return i >= 0 ? argv[i + 1] : undefined;
|
|
2311
|
+
};
|
|
2312
|
+
const json = argv.includes("--json");
|
|
2313
|
+
const topRaw = flag("top");
|
|
2314
|
+
const top = topRaw ? Math.max(1, parseInt(topRaw, 10) || 10) : 10;
|
|
2315
|
+
const daysRaw = flag("days");
|
|
2316
|
+
const since = daysRaw ? Date.now() - parseInt(daysRaw, 10) * 86_400_000 : undefined;
|
|
2317
|
+
const cwd = process.cwd();
|
|
2318
|
+
const { t, source } = loadCostThresholds(cwd);
|
|
2319
|
+
const runs = collectRuns({
|
|
2320
|
+
session: flag("session"),
|
|
2321
|
+
project: flag("project"),
|
|
2322
|
+
run: flag("run"),
|
|
2323
|
+
since,
|
|
2324
|
+
});
|
|
2325
|
+
if (!runs.length) {
|
|
2326
|
+
console.log("No multi-agent runs found in " + transcriptRoot());
|
|
2327
|
+
console.log("(fan-outs only: parent sessions are not counted — this measures delegation)");
|
|
2328
|
+
return;
|
|
2329
|
+
}
|
|
2330
|
+
const scored = runs
|
|
2331
|
+
.map((r) => ({ run: r, m: metricsFor(r, t.pricing) }))
|
|
2332
|
+
.sort((a, b) => b.m.cost.total - a.m.cost.total);
|
|
2333
|
+
const signals = runTranscriptHeuristics(runs, t);
|
|
2334
|
+
if (json) {
|
|
2335
|
+
console.log(JSON.stringify({
|
|
2336
|
+
billing_mode: t.billing_mode,
|
|
2337
|
+
cost_is_notional: t.billing_mode === "subscription",
|
|
2338
|
+
thresholds_source: source,
|
|
2339
|
+
runs: scored.map(({ run, m }) => ({
|
|
2340
|
+
runId: run.runId, kind: run.kind, project: run.project, sessionId: run.sessionId,
|
|
2341
|
+
volume: run.totals, intensity: {
|
|
2342
|
+
agents: m.agents, reported: m.reported, failed: m.failed,
|
|
2343
|
+
capacityExhausted: m.capacityExhausted, toolCalls: m.toolCalls,
|
|
2344
|
+
medianToolCalls: m.medianToolCalls, durationMs: run.durationMs,
|
|
2345
|
+
launchedBeforeFirstReport: m.launchedBeforeFirstReport,
|
|
2346
|
+
},
|
|
2347
|
+
cost: m.cost, cacheEfficiency: Number.isFinite(m.cacheEfficiency) ? m.cacheEfficiency : null,
|
|
2348
|
+
outputShare: m.outputShare,
|
|
2349
|
+
})),
|
|
2350
|
+
signals,
|
|
2351
|
+
}, null, 2));
|
|
2352
|
+
return;
|
|
2353
|
+
}
|
|
2354
|
+
// Aggregate across everything in scope.
|
|
2355
|
+
let vol = emptyTally();
|
|
2356
|
+
let agents = 0, toolCalls = 0, failed = 0, capacity = 0, reported = 0;
|
|
2357
|
+
let cost = 0, withoutCache = 0, unpriced = 0;
|
|
2358
|
+
for (const { run, m } of scored) {
|
|
2359
|
+
vol = addTally(vol, run.totals);
|
|
2360
|
+
agents += m.agents;
|
|
2361
|
+
toolCalls += m.toolCalls;
|
|
2362
|
+
failed += m.failed;
|
|
2363
|
+
capacity += m.capacityExhausted;
|
|
2364
|
+
reported += m.reported;
|
|
2365
|
+
cost += m.cost.total;
|
|
2366
|
+
withoutCache += m.cost.withoutCache;
|
|
2367
|
+
unpriced += m.cost.unpricedTokens;
|
|
2368
|
+
}
|
|
2369
|
+
const allTok = totalTokens(vol);
|
|
2370
|
+
const cw = vol.cacheWrite5m + vol.cacheWrite1h;
|
|
2371
|
+
console.log("");
|
|
2372
|
+
console.log(`MULTI-AGENT COST — ${scored.length} run(s), ${agents} subagents`);
|
|
2373
|
+
console.log(`thresholds: ${source}`);
|
|
2374
|
+
console.log("");
|
|
2375
|
+
// ── 1. VOLUME ───────────────────────────────────────────────────────────
|
|
2376
|
+
console.log("VOLUME (tokens moved)");
|
|
2377
|
+
const volRow = (label, n) => console.log(` ${label.padEnd(16)} ${fmtTok(n).padStart(8)} ${allTok ? ((100 * n) / allTok).toFixed(1).padStart(5) : " 0.0"}%`);
|
|
2378
|
+
volRow("cache read", vol.cacheRead);
|
|
2379
|
+
volRow("cache write", cw);
|
|
2380
|
+
volRow("input (fresh)", vol.input);
|
|
2381
|
+
volRow("output", vol.output);
|
|
2382
|
+
console.log(` ${"total".padEnd(16)} ${fmtTok(allTok).padStart(8)}`);
|
|
2383
|
+
console.log("");
|
|
2384
|
+
// ── 2. VALUED COST ──────────────────────────────────────────────────────
|
|
2385
|
+
const notional = t.billing_mode === "subscription";
|
|
2386
|
+
console.log(`VALUED COST (API list prices)${notional ? " — NOTIONAL, NOT BILLED" : ""}`);
|
|
2387
|
+
if (notional) {
|
|
2388
|
+
console.log(" This machine runs Claude Code on a subscription: no dollar below is");
|
|
2389
|
+
console.log(" debited. Use these figures to compare approaches, not as spend.");
|
|
2390
|
+
}
|
|
2391
|
+
const costRow = (label, n) => console.log(` ${label.padEnd(16)} ${("$" + n.toFixed(2)).padStart(8)} ${cost ? ((100 * n) / cost).toFixed(1).padStart(5) : " 0.0"}%`);
|
|
2392
|
+
let ci = 0, ccw = 0, ccr = 0, co = 0;
|
|
2393
|
+
for (const { m } of scored) {
|
|
2394
|
+
ci += m.cost.input;
|
|
2395
|
+
ccw += m.cost.cacheWrite;
|
|
2396
|
+
ccr += m.cost.cacheRead;
|
|
2397
|
+
co += m.cost.output;
|
|
2398
|
+
}
|
|
2399
|
+
costRow("cache read", ccr);
|
|
2400
|
+
costRow("cache write", ccw);
|
|
2401
|
+
costRow("input (fresh)", ci);
|
|
2402
|
+
costRow("output", co);
|
|
2403
|
+
console.log(` ${"total".padEnd(16)} ${("$" + cost.toFixed(2)).padStart(8)}`);
|
|
2404
|
+
console.log(` ${"without cache".padEnd(16)} ${("$" + withoutCache.toFixed(2)).padStart(8)} ` +
|
|
2405
|
+
`caching saved $${(withoutCache - cost).toFixed(2)} (${withoutCache ? (100 * (1 - cost / withoutCache)).toFixed(0) : "0"}%)`);
|
|
2406
|
+
if (unpriced > 0)
|
|
2407
|
+
console.log(` ⚠ ${fmtTok(unpriced)} tokens UNPRICED (no matching model in policy) — not counted above`);
|
|
2408
|
+
console.log("");
|
|
2409
|
+
// ── 3. INTENSITY (the capacity proxy) ───────────────────────────────────
|
|
2410
|
+
console.log(`INTENSITY (capacity proxy${notional ? " — the scarce resource here" : ""})`);
|
|
2411
|
+
console.log(` subagents ${String(agents).padStart(8)}`);
|
|
2412
|
+
console.log(` reported ${String(reported).padStart(8)}`);
|
|
2413
|
+
console.log(` returned nothing ${String(failed).padStart(8)}${failed ? ` (${((100 * failed) / agents).toFixed(0)}% of the fleet)` : ""}`);
|
|
2414
|
+
console.log(` died at window ${String(capacity).padStart(8)}${capacity ? " ← capacity spent for no result" : ""}`);
|
|
2415
|
+
console.log(` tool calls ${String(toolCalls).padStart(8)} (${(toolCalls / Math.max(1, agents)).toFixed(1)}/agent)`);
|
|
2416
|
+
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)"}`);
|
|
2417
|
+
console.log("");
|
|
2418
|
+
// ── Top runs ────────────────────────────────────────────────────────────
|
|
2419
|
+
console.log(`TOP RUNS BY VALUED COST (${Math.min(top, scored.length)} of ${scored.length})`);
|
|
2420
|
+
console.log(` ${"cost".padStart(8)} ${"agents".padStart(6)} ${"dead".padStart(4)} ${"tools".padStart(5)} ${"reuse".padStart(6)} ${"dur".padStart(7)} run`);
|
|
2421
|
+
for (const { run, m } of scored.slice(0, top)) {
|
|
2422
|
+
const reuse = Number.isFinite(m.cacheEfficiency) ? m.cacheEfficiency.toFixed(1) + "x" : "—";
|
|
2423
|
+
console.log(` ${("$" + m.cost.total.toFixed(2)).padStart(8)} ${String(m.agents).padStart(6)} ` +
|
|
2424
|
+
`${String(m.failed).padStart(4)} ${String(m.medianToolCalls).padStart(5)} ${reuse.padStart(6)} ` +
|
|
2425
|
+
`${fmtDur(run.durationMs).padStart(7)} ${run.runId} ${run.project.replace(/^-Users-yan-/, "")}`);
|
|
2426
|
+
}
|
|
2427
|
+
console.log("");
|
|
2428
|
+
// ── Signals ─────────────────────────────────────────────────────────────
|
|
2429
|
+
if (!signals.length) {
|
|
2430
|
+
console.log("✅ No context_burn or fanout_without_canary signals.");
|
|
2431
|
+
}
|
|
2432
|
+
else {
|
|
2433
|
+
const crit = signals.filter((s) => s.severity === "critical");
|
|
2434
|
+
console.log(`SIGNALS — ${signals.length} (${crit.length} critical)`);
|
|
2435
|
+
for (const s of signals.slice(0, 20)) {
|
|
2436
|
+
console.log(` ${s.severity === "critical" ? "🔴" : "⚠️ "} [${s.kind}] ${s.reason}`);
|
|
2437
|
+
}
|
|
2438
|
+
if (signals.length > 20)
|
|
2439
|
+
console.log(` … ${signals.length - 20} more (use --json)`);
|
|
2440
|
+
}
|
|
2441
|
+
console.log("");
|
|
2442
|
+
}
|
|
2443
|
+
// ---------------------------------------------------------------------------
|
|
2151
2444
|
// Main — route to init, CLI subcommand, or MCP server
|
|
2152
2445
|
// ---------------------------------------------------------------------------
|
|
2153
2446
|
const command = process.argv[2];
|
|
@@ -2184,6 +2477,12 @@ Usage:
|
|
|
2184
2477
|
Export hash-chained audit log (evidence aligned with
|
|
2185
2478
|
SOC 2 CC7.2 + ISO 27001 A.12.4.1 — not a certification)
|
|
2186
2479
|
contextengine audit-verify Verify audit log chain integrity (tamper detection)
|
|
2480
|
+
contextengine cost [--session ID] [--project NAME] [--run wf_ID] [--days N] [--top N] [--json]
|
|
2481
|
+
Multi-agent spend from Claude Code transcripts. Always prints
|
|
2482
|
+
VOLUME (tokens), VALUED COST (API list prices — notional on a
|
|
2483
|
+
subscription) and INTENSITY (agents, tool calls, deaths at the
|
|
2484
|
+
usage window), because volume and cost tell opposite stories.
|
|
2485
|
+
Flags context_burn + fanout_without_canary.
|
|
2187
2486
|
contextengine policy <validate|show> [args]
|
|
2188
2487
|
Author + validate the declarative .contextengine/policy.json
|
|
2189
2488
|
contextengine init-extension-secret [--force]
|
|
@@ -2197,7 +2496,7 @@ Usage:
|
|
|
2197
2496
|
Stream drift / loop / stuck-tool / fabrication alerts from the audit log
|
|
2198
2497
|
contextengine emit-event <kind> <payload-json> [--actor NAME]
|
|
2199
2498
|
Append a single event to the audit log (for integrations / scripted tests)
|
|
2200
|
-
contextengine hook <secret-scan|doc-coverage>
|
|
2499
|
+
contextengine hook <secret-scan|doc-coverage|rule-parity|commit-message-required>
|
|
2201
2500
|
Run policy-driven pre-commit checks against staged diff
|
|
2202
2501
|
(exit 1 on blocking violation; CE_JSON=1 for CI output)
|
|
2203
2502
|
contextengine install-skill [--global | --project] [--force]
|
|
@@ -2215,6 +2514,7 @@ Usage:
|
|
|
2215
2514
|
Accepts a project name or a directory path.
|
|
2216
2515
|
--all scores every discovered project (writes to each).
|
|
2217
2516
|
contextengine audit Run compliance audit (Pro)
|
|
2517
|
+
contextengine cost Multi-agent token/cost/capacity report
|
|
2218
2518
|
contextengine activate <key> <email> Activate a Pro license
|
|
2219
2519
|
contextengine deactivate Remove license and premium modules
|
|
2220
2520
|
contextengine stats Show live MCP session stats (value meter)
|
|
@@ -2304,6 +2604,12 @@ else if (command === "score") {
|
|
|
2304
2604
|
process.exit(1);
|
|
2305
2605
|
});
|
|
2306
2606
|
}
|
|
2607
|
+
else if (command === "cost") {
|
|
2608
|
+
cliCost(process.argv.slice(3)).catch((err) => {
|
|
2609
|
+
console.error("Error:", err);
|
|
2610
|
+
process.exit(1);
|
|
2611
|
+
});
|
|
2612
|
+
}
|
|
2307
2613
|
else if (command === "audit") {
|
|
2308
2614
|
cliAudit().catch((err) => {
|
|
2309
2615
|
console.error("Error:", err);
|
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
|
package/dist/detector.js
CHANGED
|
@@ -333,4 +333,120 @@ export const _internal = {
|
|
|
333
333
|
detectLoop, detectStuck, detectContextBloat, detectFabrication,
|
|
334
334
|
detectDrift, detectNoInsight, detectSilentFailure, detectStaleDocSignal,
|
|
335
335
|
};
|
|
336
|
+
import { metricsFor } from "./transcript-collector.js";
|
|
337
|
+
export const DEFAULT_COST_THRESHOLDS = {
|
|
338
|
+
billing_mode: "subscription",
|
|
339
|
+
pricing: [],
|
|
340
|
+
min_cache_efficiency: 3,
|
|
341
|
+
max_tool_calls_per_agent: 2,
|
|
342
|
+
max_cost_per_agent_usd: 3,
|
|
343
|
+
min_fanout_for_canary: 5,
|
|
344
|
+
max_failed_share: 0.05,
|
|
345
|
+
};
|
|
346
|
+
function mkRun(kind, severity, reason, payload) {
|
|
347
|
+
// evidence is AuditRecord[]; transcript signals carry their detail in
|
|
348
|
+
// payload instead of inventing records that were never written.
|
|
349
|
+
return { kind, severity, reason, evidence: [], payload, detectedAt: Date.now() };
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* context_burn — the run is paying for context it is not reusing.
|
|
353
|
+
*
|
|
354
|
+
* Fires on cache INEFFICIENCY, tool-call inflation, or per-agent valued cost.
|
|
355
|
+
* Deliberately does NOT fire on a low output/volume ratio: see
|
|
356
|
+
* [BURN-IS-COST-WEIGHTED-NOT-VOLUME] in policy.ts.
|
|
357
|
+
*/
|
|
358
|
+
export function detectContextBurn(run, t, m) {
|
|
359
|
+
const x = m ?? metricsFor(run, t.pricing);
|
|
360
|
+
if (x.agents === 0)
|
|
361
|
+
return null;
|
|
362
|
+
const reasons = [];
|
|
363
|
+
let severity = "warn";
|
|
364
|
+
const cacheWrite = run.totals.cacheWrite5m + run.totals.cacheWrite1h;
|
|
365
|
+
// Only meaningful once enough was written to judge reuse.
|
|
366
|
+
if (cacheWrite > 100_000 && x.cacheEfficiency < t.min_cache_efficiency) {
|
|
367
|
+
const writeShare = x.cost.total > 0 ? x.cost.cacheWrite / x.cost.total : 0;
|
|
368
|
+
reasons.push(`cache reused ${x.cacheEfficiency.toFixed(1)}x (floor ${t.min_cache_efficiency}x) — ` +
|
|
369
|
+
`${(cacheWrite / 1e6).toFixed(1)}M written vs ${(run.totals.cacheRead / 1e6).toFixed(1)}M read, ` +
|
|
370
|
+
`${(writeShare * 100).toFixed(0)}% of valued cost is cache WRITES`);
|
|
371
|
+
if (writeShare > 0.5)
|
|
372
|
+
severity = "critical";
|
|
373
|
+
}
|
|
374
|
+
if (x.medianToolCalls > t.max_tool_calls_per_agent) {
|
|
375
|
+
reasons.push(`median ${x.medianToolCalls} tool calls/agent (max ${t.max_tool_calls_per_agent}) — ` +
|
|
376
|
+
`agents are searching for their inputs instead of being handed them`);
|
|
377
|
+
}
|
|
378
|
+
const perAgent = x.cost.total / x.agents;
|
|
379
|
+
if (perAgent > t.max_cost_per_agent_usd) {
|
|
380
|
+
reasons.push(`$${perAgent.toFixed(2)}/agent valued (max $${t.max_cost_per_agent_usd.toFixed(2)})`);
|
|
381
|
+
}
|
|
382
|
+
if (!reasons.length)
|
|
383
|
+
return null;
|
|
384
|
+
return mkRun("context_burn", severity, `${run.runId}: ${reasons.join("; ")}`, {
|
|
385
|
+
runId: run.runId, project: run.project, sessionId: run.sessionId,
|
|
386
|
+
agents: x.agents, medianToolCalls: x.medianToolCalls,
|
|
387
|
+
cacheEfficiency: Number.isFinite(x.cacheEfficiency) ? x.cacheEfficiency : null,
|
|
388
|
+
cacheWriteTokens: cacheWrite, cacheReadTokens: run.totals.cacheRead,
|
|
389
|
+
outputShare: x.outputShare, costUsd: x.cost.total, costPerAgentUsd: perAgent,
|
|
390
|
+
billingMode: t.billing_mode, costIsNotional: t.billing_mode === "subscription",
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* fanout_without_canary — the fleet was launched before any single unit had
|
|
395
|
+
* reported, so nothing was known about per-agent consumption when the spend
|
|
396
|
+
* was committed.
|
|
397
|
+
*
|
|
398
|
+
* 🔒 LOCKED [CANARY-IS-A-TIME-ORDERING] — 2026-08-19
|
|
399
|
+
* ⛔ NEVER implement this as "no agent reported". A completed 300-agent run
|
|
400
|
+
* has 300 reports and was still un-canaried.
|
|
401
|
+
* WHY: the rule being enforced is "run ONE unit and read its consumption
|
|
402
|
+
* BEFORE scaling". That is a statement about ordering, not about outcomes,
|
|
403
|
+
* and it is only checkable by comparing each agent's start time against the
|
|
404
|
+
* earliest sibling completion.
|
|
405
|
+
* FIX: count agents that started before the first report landed. Severity
|
|
406
|
+
* rises when agents then died at the usage window — on a subscription that
|
|
407
|
+
* is the failure that actually costs something (real case: 15 of 51 agents
|
|
408
|
+
* lost in wf_41771d7b, 0 completed).
|
|
409
|
+
*/
|
|
410
|
+
export function detectFanoutWithoutCanary(run, t, m) {
|
|
411
|
+
const x = m ?? metricsFor(run, t.pricing);
|
|
412
|
+
if (x.agents < t.min_fanout_for_canary)
|
|
413
|
+
return null;
|
|
414
|
+
if (x.launchedBeforeFirstReport < t.min_fanout_for_canary)
|
|
415
|
+
return null;
|
|
416
|
+
const failedShare = x.agents ? x.failed / x.agents : 0;
|
|
417
|
+
let severity = "warn";
|
|
418
|
+
const parts = [
|
|
419
|
+
`${x.launchedBeforeFirstReport} of ${x.agents} agents launched before any had reported`,
|
|
420
|
+
];
|
|
421
|
+
if (x.capacityExhausted > 0) {
|
|
422
|
+
severity = "critical";
|
|
423
|
+
parts.push(`${x.capacityExhausted} died at the usage window (${(100 * x.capacityExhausted / x.agents).toFixed(0)}%) — ` +
|
|
424
|
+
`capacity spent for no result`);
|
|
425
|
+
}
|
|
426
|
+
else if (failedShare > t.max_failed_share) {
|
|
427
|
+
severity = "critical";
|
|
428
|
+
parts.push(`${x.failed}/${x.agents} returned nothing (${(failedShare * 100).toFixed(0)}%)`);
|
|
429
|
+
}
|
|
430
|
+
return mkRun("fanout_without_canary", severity, `${run.runId}: ${parts.join("; ")}`, {
|
|
431
|
+
runId: run.runId, project: run.project, sessionId: run.sessionId,
|
|
432
|
+
agents: x.agents, launchedBeforeFirstReport: x.launchedBeforeFirstReport,
|
|
433
|
+
reported: x.reported, failed: x.failed, capacityExhausted: x.capacityExhausted,
|
|
434
|
+
failedShare, costUsd: x.cost.total,
|
|
435
|
+
billingMode: t.billing_mode, costIsNotional: t.billing_mode === "subscription",
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
/** Run both transcript heuristics over a set of runs. */
|
|
439
|
+
export function runTranscriptHeuristics(runs, t = DEFAULT_COST_THRESHOLDS) {
|
|
440
|
+
const out = [];
|
|
441
|
+
for (const run of runs) {
|
|
442
|
+
const m = metricsFor(run, t.pricing);
|
|
443
|
+
const burn = detectContextBurn(run, t, m);
|
|
444
|
+
if (burn)
|
|
445
|
+
out.push(burn);
|
|
446
|
+
const fan = detectFanoutWithoutCanary(run, t, m);
|
|
447
|
+
if (fan)
|
|
448
|
+
out.push(fan);
|
|
449
|
+
}
|
|
450
|
+
return out;
|
|
451
|
+
}
|
|
336
452
|
//# sourceMappingURL=detector.js.map
|
package/dist/hooks.d.ts
CHANGED
|
@@ -137,4 +137,18 @@ export declare function formatCommitMessageViolations(violations: CommitMessageV
|
|
|
137
137
|
export declare function formatCommitMessageViolationsJson(violations: CommitMessageViolation[]): string;
|
|
138
138
|
export declare function formatSecretViolationsJson(violations: SecretViolation[]): string;
|
|
139
139
|
export declare function formatDocCoverageViolationsJson(violations: DocCoverageViolation[]): string;
|
|
140
|
+
export interface RuleParityViolation {
|
|
141
|
+
severity: "block" | "warn";
|
|
142
|
+
ruleId: string;
|
|
143
|
+
marker: string;
|
|
144
|
+
presentIn: string[];
|
|
145
|
+
missingFrom: string[];
|
|
146
|
+
reason: "marker-missing-from-some-files" | "marker-required-but-absent-everywhere" | "file-not-found";
|
|
147
|
+
missingFiles?: string[];
|
|
148
|
+
}
|
|
149
|
+
export declare function runRuleParity(policy: Policy, files: StagedFile[], repoRoot: string, opts?: {
|
|
150
|
+
all?: boolean;
|
|
151
|
+
}): RuleParityViolation[];
|
|
152
|
+
export declare function formatRuleParityViolations(violations: RuleParityViolation[]): string;
|
|
153
|
+
export declare function formatRuleParityViolationsJson(violations: RuleParityViolation[]): string;
|
|
140
154
|
//# sourceMappingURL=hooks.d.ts.map
|