claude-flow 3.20.0 → 3.21.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.
- package/package.json +1 -1
- 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/doctor.js +56 -0
- package/v3/@claude-flow/cli/dist/src/commands/neural.js +309 -1
- package/v3/@claude-flow/cli/dist/src/init/executor.js +17 -0
- package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +26 -2
- package/v3/@claude-flow/cli/dist/src/init/memory-package-resolver.d.ts +53 -0
- package/v3/@claude-flow/cli/dist/src/init/memory-package-resolver.js +118 -0
- 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 +9 -82
- 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/memory/memory-bridge.js +49 -8
- 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/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,347 @@
|
|
|
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
|
+
import { spawn } from 'child_process';
|
|
36
|
+
import { mkdtemp, rm } from 'fs/promises';
|
|
37
|
+
import { tmpdir } from 'os';
|
|
38
|
+
import { join } from 'path';
|
|
39
|
+
// ── Cost model (measured — exported so callers/docs share one source) ────────
|
|
40
|
+
export const FABLE_COST_MODEL = {
|
|
41
|
+
/** Measured $/call when launched from the project dir (loads CLAUDE.md). Anti-pattern. */
|
|
42
|
+
perCallProjectCwdUsd: 1.56,
|
|
43
|
+
/** Measured $/call from a clean cwd with --append-system-prompt. */
|
|
44
|
+
perCallCleanCwdUsd: 0.34,
|
|
45
|
+
/** Measured amortized $/item when batching ~20 items per call. */
|
|
46
|
+
perItemBatchedUsd: 0.02,
|
|
47
|
+
/** Default items per `claude -p` call. */
|
|
48
|
+
defaultBatchSize: 20,
|
|
49
|
+
/** The judge model. */
|
|
50
|
+
model: 'claude-fable-5',
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Estimate the USD cost of judging `itemCount` items in batches of `batchSize`.
|
|
54
|
+
* Uses the measured amortized per-item figure; callers use this to size a
|
|
55
|
+
* budget cap before opting in.
|
|
56
|
+
*/
|
|
57
|
+
export function estimateFableCostUsd(itemCount, batchSize = FABLE_COST_MODEL.defaultBatchSize) {
|
|
58
|
+
if (itemCount <= 0)
|
|
59
|
+
return 0;
|
|
60
|
+
const batches = Math.ceil(itemCount / Math.max(1, batchSize));
|
|
61
|
+
// Amortized per-item dominates; add a tiny floor per batch for the base call.
|
|
62
|
+
const perItem = itemCount * FABLE_COST_MODEL.perItemBatchedUsd;
|
|
63
|
+
const perBatchFloor = batches * 0.0; // per-item figure already includes base amortization
|
|
64
|
+
return Number((perItem + perBatchFloor).toFixed(4));
|
|
65
|
+
}
|
|
66
|
+
// ── Prompt templates (carried via --append-system-prompt) ────────────────────
|
|
67
|
+
const JUDGE_SYSTEM_PROMPT = [
|
|
68
|
+
'You are a strict evaluator judging whether an agent trajectory RESOLVED its task.',
|
|
69
|
+
'You receive a JSON array of items: [{id, task, output}].',
|
|
70
|
+
'For each item decide if `output` genuinely and correctly resolves `task`.',
|
|
71
|
+
'Be conservative: plausible-but-unverified or partial completions are NOT resolved.',
|
|
72
|
+
'Reply with ONLY a JSON array, one object per input id, no prose:',
|
|
73
|
+
'[{"id": "...", "resolved": true|false, "confidence": 0.0-1.0, "reason": "<one sentence>"}]',
|
|
74
|
+
].join('\n');
|
|
75
|
+
const REFLECT_SYSTEM_PROMPT = [
|
|
76
|
+
'You are a failure-analysis expert supporting reflective mutation (GEPA/evolve).',
|
|
77
|
+
'You receive a JSON array of items: [{id, task, output, failureHint?}].',
|
|
78
|
+
'For each item, classify the failure and propose a concrete mutation hint.',
|
|
79
|
+
'Reply with ONLY a JSON array, one object per input id, no prose:',
|
|
80
|
+
'[{"id":"...","failureClass":"<short label>","diagnosis":"<one sentence>","mutationHint":"<actionable>"}]',
|
|
81
|
+
].join('\n');
|
|
82
|
+
// ── Default spawner (real CLI) ───────────────────────────────────────────────
|
|
83
|
+
/**
|
|
84
|
+
* Default `claude -p` spawner. Pipes the prompt via stdin (never as an argv
|
|
85
|
+
* positional — mirrors the #1852 fix so shell metachars in prompts are never
|
|
86
|
+
* re-tokenized), runs in the provided (temp) cwd, and returns stdout/stderr.
|
|
87
|
+
* Parses `total_cost_usd` from the `--output-format json` envelope when present.
|
|
88
|
+
*/
|
|
89
|
+
export const defaultSpawnClaude = (argv, stdinPrompt, cwd, opts) => new Promise((resolve) => {
|
|
90
|
+
let child;
|
|
91
|
+
try {
|
|
92
|
+
child = spawn('claude', argv, {
|
|
93
|
+
cwd,
|
|
94
|
+
env: { ...process.env, CLAUDE_ENTRYPOINT: 'fable-judge' },
|
|
95
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
96
|
+
windowsHide: true,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
resolve({ stdout: '', stderr: err instanceof Error ? err.message : String(err), code: null });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
let stdout = '';
|
|
104
|
+
let stderr = '';
|
|
105
|
+
let done = false;
|
|
106
|
+
const finish = (r) => {
|
|
107
|
+
if (done)
|
|
108
|
+
return;
|
|
109
|
+
done = true;
|
|
110
|
+
clearTimeout(timer);
|
|
111
|
+
resolve(r);
|
|
112
|
+
};
|
|
113
|
+
const timer = setTimeout(() => {
|
|
114
|
+
try {
|
|
115
|
+
child.kill('SIGTERM');
|
|
116
|
+
}
|
|
117
|
+
catch { /* already dead */ }
|
|
118
|
+
finish({ stdout, stderr: stderr || `timed out after ${opts.timeoutMs}ms`, code: null });
|
|
119
|
+
}, opts.timeoutMs);
|
|
120
|
+
timer.unref?.();
|
|
121
|
+
try {
|
|
122
|
+
child.stdin?.end(stdinPrompt);
|
|
123
|
+
}
|
|
124
|
+
catch { /* surfaced via error */ }
|
|
125
|
+
child.stdout?.on('data', (d) => { stdout += d.toString(); });
|
|
126
|
+
child.stderr?.on('data', (d) => { stderr += d.toString(); });
|
|
127
|
+
child.on('error', (e) => finish({ stdout, stderr: e.message, code: null }));
|
|
128
|
+
child.on('close', (code) => {
|
|
129
|
+
finish({ stdout, stderr, code, costUsd: parseCostFromEnvelope(stdout) });
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
/** Pull `total_cost_usd`/`cost_usd` out of a claude `--output-format json` envelope. */
|
|
133
|
+
export function parseCostFromEnvelope(stdout) {
|
|
134
|
+
const env = tryParseJson(stdout);
|
|
135
|
+
if (env && typeof env === 'object') {
|
|
136
|
+
const o = env;
|
|
137
|
+
for (const k of ['total_cost_usd', 'cost_usd', 'costUsd']) {
|
|
138
|
+
if (typeof o[k] === 'number')
|
|
139
|
+
return o[k];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
// ── Harness ──────────────────────────────────────────────────────────────
|
|
145
|
+
export class FableHarness {
|
|
146
|
+
model;
|
|
147
|
+
batchSize;
|
|
148
|
+
maxBudgetUsd;
|
|
149
|
+
timeoutMs;
|
|
150
|
+
spawnClaude;
|
|
151
|
+
spentUsd = 0;
|
|
152
|
+
constructor(opts = {}) {
|
|
153
|
+
this.model = opts.model ?? FABLE_COST_MODEL.model;
|
|
154
|
+
this.batchSize = Math.max(1, opts.batchSize ?? FABLE_COST_MODEL.defaultBatchSize);
|
|
155
|
+
this.maxBudgetUsd = opts.maxBudgetUsd ?? 0;
|
|
156
|
+
this.timeoutMs = opts.timeoutMs ?? 5 * 60 * 1000;
|
|
157
|
+
this.spawnClaude = opts.spawnClaude ?? defaultSpawnClaude;
|
|
158
|
+
}
|
|
159
|
+
/** Cumulative measured spend across all calls this harness has made. */
|
|
160
|
+
getSpentUsd() {
|
|
161
|
+
return Number(this.spentUsd.toFixed(4));
|
|
162
|
+
}
|
|
163
|
+
/** True when a positive budget cap is configured (a precondition for any spend). */
|
|
164
|
+
isEnabled() {
|
|
165
|
+
return this.maxBudgetUsd > 0;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Judge a set of items in batches. Returns one JudgeResult per input id that
|
|
169
|
+
* the model returned. Items that fall outside the budget, or that the model
|
|
170
|
+
* omits, are simply absent from the result — the caller (distill-oracle) is
|
|
171
|
+
* responsible for falling back to the structural proxy for those.
|
|
172
|
+
*
|
|
173
|
+
* Spends $0 and returns [] when no budget cap is configured.
|
|
174
|
+
*/
|
|
175
|
+
async judgeBatch(items) {
|
|
176
|
+
if (items.length === 0)
|
|
177
|
+
return [];
|
|
178
|
+
if (!this.isEnabled())
|
|
179
|
+
return [];
|
|
180
|
+
const out = [];
|
|
181
|
+
for (const batch of chunk(items, this.batchSize)) {
|
|
182
|
+
if (this.spentUsd >= this.maxBudgetUsd)
|
|
183
|
+
break;
|
|
184
|
+
const parsed = await this.runBatch(JUDGE_SYSTEM_PROMPT, batch);
|
|
185
|
+
for (const raw of parsed) {
|
|
186
|
+
const r = normalizeJudge(raw);
|
|
187
|
+
if (r)
|
|
188
|
+
out.push(r);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Reflective failure analysis over items — the second cost-disciplined entry
|
|
195
|
+
* point, used by GEPA/evolve for mutation hints. Same batching + budget
|
|
196
|
+
* discipline as judgeBatch. Returns [] when no budget cap is configured.
|
|
197
|
+
*/
|
|
198
|
+
async reflectFailures(items) {
|
|
199
|
+
if (items.length === 0)
|
|
200
|
+
return [];
|
|
201
|
+
if (!this.isEnabled())
|
|
202
|
+
return [];
|
|
203
|
+
const out = [];
|
|
204
|
+
for (const batch of chunk(items, this.batchSize)) {
|
|
205
|
+
if (this.spentUsd >= this.maxBudgetUsd)
|
|
206
|
+
break;
|
|
207
|
+
const parsed = await this.runBatch(REFLECT_SYSTEM_PROMPT, batch);
|
|
208
|
+
for (const raw of parsed) {
|
|
209
|
+
const r = normalizeReflect(raw);
|
|
210
|
+
if (r)
|
|
211
|
+
out.push(r);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
/** Build the argv for a `claude -p` call. Exposed shape for testability. */
|
|
217
|
+
buildArgv(systemPrompt) {
|
|
218
|
+
const perCallCap = Math.max(0, this.maxBudgetUsd - this.spentUsd);
|
|
219
|
+
return [
|
|
220
|
+
'-p',
|
|
221
|
+
'--model', this.model,
|
|
222
|
+
'--output-format', 'json',
|
|
223
|
+
'--append-system-prompt', systemPrompt,
|
|
224
|
+
'--max-budget-usd', String(round(perCallCap)),
|
|
225
|
+
];
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Run one batch: create a FRESH EMPTY temp cwd (critical — no project
|
|
229
|
+
* context), spawn `claude -p` there with the role via --append-system-prompt,
|
|
230
|
+
* pipe the batch JSON to stdin, parse the verdict array out of the envelope,
|
|
231
|
+
* and account the measured spend.
|
|
232
|
+
*/
|
|
233
|
+
async runBatch(systemPrompt, batch) {
|
|
234
|
+
const argv = this.buildArgv(systemPrompt);
|
|
235
|
+
const stdinPrompt = JSON.stringify(batch);
|
|
236
|
+
const cwd = await mkdtemp(join(tmpdir(), 'ruflo-fable-'));
|
|
237
|
+
try {
|
|
238
|
+
const res = await this.spawnClaude(argv, stdinPrompt, cwd, { timeoutMs: this.timeoutMs });
|
|
239
|
+
// Account spend: prefer measured envelope cost, else the amortized estimate.
|
|
240
|
+
this.spentUsd += typeof res.costUsd === 'number'
|
|
241
|
+
? res.costUsd
|
|
242
|
+
: estimateFableCostUsd(batch.length, this.batchSize);
|
|
243
|
+
if (res.code !== 0 && !res.stdout)
|
|
244
|
+
return [];
|
|
245
|
+
return extractVerdictArray(res.stdout);
|
|
246
|
+
}
|
|
247
|
+
finally {
|
|
248
|
+
await rm(cwd, { recursive: true, force: true }).catch(() => { });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// ── Parsing helpers ──────────────────────────────────────────────────────
|
|
253
|
+
/**
|
|
254
|
+
* Extract the model's verdict array. `claude -p --output-format json` wraps the
|
|
255
|
+
* assistant text in an envelope `{ result: "<text>", ... }`; the text is itself
|
|
256
|
+
* the JSON array we asked for. Handle both the enveloped and bare forms, and
|
|
257
|
+
* arrays fenced in ```json blocks.
|
|
258
|
+
*/
|
|
259
|
+
export function extractVerdictArray(stdout) {
|
|
260
|
+
const trimmed = (stdout ?? '').trim();
|
|
261
|
+
if (!trimmed)
|
|
262
|
+
return [];
|
|
263
|
+
// 1. Enveloped: parse envelope, then its `result` field.
|
|
264
|
+
const env = tryParseJson(trimmed);
|
|
265
|
+
if (env && typeof env === 'object' && !Array.isArray(env)) {
|
|
266
|
+
const result = env.result;
|
|
267
|
+
if (typeof result === 'string') {
|
|
268
|
+
const inner = findJsonArray(result);
|
|
269
|
+
if (inner)
|
|
270
|
+
return inner;
|
|
271
|
+
}
|
|
272
|
+
// Some envelopes embed the array directly.
|
|
273
|
+
const direct = env.results;
|
|
274
|
+
if (Array.isArray(direct))
|
|
275
|
+
return direct;
|
|
276
|
+
}
|
|
277
|
+
if (Array.isArray(env))
|
|
278
|
+
return env;
|
|
279
|
+
// 2. Bare text: find a JSON array anywhere in stdout.
|
|
280
|
+
const arr = findJsonArray(trimmed);
|
|
281
|
+
return arr ?? [];
|
|
282
|
+
}
|
|
283
|
+
function findJsonArray(text) {
|
|
284
|
+
const fenced = text.match(/```(?:json)?\s*(\[[\s\S]*?\])\s*```/);
|
|
285
|
+
if (fenced) {
|
|
286
|
+
const v = tryParseJson(fenced[1]);
|
|
287
|
+
if (Array.isArray(v))
|
|
288
|
+
return v;
|
|
289
|
+
}
|
|
290
|
+
const bare = text.match(/\[[\s\S]*\]/);
|
|
291
|
+
if (bare) {
|
|
292
|
+
const v = tryParseJson(bare[0]);
|
|
293
|
+
if (Array.isArray(v))
|
|
294
|
+
return v;
|
|
295
|
+
}
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
function normalizeJudge(raw) {
|
|
299
|
+
if (!raw || typeof raw !== 'object')
|
|
300
|
+
return null;
|
|
301
|
+
const o = raw;
|
|
302
|
+
if (typeof o.id !== 'string')
|
|
303
|
+
return null;
|
|
304
|
+
return {
|
|
305
|
+
id: o.id,
|
|
306
|
+
resolved: o.resolved === true || o.resolved === 'true',
|
|
307
|
+
confidence: clamp01(typeof o.confidence === 'number' ? o.confidence : Number(o.confidence)),
|
|
308
|
+
reason: typeof o.reason === 'string' ? o.reason : '',
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function normalizeReflect(raw) {
|
|
312
|
+
if (!raw || typeof raw !== 'object')
|
|
313
|
+
return null;
|
|
314
|
+
const o = raw;
|
|
315
|
+
if (typeof o.id !== 'string')
|
|
316
|
+
return null;
|
|
317
|
+
return {
|
|
318
|
+
id: o.id,
|
|
319
|
+
failureClass: typeof o.failureClass === 'string' ? o.failureClass : 'unknown',
|
|
320
|
+
diagnosis: typeof o.diagnosis === 'string' ? o.diagnosis : '',
|
|
321
|
+
mutationHint: typeof o.mutationHint === 'string' ? o.mutationHint : '',
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
function tryParseJson(s) {
|
|
325
|
+
try {
|
|
326
|
+
return JSON.parse(s);
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function chunk(arr, size) {
|
|
333
|
+
const out = [];
|
|
334
|
+
for (let i = 0; i < arr.length; i += size)
|
|
335
|
+
out.push(arr.slice(i, i + size));
|
|
336
|
+
return out;
|
|
337
|
+
}
|
|
338
|
+
function clamp01(n) {
|
|
339
|
+
if (!Number.isFinite(n))
|
|
340
|
+
return 0;
|
|
341
|
+
return Math.max(0, Math.min(1, n));
|
|
342
|
+
}
|
|
343
|
+
function round(n) {
|
|
344
|
+
return Math.round(n * 100) / 100;
|
|
345
|
+
}
|
|
346
|
+
export default FableHarness;
|
|
347
|
+
//# sourceMappingURL=fable-harness.js.map
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SwarmMemoryBranches — per-agent Copy-On-Write memory branching for swarms.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this exists
|
|
5
|
+
*
|
|
6
|
+
* The v3.14.4 release uncovered a tarball-bloat regression: the Darwin
|
|
7
|
+
* git-worktree-per-agent pattern accumulated 3.3 GB of disk because each
|
|
8
|
+
* agent got a *full copy* of shared state. `agenticow` (a sibling RVF-based
|
|
9
|
+
* COW vector store by the same author) forks a branch in a measured **162
|
|
10
|
+
* bytes** regardless of base size — read-through semantics (parent ∪ edits,
|
|
11
|
+
* child wins) instead of a linear-growth snapshot.
|
|
12
|
+
*
|
|
13
|
+
* This service is the swarm-facing consumer of that primitive. The pattern
|
|
14
|
+
* mirrors `agenticow`'s own `examples/parallel-agents`:
|
|
15
|
+
*
|
|
16
|
+
* shared base .rvf
|
|
17
|
+
* → each agent forks a 162-byte COW branch (nativeAnn:true) at spawn
|
|
18
|
+
* → the agent reads/writes its branch in isolation
|
|
19
|
+
* → on success: promote branch → base (merge its edits back)
|
|
20
|
+
* → on failure: discard the branch file (throw the edits away)
|
|
21
|
+
*
|
|
22
|
+
* ## Honest scope (the seam)
|
|
23
|
+
*
|
|
24
|
+
* The current `agent_spawn` MCP path (src/mcp-tools/agent-tools.ts) stores an
|
|
25
|
+
* agent as pure JSON metadata — it does **not** create, copy, or hold any
|
|
26
|
+
* per-agent `.rvf` workspace today. So there is no full-copy for a COW branch
|
|
27
|
+
* to "replace" inline. This service therefore wires in as an **opt-in**: a
|
|
28
|
+
* swarm/agent that actually wants an isolated memory workspace passes a base
|
|
29
|
+
* memory path, and only then does a branch get forked. Default behavior is
|
|
30
|
+
* unchanged.
|
|
31
|
+
*
|
|
32
|
+
* ## Non-fatal + kill-switch (ADR-150)
|
|
33
|
+
*
|
|
34
|
+
* - `agenticow` is an optional dependency — every method degrades to
|
|
35
|
+
* `{ degraded: true }` when it is absent (never throws MODULE_NOT_FOUND).
|
|
36
|
+
* - The `CLAUDE_FLOW_NO_COW_MEMORY=1` env var hard-disables branching so an
|
|
37
|
+
* operator can kill the feature without a redeploy.
|
|
38
|
+
* - agenticow is loaded lazily (dynamic import inside `loadAgenticow`), so
|
|
39
|
+
* importing this module does NOT pull agenticow onto the CLI startup path.
|
|
40
|
+
*
|
|
41
|
+
* @module @claude-flow/cli/services/swarm-memory-branches
|
|
42
|
+
*/
|
|
43
|
+
/** Env var that hard-disables per-agent COW branching (operator kill switch). */
|
|
44
|
+
export declare const COW_KILL_SWITCH_ENV = "CLAUDE_FLOW_NO_COW_MEMORY";
|
|
45
|
+
/** True unless the operator set the kill switch. */
|
|
46
|
+
export declare function cowMemoryEnabled(): boolean;
|
|
47
|
+
/** Persisted mapping so promote/discard (a later, separate call) can find the branch. */
|
|
48
|
+
export interface BranchRecord {
|
|
49
|
+
agentId: string;
|
|
50
|
+
basePath: string;
|
|
51
|
+
branchPath: string;
|
|
52
|
+
label: string;
|
|
53
|
+
createdAt: string;
|
|
54
|
+
}
|
|
55
|
+
export interface BranchResult {
|
|
56
|
+
success: boolean;
|
|
57
|
+
agentId: string;
|
|
58
|
+
basePath?: string;
|
|
59
|
+
branchPath?: string;
|
|
60
|
+
label?: string;
|
|
61
|
+
/** Set when the operation was a no-op because COW is unavailable/disabled. */
|
|
62
|
+
degraded?: true;
|
|
63
|
+
reason?: string;
|
|
64
|
+
}
|
|
65
|
+
export interface PromoteResult {
|
|
66
|
+
success: boolean;
|
|
67
|
+
agentId: string;
|
|
68
|
+
promoted: boolean;
|
|
69
|
+
branchPath?: string;
|
|
70
|
+
basePath?: string;
|
|
71
|
+
degraded?: true;
|
|
72
|
+
reason?: string;
|
|
73
|
+
}
|
|
74
|
+
export interface DiscardResult {
|
|
75
|
+
success: boolean;
|
|
76
|
+
agentId: string;
|
|
77
|
+
discarded: boolean;
|
|
78
|
+
branchPath?: string;
|
|
79
|
+
reason?: string;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Manages the branch→base COW lifecycle for a swarm's agents. One instance per
|
|
83
|
+
* project cwd; state is persisted to `.claude-flow/swarm/cow-branches.json` so
|
|
84
|
+
* a branch forked in `branchForAgent` survives to a later `promoteAgent` /
|
|
85
|
+
* `discardAgent` call in a different process.
|
|
86
|
+
*/
|
|
87
|
+
export declare class SwarmMemoryBranches {
|
|
88
|
+
private readonly registryPath;
|
|
89
|
+
/**
|
|
90
|
+
* @param registryPath Override the registry location (tests point this at a
|
|
91
|
+
* temp dir). Defaults to `<cwd>/.claude-flow/swarm/cow-branches.json`.
|
|
92
|
+
*/
|
|
93
|
+
constructor(registryPath?: string);
|
|
94
|
+
private loadRegistry;
|
|
95
|
+
private saveRegistry;
|
|
96
|
+
/** Look up the branch record for an agent, if one was forked. */
|
|
97
|
+
getBranch(agentId: string): BranchRecord | undefined;
|
|
98
|
+
/**
|
|
99
|
+
* Deterministic per-agent branch path: `<baseDir>/.swarm-cow/<base>.<agentId>.rvf`.
|
|
100
|
+
* Keeps branch files namespaced next to their base so cleanup is obvious.
|
|
101
|
+
*/
|
|
102
|
+
private branchPathFor;
|
|
103
|
+
/**
|
|
104
|
+
* Fork a 162-byte COW branch off `base` for `agentId`. The agent then owns
|
|
105
|
+
* an isolated read/write view (parent ∪ its own edits). Idempotent: a second
|
|
106
|
+
* call for the same agent returns the existing branch record.
|
|
107
|
+
*
|
|
108
|
+
* @param base Path to the shared base `.rvf` (absolute or cwd-relative).
|
|
109
|
+
* @param agentId Agent that owns the branch (used as the COW label).
|
|
110
|
+
* @param opts.dimension Required only when `base` does not yet exist.
|
|
111
|
+
* @param opts.nativeAnn Fork with the native Rust ANN path (default true —
|
|
112
|
+
* agent branches are meant to be queried).
|
|
113
|
+
*/
|
|
114
|
+
branchForAgent(base: string, agentId: string, opts?: {
|
|
115
|
+
dimension?: number;
|
|
116
|
+
nativeAnn?: boolean;
|
|
117
|
+
}): Promise<BranchResult>;
|
|
118
|
+
/**
|
|
119
|
+
* Promote an agent's branch back into its base (merge its edits + tombstones
|
|
120
|
+
* atomically), then delete the branch file and drop it from the registry.
|
|
121
|
+
* Call on agent success. No-op (`promoted:false`) when the agent has no
|
|
122
|
+
* branch.
|
|
123
|
+
*/
|
|
124
|
+
promoteAgent(agentId: string): Promise<PromoteResult>;
|
|
125
|
+
/**
|
|
126
|
+
* Discard an agent's branch — delete the branch file + lineage manifest and
|
|
127
|
+
* drop the registry entry, WITHOUT merging anything into base. Call on agent
|
|
128
|
+
* failure. No-op (`discarded:false`) when the agent has no branch. Never
|
|
129
|
+
* touches agenticow (pure filesystem), so it works even in the degraded path.
|
|
130
|
+
*/
|
|
131
|
+
discardAgent(agentId: string): Promise<DiscardResult>;
|
|
132
|
+
private removeBranchFiles;
|
|
133
|
+
private forgetBranch;
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=swarm-memory-branches.d.ts.map
|