ruvnet-brain 4.0.1 → 4.0.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.
Files changed (195) hide show
  1. package/.claude-plugin/marketplace.json +1 -0
  2. package/README.md +4 -4
  3. package/bin/install.mjs +303 -24
  4. package/console/CONTRACT.md +172 -0
  5. package/console/activity.js +753 -0
  6. package/console/app.js +4189 -0
  7. package/console/architecture.html +1221 -0
  8. package/console/assets/depth-1.webp +0 -0
  9. package/console/assets/depth-2.webp +0 -0
  10. package/console/assets/depth-3.webp +0 -0
  11. package/console/assets/harness-vs-plain.svg +259 -0
  12. package/console/assets/hero.webp +0 -0
  13. package/console/assets/memory.webp +0 -0
  14. package/console/assets/metaharness.svg +247 -0
  15. package/console/index.html +777 -0
  16. package/console/install-architecture.html +162 -0
  17. package/console/install-mockup.html +543 -0
  18. package/console/style.css +2144 -0
  19. package/console/tips.css +926 -0
  20. package/console/tips.html +858 -0
  21. package/console/tips.js +128 -0
  22. package/docs/RELEASE-NOTES-4.0.md +88 -0
  23. package/kb/model-requirements.mjs +37 -6
  24. package/keys/ruvnet-brain-signing.pub.pem +3 -0
  25. package/package.json +8 -22
  26. package/plugin/.claude-plugin/marketplace.json +1 -0
  27. package/plugin/.claude-plugin/plugin.json +2 -3
  28. package/plugin/.codex-plugin/plugin.json +1 -1
  29. package/plugin/commands/brain-console.md +2 -2
  30. package/plugin/commands/configure.md +3 -2
  31. package/plugin/commands/rvbc.md +4 -3
  32. package/plugin/commands/rvcb.md +2 -2
  33. package/plugin/commands/whats-new.md +6 -6
  34. package/plugin/docs/RELEASE-NOTES-4.0.md +88 -0
  35. package/plugin/hooks/hooks.json +1 -2
  36. package/plugin/mcp/managed-cli-interface.mjs +47 -4
  37. package/plugin/mcp/server.mjs +90 -32
  38. package/plugin/scripts/detach.mjs +14 -0
  39. package/plugin/scripts/first-session-worker.mjs +38 -0
  40. package/plugin/scripts/ground-ruvnet.sh +16 -6
  41. package/plugin/scripts/hook-shim.mjs +34 -29
  42. package/plugin/scripts/learn-capture.sh +22 -3
  43. package/plugin/scripts/learn-flush.mjs +21 -4
  44. package/plugin/scripts/runtime-preferences.mjs +269 -0
  45. package/plugin/scripts/session-start-core.mjs +503 -0
  46. package/plugin/scripts/session-start.sh +3 -858
  47. package/plugin/scripts/whats-new.mjs +42 -0
  48. package/plugin/skills/brain-console/SKILL.md +4 -2
  49. package/plugin/skills/release-proof/SKILL.md +98 -0
  50. package/plugin/skills/release-proof/agents/openai.yaml +4 -0
  51. package/plugin/skills/release-proof/references/receipt-contract.md +44 -0
  52. package/plugin/skills/release-proof/scripts/release-proof.mjs +286 -0
  53. package/plugin/skills/ruvnet-brain/PLAYBOOK.md +5 -1
  54. package/plugin/skills/ruvnet-brain/SKILL.md +22 -7
  55. package/plugin/skills/rvbc/SKILL.md +9 -6
  56. package/plugin/skills/whats-new/SKILL.md +4 -4
  57. package/scripts/adr-backfill.mjs +107 -0
  58. package/scripts/advocacy-outcomes.mjs +808 -0
  59. package/scripts/agentdb-context.mjs +216 -0
  60. package/scripts/agentdb-fleet-doctor.mjs +101 -0
  61. package/scripts/ascii-drift.mjs +236 -0
  62. package/scripts/behavioral-l1-l4.mjs +210 -0
  63. package/scripts/brain-capability-check.mjs +72 -0
  64. package/scripts/brain-grade-groundtruth.mjs +100 -0
  65. package/scripts/brain-latency-50.mjs +227 -0
  66. package/scripts/brain-novice-50.mjs +189 -0
  67. package/scripts/brain-stamp.mjs +94 -0
  68. package/scripts/brain-state.mjs +212 -0
  69. package/scripts/build-bundle.mjs +531 -0
  70. package/scripts/build-concepts.mjs +132 -0
  71. package/scripts/build-l2.mjs +71 -0
  72. package/scripts/build-primer.mjs +73 -0
  73. package/scripts/build-symbols.mjs +68 -0
  74. package/scripts/calibrate-router.mjs +97 -0
  75. package/scripts/capability-audit.mjs +321 -0
  76. package/scripts/capability-registry.mjs +876 -0
  77. package/scripts/check-indexation.mjs +108 -0
  78. package/scripts/check-legibility.mjs +189 -0
  79. package/scripts/ci/build-fixture-kb.mjs +67 -0
  80. package/scripts/ci/learning-replay-codex-adapter.mjs +62 -0
  81. package/scripts/ci/learning-replay-recorder.mjs +59 -0
  82. package/scripts/ci/mutate-hook-timeout.mjs +70 -0
  83. package/scripts/ci/stranger-fixture-stage.mjs +17 -0
  84. package/scripts/ci/stranger-scenario.mjs +228 -0
  85. package/scripts/ci/stranger-timeout.mjs +25 -0
  86. package/scripts/ci-verdict.mjs +29 -0
  87. package/scripts/claims-verify.mjs +710 -0
  88. package/scripts/clear-claude-tmp.sh +31 -0
  89. package/scripts/console-engine.mjs +434 -0
  90. package/scripts/console-engine.test.mjs +125 -0
  91. package/scripts/corpus-qa.mjs +250 -0
  92. package/scripts/correction-detect-embed.mjs +346 -0
  93. package/scripts/correction-detect-measure.mjs +270 -0
  94. package/scripts/correction-detect.mjs +686 -0
  95. package/scripts/count-chunks.mjs +54 -0
  96. package/scripts/described-questions.json +30 -0
  97. package/scripts/design-grade.mjs +58 -0
  98. package/scripts/dev-plugin-link.sh +105 -0
  99. package/scripts/distill-project.mjs +200 -0
  100. package/scripts/doc-currency.mjs +801 -0
  101. package/scripts/eval-brain.mjs +244 -0
  102. package/scripts/fix-metaharness-memretrieve.mjs +121 -0
  103. package/scripts/fix-workstream.mjs +291 -0
  104. package/scripts/full-hints.mjs +87 -0
  105. package/scripts/gate.sh +39 -0
  106. package/scripts/gates.mjs +146 -0
  107. package/scripts/gen-console-images.mjs +54 -0
  108. package/scripts/gen-images.mjs +47 -0
  109. package/scripts/git-clone-refresh.mjs +52 -0
  110. package/scripts/git-hooks/pre-push +126 -0
  111. package/scripts/goal-match.mjs +398 -0
  112. package/scripts/goldie-research.mjs +223 -0
  113. package/scripts/goldie-weekly.sh +67 -0
  114. package/scripts/health-repair.mjs +237 -0
  115. package/scripts/helix-scenario-questions.json +10 -0
  116. package/scripts/ingest-gists.mjs +230 -0
  117. package/scripts/ingest-meeting.mjs +115 -0
  118. package/scripts/ingest-repo.mjs +79 -0
  119. package/scripts/install-npx-witness.sh +49 -0
  120. package/scripts/issue-fix.mjs +558 -0
  121. package/scripts/issue-watch.mjs +276 -0
  122. package/scripts/issue4-close-note.md +31 -0
  123. package/scripts/key-canary.mjs +91 -0
  124. package/scripts/latency-to-surface.mjs +233 -0
  125. package/scripts/learning-enable.mjs +380 -0
  126. package/scripts/learning-replay.mjs +1570 -0
  127. package/scripts/learnings.mjs +62 -0
  128. package/scripts/lesson-gate.mjs +680 -0
  129. package/scripts/lesson-lifecycle.mjs +449 -0
  130. package/scripts/lesson-promote.mjs +262 -0
  131. package/scripts/lesson-ratify.mjs +98 -0
  132. package/scripts/lesson-seed.mjs +252 -0
  133. package/scripts/lesson-store.mjs +447 -0
  134. package/scripts/loop-checkpoint.mjs +86 -0
  135. package/scripts/memdb-health.sh +14 -0
  136. package/scripts/memory-doctor.mjs +326 -0
  137. package/scripts/model-catalog.mjs +79 -0
  138. package/scripts/nightly-controller.mjs +66 -0
  139. package/scripts/nightly-gists.sh +72 -0
  140. package/scripts/nightly-wrapper.sh +172 -0
  141. package/scripts/notify.sh +12 -0
  142. package/scripts/npx-witness.sh +56 -0
  143. package/scripts/onboarding-console.mjs +2922 -0
  144. package/scripts/private-fence.mjs +69 -0
  145. package/scripts/proactivity-metrics.mjs +118 -0
  146. package/scripts/proof-questions.json +56 -0
  147. package/scripts/protected-release-invocation.mjs +76 -0
  148. package/scripts/prove.mjs +95 -0
  149. package/scripts/proxy/claude-proxied.sh +57 -0
  150. package/scripts/proxy/proxy-revert.sh +59 -0
  151. package/scripts/proxy/proxy-up.sh +60 -0
  152. package/scripts/proxy/proxy-verify.mjs +142 -0
  153. package/scripts/publication-receipt.mjs +307 -0
  154. package/scripts/published-surface-probe.mjs +241 -0
  155. package/scripts/qe/card-lane-gate.mjs +162 -0
  156. package/scripts/qe/session-start-gate.mjs +229 -0
  157. package/scripts/qe/ux-suite.mjs +323 -0
  158. package/scripts/reconcile-project.mjs +0 -0
  159. package/scripts/record-lesson.mjs +113 -0
  160. package/scripts/refresh-model-catalog.mjs +99 -0
  161. package/scripts/release-authority.mjs +93 -0
  162. package/scripts/release-proof.mjs +9 -0
  163. package/scripts/release-vector.mjs +281 -0
  164. package/scripts/release.mjs +439 -0
  165. package/scripts/remedy-registry.mjs +247 -0
  166. package/scripts/rerank-cap-eval.mjs +265 -0
  167. package/scripts/rerank-cap-warm-ab.mjs +129 -0
  168. package/scripts/route-cheap.mjs +20 -15
  169. package/scripts/router-utilization.mjs +182 -0
  170. package/scripts/routing-flywheel.mjs +596 -0
  171. package/scripts/rvf-generation.mjs +104 -0
  172. package/scripts/rvf-index-audit.mjs +138 -0
  173. package/scripts/self-update.mjs +296 -0
  174. package/scripts/selfcheck.mjs +7 -1
  175. package/scripts/sign-bundle.mjs +69 -0
  176. package/scripts/signal-watch.mjs +171 -0
  177. package/scripts/stabilization-receipt.mjs +108 -0
  178. package/scripts/stack-sync.mjs +469 -0
  179. package/scripts/stamp-existing-rvf-generations.mjs +53 -0
  180. package/scripts/stamp-sweep.mjs +144 -0
  181. package/scripts/status-honesty.mjs +102 -0
  182. package/scripts/sync-version.mjs +217 -0
  183. package/scripts/token-report.mjs +102 -0
  184. package/scripts/top100-benchmark.mjs +479 -0
  185. package/scripts/top100-corpus.mjs +112 -0
  186. package/scripts/top100-semantic-assertions.mjs +449 -0
  187. package/scripts/update-apply.mjs +9 -0
  188. package/scripts/upgrade-notice.mjs +14 -0
  189. package/scripts/verify-bundle.mjs +51 -0
  190. package/scripts/verify-channels.mjs +184 -0
  191. package/scripts/verify-model-catalog.mjs +104 -0
  192. package/scripts/verify-nightly-close-issue4.sh +31 -0
  193. package/scripts/version.mjs +40 -0
  194. package/scripts/wired-check.mjs +867 -0
  195. package/plugin/scripts/finalize-token-meter.mjs +0 -25
