claude-flow 3.19.0 → 3.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/v3/@claude-flow/cli/dist/src/agenticow/speculative-exploration.d.ts +148 -0
- package/v3/@claude-flow/cli/dist/src/agenticow/speculative-exploration.js +218 -0
- package/v3/@claude-flow/cli/dist/src/commands/autopilot.js +45 -0
- package/v3/@claude-flow/cli/dist/src/commands/neural.js +366 -12
- package/v3/@claude-flow/cli/dist/src/mcp-client.js +6 -1
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-execute-core.js +33 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-tools.js +47 -1
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agenticow-loader.d.ts +59 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agenticow-loader.js +105 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agenticow-speculate-tools.d.ts +24 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agenticow-speculate-tools.js +202 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agenticow-tools.js +186 -79
- package/v3/@claude-flow/cli/dist/src/mcp-tools/index.d.ts +1 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/index.js +2 -0
- package/v3/@claude-flow/cli/dist/src/ruvector/index.d.ts +1 -1
- package/v3/@claude-flow/cli/dist/src/ruvector/index.js +1 -1
- package/v3/@claude-flow/cli/dist/src/ruvector/lora-adapter.d.ts +35 -0
- package/v3/@claude-flow/cli/dist/src/ruvector/lora-adapter.js +108 -1
- package/v3/@claude-flow/cli/dist/src/ruvector/router-trajectory.d.ts +33 -0
- package/v3/@claude-flow/cli/dist/src/ruvector/router-trajectory.js +31 -0
- package/v3/@claude-flow/cli/dist/src/ruvector/run-transcript-recorder.d.ts +154 -0
- package/v3/@claude-flow/cli/dist/src/ruvector/run-transcript-recorder.js +209 -0
- package/v3/@claude-flow/cli/dist/src/services/checkpoint-gate.d.ts +140 -0
- package/v3/@claude-flow/cli/dist/src/services/checkpoint-gate.js +223 -0
- package/v3/@claude-flow/cli/dist/src/services/distill-oracle.d.ts +190 -0
- package/v3/@claude-flow/cli/dist/src/services/distill-oracle.js +349 -0
- package/v3/@claude-flow/cli/dist/src/services/fable-harness.d.ts +168 -0
- package/v3/@claude-flow/cli/dist/src/services/fable-harness.js +347 -0
- package/v3/@claude-flow/cli/dist/src/services/native-training.d.ts +28 -0
- package/v3/@claude-flow/cli/dist/src/services/native-training.js +62 -5
- package/v3/@claude-flow/cli/dist/src/services/swarm-memory-branches.d.ts +135 -0
- package/v3/@claude-flow/cli/dist/src/services/swarm-memory-branches.js +213 -0
- package/v3/@claude-flow/cli/dist/src/services/weight-eft.d.ts +305 -0
- package/v3/@claude-flow/cli/dist/src/services/weight-eft.js +296 -0
- package/v3/@claude-flow/cli/package.json +5 -4
|
@@ -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
|
})();
|
|
@@ -13,6 +13,19 @@
|
|
|
13
13
|
* Schema is versioned (`"v": 1`). New required fields bump the version;
|
|
14
14
|
* additive optional fields do not.
|
|
15
15
|
*
|
|
16
|
+
* COMPANION: run-transcript-recorder.ts (weight-eft capture path)
|
|
17
|
+
* --------------------------------------------------------------
|
|
18
|
+
* This recorder captures the routing DECISION only (task, embedding, scalar
|
|
19
|
+
* quality, tokens, cost) — enough to retrain the router. It deliberately does
|
|
20
|
+
* NOT carry the full message transcript, the produced patch, or a resolved
|
|
21
|
+
* boolean. `@metaharness/weight-eft` needs those to build SFT/DPO training
|
|
22
|
+
* rows, so a SEPARATE opt-in recorder — `run-transcript-recorder.ts` — captures
|
|
23
|
+
* the full run transcript to `.swarm/run-transcripts.jsonl`. Both share the
|
|
24
|
+
* `taskHash()` below as their join key, and both are off-by-default for the
|
|
25
|
+
* same PII/retention reason. Use `unifiedRecorderStatus()` (bottom of this
|
|
26
|
+
* file) to inspect both at once. Keeping them as two files keeps the routing
|
|
27
|
+
* hot path free of the heavier transcript payload.
|
|
28
|
+
*
|
|
16
29
|
* @module router-trajectory
|
|
17
30
|
*/
|
|
18
31
|
import type { ClaudeModel } from './model-router.js';
|
|
@@ -158,4 +171,24 @@ export declare function pairTrajectoryRows(rows: TrajectoryRow[]): {
|
|
|
158
171
|
};
|
|
159
172
|
/** Test seam — reset cached config so unit tests can change env vars between cases. */
|
|
160
173
|
export declare function __resetTrajectoryRecorderForTests(): void;
|
|
174
|
+
/**
|
|
175
|
+
* Unified status for BOTH the routing-decision recorder (this module) and the
|
|
176
|
+
* companion run-transcript recorder (the weight-eft capture path). The
|
|
177
|
+
* run-transcript recorder is loaded dynamically so this module has no static
|
|
178
|
+
* dependency on it (the reverse edge — run-transcript-recorder → taskHash —
|
|
179
|
+
* is the only static link, keeping the import acyclic).
|
|
180
|
+
*/
|
|
181
|
+
export declare function unifiedRecorderStatus(): Promise<{
|
|
182
|
+
routerTrajectory: {
|
|
183
|
+
enabled: boolean;
|
|
184
|
+
path: string;
|
|
185
|
+
taskCharLimit: number;
|
|
186
|
+
};
|
|
187
|
+
runTranscripts: {
|
|
188
|
+
enabled: boolean;
|
|
189
|
+
path: string;
|
|
190
|
+
} | {
|
|
191
|
+
unavailable: true;
|
|
192
|
+
};
|
|
193
|
+
}>;
|
|
161
194
|
//# sourceMappingURL=router-trajectory.d.ts.map
|
|
@@ -13,6 +13,19 @@
|
|
|
13
13
|
* Schema is versioned (`"v": 1`). New required fields bump the version;
|
|
14
14
|
* additive optional fields do not.
|
|
15
15
|
*
|
|
16
|
+
* COMPANION: run-transcript-recorder.ts (weight-eft capture path)
|
|
17
|
+
* --------------------------------------------------------------
|
|
18
|
+
* This recorder captures the routing DECISION only (task, embedding, scalar
|
|
19
|
+
* quality, tokens, cost) — enough to retrain the router. It deliberately does
|
|
20
|
+
* NOT carry the full message transcript, the produced patch, or a resolved
|
|
21
|
+
* boolean. `@metaharness/weight-eft` needs those to build SFT/DPO training
|
|
22
|
+
* rows, so a SEPARATE opt-in recorder — `run-transcript-recorder.ts` — captures
|
|
23
|
+
* the full run transcript to `.swarm/run-transcripts.jsonl`. Both share the
|
|
24
|
+
* `taskHash()` below as their join key, and both are off-by-default for the
|
|
25
|
+
* same PII/retention reason. Use `unifiedRecorderStatus()` (bottom of this
|
|
26
|
+
* file) to inspect both at once. Keeping them as two files keeps the routing
|
|
27
|
+
* hot path free of the heavier transcript payload.
|
|
28
|
+
*
|
|
16
29
|
* @module router-trajectory
|
|
17
30
|
*/
|
|
18
31
|
import { appendFileSync, mkdirSync, existsSync, statSync, renameSync, unlinkSync } from 'node:fs';
|
|
@@ -247,4 +260,22 @@ export function __resetTrajectoryRecorderForTests() {
|
|
|
247
260
|
_cfg = null;
|
|
248
261
|
_cachedSize = -1;
|
|
249
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* Unified status for BOTH the routing-decision recorder (this module) and the
|
|
265
|
+
* companion run-transcript recorder (the weight-eft capture path). The
|
|
266
|
+
* run-transcript recorder is loaded dynamically so this module has no static
|
|
267
|
+
* dependency on it (the reverse edge — run-transcript-recorder → taskHash —
|
|
268
|
+
* is the only static link, keeping the import acyclic).
|
|
269
|
+
*/
|
|
270
|
+
export async function unifiedRecorderStatus() {
|
|
271
|
+
const routerTrajectory = trajectoryRecorderStatus();
|
|
272
|
+
try {
|
|
273
|
+
const mod = await import('./run-transcript-recorder.js');
|
|
274
|
+
const s = mod.runTranscriptRecorderStatus();
|
|
275
|
+
return { routerTrajectory, runTranscripts: { enabled: s.enabled, path: s.path } };
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
return { routerTrajectory, runTranscripts: { unavailable: true } };
|
|
279
|
+
}
|
|
280
|
+
}
|
|
250
281
|
//# sourceMappingURL=router-trajectory.js.map
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* run-transcript-recorder.ts — Opt-in FULL run-transcript recorder for the
|
|
3
|
+
* weight-eft training-data export path (agenticow / ADR-150 weight-eft slice).
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS
|
|
6
|
+
* ---------------
|
|
7
|
+
* The existing `router-trajectory.ts` recorder captures only the ROUTING
|
|
8
|
+
* DECISION for a task: task text, embedding, scalar quality, tokens, cost.
|
|
9
|
+
* `@metaharness/weight-eft` needs something the routing recorder never had —
|
|
10
|
+
* the full ReAct message TRANSCRIPT, the produced patch, and a resolved
|
|
11
|
+
* boolean — to build SFT/DPO training rows. This module is that missing
|
|
12
|
+
* capture surface. It writes one JSON-line per completed run to
|
|
13
|
+
* `.swarm/run-transcripts.jsonl`, in a shape the archive-builder in
|
|
14
|
+
* `services/weight-eft.ts` maps directly to `DarwinTrajectory[]`.
|
|
15
|
+
*
|
|
16
|
+
* OFF BY DEFAULT (PII / RETENTION SURFACE)
|
|
17
|
+
* ----------------------------------------
|
|
18
|
+
* Rows carry the FULL prompt + assistant transcript + patch — a much larger
|
|
19
|
+
* PII/retention surface than the routing recorder. Mirroring why
|
|
20
|
+
* router-trajectory.ts is off-by-default, every write goes through the
|
|
21
|
+
* `CLAUDE_FLOW_RUN_TRANSCRIPTS=1` env gate. When unset (the default),
|
|
22
|
+
* `recordRunTranscript()` is a no-op. There is no way to enable it implicitly.
|
|
23
|
+
*
|
|
24
|
+
* HONESTY: `resolved` IS A PROXY
|
|
25
|
+
* ------------------------------
|
|
26
|
+
* `DarwinTrajectory.resolved` is meant to be GOLD-resolved status from the
|
|
27
|
+
* official SWE-bench harness. Ruflo has NO SWE-bench oracle. Every record
|
|
28
|
+
* therefore stamps `resolved_source` describing where the boolean actually
|
|
29
|
+
* came from, so no downstream consumer can mistake a proxy for gold:
|
|
30
|
+
* - 'gold-oracle' — a real conformant gold eval supplied it (never
|
|
31
|
+
* ruflo today; reserved for an external caller)
|
|
32
|
+
* - 'output-verifier' — ruflo's structural output-verifier confidence,
|
|
33
|
+
* thresholded — an EXPLICIT proxy
|
|
34
|
+
* - 'api-success' — the model returned without an API error — the
|
|
35
|
+
* weakest proxy (says nothing about correctness)
|
|
36
|
+
* - 'external' — supplied verbatim by the caller, provenance unknown
|
|
37
|
+
*
|
|
38
|
+
* ARCHITECTURAL CONSTRAINTS (mirror router-trajectory.ts / ADR-150)
|
|
39
|
+
* -----------------------------------------------------------------
|
|
40
|
+
* 1. OPT-IN — gated behind CLAUDE_FLOW_RUN_TRANSCRIPTS=1; default off.
|
|
41
|
+
* 2. NEVER THROWS — every fs op is try/caught at the append boundary; a
|
|
42
|
+
* failed write is silent (DEBUG-logged) and never breaks the run.
|
|
43
|
+
* 3. NO metaharness COUPLING — this module imports nothing from
|
|
44
|
+
* `@metaharness/*`; it only defines a portable record shape. The
|
|
45
|
+
* services/weight-eft.ts archive-builder does the (optional) mapping.
|
|
46
|
+
*
|
|
47
|
+
* SCHEMA (versioned, additive) — one JSONL row per completed run:
|
|
48
|
+
* { v, ts, instance_id, task_hash, model, tier, resolved, resolved_source,
|
|
49
|
+
* messages[], model_patch, sample?, source?, tokens?, cost_usd? }
|
|
50
|
+
*
|
|
51
|
+
* @module run-transcript-recorder
|
|
52
|
+
*/
|
|
53
|
+
/** An OpenAI-style tool call (the ReAct action). Mirrors weight-eft's ToolCall. */
|
|
54
|
+
export interface ToolCallLite {
|
|
55
|
+
id: string;
|
|
56
|
+
type: 'function';
|
|
57
|
+
function: {
|
|
58
|
+
name: string;
|
|
59
|
+
arguments: string;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** A chat message in an OpenAI-compatible transcript. Mirrors weight-eft's ChatMessage. */
|
|
63
|
+
export interface ChatMessageLite {
|
|
64
|
+
role: 'system' | 'user' | 'assistant' | 'tool';
|
|
65
|
+
content: string | null;
|
|
66
|
+
tool_calls?: ToolCallLite[];
|
|
67
|
+
tool_call_id?: string;
|
|
68
|
+
name?: string;
|
|
69
|
+
}
|
|
70
|
+
/** Where a `resolved` boolean actually came from (never silently "gold"). */
|
|
71
|
+
export type ResolvedSource = 'gold-oracle' | 'output-verifier' | 'api-success' | 'external';
|
|
72
|
+
/** Ruflo's cascade tier. haiku → 'cheap' (first tier), sonnet/opus → 'frontier'. */
|
|
73
|
+
export type RunTier = 'cheap' | 'frontier';
|
|
74
|
+
/** One persisted run transcript. Maps 1:1 to a DarwinTrajectory (+ provenance). */
|
|
75
|
+
export interface RunTranscriptRecord {
|
|
76
|
+
v: 1;
|
|
77
|
+
ts: string;
|
|
78
|
+
instance_id: string;
|
|
79
|
+
task_hash: string;
|
|
80
|
+
model: string;
|
|
81
|
+
tier: RunTier;
|
|
82
|
+
resolved: boolean;
|
|
83
|
+
resolved_source: ResolvedSource;
|
|
84
|
+
messages: ChatMessageLite[];
|
|
85
|
+
model_patch: string;
|
|
86
|
+
sample?: number;
|
|
87
|
+
source?: string;
|
|
88
|
+
tokens?: {
|
|
89
|
+
input: number;
|
|
90
|
+
output: number;
|
|
91
|
+
};
|
|
92
|
+
cost_usd?: number;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Record one completed run transcript. Cheap — a single appendFileSync of a
|
|
96
|
+
* JSONL row. No-op when CLAUDE_FLOW_RUN_TRANSCRIPTS is unset (the default).
|
|
97
|
+
* Never throws.
|
|
98
|
+
*
|
|
99
|
+
* `resolvedSource` is REQUIRED so a proxy can never masquerade as gold. If you
|
|
100
|
+
* only have "the API returned", pass 'api-success' — the honest weakest label.
|
|
101
|
+
*/
|
|
102
|
+
export declare function recordRunTranscript(args: {
|
|
103
|
+
/** Task/issue text — used for the FNV hash + default instance id. */
|
|
104
|
+
task: string;
|
|
105
|
+
/** Concrete model id that produced the run (e.g. "claude-haiku-4"). */
|
|
106
|
+
model: string;
|
|
107
|
+
/** Ruflo cascade tier of `model`. */
|
|
108
|
+
tier: RunTier;
|
|
109
|
+
/** The resolved boolean (see resolvedSource for what it actually means). */
|
|
110
|
+
resolved: boolean;
|
|
111
|
+
/** Provenance of `resolved`. Never omit — honesty is the point. */
|
|
112
|
+
resolvedSource: ResolvedSource;
|
|
113
|
+
/** OpenAI-shaped message transcript (system/user/assistant/tool). */
|
|
114
|
+
messages: ChatMessageLite[];
|
|
115
|
+
/** Unified diff the run produced. '' when the path produces no patch. */
|
|
116
|
+
modelPatch?: string;
|
|
117
|
+
/** Override the default "run-<hash>" instance id (contamination key). */
|
|
118
|
+
instanceId?: string;
|
|
119
|
+
/** Best-of-N sample index on the same instance (default 0). */
|
|
120
|
+
sample?: number;
|
|
121
|
+
/** Provenance tag, e.g. "agent-execute", "autopilot". */
|
|
122
|
+
source?: string;
|
|
123
|
+
tokens?: {
|
|
124
|
+
input: number;
|
|
125
|
+
output: number;
|
|
126
|
+
};
|
|
127
|
+
costUsd?: number;
|
|
128
|
+
}): {
|
|
129
|
+
recorded: boolean;
|
|
130
|
+
instanceId: string;
|
|
131
|
+
taskHash: string;
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* Read + parse the run-transcript JSONL back into records. Used by the
|
|
135
|
+
* archive-builder (`services/weight-eft.ts`). Malformed lines are skipped and
|
|
136
|
+
* counted, never thrown. Returns `[]` if the file is absent.
|
|
137
|
+
*/
|
|
138
|
+
export declare function readRunTranscripts(path?: string): {
|
|
139
|
+
records: RunTranscriptRecord[];
|
|
140
|
+
malformed: number;
|
|
141
|
+
path: string;
|
|
142
|
+
};
|
|
143
|
+
/** Map a ruflo model tier label to the weight-eft policy tier. */
|
|
144
|
+
export declare function tierForModel(model: string | undefined): RunTier;
|
|
145
|
+
/** Diagnostic for status/CLI. */
|
|
146
|
+
export declare function runTranscriptRecorderStatus(): {
|
|
147
|
+
enabled: boolean;
|
|
148
|
+
path: string;
|
|
149
|
+
maxSizeBytes: number;
|
|
150
|
+
maxRotations: number;
|
|
151
|
+
};
|
|
152
|
+
/** @internal — test seam: reset cached config so tests can flip env vars. */
|
|
153
|
+
export declare function __resetRunTranscriptRecorderForTests(): void;
|
|
154
|
+
//# sourceMappingURL=run-transcript-recorder.d.ts.map
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* run-transcript-recorder.ts — Opt-in FULL run-transcript recorder for the
|
|
3
|
+
* weight-eft training-data export path (agenticow / ADR-150 weight-eft slice).
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS
|
|
6
|
+
* ---------------
|
|
7
|
+
* The existing `router-trajectory.ts` recorder captures only the ROUTING
|
|
8
|
+
* DECISION for a task: task text, embedding, scalar quality, tokens, cost.
|
|
9
|
+
* `@metaharness/weight-eft` needs something the routing recorder never had —
|
|
10
|
+
* the full ReAct message TRANSCRIPT, the produced patch, and a resolved
|
|
11
|
+
* boolean — to build SFT/DPO training rows. This module is that missing
|
|
12
|
+
* capture surface. It writes one JSON-line per completed run to
|
|
13
|
+
* `.swarm/run-transcripts.jsonl`, in a shape the archive-builder in
|
|
14
|
+
* `services/weight-eft.ts` maps directly to `DarwinTrajectory[]`.
|
|
15
|
+
*
|
|
16
|
+
* OFF BY DEFAULT (PII / RETENTION SURFACE)
|
|
17
|
+
* ----------------------------------------
|
|
18
|
+
* Rows carry the FULL prompt + assistant transcript + patch — a much larger
|
|
19
|
+
* PII/retention surface than the routing recorder. Mirroring why
|
|
20
|
+
* router-trajectory.ts is off-by-default, every write goes through the
|
|
21
|
+
* `CLAUDE_FLOW_RUN_TRANSCRIPTS=1` env gate. When unset (the default),
|
|
22
|
+
* `recordRunTranscript()` is a no-op. There is no way to enable it implicitly.
|
|
23
|
+
*
|
|
24
|
+
* HONESTY: `resolved` IS A PROXY
|
|
25
|
+
* ------------------------------
|
|
26
|
+
* `DarwinTrajectory.resolved` is meant to be GOLD-resolved status from the
|
|
27
|
+
* official SWE-bench harness. Ruflo has NO SWE-bench oracle. Every record
|
|
28
|
+
* therefore stamps `resolved_source` describing where the boolean actually
|
|
29
|
+
* came from, so no downstream consumer can mistake a proxy for gold:
|
|
30
|
+
* - 'gold-oracle' — a real conformant gold eval supplied it (never
|
|
31
|
+
* ruflo today; reserved for an external caller)
|
|
32
|
+
* - 'output-verifier' — ruflo's structural output-verifier confidence,
|
|
33
|
+
* thresholded — an EXPLICIT proxy
|
|
34
|
+
* - 'api-success' — the model returned without an API error — the
|
|
35
|
+
* weakest proxy (says nothing about correctness)
|
|
36
|
+
* - 'external' — supplied verbatim by the caller, provenance unknown
|
|
37
|
+
*
|
|
38
|
+
* ARCHITECTURAL CONSTRAINTS (mirror router-trajectory.ts / ADR-150)
|
|
39
|
+
* -----------------------------------------------------------------
|
|
40
|
+
* 1. OPT-IN — gated behind CLAUDE_FLOW_RUN_TRANSCRIPTS=1; default off.
|
|
41
|
+
* 2. NEVER THROWS — every fs op is try/caught at the append boundary; a
|
|
42
|
+
* failed write is silent (DEBUG-logged) and never breaks the run.
|
|
43
|
+
* 3. NO metaharness COUPLING — this module imports nothing from
|
|
44
|
+
* `@metaharness/*`; it only defines a portable record shape. The
|
|
45
|
+
* services/weight-eft.ts archive-builder does the (optional) mapping.
|
|
46
|
+
*
|
|
47
|
+
* SCHEMA (versioned, additive) — one JSONL row per completed run:
|
|
48
|
+
* { v, ts, instance_id, task_hash, model, tier, resolved, resolved_source,
|
|
49
|
+
* messages[], model_patch, sample?, source?, tokens?, cost_usd? }
|
|
50
|
+
*
|
|
51
|
+
* @module run-transcript-recorder
|
|
52
|
+
*/
|
|
53
|
+
import { appendFileSync, mkdirSync, existsSync, statSync, renameSync, unlinkSync, readFileSync } from 'node:fs';
|
|
54
|
+
import { dirname, join, resolve as resolvePath } from 'node:path';
|
|
55
|
+
import { taskHash } from './router-trajectory.js';
|
|
56
|
+
let _cfg = null;
|
|
57
|
+
let _cachedSize = -1;
|
|
58
|
+
function getConfig() {
|
|
59
|
+
if (_cfg !== null)
|
|
60
|
+
return _cfg;
|
|
61
|
+
const swarmDir = process.env.CLAUDE_FLOW_SWARM_DIR ?? resolvePath(process.cwd(), '.swarm');
|
|
62
|
+
_cfg = {
|
|
63
|
+
enabled: process.env.CLAUDE_FLOW_RUN_TRANSCRIPTS === '1',
|
|
64
|
+
path: process.env.CLAUDE_FLOW_RUN_TRANSCRIPTS_PATH ?? join(swarmDir, 'run-transcripts.jsonl'),
|
|
65
|
+
maxSizeBytes: parseInt(process.env.CLAUDE_FLOW_RUN_TRANSCRIPTS_MAXSIZE ?? `${25 * 1024 * 1024}`, 10) | 0,
|
|
66
|
+
maxRotations: Math.max(0, parseInt(process.env.CLAUDE_FLOW_RUN_TRANSCRIPTS_MAXROTATIONS ?? '3', 10) || 3),
|
|
67
|
+
};
|
|
68
|
+
return _cfg;
|
|
69
|
+
}
|
|
70
|
+
function rotate(cfg) {
|
|
71
|
+
if (!existsSync(cfg.path))
|
|
72
|
+
return;
|
|
73
|
+
try {
|
|
74
|
+
if (cfg.maxRotations === 0) {
|
|
75
|
+
unlinkSync(cfg.path);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const oldest = `${cfg.path}.${cfg.maxRotations}`;
|
|
79
|
+
if (existsSync(oldest))
|
|
80
|
+
unlinkSync(oldest);
|
|
81
|
+
for (let i = cfg.maxRotations - 1; i >= 1; i--) {
|
|
82
|
+
const src = `${cfg.path}.${i}`;
|
|
83
|
+
if (existsSync(src))
|
|
84
|
+
renameSync(src, `${cfg.path}.${i + 1}`);
|
|
85
|
+
}
|
|
86
|
+
renameSync(cfg.path, `${cfg.path}.1`);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
try {
|
|
90
|
+
unlinkSync(cfg.path);
|
|
91
|
+
}
|
|
92
|
+
catch { /* */ }
|
|
93
|
+
}
|
|
94
|
+
_cachedSize = 0;
|
|
95
|
+
}
|
|
96
|
+
function appendRow(row) {
|
|
97
|
+
const cfg = getConfig();
|
|
98
|
+
if (!cfg.enabled)
|
|
99
|
+
return;
|
|
100
|
+
try {
|
|
101
|
+
const dir = dirname(cfg.path);
|
|
102
|
+
if (!existsSync(dir))
|
|
103
|
+
mkdirSync(dir, { recursive: true });
|
|
104
|
+
if (_cachedSize < 0) {
|
|
105
|
+
_cachedSize = existsSync(cfg.path) ? statSync(cfg.path).size : 0;
|
|
106
|
+
}
|
|
107
|
+
const line = JSON.stringify(row) + '\n';
|
|
108
|
+
const bytes = Buffer.byteLength(line, 'utf8');
|
|
109
|
+
if (cfg.maxSizeBytes > 0 && _cachedSize + bytes > cfg.maxSizeBytes)
|
|
110
|
+
rotate(cfg);
|
|
111
|
+
appendFileSync(cfg.path, line);
|
|
112
|
+
_cachedSize += bytes;
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
if (process.env.DEBUG) {
|
|
116
|
+
// eslint-disable-next-line no-console
|
|
117
|
+
console.error('run-transcript: appendRow failed:', e.message);
|
|
118
|
+
}
|
|
119
|
+
// Never throw — transcript collection must never break a run.
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// ============================================================================
|
|
123
|
+
// Public API
|
|
124
|
+
// ============================================================================
|
|
125
|
+
/**
|
|
126
|
+
* Record one completed run transcript. Cheap — a single appendFileSync of a
|
|
127
|
+
* JSONL row. No-op when CLAUDE_FLOW_RUN_TRANSCRIPTS is unset (the default).
|
|
128
|
+
* Never throws.
|
|
129
|
+
*
|
|
130
|
+
* `resolvedSource` is REQUIRED so a proxy can never masquerade as gold. If you
|
|
131
|
+
* only have "the API returned", pass 'api-success' — the honest weakest label.
|
|
132
|
+
*/
|
|
133
|
+
export function recordRunTranscript(args) {
|
|
134
|
+
const cfg = getConfig();
|
|
135
|
+
const hash = taskHash(args.task);
|
|
136
|
+
const instanceId = args.instanceId ?? `run-${hash}`;
|
|
137
|
+
if (!cfg.enabled)
|
|
138
|
+
return { recorded: false, instanceId, taskHash: hash };
|
|
139
|
+
const row = {
|
|
140
|
+
v: 1,
|
|
141
|
+
ts: new Date().toISOString(),
|
|
142
|
+
instance_id: instanceId,
|
|
143
|
+
task_hash: hash,
|
|
144
|
+
model: args.model,
|
|
145
|
+
tier: args.tier,
|
|
146
|
+
resolved: args.resolved,
|
|
147
|
+
resolved_source: args.resolvedSource,
|
|
148
|
+
messages: args.messages,
|
|
149
|
+
model_patch: args.modelPatch ?? '',
|
|
150
|
+
...(args.sample !== undefined ? { sample: args.sample } : {}),
|
|
151
|
+
...(args.source ? { source: args.source } : {}),
|
|
152
|
+
...(args.tokens ? { tokens: args.tokens } : {}),
|
|
153
|
+
...(args.costUsd != null ? { cost_usd: args.costUsd } : {}),
|
|
154
|
+
};
|
|
155
|
+
appendRow(row);
|
|
156
|
+
return { recorded: true, instanceId, taskHash: hash };
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Read + parse the run-transcript JSONL back into records. Used by the
|
|
160
|
+
* archive-builder (`services/weight-eft.ts`). Malformed lines are skipped and
|
|
161
|
+
* counted, never thrown. Returns `[]` if the file is absent.
|
|
162
|
+
*/
|
|
163
|
+
export function readRunTranscripts(path) {
|
|
164
|
+
const p = path ?? getConfig().path;
|
|
165
|
+
if (!existsSync(p))
|
|
166
|
+
return { records: [], malformed: 0, path: p };
|
|
167
|
+
const records = [];
|
|
168
|
+
let malformed = 0;
|
|
169
|
+
try {
|
|
170
|
+
for (const line of readFileSync(p, 'utf8').split('\n')) {
|
|
171
|
+
if (!line.trim())
|
|
172
|
+
continue;
|
|
173
|
+
try {
|
|
174
|
+
const r = JSON.parse(line);
|
|
175
|
+
if (r && typeof r === 'object' && r.v === 1 && r.instance_id && Array.isArray(r.messages)) {
|
|
176
|
+
records.push(r);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
malformed++;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
malformed++;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
// unreadable file — treat as empty, never throw
|
|
189
|
+
}
|
|
190
|
+
return { records, malformed, path: p };
|
|
191
|
+
}
|
|
192
|
+
/** Map a ruflo model tier label to the weight-eft policy tier. */
|
|
193
|
+
export function tierForModel(model) {
|
|
194
|
+
// haiku (and any explicitly-cheap label) → 'cheap' (cascade first tier).
|
|
195
|
+
// sonnet / opus / everything else → 'frontier' (the escalation tier).
|
|
196
|
+
if (!model)
|
|
197
|
+
return 'frontier';
|
|
198
|
+
return /haiku/i.test(model) ? 'cheap' : 'frontier';
|
|
199
|
+
}
|
|
200
|
+
/** Diagnostic for status/CLI. */
|
|
201
|
+
export function runTranscriptRecorderStatus() {
|
|
202
|
+
return { ...getConfig() };
|
|
203
|
+
}
|
|
204
|
+
/** @internal — test seam: reset cached config so tests can flip env vars. */
|
|
205
|
+
export function __resetRunTranscriptRecorderForTests() {
|
|
206
|
+
_cfg = null;
|
|
207
|
+
_cachedSize = -1;
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=run-transcript-recorder.js.map
|