@trazum/cli 1.32.0 → 1.34.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trazum/cli",
3
- "version": "1.32.0",
3
+ "version": "1.34.0",
4
4
  "description": "Trazum CLI: find where your LLM bill goes, price every finding per month, and enforce token budgets in CI.",
5
5
  "license": "MIT",
6
6
  "author": "David Mu\u00f1oz Rey",
@@ -37,7 +37,7 @@
37
37
  "prepublishOnly": "npm run build && npm test"
38
38
  },
39
39
  "dependencies": {
40
- "@trazum/core": "1.32.0"
40
+ "@trazum/core": "1.34.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "^26.2.0",
package/src/i18n/en.ts CHANGED
@@ -345,6 +345,11 @@ ${bold('CONFIG FILE')}
345
345
  spend { "maxUsd": 200, "byLabel": { "chat": 40 } } — money budgets for
346
346
  "trazum profile", in dollars. A budgeted label with no calls in
347
347
  the log is reported as not measured, never as a pass
348
+ waive [{ "gate": "maxUsd", "reason": "August migration", "until":
349
+ "2026-09-15" }] — a gate failure decided about, on the record.
350
+ All three fields required: a waiver with no end date is a
351
+ finding deleted with extra steps. Waived failures still print,
352
+ and the day the waiver expires the gate fails again
348
353
 
349
354
  Flags beat the config; the config beats the defaults. Budgets resolve to the
350
355
  most specific matching pattern — most literal characters wins. A boolean the
@@ -1150,6 +1155,28 @@ ${bold('EXAMPLES')}
1150
1155
  `--what-if does not know "${value}". Priced models: ${available}. Add it with --pricing if you have its rates.`,
1151
1156
  badGzip: (file, detail) =>
1152
1157
  `${file} is gzipped and would not decompress: ${detail}. Reading the rest and saying nothing would report a bill missing whatever that file held, so this stops instead. Check the file, or move it out of the directory.`,
1158
+ dryRunHeading: () => 'What this log can answer — no bill was produced',
1159
+ dryRunParsed: (parsed, skipped) =>
1160
+ `${parsed} records parse; ${skipped} lines could not be read. A dry run prices nothing: the point is what the gates you are about to wire would stand on.`,
1161
+ dryRunUnpriced: (models) =>
1162
+ `Models the price table does not know: ${models}. Their tokens parse; their dollars need a --pricing overlay.`,
1163
+ dryRunTotals: () => 'The bill itself: totals, per-model split, cache economics, the levers.',
1164
+ dryRunLabels: (share) =>
1165
+ `Per-workload findings and label budgets — "label" on ${share} of records.`,
1166
+ dryRunClock: (share) =>
1167
+ `The period, per-day and per-hour shape, --max-day-usd, --since/--until — a timestamp on ${share} of records.`,
1168
+ dryRunSessions: (share) =>
1169
+ `Conversation growth, per-conversation cost, --max-session-usd — a session on ${share} of records. Grouped by, never printed.`,
1170
+ dryRunStopReason: (share) =>
1171
+ `Truncated answers and their retry bill — a stop reason on ${share} of records.`,
1172
+ dryRunCacheTtl: (ttl, writes) =>
1173
+ `Settled cache verdicts — the "cache_creation" split on ${ttl} of ${writes} cache-writing records.`,
1174
+ dryRunNoCacheTraffic: () =>
1175
+ 'Cache verdicts: nothing wrote to the cache in this log, so there is nothing to settle — not a missing field.',
1176
+ dryRunFooter: () =>
1177
+ 'A ✗ is not a defect in the log; it is a finding this log cannot support yet. The README\'s recording recipe carries every field above.',
1178
+ dryRunNoGates: () =>
1179
+ '--dry-run produces no bill, so a gate beside it would exit green having judged nothing. Run the gates without --dry-run.',
1153
1180
  coverageHeading: () => 'What this log cannot answer yet',
1154
1181
  needsLabel: (seen) =>
1155
1182
  `"label" on ${seen} records: without it every workload is one row, so no per-workload spend, no drill-down, and the levers describe a mixture rather than a decision.`,
@@ -1177,6 +1204,10 @@ ${bold('EXAMPLES')}
1177
1204
  `The 95th percentile is ${ratio}x the median there: most conversations are cheap and a few are not, which is a tail a quota can catch. Where median and p95 sit close together the workload is simply expensive and there is no tail to hunt.`,
1178
1205
  sessionSpendOnly: (sessions, max) =>
1179
1206
  `${sessions} ${sessions === '1' ? 'conversation' : 'conversations'} in this log; the most expensive cost ${max}. Too few per workload for a percentile — a maximum is a fact at any count, and it is the figure --max-session-usd judges.`,
1207
+ waiveActive: (gate, reason, until, daysLeft) =>
1208
+ `WAIVED — the ${gate} failure above is on the record and silenced until ${until} (${daysLeft} days left): "${reason}". The bill still counts it; only the exit code is quiet, and the day the waiver expires this gate fails again.`,
1209
+ waiveExpired: (gate, until, reason) =>
1210
+ `The waiver on ${gate} expired on ${until} and no longer silences anything. It was written for: "${reason}". Renew it with a new date and a current reason, or fix what it was covering — an expired waiver left in place is a finding deleted with extra steps.`,
1180
1211
  gateLargest: (label, model, usd, share) =>
1181
1212
  `Most of it is ${label} on ${model}: ${usd}, ${share} of the bill. That is where the money is, not necessarily where the fix is.`,
1182
1213
  gateLever: (label, action, saving, overage, covers) =>
package/src/i18n/es.ts CHANGED
@@ -348,6 +348,12 @@ ${bold('FICHERO DE CONFIGURACIÓN')}
348
348
  spend { "maxUsd": 200, "byLabel": { "chat": 40 } } — presupuestos en
349
349
  dólares para "trazum profile". Una etiqueta con presupuesto y sin
350
350
  llamadas se informa como no medida, nunca como aprobada
351
+ waive [{ "gate": "maxUsd", "reason": "migración de agosto", "until":
352
+ "2026-09-15" }] — un fallo de gate sobre el que se ha decidido,
353
+ registrado. Los tres campos son obligatorios: un waiver sin
354
+ fecha de fin es un hallazgo borrado con pasos extra. Los fallos
355
+ waived se siguen imprimiendo, y el día que caduca el gate
356
+ vuelve a fallar
351
357
 
352
358
  Las opciones ganan al config; el config gana a los valores por defecto. Los
353
359
  presupuestos se resuelven con el patrón más específico que encaje — gana el
@@ -1165,6 +1171,28 @@ ${bold('EJEMPLOS')}
1165
1171
  `--what-if no conoce "${value}". Modelos con precio: ${available}. Añádelo con --pricing si tienes sus tarifas.`,
1166
1172
  badGzip: (file, detail) =>
1167
1173
  `${file} está comprimido con gzip y no se ha podido descomprimir: ${detail}. Leer el resto y no decir nada daría una factura a la que le falta lo que hubiera en ese fichero, así que se para aquí. Revisa el fichero, o sácalo del directorio.`,
1174
+ dryRunHeading: () => 'Qué puede responder este registro — no se ha producido factura',
1175
+ dryRunParsed: (parsed, skipped) =>
1176
+ `${parsed} registros se leen; ${skipped} líneas no se pudieron leer. Un dry run no valora nada: el punto es sobre qué se apoyarían los gates que vas a cablear.`,
1177
+ dryRunUnpriced: (models) =>
1178
+ `Modelos que la tabla de precios no conoce: ${models}. Sus tokens se leen; sus dólares necesitan un overlay --pricing.`,
1179
+ dryRunTotals: () => 'La factura en sí: totales, desglose por modelo, economía de caché, las palancas.',
1180
+ dryRunLabels: (share) =>
1181
+ `Hallazgos por carga y presupuestos por etiqueta — "label" en el ${share} de los registros.`,
1182
+ dryRunClock: (share) =>
1183
+ `El periodo, la forma por día y por hora, --max-day-usd, --since/--until — marca de tiempo en el ${share} de los registros.`,
1184
+ dryRunSessions: (share) =>
1185
+ `Crecimiento de conversación, coste por conversación, --max-session-usd — sesión en el ${share} de los registros. Se agrupa por ella, nunca se imprime.`,
1186
+ dryRunStopReason: (share) =>
1187
+ `Respuestas truncadas y su factura de reintentos — razón de parada en el ${share} de los registros.`,
1188
+ dryRunCacheTtl: (ttl, writes) =>
1189
+ `Veredictos de caché resueltos — el desglose "cache_creation" en ${ttl} de ${writes} registros que escriben caché.`,
1190
+ dryRunNoCacheTraffic: () =>
1191
+ 'Veredictos de caché: nada escribió en la caché en este registro, así que no hay nada que resolver — no es un campo ausente.',
1192
+ dryRunFooter: () =>
1193
+ 'Una ✗ no es un defecto del registro; es un hallazgo que este registro aún no puede sostener. La receta de registro del README lleva todos los campos de arriba.',
1194
+ dryRunNoGates: () =>
1195
+ '--dry-run no produce factura, así que un gate a su lado saldría en verde sin haber juzgado nada. Corre los gates sin --dry-run.',
1168
1196
  coverageHeading: () => 'Lo que este registro todavía no puede responder',
1169
1197
  needsLabel: (seen) =>
1170
1198
  `"label" en ${seen} registros: sin él todas las cargas son una sola fila, así que no hay gasto por carga, ni zoom, y las palancas describen una mezcla en vez de una decisión.`,
@@ -1192,6 +1220,10 @@ ${bold('EJEMPLOS')}
1192
1220
  `El percentil 95 es ${ratio}x la mediana ahí: casi todas las conversaciones son baratas y unas pocas no, y esa es una cola que una cuota puede cazar. Cuando mediana y p95 quedan cerca, la carga es cara sin más y no hay cola que perseguir.`,
1193
1221
  sessionSpendOnly: (sessions, max) =>
1194
1222
  `${sessions} ${sessions === '1' ? 'conversación' : 'conversaciones'} en este registro; la más cara costó ${max}. Demasiado pocas por carga para un percentil — un máximo es un hecho con cualquier recuento, y es la cifra que juzga --max-session-usd.`,
1223
+ waiveActive: (gate, reason, until, daysLeft) =>
1224
+ `WAIVED — el fallo de ${gate} de arriba queda registrado y silenciado hasta ${until} (${daysLeft} días restantes): "${reason}". La factura lo sigue contando; solo el código de salida calla, y el día que caduque el waiver este gate vuelve a fallar.`,
1225
+ waiveExpired: (gate, until, reason) =>
1226
+ `El waiver de ${gate} caducó el ${until} y ya no silencia nada. Se escribió por: "${reason}". Renuévalo con fecha nueva y razón vigente, o arregla lo que cubría — un waiver caducado dejado ahí es un hallazgo borrado con pasos extra.`,
1195
1227
  gateLargest: (label, model, usd, share) =>
1196
1228
  `La mayor parte es ${label} en ${model}: ${usd}, el ${share} de la factura. Ahí está el dinero, no necesariamente el arreglo.`,
1197
1229
  gateLever: (label, action, saving, overage, covers) =>
package/src/i18n/types.ts CHANGED
@@ -784,6 +784,13 @@ export interface CliMessages {
784
784
  * computed. Nothing here recommends: whether the cheaper model can do the
785
785
  * work is the reader's to judge, and the copy says so.
786
786
  */
787
+ /**
788
+ * Findings as policy. An active waiver silences one gate's exit code, on
789
+ * the record, with the reason and the days left; an expired one silences
790
+ * nothing and says so beside the failure it used to cover.
791
+ */
792
+ waiveActive(gate: string, reason: string, until: string, daysLeft: string): string;
793
+ waiveExpired(gate: string, until: string, reason: string): string;
787
794
  gateLargest(label: string, model: string, usd: string, share: string): string;
788
795
  gateLever(label: string, action: string, saving: string, overage: string, covers: boolean): string;
789
796
  /**
@@ -916,6 +923,23 @@ export interface CliMessages {
916
923
  * a skip: a total quietly missing a day is the failure this refuses.
917
924
  */
918
925
  badGzip(file: string, detail: string): string;
926
+ /**
927
+ * `--dry-run`: readiness per capability, no bill. The question somebody
928
+ * has before wiring CI, answered without making them read a report to
929
+ * discover a missing field.
930
+ */
931
+ dryRunHeading(): string;
932
+ dryRunParsed(parsed: string, skipped: string): string;
933
+ dryRunUnpriced(models: string): string;
934
+ dryRunTotals(): string;
935
+ dryRunLabels(share: string): string;
936
+ dryRunClock(share: string): string;
937
+ dryRunSessions(share: string): string;
938
+ dryRunStopReason(share: string): string;
939
+ dryRunCacheTtl(ttl: string, writes: string): string;
940
+ dryRunNoCacheTraffic(): string;
941
+ dryRunFooter(): string;
942
+ dryRunNoGates(): string;
919
943
  coverageHeading(): string;
920
944
  needsLabel(seen: string): string;
921
945
  needsSession(seen: string): string;
package/src/index.ts CHANGED
@@ -440,7 +440,7 @@ const COMMAND_FLAGS: Record<string, string[]> = {
440
440
  ],
441
441
  check: ['max-tokens', 'level', 'exact-tokens', 'markdown-out', 'baseline'],
442
442
  baseline: ['model', 'calls', 'output-tokens', 'cache-hit-rate', 'batch', 'exact-tokens', 'out', 'o'],
443
- profile: ['json', 'pricing', 'pricing-live', 'against', 'what-if', 'markdown-out', 'csv-out', 'csv-shape', 'max-usd', 'max-growth-usd', 'max-cache-loss-usd', 'max-day-usd', 'max-session-usd', 'label', 'since', 'until'],
443
+ profile: ['json', 'pricing', 'pricing-live', 'against', 'what-if', 'markdown-out', 'csv-out', 'csv-shape', 'max-usd', 'max-growth-usd', 'max-cache-loss-usd', 'max-day-usd', 'max-session-usd', 'label', 'since', 'until', 'dry-run'],
444
444
  route: ['prompt-file', 'cases', 'label', 'concurrency', 'json', 'yes', 'pricing', 'pricing-live'],
445
445
  eval: ['cases', 'level', 'concurrency', 'export', 'out', 'o', 'model'],
446
446
  prune: ['cases', 'concurrency', 'json', 'yes'],
@@ -2146,6 +2146,52 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
2146
2146
  const n = (value: number): string => value.toLocaleString(t.numberLocale);
2147
2147
  const pct = (share: number): string => `${(share * 100).toFixed(1)}%`;
2148
2148
 
2149
+ /**
2150
+ * `--dry-run`: what this log could and could not answer, and no bill.
2151
+ *
2152
+ * The question somebody has *before* wiring Trazum into CI is not "what did
2153
+ * we spend" but "will this log support the gates I want" — and answering it
2154
+ * with a full report makes them read a bill to find out a field is missing.
2155
+ * This path states readiness per capability and produces no dollar figure
2156
+ * at all, so nothing here can be mistaken for spend. It also refuses to
2157
+ * coexist with the gates: a gate over a report that was never produced
2158
+ * would exit green having judged nothing.
2159
+ */
2160
+ if (boolFlag(args, 'dry-run')) {
2161
+ const gateFlags = ['max-usd', 'max-growth-usd', 'max-cache-loss-usd', 'max-day-usd', 'max-session-usd'];
2162
+ if (gateFlags.some((flag) => typeof args.flags.get(flag) === 'string')) {
2163
+ throw new Error(t.profile.dryRunNoGates());
2164
+ }
2165
+ const cov = report.fieldCoverage;
2166
+ const parsed = cov.parsed;
2167
+ console.log(c.bold(t.profile.dryRunHeading()));
2168
+ console.log(` ${wrap(t.profile.dryRunParsed(n(parsed), n(report.skippedLines.length)), 74, ' ')}`);
2169
+ if (report.unpricedModels.length > 0) {
2170
+ console.log(` ${c.yellow('!')} ${wrap(t.profile.dryRunUnpriced(report.unpricedModels.join(', ')), 74, ' ')}`);
2171
+ }
2172
+ console.log();
2173
+ const can = (ok: boolean, line: string): void => {
2174
+ console.log(` ${ok ? c.green('✓') : c.yellow('✗')} ${wrap(line, 74, ' ')}`);
2175
+ };
2176
+ const share = (count: number): string => (parsed > 0 ? pct(count / parsed) : '0%');
2177
+ can(parsed > 0, t.profile.dryRunTotals());
2178
+ can(cov.label > 0, t.profile.dryRunLabels(share(cov.label)));
2179
+ can(cov.ts > 0, t.profile.dryRunClock(share(cov.ts)));
2180
+ can(cov.session > 0, t.profile.dryRunSessions(share(cov.session)));
2181
+ can(cov.stopReason > 0, t.profile.dryRunStopReason(share(cov.stopReason)));
2182
+ // "No cache traffic" is not a missing field: the split can only exist on
2183
+ // records that wrote, and a log that never wrote has nothing to record.
2184
+ if (cov.cacheWrites > 0) {
2185
+ can(cov.cacheTtl > 0, t.profile.dryRunCacheTtl(n(cov.cacheTtl), n(cov.cacheWrites)));
2186
+ } else {
2187
+ console.log(` ${c.dim('·')} ${wrap(t.profile.dryRunNoCacheTraffic(), 74, ' ')}`);
2188
+ }
2189
+ console.log();
2190
+ console.log(` ${c.dim(wrap(t.profile.dryRunFooter(), 74, ' '))}`);
2191
+ if (parsed === 0) process.exitCode = 1;
2192
+ return;
2193
+ }
2194
+
2149
2195
  /**
2150
2196
  * The previous log, loaded before the output paths split so the growth gate
2151
2197
  * exists under `--json` too — a CI step reads the JSON and trusts the exit
@@ -2266,6 +2312,46 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
2266
2312
  */
2267
2313
  const gateVerdicts: string[] = [];
2268
2314
  let gateFailed = false;
2315
+ /**
2316
+ * Findings as policy. A waiver silences one gate's exit code for a bounded
2317
+ * time, on the record: the failure still prints (waived is shown as
2318
+ * waived, never hidden — the bill still counts it), the reason and the
2319
+ * days left print beside it, and the day the waiver expires the gate
2320
+ * fails again louder, naming the date and the reason somebody wrote.
2321
+ * That expiry is the entire mechanism by which a waiver stays a decision
2322
+ * instead of becoming a habit.
2323
+ */
2324
+ const waiverFor = (gate: string): { entry: { gate: string; reason: string; until: string }; expired: boolean } | null => {
2325
+ const entry = (config.waive ?? []).find((w) => w.gate === gate);
2326
+ if (entry === undefined) return null;
2327
+ // The waiver covers its named day whole: expiry begins the next UTC day.
2328
+ const expiresMs = Date.parse(`${entry.until}T00:00:00Z`) + 86_400_000;
2329
+ return { entry, expired: Date.now() >= expiresMs };
2330
+ };
2331
+ /**
2332
+ * Applies a waiver to one failing gate. Returns true when the failure is
2333
+ * silenced — the caller skips its exitCode — and prints the record either
2334
+ * way, because a waived failure that vanished from the output would be a
2335
+ * finding deleted with extra steps.
2336
+ */
2337
+ const waived = (gate: string): boolean => {
2338
+ const found = waiverFor(gate);
2339
+ if (found === null) return false;
2340
+ if (found.expired) {
2341
+ console.error(
2342
+ c.red(t.profile.waiveExpired(gate, found.entry.until, found.entry.reason)),
2343
+ );
2344
+ return false;
2345
+ }
2346
+ const daysLeft = Math.max(
2347
+ 0,
2348
+ Math.ceil((Date.parse(`${found.entry.until}T00:00:00Z`) + 86_400_000 - Date.now()) / 86_400_000),
2349
+ );
2350
+ console.error(
2351
+ c.yellow(t.profile.waiveActive(gate, found.entry.reason, found.entry.until, String(daysLeft))),
2352
+ );
2353
+ return true;
2354
+ };
2269
2355
  const applyGates = (): void => {
2270
2356
  /**
2271
2357
  * Before any verdict: whether the gated figure is the whole bill. A gate
@@ -2312,7 +2398,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
2312
2398
  }
2313
2399
  if (usd > limit) {
2314
2400
  console.error(c.red(t.profile.labelBudgetFailed(label, formatUsd(usd), formatUsd(limit))));
2315
- process.exitCode = 1;
2401
+ if (!waived(`byLabel:${label}`)) process.exitCode = 1;
2316
2402
  } else {
2317
2403
  console.error(c.dim(t.profile.labelBudgetOk(label, formatUsd(usd), formatUsd(limit))));
2318
2404
  }
@@ -2385,7 +2471,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
2385
2471
  * copy says so.
2386
2472
  */
2387
2473
  explainFailure(report.total.totalUsd - maxUsd);
2388
- process.exitCode = 1;
2474
+ if (!waived('maxUsd')) process.exitCode = 1;
2389
2475
  } else {
2390
2476
  console.error(c.dim(t.profile.maxUsdOk(formatUsd(report.total.totalUsd), formatUsd(maxUsd))));
2391
2477
  explainMargin(report.total.totalUsd, maxUsd);
@@ -2419,10 +2505,13 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
2419
2505
  ),
2420
2506
  ),
2421
2507
  );