@@ -27,6 +27,7 @@ import path from 'node:path';
27
27
  import os from 'node:os';
28
28
  import { spawnSync } from 'node:child_process';
29
29
  import { fileURLToPath, pathToFileURL } from 'node:url';
30
+ import { loadRuntimePreferences, runtimeChildEnv } from '../plugin/scripts/runtime-preferences.mjs';
30
31
 
31
32
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
32
33
 
@@ -90,20 +91,15 @@ export function receiptLine(model, costs) {
90
91
  return `\x1b[2m⚡ MetaHarness: routed to ${model} — ~${pct}% cheaper (est. ${fmt$(costs.cost)} vs ${fmt$(costs.frontier)} ${ref}, saved ~${fmt$(costs.saved)})\x1b[0m`;
91
92
  }
92
93
 
93
- // Load OPENROUTER_API_KEY from ruvnet-brain/.env if not already in env. Value never printed/logged.
94
- function ensureOpenRouterKey() {
95
- if (process.env.OPENROUTER_API_KEY) return true;
94
+ function agenticFlowExecutable(env) {
95
+ const home = env.HOME || os.homedir();
96
+ const global = path.join(home, '.npm-global', 'bin', process.platform === 'win32' ? 'agentic-flow.cmd' : 'agentic-flow');
96
97
  try {
97
- const envFile = fs.readFileSync(path.join(__dirname, '..', '.env'), 'utf8');
98
- const m = envFile.match(/^OPENROUTER_API_KEY=(.+)$/m);
99
- if (m && m[1].trim()) {
100
- process.env.OPENROUTER_API_KEY = m[1].trim().replace(/^["']|["']$/g, '');
101
- return true;
102
- }
98
+ fs.accessSync(global, fs.constants.X_OK);
99
+ return global;
103
100
  } catch {
104
- /* no .env — fall through */
101
+ return 'agentic-flow';
105
102
  }
106
- return false;
107
103
  }
108
104
 
109
105
  function parseArgs(argv) {
@@ -125,16 +121,25 @@ function main() {
125
121
  console.error(`Unknown model "${args.model}" — no verified pricing, refusing to invent savings. Known: ${Object.keys(PRICING).join(', ')}`);
126
122
  process.exit(2);
127
123
  }
128
- if (!ensureOpenRouterKey()) {
129
- console.error('OPENROUTER_API_KEY not found in env or ruvnet-brain/.env — cannot route. (Key value is never printed.)');
124
+ const policy = loadRuntimePreferences();
125
+ if (policy.values.routing !== 'auto') {
126
+ console.error(policy.values.routing === 'off'
127
+ ? 'Token-smart routing is off in RuvNet Brain Console — nothing was dispatched.'
128
+ : 'Token-smart routing has not been enabled in RuvNet Brain Console — nothing was dispatched.');
129
+ process.exit(1);
130
+ }
131
+ const childEnv = runtimeChildEnv();
132
+ if (!childEnv.OPENROUTER_API_KEY) {
133
+ console.error('OPENROUTER_API_KEY is not configured in the environment or encrypted Brain credential store — cannot route. (Key value is never printed.)');
130
134
  process.exit(1);
131
135
  }
132
136
 
133
137
  const started = Date.now();
134
- const run = spawnSync('npx', ['agentic-flow@latest', '--agent', args.agent, '--model', args.model, '--task', args.task], {
138
+ const run = spawnSync(agenticFlowExecutable(childEnv), ['--agent', args.agent, '--model', args.model, '--task', args.task], {
135
139
  encoding: 'utf8',
136
140
  timeout: 180_000,
137
- env: process.env,
141
+ env: childEnv,
142
+ shell: false,
138
143
  });
139
144
 
140
145
  if (run.error || run.status !== 0) {
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env node
2
+ // scripts/router-utilization.mjs — the ONGOING view. router-optimizer.mjs answers "what SHOULD each
3
+ // bucket route to"; this answers "once you've actually been using it, how many tasks landed in each
4
+ // bucket, and what did you save versus sending every one to the frontier model?"
5
+ //
6
+ // It implements the shape rUv specifies in ruflo ADR-149 §6 (Observability, Proposed):
7
+ // • modelDistribution — per-model/per-band task counts ("how many are going to each bucket")
8
+ // • costOptimalitySaved — USD vs always paying the frontier ("what the savings are off the frontier")
9
+ //
10
+ // Pure/deterministic: it reads the real receipts ledger (route-cheap.mjs + subagent dispatch write it)
11
+ // and makes NO model or network call. Every number is grounded:
12
+ // • realized cost = the receipt's own recorded est_cost (what the routed model actually cost).
13
+ // • frontier baseline = RECOMPUTED for every receipt against the CURRENT frontier model
14
+ // (route-cheap FRONTIER — now Fable 5) from that receipt's own est token counts. Recomputing (rather
15
+ // than trusting each receipt's stored est_frontier_cost) means switching the frontier re-prices the
16
+ // whole history consistently, so the "vs Fable 5" figure is never a stale Opus-era number.
17
+ // • a receipt whose model has no verified price is counted under `unpriced` and left OUT of the $ math
18
+ // — never assigned an invented cost.
19
+ //
20
+ // Usage: node scripts/router-utilization.mjs [--json]
21
+
22
+ import fs from 'node:fs';
23
+ import { FRONTIER, priceOf, receiptsPath } from './route-cheap.mjs';
24
+
25
+ // Band = a human-legible grouping over the continuous complexity/cost axis (ADR-149: "tier_label is
26
+ // metadata, not control flow"). The four bands mirror router-optimizer.mjs. Known models are mapped
27
+ // explicitly; anything unknown is bucketed by its blended $/Mtok via bandOf()'s fallback.
28
+ const BAND_BY_MODEL = {
29
+ 'agent-booster': 'mechanical',
30
+ 'inclusionai/ling-2.6-flash': 'cheap',
31
+ 'claude-haiku-4.5': 'cheap',
32
+ 'deepseek/deepseek-chat': 'cheap',
33
+ 'deepseek/deepseek-v4-flash': 'cheap',
34
+ 'meta-llama/llama-3.3-70b-instruct': 'mid',
35
+ 'openai/gpt-4.1': 'mid',
36
+ 'x-ai/grok-4.5': 'mid',
37
+ 'claude-sonnet-5': 'mid',
38
+ 'claude-opus-4.8': 'frontier',
39
+ 'claude-fable-5': 'frontier',
40
+ };
41
+ export const BAND_ORDER = ['mechanical', 'cheap', 'mid', 'frontier'];
42
+ const BAND_LABEL = { mechanical: 'Mechanical', cheap: 'Cheap', mid: 'Mid', frontier: 'Frontier' };
43
+
44
+ /** Which band did this model's task land in? Known → explicit map; unknown → blended-price fallback. */
45
+ export function bandOf(model) {
46
+ if (BAND_BY_MODEL[model]) return BAND_BY_MODEL[model];
47
+ const p = priceOf(model);
48
+ if (!p) return null; // unpriced → caller records it under `unpriced`, never invents a cost
49
+ const blended = (p.in + p.out) / 2;
50
+ if (blended === 0) return 'mechanical';
51
+ if (blended < 2) return 'cheap';
52
+ if (blended < 15) return 'mid';
53
+ return 'frontier';
54
+ }
55
+
56
+ function tokensOf(r) {
57
+ const i = Number(r.est_in_tokens);
58
+ const o = Number(r.est_out_tokens);
59
+ if (Number.isFinite(i) && Number.isFinite(o)) return { in: i, out: o };
60
+ return null;
61
+ }
62
+
63
+ /**
64
+ * Summarize the receipts ledger into a per-band distribution + realized-vs-frontier savings.
65
+ * @param {{ receiptsFile?: string }} [opts]
66
+ */
67
+ export function utilization({ receiptsFile, frontier } = {}) {
68
+ const file = receiptsFile || receiptsPath();
69
+ // Frontier = the user's OWN house flagship (passed in by the console from the live-verified catalog);
70
+ // falls back to route-cheap's default. The counterfactual is recomputed vs THIS, so an OpenAI shop sees
71
+ // "vs GPT-5.6 Sol" and a Claude shop sees "vs Fable 5" — personalized, never one house for everyone.
72
+ const frModel = frontier?.model || FRONTIER.name;
73
+ const fr = (frontier && Number.isFinite(frontier.in) && Number.isFinite(frontier.out))
74
+ ? { in: frontier.in, out: frontier.out }
75
+ : priceOf(FRONTIER.name);
76
+
77
+ const bands = Object.fromEntries(
78
+ BAND_ORDER.map((b) => [b, { band: b, label: BAND_LABEL[b], tasks: 0, realizedUsd: 0, frontierUsd: 0, models: {} }])
79
+ );
80
+ let tasks = 0;
81
+ let unpriced = 0;
82
+ let realizedUsd = 0;
83
+ let frontierUsd = 0;
84
+ let since = null;
85
+ let until = null;
86
+
87
+ let lines = [];
88
+ try {
89
+ lines = fs.readFileSync(file, 'utf8').split('\n');
90
+ } catch {
91
+ /* no receipts yet — return the empty shape below */
92
+ }
93
+
94
+ for (const line of lines) {
95
+ if (!line.trim()) continue;
96
+ let r;
97
+ try { r = JSON.parse(line); } catch { continue; }
98
+ if (!r.model) continue;
99
+ tasks++;
100
+ if (r.ts) {
101
+ since = since && since < r.ts ? since : r.ts;
102
+ until = until && until > r.ts ? until : r.ts;
103
+ }
104
+ const band = bandOf(r.model);
105
+ const p = priceOf(r.model);
106
+ const isMech = band === 'mechanical';
107
+ // Mechanical ($0, no LLM — e.g. Agent Booster) has no per-token price but a real realized cost of $0.
108
+ // Everything else needs a verified price AND a priced frontier, or it's excluded (never invented).
109
+ if (!band || (!isMech && (!p || !fr))) { unpriced++; continue; }
110
+
111
+ const tok = tokensOf(r);
112
+ const realized = isMech ? 0
113
+ : Number.isFinite(Number(r.est_cost)) ? Number(r.est_cost)
114
+ : tok ? (tok.in * p.in + tok.out * p.out) / 1e6 : 0;
115
+ // Frontier counterfactual — ALWAYS recomputed vs the current frontier from the receipt's tokens.
116
+ const front = (tok && fr) ? (tok.in * fr.in + tok.out * fr.out) / 1e6 : realized;
117
+
118
+ const b = bands[band];
119
+ b.tasks++;
120
+ b.realizedUsd += realized;
121
+ b.frontierUsd += front;
122
+ b.models[r.model] = (b.models[r.model] || 0) + 1;
123
+ realizedUsd += realized;
124
+ frontierUsd += front;
125
+ }
126
+
127
+ const round = (n) => +Number(n).toFixed(4);
128
+ const distribution = BAND_ORDER.map((b) => {
129
+ const x = bands[b];
130
+ return {
131
+ band: b,
132
+ label: x.label,
133
+ tasks: x.tasks,
134
+ pctOfTasks: tasks ? Math.round((x.tasks / tasks) * 100) : 0,
135
+ realizedUsd: round(x.realizedUsd),
136
+ frontierUsd: round(x.frontierUsd),
137
+ savedUsd: round(x.frontierUsd - x.realizedUsd),
138
+ models: Object.entries(x.models)
139
+ .sort((a, c) => c[1] - a[1])
140
+ .map(([model, n]) => ({ model, tasks: n })),
141
+ };
142
+ });
143
+
144
+ const savedUsd = round(frontierUsd - realizedUsd);
145
+ return {
146
+ generatedAt: new Date().toISOString(),
147
+ frontierModel: frModel,
148
+ tasks,
149
+ unpriced,
150
+ since,
151
+ until,
152
+ realizedUsd: round(realizedUsd),
153
+ frontierUsd: round(frontierUsd),
154
+ costOptimalitySaved: savedUsd, // ADR-149 name
155
+ pctSaved: frontierUsd > 0 ? Math.round((savedUsd / frontierUsd) * 100) : null,
156
+ distribution, // ADR-149 modelDistribution, grouped into the four legible bands
157
+ note: `Recomputed live from ${tasks} receipt(s) against your frontier (${frModel}); token counts are the receipts’ own est. values, never projected.`,
158
+ };
159
+ }
160
+
161
+ export function printUtil(u) {
162
+ const money = (v) => (v == null ? '—' : v === 0 ? '$0' : '$' + v);
163
+ console.log(`\nRouting utilization — ${u.tasks} task(s), frontier = ${u.frontierModel}`);
164
+ if (u.since) console.log(` window: ${u.since} → ${u.until}`);
165
+ console.log(` ${'band'.padEnd(11)} ${'tasks'.padStart(6)} ${'%'.padStart(4)} ${'realized'.padStart(11)} ${'vs frontier'.padStart(12)} ${'saved'.padStart(11)}`);
166
+ for (const d of u.distribution) {
167
+ console.log(` ${d.label.padEnd(11)} ${String(d.tasks).padStart(6)} ${String(d.pctOfTasks).padStart(3)}% ${money(d.realizedUsd).padStart(11)} ${money(d.frontierUsd).padStart(12)} ${money(d.savedUsd).padStart(11)}`);
168
+ }
169
+ console.log(` ${'—'.repeat(11)}`);
170
+ console.log(` TOTAL saved vs all-frontier: ${money(u.costOptimalitySaved)} (${u.pctSaved == null ? '—' : u.pctSaved + '%'}); realized ${money(u.realizedUsd)} vs ${money(u.frontierUsd)} on ${u.frontierModel}.`);
171
+ if (u.unpriced) console.log(` (${u.unpriced} receipt(s) had an unpriced model and were excluded from the $ math.)`);
172
+ }
173
+
174
+ export function mainUtil() {
175
+ const u = utilization();
176
+ if (process.argv.includes('--json')) console.log(JSON.stringify(u, null, 2));
177
+ else printUtil(u);
178
+ }
179
+
180
+ import { fileURLToPath } from 'node:url';
181
+ import path from 'node:path';
182
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) mainUtil();