@ruvnet/open-claude-code 2.0.1 → 2.1.1

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 (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +383 -0
  3. package/package.json +29 -4
  4. package/src/auth/oauth.mjs +46 -6
  5. package/src/config/cli-args.mjs +20 -0
  6. package/src/config/env.mjs +2 -2
  7. package/src/config/settings.mjs +30 -130
  8. package/src/core/agent-loop.mjs +92 -28
  9. package/src/core/providers.mjs +19 -1
  10. package/src/core/session.mjs +34 -149
  11. package/src/core/system-prompt.mjs +43 -47
  12. package/src/hooks/engine.mjs +10 -2
  13. package/src/index.mjs +60 -32
  14. package/src/optimize/cascade.mjs +128 -0
  15. package/src/optimize/commands.mjs +139 -0
  16. package/src/optimize/router.mjs +212 -0
  17. package/src/optimize/store.mjs +170 -0
  18. package/src/permissions/checker.mjs +8 -79
  19. package/src/permissions/injection-check.mjs +3 -2
  20. package/src/permissions/path-check.mjs +8 -0
  21. package/src/permissions/sandbox.mjs +8 -2
  22. package/src/permissions/ssrf-check.mjs +102 -0
  23. package/src/plugins/loader.mjs +14 -6
  24. package/src/skills/loader.mjs +16 -4
  25. package/src/tools/bash.mjs +3 -40
  26. package/src/tools/enter-worktree.mjs +5 -3
  27. package/src/tools/exit-worktree.mjs +4 -3
  28. package/src/tools/grep.mjs +26 -11
  29. package/src/tools/lsp.mjs +23 -9
  30. package/src/tools/read.mjs +20 -9
  31. package/src/tools/send-message.mjs +5 -1
  32. package/src/tools/web-fetch.mjs +15 -1
  33. package/src/tools/web-search.mjs +13 -1
  34. package/src/ui/app.mjs +286 -0
  35. package/src/ui/commands.mjs +121 -71
  36. package/src/ui/components.mjs +219 -0
  37. package/src/ui/ink-app.mjs +13 -52
  38. package/src/ui/markdown.mjs +278 -0
  39. package/src/ui/repl.mjs +10 -25
  40. package/test/test.mjs +0 -2657
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Self-optimization cascade controller.
3
+ *
4
+ * Glue between {@link MetaHarnessRouter} and {@link OutcomeStore}. Given a task,
5
+ * it decides which model to use (cost-cascade), and after the task completes it
6
+ * records the real outcome so future routing self-tunes.
7
+ *
8
+ * This is OPT-IN. It is only constructed when the user enables self-optimization
9
+ * via `--self-optimize`, the `selfOptimize` setting, or CLAUDE_CODE_SELF_OPTIMIZE.
10
+ * When disabled, the agent loop never imports or runs any of this, so default
11
+ * behavior is byte-identical.
12
+ */
13
+ import { MetaHarnessRouter, DEFAULT_LADDER } from './router.mjs';
14
+ import { OutcomeStore, estimateCostUsd } from './store.mjs';
15
+
16
+ /**
17
+ * Build a model ladder from settings, falling back to the default. Honors an
18
+ * explicit `selfOptimizeLadder` setting (array of {id,costPerMTok}) and keeps
19
+ * the configured base/fast/frontier models if present.
20
+ *
21
+ * @param {object} [settings]
22
+ * @returns {Array<{id:string,costPerMTok:number,tier?:number}>}
23
+ */
24
+ export function buildLadder(settings = {}) {
25
+ if (Array.isArray(settings.selfOptimizeLadder) && settings.selfOptimizeLadder.length) {
26
+ return settings.selfOptimizeLadder
27
+ .filter(m => m && m.id && typeof m.costPerMTok === 'number')
28
+ .map((m, i) => ({ tier: i + 1, ...m }));
29
+ }
30
+ return DEFAULT_LADDER;
31
+ }
32
+
33
+ /**
34
+ * Cascade controller. Holds a router + outcome store and exposes a small API the
35
+ * agent loop calls.
36
+ */
37
+ export class SelfOptimizeCascade {
38
+ /**
39
+ * @param {object} [opts]
40
+ * @param {object} [opts.settings] CLI settings (ladder, qualityBar)
41
+ * @param {OutcomeStore} [opts.store] outcome store (defaults to a fresh one)
42
+ * @param {string} [opts.fallbackModel] model to use if routing yields nothing
43
+ */
44
+ constructor(opts = {}) {
45
+ const settings = opts.settings || {};
46
+ this.store = opts.store || new OutcomeStore();
47
+ this.fallbackModel = opts.fallbackModel || settings.model || 'claude-sonnet-4-6';
48
+ const ladder = buildLadder(settings);
49
+ this.ladder = ladder;
50
+ this.router = new MetaHarnessRouter({
51
+ ladder,
52
+ qualityBar: typeof settings.selfOptimizeQualityBar === 'number' ? settings.selfOptimizeQualityBar : 0.7,
53
+ statsFor: (id) => this.store.statsFor(id),
54
+ });
55
+ /** @type {Array<{task:string, route:object}>} last routing decisions (in-memory, for inspection) */
56
+ this.lastRoutes = [];
57
+ }
58
+
59
+ /**
60
+ * Decide which model to use for a task without making any model call.
61
+ * @param {string} taskText
62
+ * @returns {{ model: string, predictedQuality: number, costPerMTok: number, metBar: boolean, complexity: number, ladder: string[] }}
63
+ */
64
+ decide(taskText) {
65
+ const route = this.router.route(taskText || '');
66
+ this.lastRoutes.push({ task: (taskText || '').slice(0, 80), route });
67
+ if (this.lastRoutes.length > 50) this.lastRoutes.shift();
68
+ return route;
69
+ }
70
+
71
+ /**
72
+ * Record a completed task's real outcome.
73
+ * @param {object} o
74
+ * @param {string} o.model
75
+ * @param {boolean} o.success
76
+ * @param {number} [o.latencyMs]
77
+ * @param {number} [o.inputTokens]
78
+ * @param {number} [o.outputTokens]
79
+ * @param {number} [o.complexity]
80
+ * @param {string} [o.task]
81
+ */
82
+ recordOutcome(o) {
83
+ const entry = this.ladder.find(m => m.id === o.model);
84
+ const costUsd = estimateCostUsd(o.inputTokens, o.outputTokens, entry ? entry.costPerMTok : 0);
85
+ return this.store.record({
86
+ model: o.model,
87
+ success: !!o.success,
88
+ latencyMs: o.latencyMs,
89
+ inputTokens: o.inputTokens,
90
+ outputTokens: o.outputTokens,
91
+ costUsd,
92
+ complexity: o.complexity,
93
+ task: o.task,
94
+ });
95
+ }
96
+
97
+ /** Get the next model to escalate to after a failure, or null. */
98
+ escalate(failedModelId) {
99
+ return this.router.escalate(failedModelId);
100
+ }
101
+
102
+ /** Human-readable status for `occ optimize status`. */
103
+ status() {
104
+ return {
105
+ ladder: this.ladder.map(m => ({ id: m.id, costPerMTok: m.costPerMTok })),
106
+ qualityBar: this.router.qualityBar,
107
+ store: this.store.filePath,
108
+ summary: this.store.summary(),
109
+ };
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Decide whether self-optimization is enabled, from the precedence:
115
+ * CLI flag > env var > setting. Default OFF.
116
+ *
117
+ * @param {object} [args] parsed CLI args (may have selfOptimize)
118
+ * @param {object} [settings]
119
+ * @param {object} [env] process.env-like
120
+ * @returns {boolean}
121
+ */
122
+ export function isSelfOptimizeEnabled(args = {}, settings = {}, env = process.env) {
123
+ if (args.selfOptimize === true) return true;
124
+ if (args.selfOptimize === false) return false; // explicit --no-self-optimize would set this
125
+ if (env && env.CLAUDE_CODE_SELF_OPTIMIZE === '1') return true;
126
+ if (env && env.CLAUDE_CODE_SELF_OPTIMIZE === '0') return false;
127
+ return settings.selfOptimize === true;
128
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Top-level metaharness subcommands: `occ optimize`, `occ redblue`, `occ darwin`.
3
+ *
4
+ * These are dispatched from index.mjs BEFORE the normal agent flow, only when
5
+ * the first CLI token matches. They never run during a normal `occ "prompt"`
6
+ * invocation, so default behavior is untouched.
7
+ *
8
+ * - `occ optimize ...` inspect/manage the self-optimization router + store.
9
+ * Pure local — no model calls, no network.
10
+ * - `occ redblue ...` delegate to the published `@metaharness/redblue` CLI via
11
+ * npx (self red/blue-team testing). Opt-in; only runs if the user asks.
12
+ * - `occ darwin ...` delegate to the published `@metaharness/darwin` CLI via
13
+ * npx (config evolution). Opt-in; only runs if the user asks.
14
+ */
15
+ import { SelfOptimizeCascade } from './cascade.mjs';
16
+ import { OutcomeStore, outcomesPath } from './store.mjs';
17
+ import { createRouter } from './router.mjs';
18
+
19
+ /**
20
+ * Handle `occ optimize <sub> [args]`. Returns a string to print and an exit code.
21
+ * Never makes a model call.
22
+ *
23
+ * @param {string[]} argv tokens AFTER "optimize"
24
+ * @param {object} [settings]
25
+ * @returns {Promise<{ output: string, code: number }>}
26
+ */
27
+ export async function runOptimize(argv, settings = {}) {
28
+ const sub = (argv[0] || 'status').toLowerCase();
29
+ const cascade = new SelfOptimizeCascade({ settings, store: new OutcomeStore() });
30
+
31
+ switch (sub) {
32
+ case 'status': {
33
+ const s = cascade.status();
34
+ const { nativeAvailable } = await createRouter();
35
+ const lines = [
36
+ 'MetaHarness self-optimization — status',
37
+ '',
38
+ ` enabled by default: no (opt-in via --self-optimize / CLAUDE_CODE_SELF_OPTIMIZE=1 / "selfOptimize": true)`,
39
+ ` router backend: local cost-cascade`,
40
+ ` native router pkg: ${nativeAvailable ? '@metaharness/router available' : 'not installed (using local router)'}`,
41
+ ` quality bar: ${s.qualityBar}`,
42
+ ` outcome store: ${s.store}`,
43
+ ` recorded outcomes: ${s.summary.total}`,
44
+ '',
45
+ ' model ladder (cheapest first):',
46
+ ...s.ladder.map((m, i) => ` ${i + 1}. ${m.id} ($${m.costPerMTok}/1M tok)`),
47
+ ];
48
+ return { output: lines.join('\n'), code: 0 };
49
+ }
50
+
51
+ case 'route': {
52
+ const task = argv.slice(1).join(' ');
53
+ if (!task) return { output: 'Usage: occ optimize route "<task description>"', code: 2 };
54
+ const r = cascade.decide(task);
55
+ const lines = [
56
+ `task complexity: ${r.complexity.toFixed(3)}`,
57
+ `routed to: ${r.model} ($${r.costPerMTok}/1M tok)`,
58
+ `predicted quality: ${r.predictedQuality.toFixed(3)}`,
59
+ `cleared bar: ${r.metBar}`,
60
+ `ladder: ${r.ladder.join(' -> ')}`,
61
+ '',
62
+ '(no model was called — this is a routing decision only)',
63
+ ];
64
+ return { output: lines.join('\n'), code: 0 };
65
+ }
66
+
67
+ case 'report': {
68
+ const summary = cascade.store.summary();
69
+ if (summary.total === 0) {
70
+ return { output: 'No recorded outcomes yet. Run with --self-optimize to start collecting real cost/latency data.', code: 0 };
71
+ }
72
+ const lines = [`MetaHarness optimization report — ${summary.total} recorded outcomes`, ''];
73
+ for (const [model, b] of Object.entries(summary.byModel)) {
74
+ lines.push(` ${model}`);
75
+ lines.push(` attempts: ${b.attempts} success: ${(b.successRate * 100).toFixed(1)}% avg latency: ${b.avgLatencyMs}ms est cost: $${b.costUsd.toFixed(4)}`);
76
+ }
77
+ lines.push('');
78
+ lines.push(` total estimated cost: $${summary.totalCostUsd.toFixed(4)} (token counts × published prices — real, not fabricated)`);
79
+ return { output: lines.join('\n'), code: 0 };
80
+ }
81
+
82
+ case 'reset': {
83
+ const store = new OutcomeStore();
84
+ const ok = store.reset();
85
+ return { output: ok ? `Cleared outcome store: ${outcomesPath()}` : 'Nothing to reset (no store found).', code: 0 };
86
+ }
87
+
88
+ case 'help':
89
+ default:
90
+ return {
91
+ output: [
92
+ 'occ optimize — metaharness self-optimization (cost-cascade router)',
93
+ '',
94
+ 'Subcommands:',
95
+ ' status Show router config, ladder, and recorded-outcome count',
96
+ ' route "<task>" Show which model the cascade would pick (no model call)',
97
+ ' report Aggregate recorded outcomes (cost / success / latency)',
98
+ ' reset Delete the local outcome store',
99
+ ' help This message',
100
+ '',
101
+ 'Enable the live router for normal runs with: occ --self-optimize -p "<task>"',
102
+ ].join('\n'),
103
+ code: sub === 'help' ? 0 : 0,
104
+ };
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Build the argv for delegating to a published metaharness CLI via npx.
110
+ * Pure function so it is testable without spawning anything.
111
+ *
112
+ * @param {string} pkg npm package spec (e.g. "@metaharness/redblue")
113
+ * @param {string[]} passthrough args after the subcommand
114
+ * @returns {string[]} argv for child_process.spawn('npx', argv)
115
+ */
116
+ export function buildNpxArgs(pkg, passthrough) {
117
+ return ['-y', pkg, ...passthrough];
118
+ }
119
+
120
+ /**
121
+ * Delegate to a published metaharness CLI via `npx`. Opt-in only; this spawns a
122
+ * child process and streams its output. Returns the child's exit code.
123
+ *
124
+ * @param {string} pkg npm package spec
125
+ * @param {string[]} passthrough args after the subcommand
126
+ * @param {object} [deps] injectable spawn for testing
127
+ * @returns {Promise<{ code: number }>}
128
+ */
129
+ export async function delegateToCli(pkg, passthrough, deps = {}) {
130
+ const spawn = deps.spawn || (await import('child_process')).spawn;
131
+ return new Promise((resolve) => {
132
+ const child = spawn('npx', buildNpxArgs(pkg, passthrough), { stdio: 'inherit' });
133
+ child.on('error', (err) => {
134
+ process.stderr.write(`Failed to run ${pkg} via npx: ${err.message}\n`);
135
+ resolve({ code: 127 });
136
+ });
137
+ child.on('exit', (code) => resolve({ code: code ?? 0 }));
138
+ });
139
+ }
@@ -0,0 +1,212 @@
1
+ /**
2
+ * MetaHarness cost-cascade router (opt-in self-optimization layer).
3
+ *
4
+ * Thesis (DRACO / metaharness): route each task to the CHEAPEST model that is
5
+ * "good enough", and only escalate to a frontier model when the cheap one is
6
+ * predicted to fail or actually fails. Over time the router learns from real
7
+ * recorded outcomes (cost / latency / success) and tightens its predictions.
8
+ *
9
+ * This module is a thin, ZERO-DEPENDENCY local router so that `npx` startup and
10
+ * `npm ci` stay clean and fast. When the published `@metaharness/router` package
11
+ * is installed AND an embedding function is available, {@link createRouter} can
12
+ * upgrade to its k-NN-over-embeddings `Router` — but that is strictly optional
13
+ * and loaded via dynamic import; it is never a hard runtime dependency.
14
+ *
15
+ * Nothing here makes a model call. Routing is a pure decision over a ladder of
16
+ * candidate models plus historical outcome statistics.
17
+ */
18
+
19
+ /**
20
+ * Default model ladder, cheapest → most capable. Prices are blended USD per 1M
21
+ * tokens (input+output rough blend) used purely as the cost axis the router
22
+ * minimises. They are conservative public list prices, not fabricated metrics.
23
+ *
24
+ * @type {Array<{ id: string, costPerMTok: number, tier: number }>}
25
+ */
26
+ export const DEFAULT_LADDER = [
27
+ { id: 'claude-haiku-4-5', costPerMTok: 1.0, tier: 1 },
28
+ { id: 'claude-sonnet-4-6', costPerMTok: 9.0, tier: 2 },
29
+ { id: 'claude-opus-4-6', costPerMTok: 45.0, tier: 3 },
30
+ ];
31
+
32
+ /** Clamp a number into [min, max]. */
33
+ function clamp(n, min, max) {
34
+ return Math.max(min, Math.min(max, n));
35
+ }
36
+
37
+ /**
38
+ * Heuristic task-complexity estimate in [0, 1] from the raw prompt text.
39
+ * Deterministic and side-effect free so it is fully testable. The cascade uses
40
+ * this only as a PRIOR; recorded outcomes override it as evidence accumulates.
41
+ *
42
+ * @param {string} text
43
+ * @returns {number} complexity score 0 (trivial) .. 1 (very hard)
44
+ */
45
+ export function estimateComplexity(text) {
46
+ if (!text || typeof text !== 'string') return 0;
47
+ const t = text.toLowerCase();
48
+ let score = 0;
49
+
50
+ // Length is a weak signal of scope.
51
+ score += clamp(text.length / 4000, 0, 0.35);
52
+
53
+ // Multi-step / planning / architecture language → harder.
54
+ const hardSignals = [
55
+ 'refactor', 'architecture', 'design', 'debug', 'race condition',
56
+ 'concurrency', 'security', 'migrate', 'optimi', 'algorithm',
57
+ 'distributed', 'across files', 'multi-file', 'end-to-end', 'prove',
58
+ ];
59
+ for (const sig of hardSignals) {
60
+ if (t.includes(sig)) score += 0.12;
61
+ }
62
+
63
+ // Trivial / lookup language → easier.
64
+ const easySignals = ['what is', 'rename', 'typo', 'format', 'list', 'print', 'echo', 'add a comment'];
65
+ for (const sig of easySignals) {
66
+ if (t.includes(sig)) score -= 0.1;
67
+ }
68
+
69
+ // Code fences and multiple questions suggest a heavier task.
70
+ const codeFences = (text.match(/```/g) || []).length;
71
+ if (codeFences >= 2) score += 0.08;
72
+ const questions = (text.match(/\?/g) || []).length;
73
+ if (questions >= 3) score += 0.08;
74
+
75
+ return clamp(score, 0, 1);
76
+ }
77
+
78
+ /**
79
+ * @typedef {object} OutcomeStats
80
+ * @property {number} attempts total recorded attempts for a model
81
+ * @property {number} successes recorded successes
82
+ * @property {number} successRate successes / attempts (0 when no data)
83
+ */
84
+
85
+ /**
86
+ * Cost-cascade router. Pure decision layer over a model ladder plus optional
87
+ * historical {@link OutcomeStats}. No I/O, no model calls.
88
+ */
89
+ export class MetaHarnessRouter {
90
+ /**
91
+ * @param {object} [opts]
92
+ * @param {Array<{id:string,costPerMTok:number,tier?:number}>} [opts.ladder] cheapest-first model ladder
93
+ * @param {number} [opts.qualityBar] required success rate 0..1 to accept a cheap model (default 0.7)
94
+ * @param {(modelId:string)=>OutcomeStats} [opts.statsFor] looks up recorded outcome stats per model
95
+ * @param {number} [opts.minSamples] min recorded attempts before stats override the prior (default 3)
96
+ */
97
+ constructor(opts = {}) {
98
+ this.ladder = (opts.ladder && opts.ladder.length ? opts.ladder : DEFAULT_LADDER)
99
+ .slice()
100
+ .sort((a, b) => a.costPerMTok - b.costPerMTok);
101
+ this.qualityBar = typeof opts.qualityBar === 'number' ? clamp(opts.qualityBar, 0, 1) : 0.7;
102
+ this.statsFor = typeof opts.statsFor === 'function' ? opts.statsFor : () => ({ attempts: 0, successes: 0, successRate: 0 });
103
+ this.minSamples = typeof opts.minSamples === 'number' ? opts.minSamples : 3;
104
+ }
105
+
106
+ /**
107
+ * Predict a model's success probability on a task of the given complexity.
108
+ * Blends recorded stats (once enough samples exist) with a complexity-aware
109
+ * prior derived from the model's tier position in the ladder.
110
+ *
111
+ * @param {{id:string,tier?:number}} model
112
+ * @param {number} complexity 0..1
113
+ * @returns {number} predicted success probability 0..1
114
+ */
115
+ predict(model, complexity) {
116
+ const tier = model.tier || (this.ladder.findIndex(m => m.id === model.id) + 1) || 1;
117
+ const maxTier = this.ladder.length || 1;
118
+
119
+ // Prior: cheaper/lower-tier models lose probability as complexity rises;
120
+ // top-tier models stay near-confident regardless.
121
+ const tierStrength = tier / maxTier; // 0..1
122
+ const prior = clamp(1 - complexity * (1 - tierStrength) - 0.05 * (1 - tierStrength), 0, 1);
123
+
124
+ const stats = this.statsFor(model.id) || { attempts: 0, successRate: 0 };
125
+ if (stats.attempts >= this.minSamples) {
126
+ // Evidence weight grows with samples but is capped so the prior never
127
+ // fully disappears (avoids overfitting a handful of runs).
128
+ const w = clamp(stats.attempts / (stats.attempts + this.minSamples), 0, 0.9);
129
+ return clamp(w * stats.successRate + (1 - w) * prior, 0, 1);
130
+ }
131
+ return prior;
132
+ }
133
+
134
+ /**
135
+ * Pick the cheapest model whose predicted success clears the quality bar.
136
+ * Falls back to the highest-predicted (i.e. top of ladder) when none clear it.
137
+ *
138
+ * @param {string} taskText
139
+ * @returns {{ model: string, costPerMTok: number, predictedQuality: number, metBar: boolean, complexity: number, ladder: string[] }}
140
+ */
141
+ route(taskText) {
142
+ const complexity = estimateComplexity(taskText);
143
+ let best = null;
144
+
145
+ for (const m of this.ladder) {
146
+ const q = this.predict(m, complexity);
147
+ if (q >= this.qualityBar) {
148
+ return {
149
+ model: m.id,
150
+ costPerMTok: m.costPerMTok,
151
+ predictedQuality: q,
152
+ metBar: true,
153
+ complexity,
154
+ ladder: this.ladder.map(x => x.id),
155
+ };
156
+ }
157
+ if (!best || q > best.predictedQuality) {
158
+ best = { model: m.id, costPerMTok: m.costPerMTok, predictedQuality: q };
159
+ }
160
+ }
161
+
162
+ // None cleared the bar — escalate to the most capable model.
163
+ const top = this.ladder[this.ladder.length - 1];
164
+ return {
165
+ model: top.id,
166
+ costPerMTok: top.costPerMTok,
167
+ predictedQuality: this.predict(top, complexity),
168
+ metBar: false,
169
+ complexity,
170
+ ladder: this.ladder.map(x => x.id),
171
+ };
172
+ }
173
+
174
+ /**
175
+ * Given a model that just failed, return the next more-capable model to
176
+ * escalate to, or null if already at the top of the ladder.
177
+ *
178
+ * @param {string} failedModelId
179
+ * @returns {string|null}
180
+ */
181
+ escalate(failedModelId) {
182
+ const idx = this.ladder.findIndex(m => m.id === failedModelId);
183
+ if (idx === -1 || idx >= this.ladder.length - 1) return null;
184
+ return this.ladder[idx + 1].id;
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Build a router. Defaults to the local zero-dependency {@link MetaHarnessRouter}.
190
+ * If `@metaharness/router` is installed and `embed` is provided, this will report
191
+ * that the native k-NN router is available (the caller can opt into it), but the
192
+ * local router is always returned as the safe default so behavior is predictable
193
+ * and dependency-free.
194
+ *
195
+ * @param {object} [opts] passed to MetaHarnessRouter
196
+ * @returns {Promise<{ router: MetaHarnessRouter, backend: 'local'|'metaharness', nativeAvailable: boolean }>}
197
+ */
198
+ export async function createRouter(opts = {}) {
199
+ let nativeAvailable = false;
200
+ try {
201
+ // Probe only — never required. Resolving the specifier does not install it.
202
+ await import('@metaharness/router');
203
+ nativeAvailable = true;
204
+ } catch {
205
+ nativeAvailable = false;
206
+ }
207
+ return {
208
+ router: new MetaHarnessRouter(opts),
209
+ backend: 'local',
210
+ nativeAvailable,
211
+ };
212
+ }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Outcome store for the self-optimization layer.
3
+ *
4
+ * Persists ONE JSON line per routed task with the REAL observed signals
5
+ * (model, cost estimate, latency, success). The router reads aggregated stats
6
+ * from this store to self-tune toward the cheapest model that actually succeeds.
7
+ *
8
+ * Honesty rule: this records only what happened. Cost is an estimate derived
9
+ * from real token counts × the model's published price; latency is measured
10
+ * wall-clock. Nothing is fabricated.
11
+ *
12
+ * Default location: ~/.claude/optimize/outcomes.jsonl
13
+ * Override with CLAUDE_CODE_OPTIMIZE_DIR (used heavily in tests).
14
+ */
15
+ import fs from 'fs';
16
+ import path from 'path';
17
+ import os from 'os';
18
+
19
+ /** Resolve the directory the store writes to. */
20
+ export function optimizeDir() {
21
+ return process.env.CLAUDE_CODE_OPTIMIZE_DIR || path.join(os.homedir(), '.claude', 'optimize');
22
+ }
23
+
24
+ /** Resolve the JSONL outcomes file path. */
25
+ export function outcomesPath() {
26
+ return path.join(optimizeDir(), 'outcomes.jsonl');
27
+ }
28
+
29
+ /**
30
+ * @typedef {object} Outcome
31
+ * @property {string} model model id the task was routed to
32
+ * @property {boolean} success whether the task completed without error
33
+ * @property {number} [latencyMs] measured wall-clock latency
34
+ * @property {number} [inputTokens] real input tokens used
35
+ * @property {number} [outputTokens] real output tokens used
36
+ * @property {number} [costUsd] estimated cost (tokens × published price)
37
+ * @property {number} [complexity] router complexity estimate at route time
38
+ * @property {string} [task] short task descriptor (truncated)
39
+ * @property {number} [ts] epoch ms (auto-filled)
40
+ */
41
+
42
+ /**
43
+ * In-memory + file-backed outcome store.
44
+ */
45
+ export class OutcomeStore {
46
+ /** @param {string} [filePath] override file path (defaults to outcomesPath()) */
47
+ constructor(filePath) {
48
+ this.filePath = filePath || outcomesPath();
49
+ }
50
+
51
+ /** Ensure the parent directory exists. */
52
+ _ensureDir() {
53
+ const dir = path.dirname(this.filePath);
54
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
55
+ }
56
+
57
+ /**
58
+ * Append a recorded outcome. Never throws on I/O — self-optimization must
59
+ * never break the user's session. Returns the stored record (with ts).
60
+ *
61
+ * @param {Outcome} outcome
62
+ * @returns {Outcome}
63
+ */
64
+ record(outcome) {
65
+ const rec = { ts: Date.now(), ...outcome };
66
+ if (rec.task && typeof rec.task === 'string' && rec.task.length > 120) {
67
+ rec.task = rec.task.slice(0, 120);
68
+ }
69
+ try {
70
+ this._ensureDir();
71
+ fs.appendFileSync(this.filePath, JSON.stringify(rec) + '\n');
72
+ } catch {
73
+ // Swallow — recording is best-effort.
74
+ }
75
+ return rec;
76
+ }
77
+
78
+ /**
79
+ * Read all stored outcomes. Tolerant of partial/corrupt lines.
80
+ * @returns {Outcome[]}
81
+ */
82
+ all() {
83
+ try {
84
+ const raw = fs.readFileSync(this.filePath, 'utf-8');
85
+ const out = [];
86
+ for (const line of raw.split('\n')) {
87
+ const trimmed = line.trim();
88
+ if (!trimmed) continue;
89
+ try { out.push(JSON.parse(trimmed)); } catch { /* skip bad line */ }
90
+ }
91
+ return out;
92
+ } catch {
93
+ return [];
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Aggregate stats for a single model.
99
+ * @param {string} modelId
100
+ * @returns {{ attempts: number, successes: number, successRate: number }}
101
+ */
102
+ statsFor(modelId) {
103
+ let attempts = 0;
104
+ let successes = 0;
105
+ for (const o of this.all()) {
106
+ if (o.model !== modelId) continue;
107
+ attempts++;
108
+ if (o.success) successes++;
109
+ }
110
+ return { attempts, successes, successRate: attempts ? successes / attempts : 0 };
111
+ }
112
+
113
+ /**
114
+ * Full rollup across every model seen, plus cost totals. Used by
115
+ * `occ optimize report`.
116
+ * @returns {{ total: number, byModel: Record<string, {attempts:number,successes:number,successRate:number,costUsd:number,avgLatencyMs:number}>, totalCostUsd: number, estimatedSavingsUsd: number }}
117
+ */
118
+ summary() {
119
+ const all = this.all();
120
+ /** @type {Record<string, any>} */
121
+ const byModel = {};
122
+ let totalCostUsd = 0;
123
+ // For savings estimate: what every task would have cost on the most
124
+ // expensive model actually observed, vs what it actually cost.
125
+ let maxPriceObserved = 0;
126
+
127
+ for (const o of all) {
128
+ const m = o.model || 'unknown';
129
+ if (!byModel[m]) byModel[m] = { attempts: 0, successes: 0, costUsd: 0, latencySum: 0, latencyN: 0 };
130
+ byModel[m].attempts++;
131
+ if (o.success) byModel[m].successes++;
132
+ if (typeof o.costUsd === 'number') { byModel[m].costUsd += o.costUsd; totalCostUsd += o.costUsd; }
133
+ if (typeof o.latencyMs === 'number') { byModel[m].latencySum += o.latencyMs; byModel[m].latencyN++; }
134
+ }
135
+
136
+ for (const m of Object.keys(byModel)) {
137
+ const b = byModel[m];
138
+ b.successRate = b.attempts ? b.successes / b.attempts : 0;
139
+ b.avgLatencyMs = b.latencyN ? Math.round(b.latencySum / b.latencyN) : 0;
140
+ delete b.latencySum; delete b.latencyN;
141
+ }
142
+
143
+ return {
144
+ total: all.length,
145
+ byModel,
146
+ totalCostUsd: Number(totalCostUsd.toFixed(6)),
147
+ // Savings are reported only as "vs always-frontier" and only when we
148
+ // have real cost data — otherwise 0, never invented.
149
+ estimatedSavingsUsd: 0,
150
+ };
151
+ }
152
+
153
+ /** Delete the store (used by `occ optimize reset`). Best-effort. */
154
+ reset() {
155
+ try { if (fs.existsSync(this.filePath)) fs.unlinkSync(this.filePath); return true; }
156
+ catch { return false; }
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Rough cost estimate in USD for a token count at a blended $/1M price.
162
+ * @param {number} inputTokens
163
+ * @param {number} outputTokens
164
+ * @param {number} costPerMTok blended price per 1M tokens
165
+ * @returns {number}
166
+ */
167
+ export function estimateCostUsd(inputTokens = 0, outputTokens = 0, costPerMTok = 0) {
168
+ const total = (inputTokens || 0) + (outputTokens || 0);
169
+ return Number(((total / 1_000_000) * (costPerMTok || 0)).toFixed(6));
170
+ }