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.
Files changed (36) hide show
  1. package/package.json +2 -2
  2. package/v3/@claude-flow/cli/dist/src/agenticow/speculative-exploration.d.ts +148 -0
  3. package/v3/@claude-flow/cli/dist/src/agenticow/speculative-exploration.js +218 -0
  4. package/v3/@claude-flow/cli/dist/src/commands/autopilot.js +45 -0
  5. package/v3/@claude-flow/cli/dist/src/commands/neural.js +366 -12
  6. package/v3/@claude-flow/cli/dist/src/mcp-client.js +6 -1
  7. package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-execute-core.js +33 -0
  8. package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-tools.js +47 -1
  9. package/v3/@claude-flow/cli/dist/src/mcp-tools/agenticow-loader.d.ts +59 -0
  10. package/v3/@claude-flow/cli/dist/src/mcp-tools/agenticow-loader.js +105 -0
  11. package/v3/@claude-flow/cli/dist/src/mcp-tools/agenticow-speculate-tools.d.ts +24 -0
  12. package/v3/@claude-flow/cli/dist/src/mcp-tools/agenticow-speculate-tools.js +202 -0
  13. package/v3/@claude-flow/cli/dist/src/mcp-tools/agenticow-tools.js +186 -79
  14. package/v3/@claude-flow/cli/dist/src/mcp-tools/index.d.ts +1 -0
  15. package/v3/@claude-flow/cli/dist/src/mcp-tools/index.js +2 -0
  16. package/v3/@claude-flow/cli/dist/src/ruvector/index.d.ts +1 -1
  17. package/v3/@claude-flow/cli/dist/src/ruvector/index.js +1 -1
  18. package/v3/@claude-flow/cli/dist/src/ruvector/lora-adapter.d.ts +35 -0
  19. package/v3/@claude-flow/cli/dist/src/ruvector/lora-adapter.js +108 -1
  20. package/v3/@claude-flow/cli/dist/src/ruvector/router-trajectory.d.ts +33 -0
  21. package/v3/@claude-flow/cli/dist/src/ruvector/router-trajectory.js +31 -0
  22. package/v3/@claude-flow/cli/dist/src/ruvector/run-transcript-recorder.d.ts +154 -0
  23. package/v3/@claude-flow/cli/dist/src/ruvector/run-transcript-recorder.js +209 -0
  24. package/v3/@claude-flow/cli/dist/src/services/checkpoint-gate.d.ts +140 -0
  25. package/v3/@claude-flow/cli/dist/src/services/checkpoint-gate.js +223 -0
  26. package/v3/@claude-flow/cli/dist/src/services/distill-oracle.d.ts +190 -0
  27. package/v3/@claude-flow/cli/dist/src/services/distill-oracle.js +349 -0
  28. package/v3/@claude-flow/cli/dist/src/services/fable-harness.d.ts +168 -0
  29. package/v3/@claude-flow/cli/dist/src/services/fable-harness.js +347 -0
  30. package/v3/@claude-flow/cli/dist/src/services/native-training.d.ts +28 -0
  31. package/v3/@claude-flow/cli/dist/src/services/native-training.js +62 -5
  32. package/v3/@claude-flow/cli/dist/src/services/swarm-memory-branches.d.ts +135 -0
  33. package/v3/@claude-flow/cli/dist/src/services/swarm-memory-branches.js +213 -0
  34. package/v3/@claude-flow/cli/dist/src/services/weight-eft.d.ts +305 -0
  35. package/v3/@claude-flow/cli/dist/src/services/weight-eft.js +296 -0
  36. package/v3/@claude-flow/cli/package.json +5 -4
@@ -24,6 +24,8 @@ const trainCommand = {
24
24
  { name: 'contrastive', type: 'boolean', description: 'Use contrastive learning (InfoNCE)', default: 'true' },
25
25
  { name: 'curriculum', type: 'boolean', description: 'Enable curriculum learning', default: 'false' },
26
26
  { name: 'backend', type: 'string', description: 'Training backend: auto (native when available), native (@ruvector/ruvllm TrainingPipeline, disk checkpoints), wasm (RuVector MicroLoRA/InfoNCE)', default: 'auto' },
27
+ { name: 'val-split', type: 'number', description: 'Validation holdout fraction 0..1 (native backend). >0 reports Best Val Loss + early stopping; 0 disables', default: '0.1' },
28
+ { name: 'resume', type: 'string', description: 'Resume native training from a checkpoint path (weights on 2.5.7; epoch position on >=2.6.0). Native backend only', default: '' },
27
29
  ],
