claude-flow 3.19.0 → 3.20.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-flow",
3
- "version": "3.19.0",
3
+ "version": "3.20.0",
4
4
  "description": "Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -117,7 +117,7 @@
117
117
  "@grpc/grpc-js": ">=1.14.4",
118
118
  "form-data": ">=4.0.6",
119
119
  "http-proxy-middleware": ">=3.0.7",
120
- "@ruvector/ruvllm": ">=2.5.7"
120
+ "@ruvector/ruvllm": ">=2.6.0"
121
121
  },
122
122
  "devDependencies": {
123
123
  "@openai/codex": "^0.98.0",
@@ -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';
@@ -121,6 +121,7 @@ export const agenticowTools = [
121
121
  branchPath: { type: 'string', description: 'Path to write the branch file' },
122
122
  label: { type: 'string', description: 'Human-readable label for the branch (alnum + _.-:/@ only)' },
123
123
  dimension: { type: 'integer', description: 'Vector dimension (required only when basePath does not exist yet)' },
124
+ nativeAnn: { type: 'boolean', description: 'Use the native Rust COW dual-graph ANN path (recall@10=1.0, query spans the COW boundary in one Rust call). Default false (exact JS chain-walk). Set true when the branch will be queried.', default: false },
124
125
  },
125
126
  required: ['basePath', 'branchPath', 'label'],
126
127
  },
@@ -132,9 +133,10 @@ export const agenticowTools = [
132
133
  const basePath = resolveMemoryPath(String(input.basePath));
133
134
  const branchPath = resolveMemoryPath(String(input.branchPath));
134
135
  const dim = input.dimension;
136
+ const nativeAnn = input.nativeAnn === true;
135
137
  const base = await openWithLineage(api, basePath, dim);
136
138
  try {
137
- const branch = await base.fork(label, branchPath);
139
+ const branch = await base.fork(label, branchPath, { nativeAnn });
138
140
  // Persist lineage manifests so the branch (and base) reopen with
139
141
  // their COW chain intact. Without this, fork is in-memory only.
140
142
  await branch.save?.(manifestFor(branchPath));
@@ -145,6 +147,7 @@ export const agenticowTools = [
145
147
  basePath,
146
148
  branchPath,
147
149
  label,
150
+ nativeAnn,
148
151
  };
149
152
  }
150
153
  finally {
@@ -152,6 +155,181 @@ export const agenticowTools = [
152
155
  }
153
156
  },
154
157
  },
