@trazum/cli 1.47.0 → 1.49.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/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { open, readdir, readFile, stat, writeFile } from 'node:fs/promises';
3
3
  import { join, resolve as resolvePath } from 'node:path';
4
4
  import { gunzipSync } from 'node:zlib';
5
- import { applyRewrites, BASELINE_FILENAME, BASELINE_VERSION, breaches, cacheableMinimum, analyzeCachePrefix, billLevers, bucketedCacheEconomics, bucketedProfile, buildHistory, buildPlan, connectorFor, CONNECTORS, normalizeAnthropicUsage, normalizeOpenAIUsage, bucketsFromRecords, evaluateWatch, firedKey, pruneRecords, recordsFromBuckets, storeInventory, storedReportFrom, verifyPlan, cacheEconomics, cacheHitRate, contextPressure, comparePrompts, compareToBaseline, computeSavings, countTokensAnthropic, DEFAULT_USAGE, detectFromSource, matchLocale, parsePlanDocument, proposeInit, MIN_RATE_DAYS, parseConfig, coverageDrift, driversBetween, explainGateFailure, assignSources, fleetRollup, labelCoverage, measuredUsage, gateMargin, GATE_MARGIN_TIGHT, estimateTokens, evaluate, extractPrompts, findExamples, formatBaseline, formatSignedUsd, formatUsd, getMessages, getModel, hasMarker, LOCALES, MAX_BASELINE_BYTES, moneyIsComparable, mostSpecificMatch, nearestName, optimize, parseBaseline, PHRASE_LANGUAGES, plannedCalls, profilePrompt, profileToCsv, profileUsage, promptId, providerFromEnv, pruneExamples, refineWithLlm, rejectionText, reorderForCache, repriceProfile, reviewAgeDays, reviewExamples, RULES, sharedPrefixes, sharesOf, SOURCE_EXTENSIONS, suggestRewrites, toOtlpMetrics, toPromptfoo, TTL_1H_MS, UNLABELLED, withExactTokenCounts, } from '@trazum/core';
5
+ import { applyRewrites, BASELINE_FILENAME, BASELINE_VERSION, breaches, cacheableMinimum, analyzeCachePrefix, billLevers, bucketedCacheEconomics, bucketedProfile, buildHistory, buildPlan, connectorFor, CONNECTORS, normalizeAnthropicUsage, normalizeOpenAIUsage, bucketsFromRecords, evaluateWatch, firedKey, pruneRecords, recordsFromBuckets, storeInventory, storedReportFrom, verifyPlan, cacheEconomics, cacheHitRate, contextPressure, comparePrompts, compareToBaseline, computeSavings, countTokensAnthropic, DEFAULT_USAGE, budgetPositions, detectFromSource, matchLocale, parsePlanDocument, waiverDay, waiverHistory, proposeInit, MIN_RATE_DAYS, parseConfig, coverageDrift, driversBetween, explainGateFailure, assignSources, fleetRollup, labelCoverage, measuredUsage, gateMargin, GATE_MARGIN_TIGHT, estimateTokens, evaluate, extractPrompts, findExamples, formatBaseline, formatSignedUsd, formatUsd, getMessages, getModel, hasMarker, LOCALES, MAX_BASELINE_BYTES, moneyIsComparable, mostSpecificMatch, nearestName, optimize, parseBaseline, PHRASE_LANGUAGES, plannedCalls, profilePrompt, profileToCsv, profileUsage, promptId, providerFromEnv, pruneExamples, refineWithLlm, rejectionText, reorderForCache, repriceProfile, reviewAgeDays, reviewExamples, RULES, sharedPrefixes, sharesOf, SOURCE_EXTENSIONS, suggestRewrites, toOtlpMetrics, toPromptfoo, TTL_1H_MS, UNLABELLED, withExactTokenCounts, } from '@trazum/core';
6
6
  import { cacheDir, cacheStats, cachingProvider, clearCache } from './suggest-cache.js';
