ruvnet-brain 2.1.0 → 2.4.3

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.
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env node
2
+ // scripts/model-router-outcome.mjs — the LABELED-DATA pipeline for a future learned routing policy.
3
+ //
4
+ // WHY THIS EXISTS: both ADR-040/DRACO (verified via search_ruvnet: a hand-built self-signal threshold
5
+ // routed WORSE than always-cheapest; a learned map from a real feature beat the best fixed model) and
6
+ // ruflo's own ADR-149 point the same direction — learned routing beats hand-tuned routing, but ONLY
7
+ // once there are labeled outcomes to learn from. model-router-engine.mjs's placeholder policy
8
+ // (policy.default.mjs) is a 3-tier heuristic precisely because there is no such label set yet. This
9
+ // file is where those labels accumulate, one JSON line at a time, so that placeholder can eventually
10
+ // be replaced by something trained on real outcomes instead of a guess.
11
+ //
12
+ // A label is not just "did the task succeed" — an OVERRIDE (an operator escalating past whatever the
13
+ // engine picked) is itself a label: it says the engine's choice was wrong for that prompt shape, even
14
+ // if no downstream failure was ever observed. Log overrides the same way you log pass/fail outcomes;
15
+ // don't wait for a hard failure to record that the router under- or over-shot.
16
+ //
17
+ // Usage:
18
+ // node model-router-outcome.mjs --model <id> --success true|false [--note "..."] [--task-hash <h>]
19
+ // node model-router-outcome.mjs --summary
20
+ //
21
+ // Storage: ~/.claude/metaharness/routing-outcomes.jsonl (append-only; sibling to routing-decisions.jsonl
22
+ // and routing-receipts.jsonl — decisions record what was CHOSEN, outcomes record what HAPPENED).
23
+
24
+ import fs from 'node:fs';
25
+ import path from 'node:path';
26
+ import os from 'node:os';
27
+ import { fileURLToPath, pathToFileURL } from 'node:url';
28
+
29
+ const OUTCOMES_LOG =
30
+ process.env.MODEL_ROUTER_OUTCOMES ||
31
+ path.join(os.homedir(), '.claude', 'metaharness', 'routing-outcomes.jsonl');
32
+
33
+ function parseArgs(argv) {
34
+ const a = { model: null, success: null, note: null, taskHash: null, summary: false };
35
+ for (let i = 0; i < argv.length; i++) {
36
+ const k = argv[i];
37
+ if (k === '--model') a.model = argv[++i];
38
+ else if (k === '--success') a.success = argv[++i];
39
+ else if (k === '--note') a.note = argv[++i];
40
+ else if (k === '--task-hash') a.taskHash = argv[++i];
41
+ else if (k === '--summary') a.summary = true;
42
+ else if (k === '--help' || k === '-h') a.help = true;
43
+ }
44
+ return a;
45
+ }
46
+
47
+ function appendOutcome({ model, success, note, taskHash }) {
48
+ if (!model) throw new Error('--model is required');
49
+ if (success !== 'true' && success !== 'false') throw new Error('--success must be "true" or "false"');
50
+ const entry = { ts: new Date().toISOString(), model, success: success === 'true' };
51
+ if (note) entry.note = note;
52
+ if (taskHash) entry.taskHash = taskHash;
53
+ fs.mkdirSync(path.dirname(OUTCOMES_LOG), { recursive: true });
54
+ fs.appendFileSync(OUTCOMES_LOG, JSON.stringify(entry) + '\n');
55
+ return entry;
56
+ }
57
+
58
+ function summarize() {
59
+ let lines;
60
+ try {
61
+ lines = fs.readFileSync(OUTCOMES_LOG, 'utf8').trim().split('\n').filter(Boolean);
62
+ } catch {
63
+ return { total: 0, byModel: {} };
64
+ }
65
+ const byModel = {};
66
+ for (const l of lines) {
67
+ let d;
68
+ try { d = JSON.parse(l); } catch { continue; }
69
+ const m = byModel[d.model] || (byModel[d.model] = { total: 0, successes: 0, failures: 0 });
70
+ m.total++;
71
+ if (d.success) m.successes++; else m.failures++;
72
+ }
73
+ return { total: lines.length, byModel };
74
+ }
75
+
76
+ function main() {
77
+ const args = parseArgs(process.argv.slice(2));
78
+ if (args.help) {
79
+ process.stdout.write(fs.readFileSync(fileURLToPath(import.meta.url), 'utf8').split('\n').slice(1, 24).join('\n') + '\n');
80
+ return;
81
+ }
82
+
83
+ if (args.summary) {
84
+ const { total, byModel } = summarize();
85
+ console.log(`Routing outcomes: ${total} labeled (${OUTCOMES_LOG.replace(os.homedir(), '~')})`);
86
+ for (const [model, s] of Object.entries(byModel)) {
87
+ console.log(` ${model.padEnd(32)} total:${s.total} successes:${s.successes} failures:${s.failures}`);
88
+ }
89
+ return;
90
+ }
91
+
92
+ const entry = appendOutcome(args);
93
+ console.log(`[model-router-outcome] logged: ${JSON.stringify(entry)}`);
94
+ }
95
+
96
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
97
+ try { main(); } catch (e) { process.stderr.write(`model-router-outcome: ${e.message}\n`); process.exit(1); }
98
+ }
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env node
2
+ // model-router-setup.mjs — build THIS user's subscription profile: detect, ASK, verify, record.
3
+ //
4
+ // Stuart's mandate (2026-07-12): "Just because you verified it for me doesn't mean other people
5
+ // have the same setup. For this thing to be useful it needs to be aware of what THEY have — ask
6
+ // 'Do you have a Claude account? A ChatGPT/Codex account?', verify, and record it, so routing
7
+ // sends work to the right place." The router's whole $0-floor logic is only as true as this
8
+ // profile: a catalog that assumes a subscription the user doesn't have quietly bills them, and
9
+ // one that misses a subscription they DO have quietly wastes it.
10
+ //
11
+ // Three layers of truth, recorded with their basis so nobody over-trusts a guess:
12
+ // verified — machine-checkable proof (e.g. ~/.codex/auth.json carries ChatGPT OAuth tokens)
13
+ // user-attested — the user answered the question (plans aren't reliably probeable)
14
+ // assumed — --detect-only defaults, explicitly labeled as assumptions
15
+ //
16
+ // Output: ~/.claude/model-router/profile.json — consumed by model-router-engine.mjs, which strips
17
+ // subscription/harness claims the profile doesn't back. No profile file = catalog taken as-is
18
+ // (grandfathers pre-profile installs; the session-start hook nudges once to create one).
19
+ //
20
+ // Usage:
21
+ // node model-router-setup.mjs # interactive: detect, ask, verify, record
22
+ // node model-router-setup.mjs --detect-only # non-interactive (installer): record detections + labeled assumptions
23
+ // node model-router-setup.mjs --show # print the current profile
24
+
25
+ import fs from 'node:fs';
26
+ import path from 'node:path';
27
+ import os from 'node:os';
28
+ import { execFileSync } from 'node:child_process';
29
+ import readline from 'node:readline/promises';
30
+
31
+ const CONFIG_DIR = path.join(os.homedir(), '.claude', 'model-router');
32
+ export const PROFILE_PATH = process.env.MODEL_ROUTER_PROFILE || path.join(CONFIG_DIR, 'profile.json');
33
+ const TODAY = new Date().toISOString().slice(0, 10);
34
+
35
+ const has = (cmd) => { try { execFileSync('which', [cmd], { stdio: 'ignore' }); return true; } catch { return false; } };
36
+
37
+ // The one subscription state we can PROVE from disk: Codex's auth mode. OAuth tokens = signed in
38
+ // with ChatGPT (Plus/Pro/Business all include Codex → subscription-covered). An API key = metered.
39
+ // Shape only — never the secret values.
40
+ function detectCodex() {
41
+ const cli = has('codex');
42
+ try {
43
+ const a = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.codex', 'auth.json'), 'utf8'));
44
+ if (a.tokens) return { cli, auth: 'chatgpt-subscription' };
45
+ if (a.OPENAI_API_KEY) return { cli, auth: 'api-key-metered' };
46
+ } catch { /* not authed */ }
47
+ return { cli, auth: null };
48
+ }
49
+
50
+ function detectKeys() {
51
+ const names = ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'OPENROUTER_API_KEY', 'GOOGLE_API_KEY', 'GEMINI_API_KEY', 'XAI_API_KEY'];
52
+ return Object.fromEntries(names.map((n) => [n, !!process.env[n]]));
53
+ }
54
+
55
+ function buildDetected() {
56
+ const codex = detectCodex();
57
+ return {
58
+ updated: TODAY,
59
+ harnesses: {
60
+ // Claude plan tier (Pro vs Max vs API-billed) is not reliably probeable from disk, so
61
+ // detect-only ASSUMES subscription=true when the CLI is present: right-sizing WITHIN
62
+ // Anthropic (haiku over fable) is correct under every Claude billing mode, whereas a false
63
+ // "no subscription" would push claude-code work to BILLED OpenRouter — the worse error.
64
+ 'claude-code': has('claude')
65
+ ? { available: true, subscription: true, plan: 'unknown', basis: `assumed: claude CLI present (${TODAY}); run setup to attest your plan` }
66
+ : { available: false, subscription: false, plan: null, basis: `detected: no claude CLI (${TODAY})` },
67
+ codex: codex.auth === 'chatgpt-subscription'
68
+ ? { available: true, subscription: true, plan: 'chatgpt', basis: `verified: ~/.codex/auth.json ChatGPT OAuth tokens (${TODAY})` }
69
+ : codex.auth === 'api-key-metered'
70
+ ? { available: true, subscription: false, plan: 'api-key', basis: `verified: ~/.codex/auth.json API key — METERED (${TODAY})` }
71
+ : { available: codex.cli, subscription: false, plan: null, basis: `detected: codex ${codex.cli ? 'present, not authed' : 'not installed'} (${TODAY})` },
72
+ },
73
+ keys: detectKeys(),
74
+ };
75
+ }
76
+
77
+ function save(profile) {
78
+ fs.mkdirSync(path.dirname(PROFILE_PATH), { recursive: true });
79
+ fs.writeFileSync(PROFILE_PATH, JSON.stringify(profile, null, 2) + '\n');
80
+ }
81
+
82
+ function show(profile) {
83
+ console.log(`Model-router subscription profile (${PROFILE_PATH.replace(os.homedir(), '~')}):`);
84
+ for (const [h, p] of Object.entries(profile.harnesses)) {
85
+ console.log(` ${h.padEnd(12)} ${p.available ? 'available' : 'not available'}${p.available ? `, subscription: ${p.subscription ? 'YES ($0 marginal)' : 'NO (metered/absent)'}` : ''}`);
86
+ console.log(` ${''.padEnd(12)} basis: ${p.basis}`);
87
+ }
88
+ const keys = Object.entries(profile.keys || {}).filter(([, v]) => v).map(([k]) => k.replace('_API_KEY', ''));
89
+ console.log(` API keys present at setup: ${keys.join(', ') || 'none'}`);
90
+ }
91
+
92
+ async function interactive() {
93
+ const detected = buildDetected();
94
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
95
+ const ask = async (q, def) => {
96
+ const a = (await rl.question(`${q} [${def ? 'Y/n' : 'y/N'}] `)).trim().toLowerCase();
97
+ return a === '' ? def : a.startsWith('y');
98
+ };
99
+
100
+ console.log('Setting up YOUR subscription profile — this decides where the router sends work for $0.\n');
101
+
102
+ const cc = detected.harnesses['claude-code'];
103
+ if (cc.available) {
104
+ const sub = await ask('Do you have a Claude subscription (Pro or Max) that covers your Claude Code usage?', cc.subscription);
105
+ detected.harnesses['claude-code'] = { available: true, subscription: sub, plan: sub ? 'pro-or-max' : 'api-billed', basis: `user-attested ${TODAY}` };
106
+ }
107
+
108
+ const cx = detected.harnesses.codex;
109
+ if (cx.plan === 'chatgpt') {
110
+ console.log(`\nCodex: VERIFIED signed in with ChatGPT — your ChatGPT plan covers it ($0 marginal). Nothing to ask.`);
111
+ } else if (cx.available) {
112
+ const sub = await ask('\nDo you have a ChatGPT subscription (Plus/Pro/Business) you use to sign into the Codex CLI?', false);
113
+ if (sub) console.log(' Note: run `codex login` and sign in with ChatGPT — until then Codex runs are metered or unavailable.');
114
+ detected.harnesses.codex = { ...cx, subscription: cx.plan === 'chatgpt' ? true : false, basis: `user-attested ${TODAY} (subscription only counts once codex login shows ChatGPT auth)` };
115
+ }
116
+
117
+ rl.close();
118
+ save(detected);
119
+ console.log('');
120
+ show(detected);
121
+ console.log('\nSaved. The router now uses THIS profile for every routing decision.');
122
+ }
123
+
124
+ async function main() {
125
+ if (process.argv.includes('--show')) {
126
+ try { show(JSON.parse(fs.readFileSync(PROFILE_PATH, 'utf8'))); } catch { console.log(`No profile at ${PROFILE_PATH.replace(os.homedir(), '~')} — run: node scripts/model-router-setup.mjs`); process.exit(1); }
127
+ return;
128
+ }
129
+ if (process.argv.includes('--detect-only')) {
130
+ const p = buildDetected();
131
+ save(p);
132
+ show(p);
133
+ return;
134
+ }
135
+ await interactive();
136
+ }
137
+
138
+ const { pathToFileURL } = await import('node:url');
139
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
140
+ main().catch((e) => { console.error(`model-router-setup: ${e.message}`); process.exit(1); });
141
+ }
@@ -0,0 +1,160 @@
1
+ #!/usr/bin/env node
2
+ // scripts/model-router-status.mjs — "what is MetaHarness's current plan?", answered plainly.
3
+ //
4
+ // Shows, for each harness (claude-code, codex): the live catalog, which policy is active, what
5
+ // it would actually pick for a cheap/mid/frontier-shaped prompt right now (by calling the REAL
6
+ // engine CLI — this never reimplements selection logic, so it can't drift from what actually runs),
7
+ // which candidates are landscape-only (known, not yet wired for execution), and catalog freshness.
8
+ //
9
+ // Usage: node scripts/model-router-status.mjs [--json]
10
+
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+ import os from 'node:os';
14
+ import { execFileSync } from 'node:child_process';
15
+ import { fileURLToPath, pathToFileURL } from 'node:url';
16
+ import { loadCatalog, loadPolicy, loadProfile, applyProfile, CONFIG_DIR } from './model-router-engine.mjs';
17
+
18
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
19
+ const ENGINE = path.join(__dirname, 'model-router-engine.mjs');
20
+
21
+ const SAMPLE_PROMPTS = {
22
+ cheap: 'Fix this typo in the README.',
23
+ mid: 'Add a new API endpoint that validates input and returns paginated results.',
24
+ frontier: 'Refactor this auth module to fix a security vulnerability and prove correctness under concurrent access.',
25
+ };
26
+
27
+ function sampleDecision(harness, prompt) {
28
+ try {
29
+ const out = execFileSync('node', [ENGINE, '--harness', harness, '--prompt', prompt, '--json'], {
30
+ encoding: 'utf8',
31
+ env: { ...process.env, MODEL_ROUTER_DECISIONS: path.join(os.tmpdir(), 'model-router-status-preview.jsonl') },
32
+ });
33
+ return JSON.parse(out);
34
+ } catch (e) {
35
+ return { model: null, tier: null, reason: `engine call failed: ${e.message}` };
36
+ }
37
+ }
38
+
39
+ // HOW Codex is paid for, read from ~/.codex/auth.json's SHAPE (never its secrets): OAuth tokens =
40
+ // "Sign in with ChatGPT" — usage bills the ChatGPT plan (Plus/Pro/Business all include Codex), so
41
+ // marginal cost is $0 and the catalog's subscription:["codex"] entries are correct. An OPENAI_API_KEY
42
+ // instead = metered per-token billing — flip those subscription fields to [] or the $0-floor lies.
43
+ // Verified live on this machine 2026-07-12: tokens present, no API key -> chatgpt-subscription.
44
+ function codexAuthMode() {
45
+ try {
46
+ const a = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.codex', 'auth.json'), 'utf8'));
47
+ if (a.tokens) return 'chatgpt-subscription';
48
+ if (a.OPENAI_API_KEY) return 'api-key-metered';
49
+ } catch { /* absent or unreadable — not authed */ }
50
+ return null;
51
+ }
52
+
53
+ function detectInventory() {
54
+ const has = (cmd) => {
55
+ try { execFileSync('which', [cmd], { stdio: 'ignore' }); return true; } catch { return false; }
56
+ };
57
+ const keyPresent = (name) => !!process.env[name];
58
+ return {
59
+ claudeCli: has('claude'),
60
+ codexCli: has('codex'),
61
+ codexAuthed: fs.existsSync(path.join(os.homedir(), '.codex', 'auth.json')),
62
+ codexAuthMode: codexAuthMode(),
63
+ keys: {
64
+ ANTHROPIC_API_KEY: keyPresent('ANTHROPIC_API_KEY'),
65
+ OPENAI_API_KEY: keyPresent('OPENAI_API_KEY'),
66
+ GOOGLE_API_KEY: keyPresent('GOOGLE_API_KEY') || keyPresent('GEMINI_API_KEY'),
67
+ XAI_API_KEY: keyPresent('XAI_API_KEY'),
68
+ OPENROUTER_API_KEY: keyPresent('OPENROUTER_API_KEY'),
69
+ },
70
+ };
71
+ }
72
+
73
+ async function main() {
74
+ const jsonMode = process.argv.includes('--json');
75
+ // Profile-aware, same as the engine: what this tool displays must be what routing actually does.
76
+ const candidates = applyProfile(loadCatalog(), loadProfile());
77
+ const policy = await loadPolicy();
78
+ const catalogRaw = JSON.parse(fs.readFileSync(path.join(CONFIG_DIR, 'catalog.json'), 'utf8'));
79
+ const inventory = detectInventory();
80
+
81
+ const selectable = candidates.filter((c) => Array.isArray(c.harness) && c.harness.length > 0);
82
+ const landscapeOnly = candidates.filter((c) => !Array.isArray(c.harness) || c.harness.length === 0);
83
+
84
+ const plan = {};
85
+ for (const harness of ['claude-code', 'codex']) {
86
+ plan[harness] = {};
87
+ for (const [tierLabel, prompt] of Object.entries(SAMPLE_PROMPTS)) {
88
+ const d = sampleDecision(harness, prompt);
89
+ plan[harness][tierLabel] = { model: d.model, actualTier: d.tier, cost: d.est_input_cost_usd, reason: d.reason };
90
+ }
91
+ }
92
+
93
+ const result = {
94
+ catalogUpdated: catalogRaw.updated,
95
+ activePolicy: policy ? policy.source.replace(os.homedir(), '~') : 'NONE — falling back to cheapest priced candidate (no policy found)',
96
+ inventory,
97
+ plan,
98
+ selectableCandidateCount: selectable.length,
99
+ landscapeOnlyCandidates: landscapeOnly.map((c) => ({ id: c.id, provider: c.provider, note: c.note })),
100
+ };
101
+
102
+ if (jsonMode) {
103
+ console.log(JSON.stringify(result, null, 2));
104
+ return;
105
+ }
106
+
107
+ // ── The plain-language answer first (Stuart, 2026-07-12: "show them what the current
108
+ // recommended path is — the zero-cost options, and what it uses when it must go out for API
109
+ // calls"). Everything below it is the detailed evidence; this block is the one a person reads.
110
+ console.log('Recommended path (zero-cost first, always):');
111
+ console.log(' $0 — covered by YOUR subscriptions:');
112
+ for (const harness of ['claude-code', 'codex']) {
113
+ const p = plan[harness];
114
+ const zero = ['cheap', 'mid', 'frontier']
115
+ .map((t) => ({ t, m: p[t].model, free: p[t].cost == null && p[t].model }))
116
+ .filter((x) => x.free);
117
+ if (zero.length) {
118
+ const parts = zero.map((x) => `${x.t}→${x.m}`);
119
+ console.log(` ${harness.padEnd(12)} ${parts.join(' · ')}`);
120
+ } else {
121
+ console.log(` ${harness.padEnd(12)} (no subscription-covered models for this user — see profile)`);
122
+ }
123
+ }
124
+ const billed = selectable
125
+ .filter((c) => c.costPerMTok && typeof c.costPerMTok.out === 'number' && !(c.subscription || []).length)
126
+ .sort((a, b) => a.costPerMTok.out - b.costPerMTok.out)
127
+ .slice(0, 3);
128
+ console.log(' Paid API — only when work must leave the subscriptions (parallel fleets, background batch):');
129
+ for (const c of billed) {
130
+ console.log(` ${c.id.padEnd(28)} $${c.costPerMTok.in}/M in · $${c.costPerMTok.out}/M out (${c.tier}, verified ${String(c.verified || '').slice(0, 10)})`);
131
+ }
132
+ console.log('');
133
+ console.log(`MetaHarness Router — current plan`);
134
+ console.log(`Catalog last updated: ${result.catalogUpdated}`);
135
+ console.log(`Active policy: ${result.activePolicy}`);
136
+ console.log('');
137
+ console.log('Detected on this machine:');
138
+ const codexPay = inventory.codexAuthMode === 'chatgpt-subscription' ? 'authed via ChatGPT subscription ($0 marginal)'
139
+ : inventory.codexAuthMode === 'api-key-metered' ? 'authed via API key (METERED — subscription fields in catalog.json should be [])'
140
+ : inventory.codexAuthed ? 'authed (mode unknown)' : 'not authed';
141
+ console.log(` claude CLI: ${inventory.claudeCli ? 'present' : 'not found'} codex CLI: ${inventory.codexCli ? `present, ${codexPay}` : 'not found'}`);
142
+ const keyList = Object.entries(inventory.keys).filter(([, v]) => v).map(([k]) => k.replace('_API_KEY', ''));
143
+ console.log(` API keys present: ${keyList.length ? keyList.join(', ') : 'none'}`);
144
+ console.log('');
145
+ for (const harness of ['claude-code', 'codex']) {
146
+ console.log(`${harness}:`);
147
+ for (const tierLabel of ['cheap', 'mid', 'frontier']) {
148
+ const p = plan[harness][tierLabel];
149
+ const costStr = p.cost == null ? (p.model ? 'subscription/$0 or unpriced' : '') : `est $${p.cost}`;
150
+ console.log(` ${tierLabel.padEnd(9)} -> ${(p.model || '(none)').padEnd(28)} ${costStr}`);
151
+ }
152
+ }
153
+ console.log('');
154
+ console.log(`Selectable candidates: ${result.selectableCandidateCount} Landscape-only (known, not yet wired): ${landscapeOnly.length}`);
155
+ if (landscapeOnly.length) {
156
+ for (const c of landscapeOnly) console.log(` - ${c.id} (${c.provider}): ${c.note}`);
157
+ }
158
+ }
159
+
160
+ main().catch((e) => { process.stderr.write(`model-router-status: ${e.stack || e.message}\n`); process.exit(1); });
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env node
2
+ // scripts/route-cheap.mjs — THE place cheap-model routing happens, so the visibility line is earned.
3
+ //
4
+ // Grounding (verified, not recalled):
5
+ // - The real cheap-routing path is agentic-flow's CLI: any --model containing "/" auto-routes
6
+ // through OpenRouter (agentic-flow/src/router/index.ts exports OpenRouterProvider; verified
7
+ // live 2026-07-07 — see plugin/skills/ruvnet-brain/SKILL.md "Cost-optimal model routing").
8
+ // - NOT mcp__ruflo__agent_execute (Anthropic-only), NOT the neural router (chance-level per
9
+ // rUv's own ROUTER-PILOT.md benchmark).
10
+ // - Pricing below was pulled live from the OpenRouter API on 2026-07-07 (same SKILL.md table).
11
+ //
12
+ // What it does, visibly (Stuart directive: "invisible value = no value"):
13
+ // 1. Runs the task on a cheap model via `npx agentic-flow@latest`.
14
+ // 2. Appends a receipt to ~/.claude/metaharness/routing-receipts.jsonl (machine-wide).
15
+ // 3. Prints ONE dim line: "⚡ MetaHarness: routed to <model> (est. $X vs $Y frontier — saved ~$Z)".
16
+ //
17
+ // Usage:
18
+ // node scripts/route-cheap.mjs --task "summarize X" [--model deepseek/deepseek-chat]
19
+ // [--agent researcher] [--class research]
20
+ //
21
+ // Scope guard: read-only text work ONLY (research / summarize / classify / transform). This path
22
+ // has no file-write capability and no native tool calling — code edits stay on Claude Code.
23
+ // The OPENROUTER_API_KEY value is read from env or ruvnet-brain/.env and NEVER printed.
24
+
25
+ import fs from 'node:fs';
26
+ import path from 'node:path';
27
+ import os from 'node:os';
28
+ import { spawnSync } from 'node:child_process';
29
+ import { fileURLToPath, pathToFileURL } from 'node:url';
30
+
31
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
32
+
33
+ // $/Mtok, verified live from the OpenRouter API 2026-07-07 (SKILL.md "Cost-optimal model routing").
34
+ export const PRICING = {
35
+ 'deepseek/deepseek-chat': { in: 0.20, out: 0.80 },
36
+ 'z-ai/glm-4.6': { in: 0.43, out: 1.74 },
37
+ 'z-ai/glm-5': { in: 0.60, out: 1.92 },
38
+ 'deepseek/deepseek-v4-flash': { in: 0.077, out: 0.154 }, // verified 2026-07-12 OpenRouter /models live; successor to deepseek-chat (which resolves to legacy V3)
39
+ 'x-ai/grok-4.5': { in: 2.0, out: 6.0 }, // verified 2026-07-12 OpenRouter /models live; mid-priced frontier-adjacent
40
+ };
41
+ export const FRONTIER = { name: 'claude-opus-4.8', in: 5.0, out: 25.0 };
42
+
43
+ export function receiptsPath() {
44
+ return (
45
+ process.env.METAHARNESS_RECEIPTS ||
46
+ path.join(os.homedir(), '.claude', 'metaharness', 'routing-receipts.jsonl')
47
+ );
48
+ }
49
+
50
+ // Honest token estimate: ~4 chars/token. Labeled "est." everywhere — never presented as measured.
51
+ export const estTokens = (text) => Math.max(1, Math.ceil((text || '').length / 4));
52
+
53
+ export function estimateCosts(model, inTokens, outTokens) {
54
+ const p = PRICING[model];
55
+ if (!p) return null; // unknown model → no receipt rather than an invented number
56
+ const cost = (inTokens * p.in + outTokens * p.out) / 1e6;
57
+ const frontier = (inTokens * FRONTIER.in + outTokens * FRONTIER.out) / 1e6;
58
+ return { cost, frontier, saved: frontier - cost };
59
+ }
60
+
61
+ const fmt$ = (n) => `$${n < 0.01 ? n.toFixed(5) : n.toFixed(4)}`;
62
+
63
+ export function receiptLine(model, costs) {
64
+ return `\x1b[2m⚡ MetaHarness: routed to ${model} (est. ${fmt$(costs.cost)} vs ${fmt$(costs.frontier)} frontier — saved ~${fmt$(costs.saved)})\x1b[0m`;
65
+ }
66
+
67
+ // Load OPENROUTER_API_KEY from ruvnet-brain/.env if not already in env. Value never printed/logged.
68
+ function ensureOpenRouterKey() {
69
+ if (process.env.OPENROUTER_API_KEY) return true;
70
+ try {
71
+ const envFile = fs.readFileSync(path.join(__dirname, '..', '.env'), 'utf8');
72
+ const m = envFile.match(/^OPENROUTER_API_KEY=(.+)$/m);
73
+ if (m && m[1].trim()) {
74
+ process.env.OPENROUTER_API_KEY = m[1].trim().replace(/^["']|["']$/g, '');
75
+ return true;
76
+ }
77
+ } catch {
78
+ /* no .env — fall through */
79
+ }
80
+ return false;
81
+ }
82
+
83
+ function parseArgs(argv) {
84
+ const args = { model: 'deepseek/deepseek-chat', agent: 'researcher', class: 'research' };
85
+ for (let i = 0; i < argv.length; i++) {
86
+ const k = argv[i];
87
+ if (k === '--task' || k === '--model' || k === '--agent' || k === '--class') args[k.slice(2)] = argv[++i];
88
+ }
89
+ return args;
90
+ }
91
+
92
+ function main() {
93
+ const args = parseArgs(process.argv.slice(2));
94
+ if (!args.task) {
95
+ console.error('Usage: node scripts/route-cheap.mjs --task "<text>" [--model deepseek/deepseek-chat] [--agent researcher] [--class research]');
96
+ process.exit(2);
97
+ }
98
+ if (!PRICING[args.model]) {
99
+ console.error(`Unknown model "${args.model}" — no verified pricing, refusing to invent savings. Known: ${Object.keys(PRICING).join(', ')}`);
100
+ process.exit(2);
101
+ }
102
+ if (!ensureOpenRouterKey()) {
103
+ console.error('OPENROUTER_API_KEY not found in env or ruvnet-brain/.env — cannot route. (Key value is never printed.)');
104
+ process.exit(1);
105
+ }
106
+
107
+ const started = Date.now();
108
+ const run = spawnSync('npx', ['agentic-flow@latest', '--agent', args.agent, '--model', args.model, '--task', args.task], {
109
+ encoding: 'utf8',
110
+ timeout: 180_000,
111
+ env: process.env,
112
+ });
113
+
114
+ if (run.error || run.status !== 0) {
115
+ // Honest failure: no receipt, no savings claim.
116
+ console.error(`route-cheap: agentic-flow failed (exit ${run.status ?? 'spawn-error'}). No receipt written.`);
117
+ if (run.stderr) console.error(run.stderr.slice(-2000));
118
+ if (run.stdout) console.error(run.stdout.slice(-1000));
119
+ process.exit(run.status || 1);
120
+ }
121
+
122
+ const output = (run.stdout || '').trim();
123
+ console.log(output);
124
+
125
+ const inTok = estTokens(args.task);
126
+ const outTok = estTokens(output);
127
+ const costs = estimateCosts(args.model, inTok, outTok);
128
+ const receipt = {
129
+ ts: new Date().toISOString(),
130
+ task_class: args.class,
131
+ task: args.task.slice(0, 120),
132
+ model: args.model,
133
+ agent: args.agent,
134
+ est_in_tokens: inTok,
135
+ est_out_tokens: outTok,
136
+ est_cost: +costs.cost.toFixed(6),
137
+ est_frontier_cost: +costs.frontier.toFixed(6),
138
+ saved: +costs.saved.toFixed(6),
139
+ frontier_ref: FRONTIER.name,
140
+ token_source: 'chars/4 est',
141
+ duration_ms: Date.now() - started,
142
+ source: 'agentic-flow',
143
+ };
144
+ const file = receiptsPath();
145
+ fs.mkdirSync(path.dirname(file), { recursive: true });
146
+ fs.appendFileSync(file, JSON.stringify(receipt) + '\n');
147
+
148
+ console.log('');
149
+ console.log(receiptLine(args.model, costs));
150
+ }
151
+
152
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) main();