@compr/opscontext-mcp 2.5.2 โ†’ 2.5.4

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