7
7
  import { dayOf, formatGap, median, spanDays } from './time.js';
8
8
  // Everything that reads the filesystem, on its own entry point so the web
@@ -11,6 +11,7 @@ import { CONFIG_FILENAME, DEFAULT_EXTENSIONS, budgetFor, BUNDLED_CATALOGUE, SAFE
11
11
  import { contentAt, gitAvailable, namesByRevision, pathInRepository, repositoryRoot, revisionsFor, } from './git.js';
12
12
  import { fetchProviderUsage, findCredential } from './connect.js';
13
13
  import { STORE_DIR, appendRecords, readStore, rewriteStore } from './store-fs.js';
14
+ import { WAIVER_LOG, appendWaiverUse, readWaiverLog } from './waiver-log.js';
14
15
  import { DEFAULT_PORT, buildServer, listen } from './serve.js';
15
16
  import { WATCH_STATE_VERSION, checkWebhook, postWebhook, readWatchState, writeWatchState, } from './watch-run.js';
16
17
  import { LOCALE_ENV_VARS, detectLocale, getCliMessages } from './i18n/index.js';
@@ -2081,36 +2082,36 @@ function parseWhen(args, flag, endOfDay, t, now) {
2081
2082
  */
2082
2083
  async function commandServe(args, config, pricing, t) {
2083
2084
  const root = process.cwd();
2084
- const limitUsd = config.spend?.maxUsd;
2085
2085
  const { resolved } = await readStore(root);
2086
- const measured = resolved.records.length > 0;
2087
2086
  /**
2088
- * The window the measurement covers, carried into every answer.
2087
+ * The live budget, from `budgetPositions` the same number `store` prints
2088
+ * and the same one the MCP guard consults.
2089
2089
  *
2090
- * The position is read once at start, so a caller has to be able to see how
2091
- * old it is. A null window here would let a figure from last month read as
2092
- * current, which is the staleness this endpoint is otherwise honest about.
2090
+ * **This used to read `spend.maxUsd` against the whole store**, which is a
2091
+ * per-log gate compared against however much history the store happened to
2092
+ * hold. A year of records against a monthly limit reported as a budget
2093
+ * position, with a straight face and no way for a caller to tell. Same
2094
+ * units, different denominators, and the two surfaces disagreed by exactly
2095
+ * as much history as the machine had. `spend.monthlyUsd` is the key for a
2096
+ * calendar month and nothing infers one key from the other: a repository
2097
+ * with a per-log gate and no monthly budget has no monthly position, and
2098
+ * this says so rather than picking a number that is the right shape.
2093
2099
  */
2094
- const window = measured
2095
- ? {
2096
- fromMs: Math.min(...resolved.records.map((record) => record.fromMs)),
2097
- toMs: Math.max(...resolved.records.map((record) => record.toMs)),
2098
- }
2099
- : null;
2100
- const report = bucketedProfile({
2101
- provider: 'store',
2102
- granularity: 'bucketed',
2103
- buckets: bucketsFromRecords(resolved.records),
2104
- window,
2105
- gaps: [],
2106
- unavailable: [],
2107
- }, { catalogue: pricing });
2100
+ const budget = budgetPositions(resolved.records, config.spend, { catalogue: pricing });
2101
+ const standing = budget.positions[0] ?? null;
2102
+ const limitUsd = config.spend?.monthlyUsd;
2103
+ const measured = standing !== null && standing.coverage !== 'none';
2108
2104
  const server = buildServer({
2109
2105
  catalogue: pricing,
2110
2106
  position: () => ({
2111
- consumedUsd: measured ? report.total.totalUsd : undefined,
2107
+ // Nothing measured inside the period is `undefined`, never zero: the
2108
+ // endpoint's `cannot-tell` exists for exactly this, and a $0 consumed
2109
+ // would be the healthiest-looking budget a dead store can produce.
2110
+ consumedUsd: measured ? standing.consumedUsd : undefined,
2112
2111
  limitUsd,
2113
- window: report.span,
2112
+ // The period, not the store's span. A caller judging staleness needs to
2113
+ // know which month the figure is about.
2114
+ window: standing === null ? null : { fromMs: standing.period.fromMs, toMs: standing.period.toMs },
2114
2115
  }),
2115
2116
  });
2116
2117
  const socket = stringFlag(args, 'socket');
@@ -2122,7 +2123,10 @@ async function commandServe(args, config, pricing, t) {
2122
2123
  const where = await listen(server, socket !== undefined ? { socket } : { port });
2123
2124
  console.log(c.bold(t.serve.listening(where)));
2124
2125
  console.log(` ${c.dim(wrap(t.serve.loopbackOnly(), 74, ' '))}`);
2125
- console.log(` ${c.dim(wrap(measured ? t.serve.measuredFrom(formatUsd(report.total.totalUsd)) : t.serve.nothingMeasured(STORE_DIR), 74, ' '))}`);
2126
+ console.log(` ${c.dim(wrap(measured ? t.serve.measuredFrom(formatUsd(standing.consumedUsd)) : t.serve.nothingMeasured(STORE_DIR), 74, ' '))}`);
2127
+ if (standing !== null && standing.coverage === 'partial') {
2128
+ console.log(` ${c.yellow(wrap(t.serve.partialCoverage(standing.measuredDays, standing.elapsedDays, standing.period.id), 74, ' '))}`);
2129
+ }
2126
2130
  if (limitUsd === undefined) {
2127
2131
  console.log(` ${c.dim(wrap(t.serve.noBudget(), 74, ' '))}`);
2128
2132
  }
@@ -2375,6 +2379,51 @@ async function commandStore(args, config, pricing, t) {
2375
2379
  }
2376
2380
  const keepDays = config.store?.keepDays;
2377
2381
  console.log(` ${c.dim(wrap(keepDays === undefined ? t.store.noRetention() : t.store.retention(String(keepDays)), 74, ' '))}`);
2382
+ /**
2383
+ * The live budget, printed here because this is where the measurement lives.
2384
+ *
2385
+ * The same call `serve` makes and the same call the MCP guard makes, so the
2386
+ * three cannot disagree about how much of the month is gone — which is the
2387
+ * whole point of the number existing in one place.
2388
+ */
2389
+ renderBudget(budgetPositions(resolved.records, config.spend, { catalogue: pricing }), t, n);
2390
+ }
2391
+ /**
2392
+ * One budget standing, rendered.
2393
+ *
2394
+ * Coverage before the money, deliberately. A reader who sees "$61 of $100"
2395
+ * first has already formed a view by the time they reach "over three of
2396
+ * nineteen elapsed days", and the second sentence has to undo the first.
2397
+ */
2398
+ function renderBudget(report, t, n) {
2399
+ const standing = report.positions[0];
2400
+ if (standing === undefined) {
2401
+ if (report.unmeasuredScopes.length > 0) {
2402
+ console.log();
2403
+ console.log(` ${c.dim(wrap(t.store.budgetScopesUnmeasured(report.unmeasuredScopes.length), 74, ' '))}`);
2404
+ }
2405
+ return;
2406
+ }
2407
+ console.log();
2408
+ console.log(c.bold(t.store.budgetHeading(standing.period.id)));
2409
+ if (standing.coverage === 'none') {
2410
+ // Nothing measured is never rendered as nothing spent. A dead store and a
2411
+ // quiet month produce the same zero, and only one of them is good news.
2412
+ console.log(` ${c.red(wrap(t.store.budgetNothingMeasured(standing.elapsedDays), 74, ' '))}`);
2413
+ return;
2414
+ }
2415
+ if (standing.coverage === 'partial') {
2416
+ console.log(` ${c.yellow(wrap(t.store.budgetPartial(standing.measuredDays, standing.elapsedDays, standing.unmeasuredDays.join(', ')), 74, ' '))}`);
2417
+ }
2418
+ const share = standing.burn.consumedShare;
2419
+ console.log(` ${t.store.budgetStanding(formatUsd(standing.consumedUsd), formatUsd(standing.limitUsd), share === null ? '—' : `${Math.round(share * 100)}%`, n(standing.measuredDays), n(standing.period.days))}`);
2420
+ const line = t.store.budgetShape(standing.burn.shape, Math.round(standing.burn.elapsedShare * 100), standing.coverage);
2421
+ console.log(` ${standing.verdict === 'over' ? c.red(line) : c.dim(wrap(line, 74, ' '))}`);
2422
+ // Only where there is a shape to disclaim. "That is a shape, not a forecast"
2423
+ // under "nothing to compare against" is a disclaimer about nothing.
2424
+ if (standing.burn.shape !== 'cannot-tell') {
2425
+ console.log(` ${c.dim(wrap(t.store.budgetNeverForecast(), 74, ' '))}`);
2426
+ }
2378
2427
  }
2379
2428
  /**
2380
2429
  * `trazum connect <provider>` — the bill, read from the provider.
@@ -2518,7 +2567,7 @@ async function commandConnect(args, pricing, t) {
2518
2567
  * same action planned twice — and no series, however long, becomes a
2519
2568
  * forecast.
2520
2569
  */
2521
- async function commandHistory(args, pricing, t) {
2570
+ async function commandHistory(args, config, pricing, t) {
2522
2571
  /**
2523
2572
  * `--store` builds the series from measured spend already on disk.
2524
2573
  *
@@ -2617,7 +2666,22 @@ async function commandHistory(args, pricing, t) {
2617
2666
  if (history.periods.length < 3) {
2618
2667
  throw new Error(t.history.needsThree(String(history.periods.length)));
2619
2668
  }
2620
- const stamped = { ...history, unrecognizedFiles: unrecognized };
2669
+ /**
2670
+ * The waiver record — closing the gap 1.40 named and could not fill.
2671
+ *
2672
+ * 1.40 wanted to say "this finding has been waived three times in a row" and
2673
+ * refused to, because the only material available was the config as it
2674
+ * stands, and a past reconstructed from a present is a guess wearing a
2675
+ * record's clothes. The material exists now: since 1.48 a waiver that
2676
+ * silences a gate writes down that it did, and this reads those lines back.
2677
+ *
2678
+ * Read from the working directory rather than from the reports directory:
2679
+ * the waiver record belongs to the repository whose gates fired, and the
2680
+ * stored reports may have come from anywhere.
2681
+ */
2682
+ const waivers = await readWaiverLog('.');
2683
+ const waiverReport = waiverHistory(waivers.uses, config.waive ?? []);
2684
+ const stamped = { ...history, unrecognizedFiles: unrecognized, waivers: waiverReport };
2621
2685
  const n = (value) => value.toLocaleString(t.numberLocale);
2622
2686
  const day = (ms) => new Date(ms).toISOString().slice(0, 10);
2623
2687
  const pct = (value) => `${(value * 100).toFixed(1)}%`;
@@ -2659,6 +2723,56 @@ async function commandHistory(args, pricing, t) {
2659
2723
  for (const name of unrecognized) {
2660
2724
  out.push(md ? `- ${t.history.unrecognized(name)}` : ` ${t.history.unrecognized(name)}`);
2661
2725
  }
2726
+ /**
2727
+ * The waiver record, printed only once something has been recorded.
2728
+ *
2729
+ * Silent on a repository that has never waived anything, rather than a
2730
+ * heading over "0 uses" — an empty section teaches a reader to skip the
2731
+ * section, and this is the one they should not learn to skip.
2732
+ */
2733
+ if (waivers.present) {
2734
+ out.push('');
2735
+ out.push(md ? `### ${t.history.waiverHeading()}` : t.history.waiverHeading());
2736
+ if (waiverReport.totalUses === 0) {
2737
+ out.push(md ? `- ${t.history.waiverNoneRecorded()}` : ` ${t.history.waiverNoneRecorded()}`);
2738
+ }
2739
+ else {
2740
+ const since = t.history.waiverSince(waiverReport.since ?? '', waiverReport.totalUses);
2741
+ out.push(md ? `- ${since}` : ` ${since}`);
2742
+ out.push(md ? `- _${t.history.waiverStartsHere()}_` : ` ${t.history.waiverStartsHere()}`);
2743
+ for (const habit of waiverReport.habits) {
2744
+ out.push('');
2745
+ const head = t.history.waiverHabit(habit.gate, habit.uses, habit.days, habit.firstDay, habit.lastDay);
2746
+ out.push(md ? `- **${head}**` : ` ${head}`);
2747
+ const rows = [t.history.waiverVerdict(habit.verdict)];
2748
+ // The reason as it stands *now* — never read backwards onto an
2749
+ // older use, which is the same mistake the record exists to avoid.
2750
+ const latest = habit.reasons[habit.reasons.length - 1];
2751
+ if (latest !== undefined)
2752
+ rows.push(t.history.waiverReasonNow(latest));
2753
+ if (habit.reasons.length > 1)
2754
+ rows.push(t.history.waiverReasonsChanged(habit.reasons.length));
2755
+ const firstExpiry = habit.expiries[0];
2756
+ const lastExpiry = habit.expiries[habit.expiries.length - 1];
2757
+ if (habit.expiries.length > 1 && firstExpiry !== undefined && lastExpiry !== undefined) {
2758
+ rows.push(t.history.waiverExpiriesMoved(firstExpiry, lastExpiry, habit.expiries.length - 1));
2759
+ }
2760
+ if (!habit.stillConfigured)
2761
+ rows.push(t.history.waiverNoLongerConfigured());
2762
+ for (const row of rows)
2763
+ out.push(md ? ` - ${row}` : ` ${row}`);
2764
+ }
2765
+ }
2766
+ if (waiverReport.neverUsed.length > 0) {
2767
+ out.push('');
2768
+ const dead = t.history.waiverNeverUsed(waiverReport.neverUsed.join(', '));
2769
+ out.push(md ? `- ${dead}` : ` ${dead}`);
2770
+ }
2771
+ if (waivers.unreadable.length > 0) {
2772
+ const bad = t.history.waiverUnreadable(waivers.unreadable.length, WAIVER_LOG);
2773
+ out.push(md ? `- ${bad}` : ` ${bad}`);
2774
+ }
2775
+ }
2662
2776
  if (fromStore) {
2663
2777
  out.push('');
2664
2778
  const note = t.history.storeNoLabels();
@@ -3332,7 +3446,16 @@ async function commandProfile(args, config, pricing, t) {
3332
3446
  * way, because a waived failure that vanished from the output would be a
3333
3447
  * finding deleted with extra steps.
3334
3448
  */
3335
- const waived = (gate) => {
3449
+ /**
3450
+ * Uses recorded this run, flushed after the gates have finished.
3451
+ *
3452
+ * Collected rather than written inline because `waived` is synchronous and
3453
+ * called from seven places inside the gate pass. Writing from each of them
3454
+ * would mean seven awaits threaded through the exit-code logic — the one
3455
+ * part of this command where a mistake turns a red build green.
3456
+ */
3457
+ const waiverUses = [];
3458
+ const waived = (gate, measuredUsd = null, limitUsd = null) => {
3336
3459
  const found = waiverFor(gate);
3337
3460
  if (found === null)
3338
3461
  return false;
@@ -3342,6 +3465,29 @@ async function commandProfile(args, config, pricing, t) {
3342
3465
  }
3343
3466
  const daysLeft = Math.max(0, Math.ceil((Date.parse(`${found.entry.until}T00:00:00Z`) + 86_400_000 - Date.now()) / 86_400_000));
3344
3467
  console.error(c.yellow(t.profile.waiveActive(gate, found.entry.reason, found.entry.until, String(daysLeft))));
3468
+ /**
3469
+ * Recorded **when it silences something**, never when it is configured.
3470
+ *
3471
+ * A waiver nobody's build has ever hit is not a habit — it is dead config
3472
+ * — and the history reports the two apart. This is also the only honest
3473
+ * way to build the record 1.40 refused to invent: it starts today and
3474
+ * says so, rather than reconstructing a past from the present.
3475
+ *
3476
+ * The reason and the expiry are taken from the config **as it stands at
3477
+ * this moment**, because that is the decision that was actually in force.
3478
+ * Reading today's reason back onto last quarter's use is the same mistake
3479
+ * one layer down.
3480
+ */
3481
+ waiverUses.push({
3482
+ schemaVersion: 1,
3483
+ day: waiverDay(new Date()),
3484
+ gate,
3485
+ reason: found.entry.reason,
3486
+ until: found.entry.until,
3487
+ commit: process.env.GITHUB_SHA ?? process.env.CI_COMMIT_SHA ?? null,
3488
+ measuredUsd,
3489
+ limitUsd,
3490
+ });
3345
3491
  return true;
3346
3492
  };
3347
3493
  const applyGates = () => {
@@ -3391,7 +3537,7 @@ async function commandProfile(args, config, pricing, t) {
3391
3537
  }
3392
3538
  if (usd > limit) {
3393
3539
  console.error(c.red(t.profile.labelBudgetFailed(label, formatUsd(usd), formatUsd(limit))));
3394
- if (!waived(`byLabel:${label}`))
3540
+ if (!waived(`byLabel:${label}`, usd, limit))
3395
3541
  process.exitCode = 1;
3396
3542
  }
3397
3543
  else {
@@ -3453,7 +3599,7 @@ async function commandProfile(args, config, pricing, t) {
3453
3599
  * copy says so.
3454
3600
  */
3455
3601
  explainFailure(report.total.totalUsd - maxUsd);
3456
- if (!waived('maxUsd'))
3602
+ if (!waived('maxUsd', report.total.totalUsd, maxUsd))
3457
3603
  process.exitCode = 1;
3458
3604
  }
3459
3605
  else {
@@ -3488,7 +3634,7 @@ async function commandProfile(args, config, pricing, t) {
3488
3634
  }
3489
3635
  else if (againstDelta > maxGrowth) {
3490
3636
  console.error(c.red(t.profile.maxGrowthUsdFailed(formatSignedUsd(againstDelta), formatUsd(maxGrowth))));
3491
- if (!waived('maxGrowthUsd'))
3637
+ if (!waived('maxGrowthUsd', againstDelta, maxGrowth))
3492
3638
  process.exitCode = 1;
3493
3639
  }
3494
3640
  }
@@ -3505,12 +3651,12 @@ async function commandProfile(args, config, pricing, t) {
3505
3651
  const gateCache = cacheEconomics(report.total);
3506
3652
  if (gateCache.deltaUsd > maxLoss) {
3507
3653
  console.error(c.red(t.profile.maxCacheLossFailed(formatUsd(gateCache.deltaUsd), formatUsd(maxLoss))));
3508
- if (!waived('maxCacheLossUsd'))
3654
+ if (!waived('maxCacheLossUsd', gateCache.deltaUsd, maxLoss))
3509
3655
  process.exitCode = 1;
3510
3656
  }
3511
3657
  else if (gateCache.worstCaseDeltaUsd > maxLoss) {
3512
3658
  console.error(c.red(t.profile.maxCacheLossWorstCase(report.total.assumedWriteTtlCalls, formatUsd(gateCache.worstCaseDeltaUsd), formatUsd(maxLoss))));
3513
- if (!waived('maxCacheLossUsd'))
3659
+ if (!waived('maxCacheLossUsd', gateCache.worstCaseDeltaUsd, maxLoss))
3514
3660
  process.exitCode = 1;
3515
3661
  }
3516
3662
  else {
@@ -3556,7 +3702,7 @@ async function commandProfile(args, config, pricing, t) {
3556
3702
  if (worst.usd > maxDay) {
3557
3703
  console.error(c.red(`${t.profile.maxDayFailed(worst.day, formatUsd(worst.usd), formatUsd(maxDay))}${suspect}`));
3558
3704
  explainFailure(worst.usd - maxDay, { namesLargest: true });
3559
- if (!waived('maxDayUsd'))
3705
+ if (!waived('maxDayUsd', worst.usd, maxDay))
3560
3706
  process.exitCode = 1;
3561
3707
  }
3562
3708
  else {
@@ -3598,7 +3744,7 @@ async function commandProfile(args, config, pricing, t) {
3598
3744
  else if (report.sessionSpend.maxUsd > maxSession) {
3599
3745
  console.error(c.red(t.profile.maxSessionFailed(formatUsd(report.sessionSpend.maxUsd), formatUsd(maxSession), n(report.sessionSpend.sessions))));
3600
3746
  explainFailure(report.sessionSpend.maxUsd - maxSession);
3601
- if (!waived('maxSessionUsd'))
3747
+ if (!waived('maxSessionUsd', report.sessionSpend.maxUsd, maxSession))
3602
3748
  process.exitCode = 1;
3603
3749
  }
3604
3750
  else {
@@ -3631,6 +3777,26 @@ async function commandProfile(args, config, pricing, t) {
3631
3777
  gateFailed = process.exitCode === 1;
3632
3778
  }
3633
3779
  };
3780
+ /**
3781
+ * Writes down every waiver that silenced something this run.
3782
+ *
3783
+ * **A failure here never fails the build.** The gate's job is the exit code;
3784
+ * a read-only checkout or a full disk must not turn a passing build red on
3785
+ * account of bookkeeping. The problem is reported and the gate's own verdict
3786
+ * stands — which is also why this runs after `recordGates` rather than
3787
+ * inside it: nothing about the exit code depends on the write.
3788
+ */
3789
+ const recordWaiverUses = async () => {
3790
+ if (waiverUses.length === 0)
3791
+ return;
3792
+ for (const use of waiverUses) {
3793
+ const failed = await appendWaiverUse('.', use);
3794
+ if (failed !== null) {
3795
+ console.error(c.dim(t.profile.waiveNotRecorded(WAIVER_LOG, failed)));
3796
+ return;
3797
+ }
3798
+ }
3799
+ };
3634
3800
  /**
3635
3801
  * The side files the caller asked for. Written on **both** output paths:
3636
3802
  * under --json the human rendering returns early, and the first version of
@@ -3779,6 +3945,7 @@ async function commandProfile(args, config, pricing, t) {
3779
3945
  ...(whatIf !== null ? { whatIf } : {}),
3780
3946
  }, null, 2));
3781
3947
  recordGates();
3948
+ await recordWaiverUses();
3782
3949
  await writeSideFiles();
3783
3950
  return;
3784
3951
  }
@@ -4786,6 +4953,7 @@ async function commandProfile(args, config, pricing, t) {
4786
4953
  }
4787
4954
  reportProfileGaps(report, t, n, pricingStale);
4788
4955
  recordGates();
4956
+ await recordWaiverUses();
4789
4957
  await writeSideFiles();
4790
4958
  }
4791
4959
  /**
@@ -6082,7 +6250,7 @@ async function main() {
6082
6250
  await commandVerify(args, pricing, t);
6083
6251
  break;
6084
6252
  case 'history':
6085
- await commandHistory(args, pricing, t);
6253
+ await commandHistory(args, config, pricing, t);
6086
6254
  break;
6087
6255
  case 'connect':
6088
6256
  await commandConnect(args, pricing, t);