158
+ {
159
+ name: 'agenticow_ingest',
160
+ description: 'agenticow — write vectors (with optional text payloads) into an .rvf memory branch or base. Records: [{id?, vector, text?}] — id auto-assigns when omitted. This is the write half that makes a branch usable: agenticow_branch creates an empty COW child, but without ingest it has nothing to read back. Use after branching to populate an agent/session-local memory. Editing the base directly is wrong when the writes are speculative — ingest into a branch, then promote only if validated. Persists via .agenticow.json lineage manifest.',
161
+ category: 'memory',
162
+ tags: ['agenticow', 'memory', 'cow', 'ingest', 'write'],
163
+ inputSchema: {
164
+ type: 'object',
165
+ properties: {
166
+ path: { type: 'string', description: 'Path to .rvf memory file (branch or base)' },
167
+ records: {
168
+ type: 'array',
169
+ description: 'Vectors to ingest: [{id?: number, vector: number[], text?: string}]',
170
+ items: {
171
+ type: 'object',
172
+ properties: {
173
+ id: { type: 'integer', description: 'Explicit id (auto-assigned when omitted)' },
174
+ vector: { type: 'array', items: { type: 'number' }, description: 'Embedding vector (length must equal the memory dimension)' },
175
+ text: { type: 'string', description: 'Optional payload surfaced on query hits' },
176
+ },
177
+ required: ['vector'],
178
+ },
179
+ },
180
+ dimension: { type: 'integer', description: 'Vector dimension (required only when path does not exist yet)' },
181
+ },
182
+ required: ['path', 'records'],
183
+ },
184
+ handler: async (input) => {
185
+ const api = await loadAgenticow();
186
+ if (!api)
187
+ return degradedResult('agenticow-not-found');
188
+ const path = resolveMemoryPath(String(input.path));
189
+ const records = input.records;
190
+ if (!Array.isArray(records) || records.length === 0) {
191
+ throw new Error('records must be a non-empty array of {id?, vector, text?}');
192
+ }
193
+ for (const r of records) {
194
+ if (!Array.isArray(r.vector) || r.vector.length === 0) {
195
+ throw new Error('each record requires a non-empty numeric vector');
196
+ }
197
+ }
198
+ const dim = input.dimension ?? records[0].vector.length;
199
+ const mem = await openWithLineage(api, path, dim);
200
+ try {
201
+ const result = await mem.ingest(records.map((r) => ({
202
+ ...(typeof r.id === 'number' ? { id: r.id } : {}),
203
+ vector: r.vector,
204
+ ...(r.text !== undefined ? { text: r.text } : {}),
205
+ })));
206
+ await mem.save?.(manifestFor(path));
207
+ return { success: true, path, ingested: result };
208
+ }
209
+ finally {
210
+ await mem.close?.();
211
+ }
212
+ },
213
+ },
214
+ {
215
+ name: 'agenticow_query',
216
+ description: 'agenticow — k-NN read across an .rvf memory branch\'s full COW lineage (parent ∪ edits, child wins), returning {id, distance, branch, text}. Read-only (no manifest write). The `branch` field on each hit tells you which lineage node the result came from — the read-through semantics that make branching useful. Use to retrieve from an agent/session branch without materializing a full copy. Re-opening the base and manually merging edits is wrong: query already spans the chain (and uses the single-call Rust path when the branch was forked with nativeAnn).',
217
+ category: 'memory',
218
+ tags: ['agenticow', 'memory', 'cow', 'query', 'read', 'search'],
219
+ inputSchema: {
220
+ type: 'object',
221
+ properties: {
222
+ path: { type: 'string', description: 'Path to .rvf memory file' },
223
+ vector: { type: 'array', items: { type: 'number' }, description: 'Query embedding vector' },
224
+ k: { type: 'integer', description: 'Number of nearest neighbors to return', default: 10 },
225
+ efSearch: { type: 'integer', description: 'HNSW efSearch per lineage store (higher = better recall, slower)' },
226
+ },
227
+ required: ['path', 'vector'],
228
+ },
229
+ handler: async (input) => {
230
+ const api = await loadAgenticow();
231
+ if (!api)
232
+ return degradedResult('agenticow-not-found');
233
+ const path = resolveMemoryPath(String(input.path));
234
+ const vector = input.vector;
235
+ if (!Array.isArray(vector) || vector.length === 0) {
236
+ throw new Error('vector must be a non-empty numeric array');
237
+ }
238
+ const k = typeof input.k === 'number' ? input.k : 10;
239
+ const opts = {};
240
+ if (typeof input.efSearch === 'number')
241
+ opts.efSearch = input.efSearch;
242
+ const mem = await openWithLineage(api, path);
243
+ try {
244
+ const hits = await mem.query(vector, k, opts);
245
+ return { success: true, path, k, hits };
246
+ }
247
+ finally {
248
+ await mem.close?.();
249
+ }
250
+ },
251
+ },
252
+ {
253
+ name: 'agenticow_diff',
254
+ description: 'agenticow — show what a branch changed relative to its lineage: {added, overridden, deleted} vector-id lists. Use before promote to preview the exact merge, or to audit what an agent/session branch actually wrote. Diffing by re-querying is wrong because deletions (tombstones) are invisible to a read — diff() surfaces them explicitly. Requires the branch was opened with edit tracking (default on).',
255
+ category: 'memory',
256
+ tags: ['agenticow', 'memory', 'cow', 'diff'],
257
+ inputSchema: {
258
+ type: 'object',
259
+ properties: {
260
+ path: { type: 'string', description: 'Path to branch .rvf file' },
261
+ },
262
+ required: ['path'],
263
+ },
264
+ handler: async (input) => {
265
+ const api = await loadAgenticow();
266
+ if (!api)
267
+ return degradedResult('agenticow-not-found');
268
+ const path = resolveMemoryPath(String(input.path));
269
+ const mem = await openWithLineage(api, path);
270
+ try {
271
+ const diff = await mem.diff();
272
+ return { success: true, path, diff };
273
+ }
274
+ finally {
275
+ await mem.close?.();
276
+ }
277
+ },
278
+ },
279
+ {
280
+ name: 'agenticow_lineage',
281
+ description: 'agenticow — walk the COW chain of an .rvf memory file: an ordered list of nodes (role working|checkpoint|base, id, label, parent, createdAt, mutations, tombstones). Use to understand branch history, find checkpoint ids for a targeted rollback, or debug a promote. Guessing the chain from filenames is wrong — lineage is the authoritative structure the store maintains.',
282
+ category: 'memory',
283
+ tags: ['agenticow', 'memory', 'cow', 'lineage', 'history'],
284
+ inputSchema: {
285
+ type: 'object',
286
+ properties: {
287
+ path: { type: 'string', description: 'Path to .rvf memory file' },
288
+ },
289
+ required: ['path'],
290
+ },
291
+ handler: async (input) => {
292
+ const api = await loadAgenticow();
293
+ if (!api)
294
+ return degradedResult('agenticow-not-found');
295
+ const path = resolveMemoryPath(String(input.path));
296
+ const mem = await openWithLineage(api, path);
297
+ try {
298
+ const lineage = await mem.lineage();
299
+ return { success: true, path, lineage };
300
+ }
301
+ finally {
302
+ await mem.close?.();
303
+ }
304
+ },
305
+ },
306
+ {
307
+ name: 'agenticow_status',
308
+ description: 'agenticow — health/geometry of an .rvf memory file: {totalVectors, totalSegments, fileSize, currentEpoch, deadSpaceRatio, readOnly, chainDepth, dimension, metric}. Use to check vector count before/after ingest, spot compaction pressure (deadSpaceRatio), or confirm the dimension before ingesting into a shared base. Pure read.',
309
+ category: 'memory',
310
+ tags: ['agenticow', 'memory', 'cow', 'status'],
311
+ inputSchema: {
312
+ type: 'object',
313
+ properties: {
314
+ path: { type: 'string', description: 'Path to .rvf memory file' },
315
+ },
316
+ required: ['path'],
317
+ },
318
+ handler: async (input) => {
319
+ const api = await loadAgenticow();
320
+ if (!api)
321
+ return degradedResult('agenticow-not-found');
322
+ const path = resolveMemoryPath(String(input.path));
323
+ const mem = await openWithLineage(api, path);
324
+ try {
325
+ const status = await mem.status();
326
+ return { success: true, path, status };
327
+ }
328
+ finally {
329
+ await mem.close?.();
330
+ }
331
+ },
332
+ },
155
333
  {
156
334
  name: 'agenticow_checkpoint',
157
335
  description: 'agenticow — freeze a labelled restore point on an .rvf memory file. Subsequent edits stay in a fresh COW child; rollback returns here. Use when you are about to run an experimental Darwin tick or speculative agent edit that may need to be discarded. Relying on the working node alone is wrong because there is no "undo last N writes" semantics — without a checkpoint, a bad ingest contaminates the base. Persists via .agenticow.json lineage manifest so it survives close+reopen.',
@@ -191,6 +369,7 @@ export const agenticowTools = [
191
369
  type: 'object',
192
370
  properties: {
193
371
  path: { type: 'string', description: 'Path to .rvf memory file' },
372
+ checkpointId: { type: 'string', description: 'Target checkpoint id from agenticow_lineage (omit to roll back to the most recent checkpoint)' },
194
373
  },
195
374
  required: ['path'],
196
375
  },
@@ -199,9 +378,10 @@ export const agenticowTools = [
199
378
  if (!api)
200
379
  return degradedResult('agenticow-not-found');
201
380
  const path = resolveMemoryPath(String(input.path));
381
+ const checkpointId = input.checkpointId ? String(input.checkpointId) : undefined;
202
382
  const mem = await openWithLineage(api, path);
203
383
  try {
204
- const r = await mem.rollback();
384
+ const r = checkpointId ? await mem.rollback(checkpointId) : await mem.rollback();
205
385
  await mem.save?.(manifestFor(path));
206
386
  return { success: true, path, rolledBack: true, result: r };
207
387
  }
@@ -18,7 +18,7 @@ export { DiffClassifier, createDiffClassifier, analyzeDiff, analyzeDiffSync, ass
18
18
  export { CoverageRouter, createCoverageRouter, coverageRoute, coverageSuggest, coverageGaps, clearCoverageCache, getCoverageCacheStats, type CoverageRouterConfig, type FileCoverage, type CoverageReport, type CoverageRouteResult, type CoverageSuggestResult, type CoverageGapsResult, type CoverageRouteOptions, type CoverageSuggestOptions, type CoverageGapsOptions, } from './coverage-router.js';
19
19
  export { coverageRouterTools, hooksCoverageRoute, hooksCoverageSuggest, hooksCoverageGaps } from './coverage-tools.js';
20
20
  export { buildDependencyGraph, analyzeGraph, analyzeMinCutBoundaries, analyzeModuleCommunities, detectCircularDependencies, exportToDot, loadRuVector, fallbackMinCut, fallbackLouvain, clearGraphCaches, getGraphCacheStats, type GraphNode, type GraphEdge, type DependencyGraph, type MinCutBoundary, type ModuleCommunity, type CircularDependency, type GraphAnalysisResult, } from './graph-analyzer.js';
21
- export { LoRAAdapter, getLoRAAdapter, resetLoRAAdapter, createLoRAAdapter, adaptEmbedding, trainLoRA, getLoRAStats, DEFAULT_RANK, DEFAULT_ALPHA, INPUT_DIM as LORA_INPUT_DIM, OUTPUT_DIM as LORA_OUTPUT_DIM, type LoRAConfig, type LoRAWeights, type AdaptationResult, type LoRAStats, } from './lora-adapter.js';
21
+ export { LoRAAdapter, getLoRAAdapter, resetLoRAAdapter, createLoRAAdapter, adaptEmbedding, trainLoRA, getLoRAStats, loadLatestCheckpoint, latestCheckpointInfo, formatCheckpointAge, DEFAULT_RANK, DEFAULT_ALPHA, INPUT_DIM as LORA_INPUT_DIM, OUTPUT_DIM as LORA_OUTPUT_DIM, type LoRAConfig, type LoRAWeights, type AdaptationResult, type LoRAStats, type CheckpointInfo, } from './lora-adapter.js';
22
22
  export { ModelRouter, getModelRouter, resetModelRouter, createModelRouter, routeToModel, routeToModelFull, analyzeTaskComplexity, getModelRouterStats, recordModelOutcome, MODEL_CAPABILITIES, COMPLEXITY_INDICATORS, type ClaudeModel, type ModelRouterConfig, type ModelRoutingResult, type ComplexityAnalysis, } from './model-router.js';
23
23
  export { SemanticRouter, createSemanticRouter, type Intent, type RouteResult, type RouterConfig, } from './semantic-router.js';
24
24
  export { isRuvllmWasmAvailable, initRuvllmWasm, getRuvllmStatus, createHnswRouter, createSonaInstant, createMicroLora, formatChat, createKvCache, createGenerateConfig, createBufferPool, createInferenceArena, HNSW_MAX_SAFE_PATTERNS, type HnswRouterConfig, type HnswPattern, type HnswRouteResult, type SonaConfig, type MicroLoraConfig, type ChatMessage, type GenerateOptions, type RuvllmStatus, } from './ruvllm-wasm.js';
@@ -36,7 +36,7 @@ clearGraphCaches, getGraphCacheStats, } from './graph-analyzer.js';
36
36
  // consumers (hooks-tools.ts, neural-tools.ts) import from '@claude-flow/neural'
37
37
  // explicitly; re-exporting through this barrel pulls the package's
38
38
  // transitive @ruvector/sona dep into vitest's eager resolution.
39
- export { LoRAAdapter, getLoRAAdapter, resetLoRAAdapter, createLoRAAdapter, adaptEmbedding, trainLoRA, getLoRAStats, DEFAULT_RANK, DEFAULT_ALPHA, INPUT_DIM as LORA_INPUT_DIM, OUTPUT_DIM as LORA_OUTPUT_DIM, } from './lora-adapter.js';
39
+ export { LoRAAdapter, getLoRAAdapter, resetLoRAAdapter, createLoRAAdapter, adaptEmbedding, trainLoRA, getLoRAStats, loadLatestCheckpoint, latestCheckpointInfo, formatCheckpointAge, DEFAULT_RANK, DEFAULT_ALPHA, INPUT_DIM as LORA_INPUT_DIM, OUTPUT_DIM as LORA_OUTPUT_DIM, } from './lora-adapter.js';
40
40
  export { ModelRouter, getModelRouter, resetModelRouter, createModelRouter, routeToModel, routeToModelFull, analyzeTaskComplexity, getModelRouterStats, recordModelOutcome, MODEL_CAPABILITIES, COMPLEXITY_INDICATORS, } from './model-router.js';
41
41
  export { SemanticRouter, createSemanticRouter, } from './semantic-router.js';
42
42
  // ── RuVector LLM WASM (inference utilities) ─────────────────
@@ -213,6 +213,41 @@ export declare class LoRAAdapter {
213
213
  scaling: number;
214
214
  }): boolean;
215
215
  }
