@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 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 { getStagedFiles, runSecretScan, runDocCoverage, runCommitMessageRequired, formatSecretViolations, formatDocCoverageViolations, formatSecretViolationsJson, formatDocCoverageViolationsJson, formatCommitMessageViolations, formatCommitMessageViolationsJson, } from "./hooks.js";
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
- if (stagedFiles.length === 0)
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);
@@ -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