claude-flow 3.19.0 → 3.21.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 +2 -2
- package/v3/@claude-flow/cli/dist/src/agenticow/speculative-exploration.d.ts +148 -0
- package/v3/@claude-flow/cli/dist/src/agenticow/speculative-exploration.js +218 -0
- package/v3/@claude-flow/cli/dist/src/commands/autopilot.js +45 -0
- package/v3/@claude-flow/cli/dist/src/commands/neural.js +366 -12
- package/v3/@claude-flow/cli/dist/src/mcp-client.js +6 -1
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-execute-core.js +33 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-tools.js +47 -1
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agenticow-loader.d.ts +59 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agenticow-loader.js +105 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agenticow-speculate-tools.d.ts +24 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agenticow-speculate-tools.js +202 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agenticow-tools.js +186 -79
- package/v3/@claude-flow/cli/dist/src/mcp-tools/index.d.ts +1 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/index.js +2 -0
- package/v3/@claude-flow/cli/dist/src/ruvector/index.d.ts +1 -1
- package/v3/@claude-flow/cli/dist/src/ruvector/index.js +1 -1
- package/v3/@claude-flow/cli/dist/src/ruvector/lora-adapter.d.ts +35 -0
- package/v3/@claude-flow/cli/dist/src/ruvector/lora-adapter.js +108 -1
- package/v3/@claude-flow/cli/dist/src/ruvector/router-trajectory.d.ts +33 -0
- package/v3/@claude-flow/cli/dist/src/ruvector/router-trajectory.js +31 -0
- package/v3/@claude-flow/cli/dist/src/ruvector/run-transcript-recorder.d.ts +154 -0
- package/v3/@claude-flow/cli/dist/src/ruvector/run-transcript-recorder.js +209 -0
- package/v3/@claude-flow/cli/dist/src/services/checkpoint-gate.d.ts +140 -0
- package/v3/@claude-flow/cli/dist/src/services/checkpoint-gate.js +223 -0
- package/v3/@claude-flow/cli/dist/src/services/distill-oracle.d.ts +190 -0
- package/v3/@claude-flow/cli/dist/src/services/distill-oracle.js +349 -0
- package/v3/@claude-flow/cli/dist/src/services/fable-harness.d.ts +168 -0
- package/v3/@claude-flow/cli/dist/src/services/fable-harness.js +347 -0
- package/v3/@claude-flow/cli/dist/src/services/native-training.d.ts +28 -0
- package/v3/@claude-flow/cli/dist/src/services/native-training.js +62 -5
- package/v3/@claude-flow/cli/dist/src/services/swarm-memory-branches.d.ts +135 -0
- package/v3/@claude-flow/cli/dist/src/services/swarm-memory-branches.js +213 -0
- package/v3/@claude-flow/cli/dist/src/services/weight-eft.d.ts +305 -0
- package/v3/@claude-flow/cli/dist/src/services/weight-eft.js +296 -0
- package/v3/@claude-flow/cli/package.json +5 -4
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* distill-oracle.ts — Tiered `resolved` oracle for distill / weight-EFT SFT data.
|
|
3
|
+
*
|
|
4
|
+
* THE PROBLEM: weight-EFT's SFT data wants a gold `resolved: boolean` per
|
|
5
|
+
* trajectory. Ruflo has no SWE-bench oracle, so today `resolved` is
|
|
6
|
+
* structural-confidence — a proxy that risks distilling plausible-but-wrong
|
|
7
|
+
* completions. This module replaces that single proxy with a TIERED labeler,
|
|
8
|
+
* where every label carries HONEST PROVENANCE (ADR-169 reporting integrity — a
|
|
9
|
+
* proxy is never presented as ground truth).
|
|
10
|
+
*
|
|
11
|
+
* ── TIERS (tried in order, per trajectory) ──────────────────────────────────
|
|
12
|
+
* TIER 1 — MECHANICAL ORACLE (provenance `oracle:test-exec`, real ground truth)
|
|
13
|
+
* When a trajectory carries a test spec (SWE-bench FAIL_TO_PASS shape) or
|
|
14
|
+
* maps to a metaharness/darwin bench-suite case, EXECUTE the real eval and
|
|
15
|
+
* set resolved = tests-pass. This needs Docker/compute, so it runs on a
|
|
16
|
+
* REMOTE host over SSH (host parameterized via `remote` / env
|
|
17
|
+
* RUFLO_DISTILL_REMOTE — never hard-coded). DRY-RUN by default: it prints
|
|
18
|
+
* the ssh/darwin-bench/eval commands + a preflight plan and does NOT touch
|
|
19
|
+
* the network; only `execute: true` runs the real eval (with a wrapped,
|
|
20
|
+
* non-fatal preflight probe first).
|
|
21
|
+
*
|
|
22
|
+
* TIER 2 — FABLE JUDGE (provenance `judge:fable`, smarter proxy)
|
|
23
|
+
* For trajectories with no mechanical spec, judge via the cost-disciplined
|
|
24
|
+
* headless Fable harness (fable-harness.ts). Opt-in and OFF by default;
|
|
25
|
+
* spends nothing unless `fableJudge: true` AND a `maxBudgetUsd` cap is set.
|
|
26
|
+
*
|
|
27
|
+
* TIER 3 — STRUCTURAL PROXY (provenance `proxy:structural`, weakest)
|
|
28
|
+
* The existing output-verifier structural confidence. Always available,
|
|
29
|
+
* $0, no external calls. Explicitly the weakest signal, clearly labeled.
|
|
30
|
+
*
|
|
31
|
+
* DEFAULT (no opts): dry-run oracle preflight + structural-proxy fallback.
|
|
32
|
+
* ZERO spend, no SSH exec, no Fable call. Everything degrades gracefully
|
|
33
|
+
* (ADR-150) — a missing remote or a failed probe is reported, never fatal.
|
|
34
|
+
*
|
|
35
|
+
* @module services/distill-oracle
|
|
36
|
+
*/
|
|
37
|
+
import { spawn } from 'child_process';
|
|
38
|
+
import { verifyAndEscalate } from '../ruvector/output-verifier.js';
|
|
39
|
+
import { FableHarness, } from './fable-harness.js';
|
|
40
|
+
// ── Public API ───────────────────────────────────────────────────────────
|
|
41
|
+
/**
|
|
42
|
+
* Label each trajectory with `resolved` + honest provenance, trying the tiers
|
|
43
|
+
* in order per trajectory. See the module header for tier semantics and the
|
|
44
|
+
* zero-spend default guarantee.
|
|
45
|
+
*/
|
|
46
|
+
export async function labelResolved(trajectories, opts = {}) {
|
|
47
|
+
const results = new Array(trajectories.length);
|
|
48
|
+
const preflights = new Map();
|
|
49
|
+
const remote = resolveRemote(opts.remote);
|
|
50
|
+
const fableActive = Boolean(opts.fableJudge) && Number(opts.maxBudgetUsd) > 0;
|
|
51
|
+
// ── Tier 1: mechanical oracle (real exec only under execute:true) ──────────
|
|
52
|
+
const fableQueue = [];
|
|
53
|
+
for (let i = 0; i < trajectories.length; i++) {
|
|
54
|
+
const t = trajectories[i];
|
|
55
|
+
if (hasMechanicalSpec(t)) {
|
|
56
|
+
const plan = buildOraclePlan(t.testSpec, remote);
|
|
57
|
+
if (opts.execute) {
|
|
58
|
+
const runner = opts.runner ?? createSshOracleRunner();
|
|
59
|
+
// Wrapped probe: a failure is REPORTED, not thrown.
|
|
60
|
+
const probe = await safeProbe(runner, plan);
|
|
61
|
+
if (probe.ok) {
|
|
62
|
+
const outcome = await safeRunEval(runner, plan, t.testSpec);
|
|
63
|
+
results[i] = {
|
|
64
|
+
...t,
|
|
65
|
+
resolved: outcome.resolved,
|
|
66
|
+
resolvedBy: 'oracle:test-exec',
|
|
67
|
+
resolvedConfidence: 1,
|
|
68
|
+
resolvedReason: outcome.reason,
|
|
69
|
+
oraclePreflight: { dryRun: false, plan, probe },
|
|
70
|
+
};
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
// Probe failed → report and fall through to next tier.
|
|
74
|
+
preflights.set(i, {
|
|
75
|
+
dryRun: false,
|
|
76
|
+
plan,
|
|
77
|
+
probe,
|
|
78
|
+
fellThroughBecause: `preflight failed: ${probe.reason}`,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
// DRY-RUN: emit the plan/commands, touch nothing, fall through.
|
|
83
|
+
preflights.set(i, {
|
|
84
|
+
dryRun: true,
|
|
85
|
+
plan,
|
|
86
|
+
fellThroughBecause: remote
|
|
87
|
+
? 'dry-run (pass execute:true to run the real eval over SSH)'
|
|
88
|
+
: 'dry-run and no remote configured (set remote / RUFLO_DISTILL_REMOTE)',
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// ── Route to Tier 2 (batched later) or Tier 3 now ──
|
|
93
|
+
if (fableActive) {
|
|
94
|
+
fableQueue.push({ index: i, item: toJudgeItem(t) });
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
results[i] = await proxyLabel(t, opts, preflights.get(i));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// ── Tier 2: batched Fable judge for queued items ──────────────────────────
|
|
101
|
+
if (fableActive && fableQueue.length > 0) {
|
|
102
|
+
const harness = opts.harness ??
|
|
103
|
+
new FableHarness({ maxBudgetUsd: opts.maxBudgetUsd, batchSize: opts.fableBatchSize });
|
|
104
|
+
let verdicts = [];
|
|
105
|
+
try {
|
|
106
|
+
verdicts = await harness.judgeBatch(fableQueue.map((q) => q.item));
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
verdicts = []; // graceful: any harness failure ⇒ proxy fallback for all
|
|
110
|
+
}
|
|
111
|
+
const byId = new Map(verdicts.map((v) => [v.id, v]));
|
|
112
|
+
for (const { index, item } of fableQueue) {
|
|
113
|
+
const v = byId.get(item.id);
|
|
114
|
+
const t = trajectories[index];
|
|
115
|
+
if (v) {
|
|
116
|
+
results[index] = {
|
|
117
|
+
...t,
|
|
118
|
+
resolved: v.resolved,
|
|
119
|
+
resolvedBy: 'judge:fable',
|
|
120
|
+
resolvedConfidence: v.confidence,
|
|
121
|
+
resolvedReason: v.reason,
|
|
122
|
+
...(preflights.has(index) ? { oraclePreflight: preflights.get(index) } : {}),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
// Model omitted this id / budget exhausted → Tier-3 proxy fallback.
|
|
127
|
+
results[index] = await proxyLabel(t, opts, preflights.get(index));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// ── Safety net: nothing should be undefined, but fall back to proxy if so ──
|
|
132
|
+
for (let i = 0; i < results.length; i++) {
|
|
133
|
+
if (results[i] === undefined) {
|
|
134
|
+
results[i] = await proxyLabel(trajectories[i], opts, preflights.get(i));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return results;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Reflective failure analysis (GEPA/evolve mutation input). Thin, cost-
|
|
141
|
+
* disciplined pass-through to the Fable harness. Opt-in: returns [] unless a
|
|
142
|
+
* harness with a budget cap is provided/configured.
|
|
143
|
+
*/
|
|
144
|
+
export async function reflectFailures(items, opts = {}) {
|
|
145
|
+
const harness = opts.harness ??
|
|
146
|
+
new FableHarness({ maxBudgetUsd: opts.maxBudgetUsd, batchSize: opts.fableBatchSize });
|
|
147
|
+
if (!harness.isEnabled())
|
|
148
|
+
return [];
|
|
149
|
+
return harness.reflectFailures(items);
|
|
150
|
+
}
|
|
151
|
+
// ── Tier-3 structural proxy ──────────────────────────────────────────────
|
|
152
|
+
async function proxyLabel(t, opts, preflight) {
|
|
153
|
+
const verdict = await verifyAndEscalate({
|
|
154
|
+
task: t.task ?? '',
|
|
155
|
+
output: t.output ?? '',
|
|
156
|
+
taskKind: t.taskKind,
|
|
157
|
+
});
|
|
158
|
+
const threshold = opts.minStructuralConfidence;
|
|
159
|
+
const resolved = typeof threshold === 'number' ? verdict.score >= threshold : verdict.confident;
|
|
160
|
+
return {
|
|
161
|
+
...t,
|
|
162
|
+
resolved,
|
|
163
|
+
resolvedBy: 'proxy:structural',
|
|
164
|
+
resolvedConfidence: verdict.score,
|
|
165
|
+
resolvedReason: verdict.confident
|
|
166
|
+
? 'structural signals confident'
|
|
167
|
+
: `structural signals weak: ${verdict.reasons.slice(0, 3).join('; ')}`,
|
|
168
|
+
...(preflight ? { oraclePreflight: preflight } : {}),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
// ── Tier-1 planning ──────────────────────────────────────────────────────
|
|
172
|
+
/** A trajectory can be mechanically evaluated iff it carries an actionable spec. */
|
|
173
|
+
export function hasMechanicalSpec(t) {
|
|
174
|
+
const s = t.testSpec;
|
|
175
|
+
if (!s)
|
|
176
|
+
return false;
|
|
177
|
+
return Boolean((s.failToPass && s.failToPass.length > 0) ||
|
|
178
|
+
s.evalCommand ||
|
|
179
|
+
s.benchSuite ||
|
|
180
|
+
s.benchCase);
|
|
181
|
+
}
|
|
182
|
+
/** Resolve the remote host from opts → env, never a hard-coded host. */
|
|
183
|
+
export function resolveRemote(remote) {
|
|
184
|
+
const r = (remote ?? process.env.RUFLO_DISTILL_REMOTE ?? '').trim();
|
|
185
|
+
return r.length > 0 ? r : null;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Build the concrete command plan for a spec. Pure — constructs command strings
|
|
189
|
+
* only; executes nothing. `remote` is always substituted from the parameter,
|
|
190
|
+
* never hard-coded.
|
|
191
|
+
*/
|
|
192
|
+
export function buildOraclePlan(spec, remote) {
|
|
193
|
+
const host = remote; // may be null; commands render `<remote>` placeholder then
|
|
194
|
+
const r = host ?? '<remote>';
|
|
195
|
+
const sshLocal = host ? `ssh ${host} ` : `ssh <remote> `;
|
|
196
|
+
const workdir = spec.workdir ?? (spec.repo ? `~/ruflo-distill/${sanitize(spec.repo)}` : '~/ruflo-distill/work');
|
|
197
|
+
// Preflight probes — reachability + the tools Tier-1 needs on the remote.
|
|
198
|
+
const probeCommands = [
|
|
199
|
+
`ssh -o BatchMode=yes -o ConnectTimeout=8 ${r} 'echo ok'`,
|
|
200
|
+
`${sshLocal}'command -v docker >/dev/null 2>&1 && echo docker-present || echo docker-missing'`,
|
|
201
|
+
`${sshLocal}'command -v darwin >/dev/null 2>&1 || npx --yes @metaharness/darwin --version'`,
|
|
202
|
+
];
|
|
203
|
+
let kind;
|
|
204
|
+
const evalCommands = [];
|
|
205
|
+
let parseHint;
|
|
206
|
+
if (spec.benchSuite || spec.benchCase) {
|
|
207
|
+
kind = 'ssh-darwin-bench';
|
|
208
|
+
const suite = spec.benchSuite ? `--suite ${shq(spec.benchSuite)}` : '';
|
|
209
|
+
const kase = spec.benchCase ? `--case ${shq(spec.benchCase)}` : '';
|
|
210
|
+
evalCommands.push(`${sshLocal}'cd ${workdir} && npx --yes @metaharness/darwin bench run ${suite} ${kase} --json'`.replace(/\s+/g, ' ').trim());
|
|
211
|
+
parseHint = 'resolved = darwin bench run reports the case scored PASS (exit 0, json.passed=true)';
|
|
212
|
+
}
|
|
213
|
+
else if (spec.evalCommand) {
|
|
214
|
+
kind = 'ssh-eval';
|
|
215
|
+
evalCommands.push(`${sshLocal}'cd ${workdir} && ${spec.evalCommand}'`);
|
|
216
|
+
parseHint = 'resolved = evalCommand exits 0';
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
kind = 'ssh-swebench-eval';
|
|
220
|
+
if (spec.patch) {
|
|
221
|
+
evalCommands.push(`${sshLocal}'cd ${workdir} && git apply - <<"PATCH"\n${spec.patch}\nPATCH'`);
|
|
222
|
+
}
|
|
223
|
+
const f2p = (spec.failToPass ?? []).map(shq).join(' ');
|
|
224
|
+
const p2p = (spec.passToPass ?? []).map(shq).join(' ');
|
|
225
|
+
evalCommands.push(`${sshLocal}'cd ${workdir} && npx --yes @metaharness/darwin swebench-eval` +
|
|
226
|
+
`${spec.repo ? ` --repo ${shq(spec.repo)}` : ''}` +
|
|
227
|
+
`${spec.baseCommit ? ` --base ${shq(spec.baseCommit)}` : ''}` +
|
|
228
|
+
`${f2p ? ` --fail-to-pass ${f2p}` : ''}` +
|
|
229
|
+
`${p2p ? ` --pass-to-pass ${p2p}` : ''} --json'`);
|
|
230
|
+
parseHint = 'resolved = all FAIL_TO_PASS now pass AND all PASS_TO_PASS still pass';
|
|
231
|
+
}
|
|
232
|
+
return { kind, remote: host, probeCommands, evalCommands, parseHint };
|
|
233
|
+
}
|
|
234
|
+
// ── Default SSH runner (real exec; injectable for tests) ─────────────────────
|
|
235
|
+
/**
|
|
236
|
+
* Default command executor: shells out, piping any heredoc/stdin via argv only
|
|
237
|
+
* (commands are self-contained strings run through `sh -c`). Never invoked on
|
|
238
|
+
* the default (dry-run) path.
|
|
239
|
+
*/
|
|
240
|
+
export const defaultCommandExec = (cmd, args, opts) => new Promise((resolve) => {
|
|
241
|
+
let child;
|
|
242
|
+
try {
|
|
243
|
+
child = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
resolve({ stdout: '', stderr: err instanceof Error ? err.message : String(err), code: null });
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
let stdout = '';
|
|
250
|
+
let stderr = '';
|
|
251
|
+
let done = false;
|
|
252
|
+
const finish = (r) => { if (!done) {
|
|
253
|
+
done = true;
|
|
254
|
+
clearTimeout(timer);
|
|
255
|
+
resolve(r);
|
|
256
|
+
} };
|
|
257
|
+
const timer = setTimeout(() => {
|
|
258
|
+
try {
|
|
259
|
+
child.kill('SIGTERM');
|
|
260
|
+
}
|
|
261
|
+
catch { /* dead */ }
|
|
262
|
+
finish({ stdout, stderr: stderr || `timed out after ${opts.timeoutMs}ms`, code: null });
|
|
263
|
+
}, opts.timeoutMs);
|
|
264
|
+
timer.unref?.();
|
|
265
|
+
child.stdout?.on('data', (d) => { stdout += d.toString(); });
|
|
266
|
+
child.stderr?.on('data', (d) => { stderr += d.toString(); });
|
|
267
|
+
child.on('error', (e) => finish({ stdout, stderr: e.message, code: null }));
|
|
268
|
+
child.on('close', (code) => finish({ stdout, stderr, code }));
|
|
269
|
+
});
|
|
270
|
+
/**
|
|
271
|
+
* Build a real SSH-backed oracle runner. `exec` is injectable so tests assert
|
|
272
|
+
* command construction without ever touching a network. Each plan command is
|
|
273
|
+
* run via `sh -c <command-string>` so the fully-rendered ssh line executes as-is.
|
|
274
|
+
*/
|
|
275
|
+
export function createSshOracleRunner(exec = defaultCommandExec, timeoutMs = 15 * 60 * 1000) {
|
|
276
|
+
const run = (command) => exec('sh', ['-c', command], { timeoutMs });
|
|
277
|
+
return {
|
|
278
|
+
async preflight(plan) {
|
|
279
|
+
if (!plan.remote)
|
|
280
|
+
return { ok: false, reason: 'no remote configured' };
|
|
281
|
+
for (const c of plan.probeCommands) {
|
|
282
|
+
const res = await run(c);
|
|
283
|
+
if (res.code !== 0) {
|
|
284
|
+
return { ok: false, reason: `probe failed (${c.slice(0, 60)}…): ${(res.stderr || '').slice(0, 120)}` };
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return { ok: true, reason: 'remote reachable; docker/darwin present' };
|
|
288
|
+
},
|
|
289
|
+
async runEval(plan) {
|
|
290
|
+
let lastReason = 'no eval commands';
|
|
291
|
+
for (const c of plan.evalCommands) {
|
|
292
|
+
const res = await run(c);
|
|
293
|
+
if (res.code !== 0) {
|
|
294
|
+
return { resolved: false, reason: `eval failed (exit ${res.code}): ${(res.stderr || res.stdout || '').slice(0, 160)}` };
|
|
295
|
+
}
|
|
296
|
+
lastReason = interpretEvalStdout(res.stdout);
|
|
297
|
+
if (lastReason.startsWith('NOT_RESOLVED')) {
|
|
298
|
+
return { resolved: false, reason: lastReason };
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return { resolved: true, reason: `${plan.parseHint} — ${lastReason}` };
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
/** Interpret an eval command's JSON stdout for an explicit pass/fail signal. */
|
|
306
|
+
function interpretEvalStdout(stdout) {
|
|
307
|
+
const trimmed = (stdout ?? '').trim();
|
|
308
|
+
const m = trimmed.match(/\{[\s\S]*\}/);
|
|
309
|
+
if (m) {
|
|
310
|
+
try {
|
|
311
|
+
const o = JSON.parse(m[0]);
|
|
312
|
+
if (o.passed === false || o.resolved === false)
|
|
313
|
+
return 'NOT_RESOLVED (json.passed=false)';
|
|
314
|
+
if (o.passed === true || o.resolved === true)
|
|
315
|
+
return 'json.passed=true';
|
|
316
|
+
}
|
|
317
|
+
catch { /* fall through to exit-code semantics */ }
|
|
318
|
+
}
|
|
319
|
+
return 'exit 0';
|
|
320
|
+
}
|
|
321
|
+
// ── small helpers ────────────────────────────────────────────────────────
|
|
322
|
+
async function safeProbe(runner, plan) {
|
|
323
|
+
try {
|
|
324
|
+
return await runner.preflight(plan);
|
|
325
|
+
}
|
|
326
|
+
catch (err) {
|
|
327
|
+
return { ok: false, reason: `probe threw: ${err instanceof Error ? err.message : String(err)}` };
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
async function safeRunEval(runner, plan, spec) {
|
|
331
|
+
try {
|
|
332
|
+
return await runner.runEval(plan, spec);
|
|
333
|
+
}
|
|
334
|
+
catch (err) {
|
|
335
|
+
return { resolved: false, reason: `eval threw: ${err instanceof Error ? err.message : String(err)}` };
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
function toJudgeItem(t) {
|
|
339
|
+
return { id: t.id, task: t.task ?? '', output: t.output ?? '' };
|
|
340
|
+
}
|
|
341
|
+
function sanitize(s) {
|
|
342
|
+
return s.replace(/[^A-Za-z0-9._-]/g, '_');
|
|
343
|
+
}
|
|
344
|
+
/** Single-quote a token for safe interpolation into a shell command string. */
|
|
345
|
+
function shq(s) {
|
|
346
|
+
return `'${String(s).replace(/'/g, `'\\''`)}'`;
|
|
347
|
+
}
|
|
348
|
+
export default labelResolved;
|
|
349
|
+
//# sourceMappingURL=distill-oracle.js.map
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fable-harness.ts — Cost-disciplined headless Fable LLM-as-judge harness.
|
|
3
|
+
*
|
|
4
|
+
* This is TIER 2 of the tiered `resolved` oracle (see distill-oracle.ts). When
|
|
5
|
+
* a trajectory has NO mechanical test spec to execute, we judge whether its
|
|
6
|
+
* completion actually resolved the task with a headless Fable model — a smarter
|
|
7
|
+
* proxy than the structural verifier, but still a proxy (provenance tag
|
|
8
|
+
* `judge:fable`, never presented as ground truth per ADR-169).
|
|
9
|
+
*
|
|
10
|
+
* ── MEASURED COST DATA (load-bearing — this file is built around it) ────────
|
|
11
|
+
* `claude -p --model claude-fable-5 --output-format json` costs, per call:
|
|
12
|
+
* • ~$1.56 when launched FROM THE PROJECT DIR — it auto-loads CLAUDE.md and
|
|
13
|
+
* ~56k cache tokens of project context we do NOT want for judging.
|
|
14
|
+
* • ~$0.34 from a CLEAN empty cwd with `--append-system-prompt` for the role.
|
|
15
|
+
* • ~$0.02/item when we BATCH ~20 items into a single call (context amortizes
|
|
16
|
+
* ~free across the batch).
|
|
17
|
+
*
|
|
18
|
+
* Therefore this harness MUST, and does:
|
|
19
|
+
* 1. run `claude -p` from a FRESH EMPTY TEMP cwd — never the project dir, so
|
|
20
|
+
* no CLAUDE.md / project context is loaded (the 5x cost driver);
|
|
21
|
+
* 2. carry the judge/reflect role via `--append-system-prompt`, not a project
|
|
22
|
+
* system prompt;
|
|
23
|
+
* 3. BATCH N items per call (default 20) so per-item cost collapses to ~$0.02;
|
|
24
|
+
* 4. pass `--max-budget-usd` as a hard per-call cap and stop launching batches
|
|
25
|
+
* once the cumulative measured spend reaches the caller's budget;
|
|
26
|
+
* 5. be OPT-IN and OFF BY DEFAULT — nothing here runs unless a caller
|
|
27
|
+
* explicitly constructs the harness AND provides a budget cap.
|
|
28
|
+
*
|
|
29
|
+
* SAFETY: constructing this class spends nothing. Only `judgeBatch` /
|
|
30
|
+
* `reflectFailures` spawn `claude`, and only when `maxBudgetUsd > 0`. The
|
|
31
|
+
* `spawnClaude` implementation is injectable so tests never touch the real CLI.
|
|
32
|
+
*
|
|
33
|
+
* @module services/fable-harness
|
|
34
|
+
*/
|
|
35
|
+
export declare const FABLE_COST_MODEL: {
|
|
36
|
+
/** Measured $/call when launched from the project dir (loads CLAUDE.md). Anti-pattern. */
|
|
37
|
+
readonly perCallProjectCwdUsd: 1.56;
|
|
38
|
+
/** Measured $/call from a clean cwd with --append-system-prompt. */
|
|
39
|
+
readonly perCallCleanCwdUsd: 0.34;
|
|
40
|
+
/** Measured amortized $/item when batching ~20 items per call. */
|
|
41
|
+
readonly perItemBatchedUsd: 0.02;
|
|
42
|
+
/** Default items per `claude -p` call. */
|
|
43
|
+
readonly defaultBatchSize: 20;
|
|
44
|
+
/** The judge model. */
|
|
45
|
+
readonly model: "claude-fable-5";
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Estimate the USD cost of judging `itemCount` items in batches of `batchSize`.
|
|
49
|
+
* Uses the measured amortized per-item figure; callers use this to size a
|
|
50
|
+
* budget cap before opting in.
|
|
51
|
+
*/
|
|
52
|
+
export declare function estimateFableCostUsd(itemCount: number, batchSize?: number): number;
|
|
53
|
+
/** One item to judge: did `output` actually resolve `task`? */
|
|
54
|
+
export interface JudgeItem {
|
|
55
|
+
id: string;
|
|
56
|
+
task: string;
|
|
57
|
+
output: string;
|
|
58
|
+
}
|
|
59
|
+
/** Verdict for a single judged item. */
|
|
60
|
+
export interface JudgeResult {
|
|
61
|
+
id: string;
|
|
62
|
+
resolved: boolean;
|
|
63
|
+
/** 0..1 self-reported judge confidence. */
|
|
64
|
+
confidence: number;
|
|
65
|
+
reason: string;
|
|
66
|
+
}
|
|
67
|
+
/** One item for reflective failure analysis (GEPA/evolve mutation input). */
|
|
68
|
+
export interface ReflectItem {
|
|
69
|
+
id: string;
|
|
70
|
+
task: string;
|
|
71
|
+
output: string;
|
|
72
|
+
/** Optional signal that this trajectory is believed to have failed. */
|
|
73
|
+
failureHint?: string;
|
|
74
|
+
}
|
|
75
|
+
/** Reflective diagnosis for a single item (the reflective-mutation SOTA trick). */
|
|
76
|
+
export interface ReflectResult {
|
|
77
|
+
id: string;
|
|
78
|
+
failureClass: string;
|
|
79
|
+
diagnosis: string;
|
|
80
|
+
mutationHint: string;
|
|
81
|
+
}
|
|
82
|
+
/** Result of a raw claude spawn. */
|
|
83
|
+
export interface ClaudeSpawnResult {
|
|
84
|
+
stdout: string;
|
|
85
|
+
stderr: string;
|
|
86
|
+
code: number | null;
|
|
87
|
+
/** Measured spend for this call, parsed from the JSON envelope when present. */
|
|
88
|
+
costUsd?: number;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Injectable spawner. Receives the argv (after `claude`), the prompt to pipe to
|
|
92
|
+
* stdin, and the cwd (a fresh empty temp dir). Default implementation shells out
|
|
93
|
+
* to the real `claude` CLI; tests inject a fake.
|
|
94
|
+
*/
|
|
95
|
+
export type ClaudeSpawnFn = (argv: string[], stdinPrompt: string, cwd: string, opts: {
|
|
96
|
+
timeoutMs: number;
|
|
97
|
+
}) => Promise<ClaudeSpawnResult>;
|
|
98
|
+
export interface FableHarnessOptions {
|
|
99
|
+
/** Model id (default claude-fable-5). */
|
|
100
|
+
model?: string;
|
|
101
|
+
/** Items per `claude -p` call (default 20). */
|
|
102
|
+
batchSize?: number;
|
|
103
|
+
/**
|
|
104
|
+
* Hard budget cap in USD across all calls this harness makes. REQUIRED to be
|
|
105
|
+
* > 0 for any spend to happen — 0/undefined means the harness refuses to
|
|
106
|
+
* spawn (safe default).
|
|
107
|
+
*/
|
|
108
|
+
maxBudgetUsd?: number;
|
|
109
|
+
/** Per-call timeout (default 5 min). */
|
|
110
|
+
timeoutMs?: number;
|
|
111
|
+
/** Injected spawner for tests; defaults to the real `claude` CLI. */
|
|
112
|
+
spawnClaude?: ClaudeSpawnFn;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Default `claude -p` spawner. Pipes the prompt via stdin (never as an argv
|
|
116
|
+
* positional — mirrors the #1852 fix so shell metachars in prompts are never
|
|
117
|
+
* re-tokenized), runs in the provided (temp) cwd, and returns stdout/stderr.
|
|
118
|
+
* Parses `total_cost_usd` from the `--output-format json` envelope when present.
|
|
119
|
+
*/
|
|
120
|
+
export declare const defaultSpawnClaude: ClaudeSpawnFn;
|
|
121
|
+
/** Pull `total_cost_usd`/`cost_usd` out of a claude `--output-format json` envelope. */
|
|
122
|
+
export declare function parseCostFromEnvelope(stdout: string): number | undefined;
|
|
123
|
+
export declare class FableHarness {
|
|
124
|
+
private readonly model;
|
|
125
|
+
private readonly batchSize;
|
|
126
|
+
private readonly maxBudgetUsd;
|
|
127
|
+
private readonly timeoutMs;
|
|
128
|
+
private readonly spawnClaude;
|
|
129
|
+
private spentUsd;
|
|
130
|
+
constructor(opts?: FableHarnessOptions);
|
|
131
|
+
/** Cumulative measured spend across all calls this harness has made. */
|
|
132
|
+
getSpentUsd(): number;
|
|
133
|
+
/** True when a positive budget cap is configured (a precondition for any spend). */
|
|
134
|
+
isEnabled(): boolean;
|
|
135
|
+
/**
|
|
136
|
+
* Judge a set of items in batches. Returns one JudgeResult per input id that
|
|
137
|
+
* the model returned. Items that fall outside the budget, or that the model
|
|
138
|
+
* omits, are simply absent from the result — the caller (distill-oracle) is
|
|
139
|
+
* responsible for falling back to the structural proxy for those.
|
|
140
|
+
*
|
|
141
|
+
* Spends $0 and returns [] when no budget cap is configured.
|
|
142
|
+
*/
|
|
143
|
+
judgeBatch(items: JudgeItem[]): Promise<JudgeResult[]>;
|
|
144
|
+
/**
|
|
145
|
+
* Reflective failure analysis over items — the second cost-disciplined entry
|
|
146
|
+
* point, used by GEPA/evolve for mutation hints. Same batching + budget
|
|
147
|
+
* discipline as judgeBatch. Returns [] when no budget cap is configured.
|
|
148
|
+
*/
|
|
149
|
+
reflectFailures(items: ReflectItem[]): Promise<ReflectResult[]>;
|
|
150
|
+
/** Build the argv for a `claude -p` call. Exposed shape for testability. */
|
|
151
|
+
buildArgv(systemPrompt: string): string[];
|
|
152
|
+
/**
|
|
153
|
+
* Run one batch: create a FRESH EMPTY temp cwd (critical — no project
|
|
154
|
+
* context), spawn `claude -p` there with the role via --append-system-prompt,
|
|
155
|
+
* pipe the batch JSON to stdin, parse the verdict array out of the envelope,
|
|
156
|
+
* and account the measured spend.
|
|
157
|
+
*/
|
|
158
|
+
private runBatch;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Extract the model's verdict array. `claude -p --output-format json` wraps the
|
|
162
|
+
* assistant text in an envelope `{ result: "<text>", ... }`; the text is itself
|
|
163
|
+
* the JSON array we asked for. Handle both the enveloped and bare forms, and
|
|
164
|
+
* arrays fenced in ```json blocks.
|
|
165
|
+
*/
|
|
166
|
+
export declare function extractVerdictArray(stdout: string): unknown[];
|
|
167
|
+
export default FableHarness;
|
|
168
|
+
//# sourceMappingURL=fable-harness.d.ts.map
|