28
30
  examples: [
29
31
  { command: 'claude-flow neural train -p coordination -e 100', description: 'Train coordination patterns' },
@@ -41,6 +43,17 @@ const trainCommand = {
41
43
  // 'wasm' = RuVector MicroLoRA/InfoNCE (pre-3.19 behavior),
42
44
  // 'auto' = native when the module resolves, else wasm.
43
45
  const backendFlag = String(ctx.flags.backend || 'auto');
46
+ // Feature: validation split + resume (native TrainingPipeline leg).
47
+ const valSplitRaw = parseFloat(ctx.flags['val-split'] ?? '0.1');
48
+ const valSplit = Number.isFinite(valSplitRaw) ? Math.max(0, Math.min(1, valSplitRaw)) : 0.1;
49
+ const resumePath = ctx.flags.resume ? String(ctx.flags.resume) : undefined;
50
+ // --resume is a native-only capability; refuse the WASM combination up
51
+ // front so the user gets a clear error rather than a silently-ignored flag.
52
+ if (resumePath && backendFlag === 'wasm') {
53
+ output.writeln();
54
+ output.writeln(output.error('--resume is only supported by the native backend; drop --backend wasm.'));
55
+ return { success: false, exitCode: 1 };
56
+ }
44
57
  const useWasm = ctx.flags.wasm !== false;
45
58
  const useFlash = ctx.flags.flash !== false;
46
59
  const useMoE = ctx.flags.moe === true;
@@ -195,18 +208,34 @@ const trainCommand = {
195
208
  const nativeTraining = await import('../services/native-training.js');
196
209
  const useNative = backendFlag === 'native'
197
210
  || (backendFlag === 'auto' && nativeTraining.nativeTrainingAvailable());
211
+ // --resume only works on the native pipeline; if native is unavailable
212
+ // (module absent), fail loudly rather than silently fresh-train.
213
+ if (resumePath && !useNative) {
214
+ spinner.fail('--resume requires the native @ruvector/ruvllm backend, which is not available');
215
+ return { success: false, exitCode: 1 };
216
+ }
198
217
  let nativeResult = null;
199
218
  if (useNative) {
200
219
  spinner.setText(`Training ${patternType} on native @ruvector/ruvllm pipeline...`);
201
220
  const path = await import('path');
202
- nativeResult = await nativeTraining.runNativeTraining({
203
- embeddings,
204
- epochs,
205
- batchSize,
206
- learningRate,
207
- dim,
208
- checkpointPath: path.join(process.cwd(), '.claude-flow', 'neural', `lora-checkpoint-${Date.now()}.json`),
209
- });
221
+ try {
222
+ nativeResult = await nativeTraining.runNativeTraining({
223
+ embeddings,
224
+ epochs,
225
+ batchSize,
226
+ learningRate,
227
+ dim,
228
+ validationSplit: valSplit,
229
+ resumeFrom: resumePath,
230
+ checkpointPath: path.join(process.cwd(), '.claude-flow', 'neural', `lora-checkpoint-${Date.now()}.json`),
231
+ });
232
+ }
233
+ catch (err) {
234
+ // ResumeFailedError — an explicit --resume that could not load is a
235
+ // loud, exit-1 failure, never a silent fall-through to fresh training.
236
+ spinner.fail(`Resume failed: ${err.message}`);
237
+ return { success: false, exitCode: 1 };
238
+ }
210
239
  if (!nativeResult && backendFlag === 'native') {
211
240
  spinner.fail('Native backend requested (--backend native) but @ruvector/ruvllm training failed');
212
241
  return { success: false, exitCode: 1 };
@@ -325,7 +354,20 @@ const trainCommand = {
325
354
  ];
326
355
  // Native pipeline metrics (#2549 — the LoRA leg trained on ruvllm)
327
356
  if (nativeResult) {
328
- tableData.push({ metric: 'Backend', value: 'native (@ruvector/ruvllm TrainingPipeline)' }, { metric: 'Native Steps', value: String(nativeResult.steps) }, { metric: 'Final Loss', value: nativeResult.finalLoss.toExponential(3) }, { metric: 'Early Stopped', value: nativeResult.earlyStopped ? 'yes' : 'no' });
357
+ tableData.push({ metric: 'Backend', value: 'native (@ruvector/ruvllm TrainingPipeline)' }, { metric: 'Native Steps', value: String(nativeResult.steps) }, { metric: 'Final Loss', value: nativeResult.finalLoss.toExponential(3) });
358
+ // Validation metrics only surface when a holdout actually ran
359
+ // (bestValLoss is non-null); Early Stopped is only meaningful then.
360
+ if (nativeResult.bestValLoss !== null && nativeResult.bestValLoss !== undefined) {
361
+ tableData.push({ metric: 'Best Val Loss', value: nativeResult.bestValLoss.toExponential(3) }, { metric: 'Early Stopped', value: nativeResult.earlyStopped ? 'yes' : 'no' });
362
+ }
363
+ if (nativeResult.resumed) {
364
+ tableData.push({
365
+ metric: 'Resumed',
366
+ value: nativeResult.resumeMode === 'resumeFrom'
367
+ ? `${resumePath} (epoch position restored)`
368
+ : `${resumePath} (weights only — epoch-position resume needs @ruvector/ruvllm >=2.6.0)`,
369
+ });
370
+ }
329
371
  if (nativeResult.checkpointPath) {
330
372
  tableData.push({
331
373
  metric: 'Checkpoint',
@@ -523,10 +565,14 @@ const statusCommand = {
523
565
  details: stats._trainingBackend === 'ruvllm'
524
566
  ? await (async () => {
525
567
  try {
526
- const { nativeCheckpointsSupported } = await import('../ruvector/lora-adapter.js');
527
- return nativeCheckpointsSupported()
568
+ const { nativeCheckpointsSupported, latestCheckpointInfo } = await import('../ruvector/lora-adapter.js');
569
+ // Most important info first (truncation-friendly): backend
570
+ // capability, then the newest checkpoint + age when one exists.
571
+ const base = nativeCheckpointsSupported()
528
572
  ? 'native @ruvector/ruvllm pipeline + disk checkpoints'
529
573
  : 'native @ruvector/ruvllm pipeline (checkpoints need >=2.5.7)';
574
+ const cp = latestCheckpointInfo();
575
+ return cp ? `${base} · latest: ${cp.filename} (${cp.ageLabel})` : base;
530
576
  }
531
577
  catch {
532
578
  return 'native @ruvector/ruvllm pipeline';
@@ -3937,11 +3983,319 @@ const routerCommand = {
3937
3983
  return { success: true };
3938
3984
  },
3939
3985
  };
3986
+ // ============================================================================
3987
+ // ADR-150 weight-eft slice — `neural distill export | plan | eval | train`
3988
+ //
3989
+ // Turns ruflo's captured run transcripts into AUDITED TRAINING DATA + a
3990
+ // COST-PARETO measurement + a GPU TRAINING PLAN via the optional
3991
+ // `@metaharness/weight-eft` dependency. HARD honesty rule: this ships training
3992
+ // DATA + a cost audit + a GPU plan — it does NOT train a model and does NOT
3993
+ // "reduce escalation". weight-eft's own `train` never spawns; `resolved` in the
3994
+ // captured archive is a PROXY (no SWE-bench gold oracle). Every path degrades
3995
+ // gracefully ({degraded:true}) when the optional dep is absent (ADR-150).
3996
+ // ============================================================================
3997
+ const distillExportCommand = {
3998
+ name: 'export',
3999
+ description: 'Export captured run transcripts → SFT (OpenAI chat) + DPO (TRL preference) JSONL + a guard report (contamination / reward-hack / long-context). $0, offline. Does NOT train.',
4000
+ options: [
4001
+ { name: 'archive', short: 'a', type: 'string', description: 'Run-transcript JSONL to read (default: $CLAUDE_FLOW_RUN_TRANSCRIPTS_PATH or .swarm/run-transcripts.jsonl)' },
4002
+ { name: 'out-dir', short: 'o', type: 'string', description: 'Output dir for sft.jsonl / dpo.jsonl / export-report.json', default: '.claude-flow/neural/weft-export' },
4003
+ { name: 'eval-holdout', type: 'string', description: 'Comma-separated instance_ids reserved for eval (contamination guard). Excluded + asserted-disjoint.' },
4004
+ { name: 'max-tokens', type: 'number', description: 'Per-trajectory token budget (default weight-eft 28000)' },
4005
+ { name: 'truncate', type: 'boolean', description: 'Truncate over-length trajectories instead of dropping', default: 'false' },
4006
+ { name: 'keep-reward-hacked', type: 'boolean', description: 'Disable the reward-hacking filter (debug only; NOT recommended)', default: 'false' },
4007
+ { name: 'format', short: 'f', type: 'string', description: 'Output format: table, json', default: 'table' },
4008
+ ],
4009
+ examples: [
4010
+ { command: 'claude-flow neural distill export', description: 'Export from the default captured .swarm/run-transcripts.jsonl' },
4011
+ { command: 'claude-flow neural distill export -a runs.jsonl -o ./out --eval-holdout astropy__astropy-1', description: 'Export a specific archive, holding out one instance' },
4012
+ ],
4013
+ action: async (ctx) => {
4014
+ const fs = await import('node:fs');
4015
+ const path = await import('node:path');
4016
+ const { readRunTranscripts } = await import('../ruvector/run-transcript-recorder.js');
4017
+ const { buildArchiveFromRecords, runExport } = await import('../services/weight-eft.js');
4018
+ const fmt = ctx.flags.format || 'table';
4019
+ const archivePath = ctx.flags.archive
4020
+ ?? process.env.CLAUDE_FLOW_RUN_TRANSCRIPTS_PATH
4021
+ ?? path.resolve(process.cwd(), '.swarm', 'run-transcripts.jsonl');
4022
+ const { records, malformed } = readRunTranscripts(archivePath);
4023
+ if (records.length === 0) {
4024
+ const msg = `No run transcripts at ${archivePath}. Enable capture with CLAUDE_FLOW_RUN_TRANSCRIPTS=1, or pass --archive <file>.`;
4025
+ if (fmt === 'json')
4026
+ output.writeln(JSON.stringify({ ok: false, archivePath, records: 0, malformed, error: msg }, null, 2));
4027
+ else
4028
+ output.printError(msg);
4029
+ return { success: false, exitCode: 1, data: { archivePath, records: 0 } };
4030
+ }
4031
+ const { trajectories, stats, proxyNote } = buildArchiveFromRecords(records);
4032
+ const holdout = (ctx.flags['eval-holdout'] ?? ctx.flags.evalHoldout)?.split(',').map((s) => s.trim()).filter(Boolean) ?? [];
4033
+ const maxTokens = (ctx.flags['max-tokens'] ?? ctx.flags.maxTokens) != null ? parseInt(String((ctx.flags['max-tokens'] ?? ctx.flags.maxTokens)), 10) : undefined;
4034
+ const res = await runExport({
4035
+ archive: trajectories,
4036
+ evalHoldout: holdout,
4037
+ maxTokens,
4038
+ truncateOverLength: ctx.flags.truncate === true,
4039
+ dropRewardHacked: (ctx.flags['keep-reward-hacked'] ?? ctx.flags.keepRewardHacked) === true ? false : undefined,
4040
+ });
4041
+ if (res.degraded) {
4042
+ // ADR-150 graceful degradation: dep absent → not a runtime failure.
4043
+ const payload = { degraded: true, reason: res.reason, archiveTrajectories: trajectories.length };
4044
+ if (fmt === 'json')
4045
+ output.writeln(JSON.stringify(payload, null, 2));
4046
+ else {
4047
+ output.writeln(output.warning(`weight-eft unavailable (${res.reason}).`));
4048
+ output.writeln(output.dim('Install the optional dep: npm i @metaharness/weight-eft. Archive was built (' + trajectories.length + ' trajectories) but not exported.'));
4049
+ }
4050
+ return { success: true, exitCode: 0, data: payload };
4051
+ }
4052
+ const outDir = path.resolve(process.cwd(), (ctx.flags['out-dir'] ?? ctx.flags.outDir) || '.claude-flow/neural/weft-export');
4053
+ fs.mkdirSync(outDir, { recursive: true });
4054
+ const sftPath = path.join(outDir, 'sft.jsonl');
4055
+ const dpoPath = path.join(outDir, 'dpo.jsonl');
4056
+ const reportPath = path.join(outDir, 'export-report.json');
4057
+ fs.writeFileSync(sftPath, res.sftJsonl);
4058
+ fs.writeFileSync(dpoPath, res.dpoJsonl);
4059
+ fs.writeFileSync(reportPath, JSON.stringify({ report: res.report, archiveStats: stats, proxyNote }, null, 2));
4060
+ const payload = {
4061
+ ok: true, archivePath, outDir, sftPath, dpoPath, reportPath,
4062
+ sftRows: res.sftRows, dpoRows: res.dpoRows, malformed,
4063
+ archiveStats: stats, report: res.report, proxyNote,
4064
+ };
4065
+ if (fmt === 'json') {
4066
+ output.writeln(JSON.stringify(payload, null, 2));
4067
+ return { success: true, data: payload };
4068
+ }
4069
+ output.writeln();
4070
+ output.writeln(output.bold('weight-eft export — audited training data ($0, no model trained)'));
4071
+ output.writeln(` archive: ${archivePath} (${records.length} records, ${malformed} malformed skipped)`);
4072
+ output.writeln(` trajectories: ${stats.total} (cheap ${stats.byTier.cheap} / frontier ${stats.byTier.frontier}), resolved ${stats.resolved}`);
4073
+ output.writeln(` SFT rows: ${res.sftRows} → ${sftPath}`);
4074
+ output.writeln(` DPO rows: ${res.dpoRows} → ${dpoPath}`);
4075
+ output.writeln(` guards: holdout=${res.report.excludedByHoldout} reward-hacked=${res.report.droppedRewardHacked} over-length=${res.report.droppedOverLength} truncated=${res.report.truncatedOverLength}`);
4076
+ output.writeln(` report: ${reportPath}`);
4077
+ output.writeln();
4078
+ output.writeln(output.warning('resolved provenance: ' + JSON.stringify(stats.byResolvedSource)));
4079
+ output.writeln(output.dim(proxyNote));
4080
+ return { success: true, data: payload };
4081
+ },
4082
+ };
4083
+ const distillPlanCommand = {
4084
+ name: 'plan',
4085
+ description: 'Print the two-stage (SFT → on-policy DPO) GPU training plan + the exact `ruvllm microlora` commands a GPU host would run. $0 dry-run — NEVER spawns a tune.',
4086
+ options: [
4087
+ { name: 'sft', type: 'string', description: 'Path to sft.jsonl (default: .claude-flow/neural/weft-export/sft.jsonl)' },
4088
+ { name: 'dpo', type: 'string', description: 'Path to dpo.jsonl (default: .claude-flow/neural/weft-export/dpo.jsonl)' },
4089
+ { name: 'base', short: 'b', type: 'string', description: 'Base model id to tune (7-14B band). Default Qwen2.5-Coder-7B-Instruct' },
4090
+ { name: 'params-b', type: 'number', description: 'Base model param count in billions (gate [1,14]). Default 7' },
4091
+ { name: 'adapter-prefix', type: 'string', description: 'Adapter output prefix', default: 'ruflo-weft' },
4092
+ { name: 'format', short: 'f', type: 'string', description: 'Output format: table, json', default: 'table' },
4093
+ ],
4094
+ examples: [
4095
+ { command: 'claude-flow neural distill plan', description: 'Print the GPU plan for the last export ($0 dry-run)' },
4096
+ { command: 'claude-flow neural distill plan --base Qwen/Qwen2.5-Coder-7B-Instruct --params-b 7', description: 'Plan for a specific base model' },
4097
+ ],
4098
+ action: async (ctx) => {
4099
+ const path = await import('node:path');
4100
+ const { runPlan, DEFAULT_BASE_MODEL } = await import('../services/weight-eft.js');
4101
+ const fmt = ctx.flags.format || 'table';
4102
+ const sftPath = ctx.flags.sft || path.resolve(process.cwd(), '.claude-flow/neural/weft-export/sft.jsonl');
4103
+ const dpoPath = ctx.flags.dpo || path.resolve(process.cwd(), '.claude-flow/neural/weft-export/dpo.jsonl');
4104
+ const base = ctx.flags.base
4105
+ ? { id: String(ctx.flags.base), paramsB: (ctx.flags['params-b'] ?? ctx.flags.paramsB) != null ? parseInt(String((ctx.flags['params-b'] ?? ctx.flags.paramsB)), 10) : 7 }
4106
+ : DEFAULT_BASE_MODEL;
4107
+ const res = await runPlan({ base, sftPath, dpoPath, adapterPrefix: String((ctx.flags['adapter-prefix'] ?? ctx.flags.adapterPrefix) || 'ruflo-weft') });
4108
+ if (res.degraded) {
4109
+ const payload = { degraded: true, reason: res.reason };
4110
+ if (fmt === 'json')
4111
+ output.writeln(JSON.stringify(payload, null, 2));
4112
+ else {
4113
+ output.writeln(output.warning(`weight-eft unavailable (${res.reason}).`));
4114
+ output.writeln(output.dim('Install: npm i @metaharness/weight-eft'));
4115
+ }
4116
+ return { success: true, exitCode: 0, data: payload };
4117
+ }
4118
+ const payload = { ok: true, base: res.base, sft: res.sft, dpo: res.dpo, dryRun: true };
4119
+ if (fmt === 'json') {
4120
+ output.writeln(JSON.stringify(payload, null, 2));
4121
+ return { success: true, data: payload };
4122
+ }
4123
+ output.writeln();
4124
+ output.writeln(output.bold(`weight-eft GPU training plan ($0 dry-run — no tune runs from ruflo)`));
4125
+ output.writeln(` base model: ${res.base.id} (${res.base.paramsB}B)`);
4126
+ output.writeln(output.dim(' SFT stage:'));
4127
+ output.writeln(` ${res.sft.summary}`);
4128
+ output.writeln(` $ ${res.sft.command}`);
4129
+ output.writeln(output.dim(' DPO stage (on-policy, init from SFT adapter):'));
4130
+ output.writeln(` ${res.dpo.summary}`);
4131
+ output.writeln(` $ ${res.dpo.command}`);
4132
+ output.writeln();
4133
+ output.writeln(output.dim('These commands run on a GPU host; ruflo does not execute them. See `neural distill train --remote` for a spend-gated remote path.'));
4134
+ return { success: true, data: payload };
4135
+ },
4136
+ };
4137
+ const distillEvalCommand = {
4138
+ name: 'eval',
4139
+ description: 'Fold two CascadeOutcome[] JSON files (base vs adapter) into the cost-Pareto delta — escalation-rate reduction + $/resolved. $0. Measures cost, does NOT claim a tune ran.',
4140
+ options: [
4141
+ { name: 'base-outcomes', type: 'string', description: 'JSON file: CascadeOutcome[] for the BASE cascade run', required: true },
4142
+ { name: 'adapter-outcomes', type: 'string', description: 'JSON file: CascadeOutcome[] for the ADAPTER cascade run', required: true },
4143
+ { name: 'format', short: 'f', type: 'string', description: 'Output format: table, json', default: 'table' },
4144
+ ],
4145
+ examples: [
4146
+ { command: 'claude-flow neural distill eval --base-outcomes base.json --adapter-outcomes adapter.json', description: 'Cost-Pareto delta between base and adapter cascade runs' },
4147
+ ],
4148
+ action: async (ctx) => {
4149
+ const fs = await import('node:fs');
4150
+ const { runEval } = await import('../services/weight-eft.js');
4151
+ const fmt = ctx.flags.format || 'table';
4152
+ const basePath = (ctx.flags['base-outcomes'] ?? ctx.flags.baseOutcomes);
4153
+ const adapterPath = (ctx.flags['adapter-outcomes'] ?? ctx.flags.adapterOutcomes);
4154
+ if (!basePath || !adapterPath) {
4155
+ output.printError('Both --base-outcomes and --adapter-outcomes are required.');
4156
+ return { success: false, exitCode: 2 };
4157
+ }
4158
+ let baseOutcomes;
4159
+ let adapterOutcomes;
4160
+ try {
4161
+ baseOutcomes = JSON.parse(fs.readFileSync(basePath, 'utf8'));
4162
+ adapterOutcomes = JSON.parse(fs.readFileSync(adapterPath, 'utf8'));
4163
+ }
4164
+ catch (e) {
4165
+ output.printError(`Failed to read outcome files: ${e.message}`);
4166
+ return { success: false, exitCode: 1 };
4167
+ }
4168
+ if (!Array.isArray(baseOutcomes) || !Array.isArray(adapterOutcomes)) {
4169
+ output.printError('Both files must contain a JSON array of CascadeOutcome objects.');
4170
+ return { success: false, exitCode: 1 };
4171
+ }
4172
+ const res = await runEval({ baseOutcomes: baseOutcomes, adapterOutcomes: adapterOutcomes });
4173
+ if (res.degraded) {
4174
+ const payload = { degraded: true, reason: res.reason };
4175
+ if (fmt === 'json')
4176
+ output.writeln(JSON.stringify(payload, null, 2));
4177
+ else {
4178
+ output.writeln(output.warning(`weight-eft unavailable (${res.reason}).`));
4179
+ output.writeln(output.dim('Install: npm i @metaharness/weight-eft'));
4180
+ }
4181
+ return { success: true, exitCode: 0, data: payload };
4182
+ }
4183
+ if (fmt === 'json') {
4184
+ output.writeln(JSON.stringify({ ok: true, delta: res.delta }, null, 2));
4185
+ return { success: true, data: res.delta };
4186
+ }
4187
+ output.writeln();
4188
+ output.writeln(output.bold('weight-eft cost-Pareto delta (measurement only)'));
4189
+ output.writeln(` cheap-resolve lift: ${res.delta.cheapResolveLift.toFixed(4)}`);
4190
+ output.writeln(` escalation-rate reduction: ${res.delta.escalationRateReduction.toFixed(4)}`);
4191
+ output.writeln(` $/resolved reduction: ${res.delta.costPerResolvedReduction.toFixed(6)}`);
4192
+ output.writeln(` resolve-rate delta: ${res.delta.resolveRateDelta.toFixed(4)} (expected ≈ 0 — ceiling unmoved)`);
4193
+ output.writeln(` verdict: ${res.delta.verdict}`);
4194
+ return { success: true, data: res.delta };
4195
+ },
4196
+ };
4197
+ const distillTrainCommand = {
4198
+ name: 'train',
4199
+ description: 'Remote-GPU LoRA tune over SSH — DRY-RUN by default (prints ssh/rsync/ruvllm commands + read-only preflight). Real compute ONLY with --execute --yes (spends GPU time on YOUR host). Not a $0/local tune.',
4200
+ options: [
4201
+ { name: 'remote', short: 'r', type: 'string', description: 'SSH host or tailscale name (default: $RUFLO_DISTILL_REMOTE). Never hard-coded.' },
4202
+ { name: 'base', short: 'b', type: 'string', description: 'Base model id to tune. Default Qwen2.5-Coder-7B-Instruct' },
4203
+ { name: 'sft', type: 'string', description: 'Local sft.jsonl (default: .claude-flow/neural/weft-export/sft.jsonl)' },
4204
+ { name: 'dpo', type: 'string', description: 'Local dpo.jsonl (default: .claude-flow/neural/weft-export/dpo.jsonl)' },
4205
+ { name: 'adapter-dir', type: 'string', description: 'Local dir to fetch the trained adapter into', default: '.claude-flow/neural' },
4206
+ { name: 'ssh-user', type: 'string', description: 'SSH user (default: current user)' },
4207
+ { name: 'ssh-port', type: 'number', description: 'SSH port', default: '22' },
4208
+ { name: 'remote-workdir', type: 'string', description: 'Remote working dir (default: ~/.ruflo-weft/<runId>)' },
4209
+ { name: 'execute', type: 'boolean', description: 'Opt in to REAL GPU compute on the remote host (still needs --yes)', default: 'false' },
4210
+ { name: 'yes', type: 'boolean', description: 'Second confirmation gate; required with --execute to actually spend', default: 'false' },
4211
+ { name: 'preflight', type: 'boolean', description: 'Opt in to read-only reachability/GPU probes against the host (bare dry-run is fully offline and contacts nothing)', default: 'false' },
4212
+ { name: 'format', short: 'f', type: 'string', description: 'Output format: table, json', default: 'table' },
4213
+ ],
4214
+ examples: [
4215
+ { command: 'claude-flow neural distill train --remote gpu-box', description: 'OFFLINE DRY-RUN: print the ssh/rsync/ruvllm commands only (no host contact)' },
4216
+ { command: 'claude-flow neural distill train --remote gpu-box --preflight', description: 'DRY-RUN + read-only reachability/GPU probes against the host' },
4217
+ { command: 'RUFLO_DISTILL_REMOTE=gpu-box claude-flow neural distill train --execute --yes', description: 'Run the real remote tune (spends GPU time on your host)' },
4218
+ ],
4219
+ action: async (ctx) => {
4220
+ const path = await import('node:path');
4221
+ const { runRemoteTrain } = await import('../services/weight-eft.js');
4222
+ const fmt = ctx.flags.format || 'table';
4223
+ const host = ctx.flags.remote || process.env.RUFLO_DISTILL_REMOTE;
4224
+ if (!host) {
4225
+ output.printError('No remote host. Pass --remote <host> or set RUFLO_DISTILL_REMOTE.');
4226
+ return { success: false, exitCode: 2 };
4227
+ }
4228
+ const res = await runRemoteTrain({
4229
+ host,
4230
+ base: ctx.flags.base ? String(ctx.flags.base) : undefined,
4231
+ sftPath: ctx.flags.sft || path.resolve(process.cwd(), '.claude-flow/neural/weft-export/sft.jsonl'),
4232
+ dpoPath: ctx.flags.dpo || path.resolve(process.cwd(), '.claude-flow/neural/weft-export/dpo.jsonl'),
4233
+ adapterDir: (ctx.flags['adapter-dir'] ?? ctx.flags.adapterDir) || '.claude-flow/neural',
4234
+ sshUser: (ctx.flags['ssh-user'] ?? ctx.flags.sshUser) ? String((ctx.flags['ssh-user'] ?? ctx.flags.sshUser)) : undefined,
4235
+ sshPort: (ctx.flags['ssh-port'] ?? ctx.flags.sshPort) != null ? parseInt(String((ctx.flags['ssh-port'] ?? ctx.flags.sshPort)), 10) : undefined,
4236
+ remoteWorkdir: (ctx.flags['remote-workdir'] ?? ctx.flags.remoteWorkdir) ? String((ctx.flags['remote-workdir'] ?? ctx.flags.remoteWorkdir)) : undefined,
4237
+ execute: ctx.flags.execute === true,
4238
+ yes: ctx.flags.yes === true,
4239
+ preflight: ctx.flags.preflight === true,
4240
+ });
4241
+ if ('degraded' in res && res.degraded) {
4242
+ const payload = { degraded: true, reason: res.reason };
4243
+ if (fmt === 'json')
4244
+ output.writeln(JSON.stringify(payload, null, 2));
4245
+ else
4246
+ output.writeln(output.warning(`remote-train unavailable (${res.reason}).`));
4247
+ return { success: true, exitCode: 0, data: payload };
4248
+ }
4249
+ if (fmt === 'json') {
4250
+ output.writeln(JSON.stringify(res, null, 2));
4251
+ return { success: res.mode !== 'preflight-failed', data: res };
4252
+ }
4253
+ output.writeln();
4254
+ output.writeln(output.bold(`weight-eft remote-GPU tune [${res.mode}] on ${res.plan.host}`));
4255
+ if (res.mode === 'dry-run')
4256
+ output.writeln(output.dim('DRY-RUN — no data transferred, no training. Re-run with --execute --yes to spend GPU time on your host.'));
4257
+ if (res.reason)
4258
+ output.writeln(output.warning(res.reason));
4259
+ output.writeln(` base: ${res.plan.base} remote workdir: ${res.plan.remoteWorkdir} adapter → ${res.plan.adapterDir}/${res.plan.dpoAdapter}`);
4260
+ if (res.preflight) {
4261
+ output.writeln(output.dim(' preflight (read-only probes):'));
4262
+ for (const p of res.preflight)
4263
+ output.writeln(` [${p.ok ? 'ok ' : 'FAIL'}] ${p.label}: ${p.detail}`);
4264
+ }
4265
+ output.writeln(output.dim(' commands that ' + (res.mode === 'executed' ? 'ran' : 'WOULD run') + ':'));
4266
+ for (const c of res.plan.humanCommands)
4267
+ output.writeln(` $ ${c}`);
4268
+ if (res.steps) {
4269
+ output.writeln(output.dim(' execution:'));
4270
+ for (const s of res.steps)
4271
+ output.writeln(` [${s.ok ? 'ok ' : 'FAIL'}] ${s.label}: ${s.detail}`);
4272
+ }
4273
+ output.writeln();
4274
+ output.writeln(output.dim('Honesty: ruflo does not train locally or at $0. This is an explicit, user-triggered remote-GPU spend. resolved-gold in the SFT data is still a proxy.'));
4275
+ return { success: res.mode !== 'preflight-failed', data: res };
4276
+ },
4277
+ };
4278
+ const distillCommand = {
4279
+ name: 'distill',
4280
+ description: 'weight-eft training-data + cost-audit slice (ADR-150): export | plan | eval | train. Ships audited SFT/DPO data + a cost-Pareto measurement + a GPU plan. Does NOT train a model or reduce escalation.',
4281
+ subcommands: [distillExportCommand, distillPlanCommand, distillEvalCommand, distillTrainCommand],
4282
+ examples: [
4283
+ { command: 'claude-flow neural distill export', description: 'Captured transcripts → audited SFT/DPO JSONL + guard report ($0)' },
4284
+ { command: 'claude-flow neural distill plan', description: 'Print the GPU training plan + ruvllm commands ($0 dry-run)' },
4285
+ { command: 'claude-flow neural distill eval --base-outcomes b.json --adapter-outcomes a.json', description: 'Cost-Pareto delta ($0)' },
4286
+ { command: 'claude-flow neural distill train --remote gpu-box', description: 'Remote-GPU tune DRY-RUN (spend-gated behind --execute --yes)' },
4287
+ ],
4288
+ action: async () => {
4289
+ output.writeln('Use a subcommand: export | plan | eval | train');
4290
+ output.writeln(output.dim('Ships audited training DATA + a cost audit + a GPU plan. It does NOT train a model or reduce escalation (weight-eft train never spawns; resolved is a proxy).'));
4291
+ return { success: true };
4292
+ },
4293
+ };
3940
4294
  // Main neural command
3941
4295
  export const neuralCommand = {
3942
4296
  name: 'neural',
3943
4297
  description: 'Neural pattern training, MoE, Flash Attention, pattern learning',
3944
- subcommands: [trainCommand, statusCommand, patternsCommand, predictCommand, optimizeCommand, benchmarkCommand, listCommand, exportCommand, importCommand, routerCommand],
4298
+ subcommands: [trainCommand, statusCommand, patternsCommand, predictCommand, optimizeCommand, benchmarkCommand, listCommand, exportCommand, importCommand, routerCommand, distillCommand],
3945
4299
  examples: [
3946
4300
  { command: 'claude-flow neural status', description: 'Check neural system status' },
3947
4301
  { command: 'claude-flow neural train -p coordination', description: 'Train coordination patterns' },
@@ -50,6 +50,9 @@ import { metaharnessTools } from './mcp-tools/metaharness-tools.js';
50
50
  // agenticow@~0.2.3 — Copy-On-Write memory branching tools (162-byte branches);
51
51
  // optional runtime dep, every handler returns `{degraded: true}` when missing.
52
52
  import { agenticowTools } from './mcp-tools/agenticow-tools.js';
53
+ // agenticow step 4 — speculative branch-and-promote (A/B memory exploration).
54
+ // Optional runtime dep, degrades to `{degraded: true}` when agenticow is missing.
55
+ import { agenticowSpeculateTools } from './mcp-tools/agenticow-speculate-tools.js';
53
56
  // ADR-164 — AgentBBS federated business-domain BBS rooms (Phase 1).
54
57
  // Optional runtime dep, every handler returns `{degraded: true}` when missing.
55
58
  import { agentbbsTools } from './mcp-tools/agentbbs-tools.js';
@@ -139,8 +142,10 @@ registerTools([
139
142
  ...coverageRouterTools,
140
143
  // ADR-150 — MetaHarness static-analysis tools (5)
141
144
  ...metaharnessTools,
142
- // agenticow@~0.2.3 — COW memory branching (4 tools, graceful-degraded when missing)
145
+ // agenticow@~0.2.4 — COW memory branching (9 tools: branch/checkpoint/rollback/promote + ingest/query/diff/lineage/status, graceful-degraded when missing)
143
146
  ...agenticowTools,
147
+ // agenticow step 4 — speculative branch-and-promote (1 tool, graceful-degraded when missing)
148
+ ...agenticowSpeculateTools,
144
149
  // ADR-164 — AgentBBS federated business-domain BBS rooms (4 tools, Phase 1, graceful-degraded)
145
150
  ...agentbbsTools,
146
151
  // ADR-164 Phase 2 + Phase 3 — business_pod_validate + business_pod_route_backend
@@ -512,6 +512,39 @@ export async function executeAgentTask(input) {
512
512
  }
513
513
  catch { /* never break execution */ }
514
514
  }
515
+ // ADR-150 weight-eft capture seam — the ONLY place a run's full outcome is
516
+ // known here: prompt (issue), assistant transcript, model, tier, and a
517
+ // resolved PROXY. Gated behind CLAUDE_FLOW_RUN_TRANSCRIPTS=1 (off by
518
+ // default; PII/retention surface, mirrors the router trajectory recorder).
519
+ // HONESTY: `resolved` here is the WEAKEST proxy — 'api-success' means only
520
+ // that the model returned without an API error, NOT that the output is
521
+ // correct (ruflo has no SWE-bench gold oracle). model_patch is '' because
522
+ // this single-shot execute path produces no unified diff. Both facts are
523
+ // stamped so no downstream weight-eft export mistakes this for gold data.
524
+ if (process.env.CLAUDE_FLOW_RUN_TRANSCRIPTS === '1') {
525
+ try {
526
+ const { recordRunTranscript, tierForModel } = await import('../ruvector/run-transcript-recorder.js');
527
+ const modelId = agent.modelId ?? String(agent.model ?? 'unknown');
528
+ const messages = [];
529
+ if (systemPrompt)
530
+ messages.push({ role: 'system', content: systemPrompt });
531
+ messages.push({ role: 'user', content: input.prompt });
532
+ if (result.success && typeof result.output === 'string') {
533
+ messages.push({ role: 'assistant', content: result.output });
534
+ }
535
+ recordRunTranscript({
536
+ task: input.prompt,
537
+ model: modelId,
538
+ tier: tierForModel(modelId),
539
+ resolved: outcome === 'success',
540
+ resolvedSource: 'api-success',
541
+ messages,
542
+ source: 'agent-execute',
543
+ tokens: result.usage ? { input: result.usage.inputTokens, output: result.usage.outputTokens } : undefined,
544
+ });
545
+ }
546
+ catch { /* never break execution */ }
547
+ }
515
548
  }
516
549
  catch {
517
550
  // Silent — bandit feedback must never block routing.
@@ -224,6 +224,8 @@ export const agentTools = [
224
224
  description: 'Claude model alias (haiku=fast/cheap, sonnet=balanced, opus=current Opus 4.8, opus-4.7=prior Opus pin)'
225
225
  },
226
226
  task: { type: 'string', description: 'Task description for intelligent model routing' },
227
+ memoryBase: { type: 'string', description: 'Opt-in: base .rvf memory file to fork a per-agent Copy-On-Write branch from (agenticow). When set, the agent gets an isolated ~162-byte COW branch instead of a full copy — promote on success, discard on terminate. Requires the optional `agenticow` dep; degrades to a no-op when absent or when CLAUDE_FLOW_NO_COW_MEMORY=1.' },
228
+ memoryDimension: { type: 'integer', description: 'Vector dimension for the COW base (required only when memoryBase does not exist yet)' },
227
229
  },
228
230
  required: ['agentType'],
229
231
  },
@@ -295,6 +297,31 @@ export const agentTools = [
295
297
  await addNode({ id: agentId, type: 'agent', name: agentType });
296
298
  }
297
299
  catch { /* graph-node not available */ }
300
+ // ACOW — opt-in per-agent COW memory branch. Only when the caller
301
+ // supplies `memoryBase` does the agent get an isolated 162-byte branch
302
+ // (vs a full .rvf copy). Lazy-imported so agenticow stays off the
303
+ // startup path. Non-fatal: a branch failure never blocks the spawn —
304
+ // the agent is already registered above.
305
+ let memoryBranch;
306
+ if (input.memoryBase) {
307
+ try {
308
+ const { SwarmMemoryBranches } = await import('../services/swarm-memory-branches.js');
309
+ const svc = new SwarmMemoryBranches();
310
+ const br = await svc.branchForAgent(String(input.memoryBase), agentId, {
311
+ dimension: typeof input.memoryDimension === 'number' ? input.memoryDimension : undefined,
312
+ });
313
+ if (br.branchPath) {
314
+ memoryBranch = br.branchPath;
315
+ agent.memoryBranch = br.branchPath;
316
+ agent.memoryBase = br.basePath;
317
+ store.agents[agentId] = agent;
318
+ saveAgentStore(store);
319
+ }
320
+ // else: degraded (agenticow missing / kill-switched) — agent stands
321
+ // without an isolated branch; callers see no memoryBranch field.
322
+ }
323
+ catch { /* COW branch is best-effort; agent already registered */ }
324
+ }
298
325
  // Include deterministic codemod routing info if applicable
299
326
  const response = {
300
327
  success: true,
@@ -307,6 +334,7 @@ export const agentTools = [
307
334
  ...(routingResult.openrouterModel ? { openrouterModel: routingResult.openrouterModel } : {}),
308
335
  status: 'registered',
309
336
  createdAt: agent.createdAt,
337
+ ...(memoryBranch ? { memoryBranch, memoryBase: agent.memoryBase } : {}),
310
338
  note: 'Agent registered for coordination. Three execution paths: ' +
311
339
  '(1) call agent_execute(agentId, prompt) — direct LLM call via Anthropic Messages API (requires ANTHROPIC_API_KEY); ' +
312
340
  '(2) Claude Code Task tool — spawns a real subagent; ' +
@@ -375,6 +403,7 @@ export const agentTools = [
375
403
  properties: {
376
404
  agentId: { type: 'string', description: 'ID of agent to terminate' },
377
405
  force: { type: 'boolean', description: 'Force immediate termination' },
406
+ promoteMemory: { type: 'boolean', description: 'When the agent has a per-agent COW memory branch (spawned with memoryBase), promote (merge) its edits into the shared base on terminate. Default false → discard the branch (throw its edits away). No-op when the agent has no branch.' },
378
407
  },
379
408
  required: ['agentId'],
380
409
  },
@@ -385,13 +414,30 @@ export const agentTools = [
385
414
  const store = loadAgentStore();
386
415
  const agentId = input.agentId;
387
416
  if (store.agents[agentId]) {
388
- store.agents[agentId].status = 'terminated';
417
+ const rec = store.agents[agentId];
418
+ rec.status = 'terminated';
389
419
  saveAgentStore(store);
420
+ // ACOW — resolve the agent's COW memory branch on teardown: promote
421
+ // (merge into base) when asked, else discard. Non-fatal — a failure
422
+ // here never blocks termination. Discard needs no agenticow (pure fs),
423
+ // so cleanup still works in the degraded path.
424
+ let memory;
425
+ if (rec.memoryBranch) {
426
+ try {
427
+ const { SwarmMemoryBranches } = await import('../services/swarm-memory-branches.js');
428
+ const svc = new SwarmMemoryBranches();
429
+ memory = input.promoteMemory
430
+ ? (await svc.promoteAgent(agentId))
431
+ : (await svc.discardAgent(agentId));
432
+ }
433
+ catch { /* best-effort teardown */ }
434
+ }
390
435
  return {
391
436
  success: true,
392
437
  agentId,
393
438
  terminated: true,
394
439
  terminatedAt: new Date().toISOString(),
440
+ ...(memory ? { memory } : {}),
395
441
  };
396
442
  }
397
443
  return {
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Agenticow shared loader + helpers.
3
+ *
4
+ * Extracted from `agenticow-tools.ts` so both the MCP tool surface AND the
5
+ * `SwarmMemoryBranches` service (src/services/swarm-memory-branches.ts) share
6
+ * one loader, one degradation contract, and one set of path/lineage helpers —
7
+ * rather than duplicating the optional-dep dance in two places.
8
+ *
9
+ * Architectural constraint (ADR-150, mirrors metaharness-tools.ts):
10
+ * - `agenticow` lives in `optionalDependencies` — never a hard runtime dep.
11
+ * - When the package is missing, callers get `null` from `loadAgenticow()`
12
+ * (or a `{degraded:true}` result via `degradedResult`) and MUST fall back
13
+ * gracefully — never throw a MODULE_NOT_FOUND.
14
+ *
15
+ * @module @claude-flow/cli/mcp-tools/agenticow-loader
16
+ */
17
+ export declare const PACKAGE_NAME = "agenticow";
18
+ export interface AgenticowApi {
19
+ open: (file: string, opts?: {
20
+ dimension?: number;
21
+ metric?: string;
22
+ }) => Promise<any>;
23
+ openBase?: (file: string, opts?: any) => Promise<any>;
24
+ AgenticMemory: any;
25
+ }
26
+ /**
27
+ * Lazily import `agenticow`. Returns the module on success, or `null` when the
28
+ * package is not installed (the optional-dep degraded path). Any OTHER load
29
+ * error (e.g. a corrupt install) is re-thrown so it is not silently masked.
30
+ */
31
+ export declare function loadAgenticow(): Promise<AgenticowApi | null>;
32
+ /** Reset the module-level load cache. Test-only seam. */
33
+ export declare function __resetAgenticowCache(): void;
34
+ export declare function degradedResult(reason: string): {
35
+ success: true;
36
+ degraded: true;
37
+ reason: string;
38
+ };
39
+ /**
40
+ * Resolve a user-supplied memory path against the project cwd, rejecting path
41
+ * traversal and NUL bytes (D-2 style hardening — same rule the MCP verbs use).
42
+ */
43
+ export declare function resolveMemoryPath(path: string): string;
44
+ /**
45
+ * Lineage manifest companion path. agenticow persists the COW chain
46
+ * (working → checkpoints → base) into `<file>.agenticow.json` next to the
47
+ * `.rvf` data file. Without it, forks/checkpoints are in-memory only and
48
+ * disappear when the AgenticMemory handle closes.
49
+ */
50
+ export declare function manifestFor(file: string): string;
51
+ /** Validate a COW branch/checkpoint label (alnum + a small safe symbol set). */
52
+ export declare function validateLabel(label: string): string;
53
+ /**
54
+ * Open (or create) a memory file, restoring its COW chain from the lineage
55
+ * manifest when one exists. When neither the `.rvf` nor the manifest exists,
56
+ * `dimension` is required to create a fresh base.
57
+ */
58
+ export declare function openWithLineage(api: AgenticowApi, file: string, dimension?: number): Promise<any>;
59
+ //# sourceMappingURL=agenticow-loader.d.ts.map