2508
+ // Deliberately unwaivable: this failure is "the comparison cannot
2509
+ // be made", and a waiver on unmeasurability would be a decision to
2510
+ // stop measuring — not a budget decision with an end date.
2422
2511
  process.exitCode = 1;
2423
2512
  } else if (againstDelta > maxGrowth) {
2424
2513
  console.error(c.red(t.profile.maxGrowthUsdFailed(formatSignedUsd(againstDelta), formatUsd(maxGrowth))));
2425
- process.exitCode = 1;
2514
+ if (!waived('maxGrowthUsd')) process.exitCode = 1;
2426
2515
  }
2427
2516
  }
2428
2517
  /**
@@ -2440,7 +2529,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
2440
2529
  console.error(
2441
2530
  c.red(t.profile.maxCacheLossFailed(formatUsd(gateCache.deltaUsd), formatUsd(maxLoss))),
2442
2531
  );
2443
- process.exitCode = 1;
2532
+ if (!waived('maxCacheLossUsd')) process.exitCode = 1;
2444
2533
  } else if (gateCache.worstCaseDeltaUsd > maxLoss) {
2445
2534
  console.error(
2446
2535
  c.red(
@@ -2451,7 +2540,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
2451
2540
  ),
2452
2541
  ),
2453
2542
  );
2454
- process.exitCode = 1;
2543
+ if (!waived('maxCacheLossUsd')) process.exitCode = 1;
2455
2544
  } else {
2456
2545
  console.error(
2457
2546
  c.dim(t.profile.maxCacheLossOk(formatUsd(Math.max(0, gateCache.worstCaseDeltaUsd)), formatUsd(maxLoss))),
@@ -2500,7 +2589,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
2500
2589
  c.red(`${t.profile.maxDayFailed(worst.day, formatUsd(worst.usd), formatUsd(maxDay))}${suspect}`),
2501
2590
  );
2502
2591
  explainFailure(worst.usd - maxDay, { namesLargest: true });
2503
- process.exitCode = 1;
2592
+ if (!waived('maxDayUsd')) process.exitCode = 1;
2504
2593
  } else {
2505
2594
  console.error(c.dim(t.profile.maxDayOk(worst.day, formatUsd(worst.usd), formatUsd(maxDay))));
2506
2595
  explainMargin(worst.usd, maxDay);
@@ -2542,7 +2631,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
2542
2631
  c.red(t.profile.maxSessionFailed(formatUsd(report.sessionSpend.maxUsd), formatUsd(maxSession), n(report.sessionSpend.sessions))),
2543
2632
  );
2544
2633
  explainFailure(report.sessionSpend.maxUsd - maxSession);
2545
- process.exitCode = 1;
2634
+ if (!waived('maxSessionUsd')) process.exitCode = 1;
2546
2635
  } else {
2547
2636
  console.error(
2548
2637
  c.dim(t.profile.maxSessionOk(formatUsd(report.sessionSpend.maxUsd), formatUsd(maxSession), n(report.sessionSpend.sessions))),