216
+ /**
217
+ * Info about the newest on-disk training checkpoint.
218
+ */
219
+ export interface CheckpointInfo {
220
+ /** Absolute path to the checkpoint file */
221
+ path: string;
222
+ /** Basename, e.g. lora-checkpoint-1712345678901.json */
223
+ filename: string;
224
+ /** Age in ms derived from the filename timestamp (falls back to mtime) */
225
+ ageMs: number;
226
+ /** Human-friendly age, e.g. "2h ago" */
227
+ ageLabel: string;
228
+ }
229
+ /** Compact relative-age label. Most-significant unit only. */
230
+ export declare function formatCheckpointAge(ms: number): string;
231
+ /**
232
+ * Find the newest `lora-checkpoint-<ts>.json` without loading it. Pure disk
233
+ * scan — safe to call from read-only status surfaces. Returns null when no
234
+ * checkpoint exists or the directory is unreadable.
235
+ */
236
+ export declare function latestCheckpointInfo(): CheckpointInfo | null;
237
+ /**
238
+ * Flywheel entry point: locate the newest trained checkpoint and load it into
239
+ * `adapter` via the adapter's existing loadCheckpoint path, so routing /
240
+ * adaptation benefits from prior `neural train` runs.
241
+ *
242
+ * Contract: lazy (call on first adaptation use, never at CLI startup),
243
+ * non-fatal on any failure, and kill-switchable via
244
+ * CLAUDE_FLOW_NO_CHECKPOINT_AUTOLOAD=1.
245
+ */
246
+ export declare function loadLatestCheckpoint(adapter: LoRAAdapter): Promise<{
247
+ loaded: boolean;
248
+ path?: string;
249
+ ageMs?: number;
250
+ }>;
216
251
  /**
217
252
  * Get or create singleton LoRA adapter instance
218
253
  */
