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
@@ -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