@@ -17,7 +17,7 @@
17
17
  *
18
18
  * @module lora-adapter
19
19
  */
20
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
20
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'fs';
21
21
  import { dirname, join } from 'path';
22
22
  import { createRequire } from 'module';
23
23
  // ============================================================================
@@ -527,6 +527,105 @@ export class LoRAAdapter {
527
527
  }
528
528
  }
529
529
  }
530
+ /**
531
+ * Directories `neural train` writes checkpoints to. It only ever writes to
532
+ * `<cwd>/.claude-flow/neural` (see commands/neural.ts + services/native-training.ts),
533
+ * so that is the single source of truth — kept in a helper so the discovery
534
+ * and status surfaces stay consistent.
535
+ */
536
+ function checkpointDirs() {
537
+ return [join(process.cwd(), '.claude-flow', 'neural')];
538
+ }
539
+ /** Compact relative-age label. Most-significant unit only. */
540
+ export function formatCheckpointAge(ms) {
541
+ const s = Math.floor(ms / 1000);
542
+ if (s < 60)
543
+ return `${s}s ago`;
544
+ const m = Math.floor(s / 60);
545
+ if (m < 60)
546
+ return `${m}m ago`;
547
+ const h = Math.floor(m / 60);
548
+ if (h < 24)
549
+ return `${h}h ago`;
550
+ const d = Math.floor(h / 24);
551
+ return `${d}d ago`;
552
+ }
553
+ /**
554
+ * Find the newest `lora-checkpoint-<ts>.json` without loading it. Pure disk
555
+ * scan — safe to call from read-only status surfaces. Returns null when no
556
+ * checkpoint exists or the directory is unreadable.
557
+ */
558
+ export function latestCheckpointInfo() {
559
+ try {
560
+ let best = null;
561
+ for (const dir of checkpointDirs()) {
562
+ if (!existsSync(dir))
563
+ continue;
564
+ for (const f of readdirSync(dir)) {
565
+ const match = /^lora-checkpoint-(\d+)\.json$/.exec(f);
566
+ if (!match)
567
+ continue;
568
+ const ts = Number(match[1]);
569
+ if (!Number.isFinite(ts))
570
+ continue;
571
+ if (!best || ts > best.ts)
572
+ best = { path: join(dir, f), filename: f, ts };
573
+ }
574
+ }
575
+ if (!best)
576
+ return null;
577
+ const ageMs = Math.max(0, Date.now() - best.ts);
578
+ return { path: best.path, filename: best.filename, ageMs, ageLabel: formatCheckpointAge(ageMs) };
579
+ }
580
+ catch {
581
+ return null;
582
+ }
583
+ }
584
+ /**
585
+ * Validate a file looks like a ruvllm training checkpoint before loading it,
586
+ * so a stray/corrupt JSON in the neural dir can't poison routing weights.
587
+ * Accepts both the JS-fallback envelope ({A,B,scaling}) and native ruvllm
588
+ * checkpoint shapes (weights/adapter/epoch/step/loss/version/index markers).
589
+ */
590
+ function isCheckpointEnvelope(path) {
591
+ try {
592
+ const data = JSON.parse(readFileSync(path, 'utf-8'));
593
+ if (!data || typeof data !== 'object')
594
+ return false;
595
+ const obj = data;
596
+ if (Array.isArray(obj.A) && Array.isArray(obj.B))
597
+ return true;
598
+ return ['weights', 'adapter', 'epoch', 'step', 'loss', 'version', 'index'].some((k) => k in obj);
599
+ }
600
+ catch {
601
+ return false;
602
+ }
603
+ }
604
+ /**
605
+ * Flywheel entry point: locate the newest trained checkpoint and load it into
606
+ * `adapter` via the adapter's existing loadCheckpoint path, so routing /
607
+ * adaptation benefits from prior `neural train` runs.
608
+ *
609
+ * Contract: lazy (call on first adaptation use, never at CLI startup),
610
+ * non-fatal on any failure, and kill-switchable via
611
+ * CLAUDE_FLOW_NO_CHECKPOINT_AUTOLOAD=1.
612
+ */
613
+ export async function loadLatestCheckpoint(adapter) {
614
+ if (process.env.CLAUDE_FLOW_NO_CHECKPOINT_AUTOLOAD === '1')
615
+ return { loaded: false };
616
+ try {
617
+ const info = latestCheckpointInfo();
618
+ if (!info)
619
+ return { loaded: false };
620
+ if (!isCheckpointEnvelope(info.path))
621
+ return { loaded: false, path: info.path, ageMs: info.ageMs };
622
+ const loaded = await adapter.loadCheckpoint(info.path);
623
+ return { loaded, path: info.path, ageMs: info.ageMs };
624
+ }
625
+ catch {
626
+ return { loaded: false };
627
+ }
628
+ }
530
629
  // ============================================================================
531
630
  // Singleton & Factory Functions
532
631
  // ============================================================================
@@ -545,6 +644,14 @@ export async function getLoRAAdapter() {
545
644
  initPromise = (async () => {
546
645
  const adapter = new LoRAAdapter();
547
646
  await adapter.initialize();
647
+ // Flywheel: fold in the newest trained checkpoint so routing/adaptation
648
+ // benefits from prior `neural train` runs. This singleton is constructed
649
+ // lazily on first adaptation use (NOT at CLI startup), so the checkpoint
650
+ // scan + ruvllm load stay off the hot `--help` path. Non-fatal.
651
+ try {
652
+ await loadLatestCheckpoint(adapter);
653
+ }
654
+ catch { /* auto-load is best-effort — never block adaptation */ }
548
655
  loraInstance = adapter;
549
656
  return adapter;
550
657
  })();
@@ -25,6 +25,14 @@ export interface NativeTrainingResult {
25
25
  earlyStopped: boolean;
26
26
  checkpointPath?: string;
27
27
  checkpointBytes?: number;
28
+ /** True when training resumed from a --resume checkpoint. */
29
+ resumed?: boolean;
30
+ /**
31
+ * How the resume happened: 'resumeFrom' (ruvllm >=2.6.0 — restores epoch
32
+ * position + optimizer state) or 'loadCheckpoint' (2.5.7 fallback —
33
+ * weights only, training restarts from epoch 0).
34
+ */
35
+ resumeMode?: 'resumeFrom' | 'loadCheckpoint';
28
36
  }
29
37
  export interface NativeTrainingOptions {
30
38
  embeddings: Float32Array[];
@@ -32,9 +40,29 @@ export interface NativeTrainingOptions {
32
40
  batchSize: number;
33
41
  learningRate: number;
34
42
  dim: number;
43
+ /**
44
+ * Validation holdout fraction (0..1). >0 makes the pipeline report
45
+ * bestValLoss + early-stopping; 0 (or omitted) disables validation.
46
+ * validationSplit exists in ruvllm 2.5.7 — no version gate needed.
47
+ */
48
+ validationSplit?: number;
49
+ /**
50
+ * Resume weights (and, on >=2.6.0, epoch position) from this checkpoint
51
+ * BEFORE training. A missing/invalid path throws ResumeFailedError — an
52
+ * explicit --resume must fail loudly, never silently fresh-train.
53
+ */
54
+ resumeFrom?: string;
35
55
  /** When set, the TRAINED pipeline checkpoints here (ruvllm >=2.5.7). */
36
56
  checkpointPath?: string;
37
57
  }
58
+ /**
59
+ * Thrown when an explicit --resume checkpoint cannot be found or loaded.
60
+ * Distinct from the generic "native unavailable → null" degradation so the
61
+ * caller can surface a loud, exit-1 failure rather than fresh training.
62
+ */
63
+ export declare class ResumeFailedError extends Error {
64
+ constructor(message: string);
65
+ }
38
66
  export declare function nativeTrainingAvailable(): boolean;
39
67
  export declare function runNativeTraining(opts: NativeTrainingOptions): Promise<NativeTrainingResult | null>;
40
68
  //# sourceMappingURL=native-training.d.ts.map
@@ -19,6 +19,17 @@
19
19
  import { createRequire } from 'module';
20
20
  import { mkdirSync, existsSync } from 'fs';
21
21
  import { dirname } from 'path';
22
+ /**
23
+ * Thrown when an explicit --resume checkpoint cannot be found or loaded.
24
+ * Distinct from the generic "native unavailable → null" degradation so the
25
+ * caller can surface a loud, exit-1 failure rather than fresh training.
26
+ */
27
+ export class ResumeFailedError extends Error {
28
+ constructor(message) {
29
+ super(message);
30
+ this.name = 'ResumeFailedError';
31
+ }
32
+ }
22
33
  export function nativeTrainingAvailable() {
23
34
  try {
24
35
  createRequire(import.meta.url).resolve('@ruvector/ruvllm');
@@ -29,19 +40,53 @@ export function nativeTrainingAvailable() {
29
40
  }
30
41
  }
31
42
  export async function runNativeTraining(opts) {
32
- const { embeddings, epochs, batchSize, learningRate, dim, checkpointPath } = opts;
43
+ const { embeddings, epochs, batchSize, learningRate, dim, validationSplit, resumeFrom, checkpointPath } = opts;
33
44
  if (embeddings.length < 2)
34
45
  return null;
35
46
  try {
36
47
  const req = createRequire(import.meta.url);
37
48
  const ruvllm = req('@ruvector/ruvllm');
38
- const pipeline = new ruvllm.TrainingPipeline({
49
+ const pipelineConfig = {
39
50
  learningRate,
40
51
  batchSize,
41
52
  epochs,
42
53
  inputDim: dim,
43
54
  outputDim: dim,
44
- });
55
+ };
56
+ // 0 (or omitted) disables validation; only pass a real holdout through.
57
+ if (typeof validationSplit === 'number' && validationSplit > 0) {
58
+ pipelineConfig.validationSplit = validationSplit;
59
+ }
60
+ const pipeline = new ruvllm.TrainingPipeline(pipelineConfig);
61
+ // Resume BEFORE training. Prefer resumeFrom() (2.6.0 — epoch position +
62
+ // optimizer state); fall back to loadCheckpoint() (2.5.7 — weights only).
63
+ // Any failure with an explicit --resume is loud (ResumeFailedError),
64
+ // never silent fresh training.
65
+ let resumed = false;
66
+ let resumeMode;
67
+ if (resumeFrom) {
68
+ if (!existsSync(resumeFrom)) {
69
+ throw new ResumeFailedError(`--resume checkpoint not found: ${resumeFrom}`);
70
+ }
71
+ try {
72
+ if (typeof pipeline.resumeFrom === 'function') {
73
+ pipeline.resumeFrom(resumeFrom);
74
+ resumeMode = 'resumeFrom';
75
+ }
76
+ else {
77
+ const ok = pipeline.loadCheckpoint(resumeFrom);
78
+ if (ok === false)
79
+ throw new Error('loadCheckpoint returned false');
80
+ resumeMode = 'loadCheckpoint';
81
+ }
82
+ resumed = true;
83
+ }
84
+ catch (e) {
85
+ if (e instanceof ResumeFailedError)
86
+ throw e;
87
+ throw new ResumeFailedError(`--resume failed to load checkpoint ${resumeFrom}: ${e.message}`);
88
+ }
89
+ }
45
90
  // Pattern-alignment pairs, chunked into pipeline batches.
46
91
  const inputs = [];
47
92
  const targets = [];
@@ -55,13 +100,21 @@ export async function runNativeTraining(opts) {
55
100
  pipeline.addBatch(inputs.slice(i, i + batchSize), targets.slice(i, i + batchSize), qualities.slice(i, i + batchSize));
56
101
  }
57
102
  const r = pipeline.train();
103
+ // When validation is disabled the pipeline reports Infinity for
104
+ // bestValLoss — normalize any non-finite value to null so downstream
105
+ // "bestValLoss !== null ⇒ validation ran" holds for the results table.
106
+ const bestValLoss = typeof r.bestValLoss === 'number' && Number.isFinite(r.bestValLoss)
107
+ ? r.bestValLoss
108
+ : null;
58
109
  const result = {
59
110
  epochs: r.epochs,
60
111
  steps: r.steps,
61
112
  finalLoss: r.finalLoss,
62
- bestValLoss: r.bestValLoss ?? null,
113
+ bestValLoss,
63
114
  durationMs: r.durationMs,
64
115
  earlyStopped: !!r.earlyStopped,
116
+ resumed,
117
+ resumeMode,
65
118
  };
66
119
  if (checkpointPath) {
67
120
  try {
@@ -77,7 +130,11 @@ export async function runNativeTraining(opts) {
77
130
  }
78
131
  return result;
79
132
  }
80
- catch {
133
+ catch (err) {
134
+ // An explicit --resume failure must propagate as a loud error; every
135
+ // other failure degrades to null so callers fall back to the WASM path.
136
+ if (err instanceof ResumeFailedError)
137
+ throw err;
81
138
  return null;
82
139
  }
83
140
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-flow/cli",
3
- "version": "3.19.0",
3
+ "version": "3.20.0",
4
4
  "type": "module",
5
5
  "description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
6
6
  "main": "dist/src/index.js",