claude-flow 3.20.0 → 3.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/package.json +1 -1
  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/doctor.js +56 -0
  6. package/v3/@claude-flow/cli/dist/src/commands/neural.js +309 -1
  7. package/v3/@claude-flow/cli/dist/src/init/executor.js +17 -0
  8. package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +26 -2
  9. package/v3/@claude-flow/cli/dist/src/init/memory-package-resolver.d.ts +53 -0
  10. package/v3/@claude-flow/cli/dist/src/init/memory-package-resolver.js +118 -0
  11. package/v3/@claude-flow/cli/dist/src/mcp-client.js +6 -1
  12. package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-execute-core.js +33 -0
  13. package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-tools.js +47 -1
  14. package/v3/@claude-flow/cli/dist/src/mcp-tools/agenticow-loader.d.ts +59 -0
  15. package/v3/@claude-flow/cli/dist/src/mcp-tools/agenticow-loader.js +105 -0
  16. package/v3/@claude-flow/cli/dist/src/mcp-tools/agenticow-speculate-tools.d.ts +24 -0
  17. package/v3/@claude-flow/cli/dist/src/mcp-tools/agenticow-speculate-tools.js +202 -0
  18. package/v3/@claude-flow/cli/dist/src/mcp-tools/agenticow-tools.js +9 -82
  19. package/v3/@claude-flow/cli/dist/src/mcp-tools/index.d.ts +1 -0
  20. package/v3/@claude-flow/cli/dist/src/mcp-tools/index.js +2 -0
  21. package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +49 -8
  22. package/v3/@claude-flow/cli/dist/src/ruvector/router-trajectory.d.ts +33 -0
  23. package/v3/@claude-flow/cli/dist/src/ruvector/router-trajectory.js +31 -0
  24. package/v3/@claude-flow/cli/dist/src/ruvector/run-transcript-recorder.d.ts +154 -0
  25. package/v3/@claude-flow/cli/dist/src/ruvector/run-transcript-recorder.js +209 -0
  26. package/v3/@claude-flow/cli/dist/src/services/checkpoint-gate.d.ts +140 -0
  27. package/v3/@claude-flow/cli/dist/src/services/checkpoint-gate.js +223 -0
  28. package/v3/@claude-flow/cli/dist/src/services/distill-oracle.d.ts +190 -0
  29. package/v3/@claude-flow/cli/dist/src/services/distill-oracle.js +349 -0
  30. package/v3/@claude-flow/cli/dist/src/services/fable-harness.d.ts +168 -0
  31. package/v3/@claude-flow/cli/dist/src/services/fable-harness.js +347 -0
  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
@@ -680,6 +680,31 @@ export async function bridgeStoreEntry(options) {
680
680
  catch { /* vector_indexes may not exist on legacy DBs — fall through */ }
681
681
  const stmt = ctx.db.prepare(insertSql);
682
682
  stmt.run(id, key, namespace, value, embeddingJson, dimensions || null, model, tags.length > 0 ? JSON.stringify(tags) : null, '{}', now, now, ttl ? now + (ttl * 1000) : null);
683
+ // #2558: keep `vector_indexes.total_vectors` accurate so status/tooling
684
+ // stop reporting "HNSW index: 0 vectors" while embedded entries exist.
685
+ try {
686
+ ctx.db
687
+ .prepare(`UPDATE vector_indexes SET
688
+ total_vectors = (SELECT COUNT(*) FROM memory_entries
689
+ WHERE namespace = ? AND embedding IS NOT NULL),
690
+ updated_at = ?
691
+ WHERE name = ?`)
692
+ .run(namespace, now, namespace);
693
+ }
694
+ catch { /* vector_indexes may not exist on legacy DBs — non-fatal */ }
695
+ // #2558: better-sqlite3 opens the DB in WAL mode and, for the small write
696
+ // volumes typical of CLI usage, may never reach the auto-checkpoint
697
+ // threshold — leaving committed rows only in the -wal file. WAL-blind
698
+ // readers (the sql.js fallback search path; the statusline's read-only
699
+ // `sqlite3` vector count) then see a stale/empty main DB file and report
700
+ // "0 vectors" / empty search. A PASSIVE checkpoint flushes committed pages
701
+ // into the main file without blocking writers. Best-effort, never fatal.
702
+ try {
703
+ if (typeof ctx.db.pragma === 'function') {
704
+ ctx.db.pragma('wal_checkpoint(PASSIVE)');
705
+ }
706
+ }
707
+ catch { /* non-WAL, busy, or unsupported — non-fatal */ }
683
708
  // Phase 2: Write-through to TieredCache
684
709
  const safeNs = String(namespace).replace(/:/g, '_');
685
710
  const safeKey = String(key).replace(/:/g, '_');
@@ -770,17 +795,33 @@ export async function bridgeSearchEntries(options) {
770
795
  // Normalize BM25 to 0-1 range (cap at 10 for normalization)
771
796
  bm25ScoreVal = Math.min(bm25ScoreVal / 10, 1.0);
772
797
  }
773
- // Reciprocal rank fusion: combine semantic and BM25
774
- // Weight: 0.7 semantic + 0.3 BM25 when both embeddings present
775
- // Fall back to BM25-only when either query or row lacks an embedding
776
- const score = semanticScore > 0
777
- ? (0.7 * semanticScore + 0.3 * bm25ScoreVal)
778
- : bm25ScoreVal;
798
+ // #2558: keyword-coverage floor for the lexical signal.
799
+ // BM25's IDF collapses toward zero when a term appears in most/all
800
+ // documents (routine on small memory corpora), and the /10 normalization
801
+ // crushed exact-keyword hits well below the default 0.3 threshold — so
802
+ // `memory search` recalled NOTHING even when the content literally
803
+ // contained the query term (issue #2558: "keyword recall random"). The
804
+ // pre-BM25 fallback guaranteed keyword recall via matchCount/words*0.5;
805
+ // this restores that guarantee. `coverage` is the fraction of query
806
+ // terms present in the document — a full-coverage hit must always be
807
+ // recallable regardless of IDF.
808
+ const contentLower = String(row.content || '').toLowerCase();
809
+ const matchedTerms = queryTerms.filter(t => contentLower.includes(t)).length;
810
+ const coverage = queryTerms.length > 0 ? matchedTerms / queryTerms.length : 0;
811
+ const lexicalScore = Math.max(bm25ScoreVal, coverage);
812
+ // Recall-friendly fusion: a strong semantic OR lexical signal alone must
813
+ // clear the threshold. `blended` (0.6 semantic + 0.4 lexical) drives
814
+ // ranking; taking max() with the raw semantic score means (a) a genuinely
815
+ // similar entry is never dropped just because it lacks the query's exact
816
+ // words, and (b) a full-coverage keyword hit (lexical=1 → blended≥0.4) is
817
+ // never dropped just because its embedding cosine is low or negative.
818
+ const blended = 0.6 * Math.max(0, semanticScore) + 0.4 * lexicalScore;
819
+ const score = Math.max(blended, semanticScore);
779
820
  if (score >= threshold) {
780
821
  // Phase 4: ExplainableRecall provenance
781
822
  const provenance = queryEmbedding
782
- ? `semantic:${semanticScore.toFixed(3)}+bm25:${bm25ScoreVal.toFixed(3)}`
783
- : `bm25:${bm25ScoreVal.toFixed(3)}`;
823
+ ? `semantic:${semanticScore.toFixed(3)}+lexical:${lexicalScore.toFixed(3)}`
824
+ : `lexical:${lexicalScore.toFixed(3)}`;
784
825
  results.push({
785
826
  id: String(row.id).substring(0, 12),
786
827
  key: row.key || String(row.id).substring(0, 15),
@@ -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
@@ -0,0 +1,140 @@
1
+ /**
2
+ * CheckpointGate — agenticow-backed checkpoint/rollback gate for autopilot
3
+ * loops and long-horizon workflows (agenticow integration, step 3).
4
+ *
5
+ * ── The pattern ───────────────────────────────────────────────────────────
6
+ * Before a risky loop tick that mutates `.rvf` memory, take an O(1) agenticow
7
+ * checkpoint (162 bytes, fixed cost — see mcp-tools/agenticow-tools.ts). Run
8
+ * the tick. If it regresses — throws, or the caller's verdict says the outcome
9
+ * got worse (verifier fail / error / worse metric) — roll the memory back to
10
+ * the checkpoint. Rollback is O(edits-since-checkpoint), NOT an O(N) rebuild,
11
+ * and the earlier history stays intact via the `.agenticow.json` lineage
12
+ * manifest. On success the checkpoint is simply kept as the new good baseline.
13
+ *
14
+ * The three lifecycle verbs (checkpoint / rollback / promote) already exist in
15
+ * the `agenticow` package and are surfaced as MCP tools in
16
+ * `mcp-tools/agenticow-tools.ts`. This module is the *orchestration hook* that
17
+ * calls them from inside a loop — the MCP surface is for agents, this is for
18
+ * the in-process loop.
19
+ *
20
+ * ── Architectural constraint (ADR-150) ────────────────────────────────────
21
+ * `agenticow` lives in `optionalDependencies` and must NEVER be a hard runtime
22
+ * dependency. This gate degrades gracefully in three ways, each of which runs
23
+ * the tick UNGUARDED rather than failing:
24
+ * 1. package missing → { degraded: true, reason: 'agenticow-not-found' }
25
+ * 2. kill-switch env set → { degraded: true, reason: 'kill-switch' }
26
+ * 3. no memory path configured → { degraded: true, reason: 'no-memory-path' }
27
+ * The package is lazy-loaded on the first guard() call, so importing this
28
+ * module has zero startup cost.
29
+ *
30
+ * The optional-dep loader + path/label/lineage helpers are the canonical ones
31
+ * from `mcp-tools/agenticow-loader.ts` — one implementation shared across every
32
+ * agenticow consumer (verbs, swarm branches, speculative, oracle, this gate).
33
+ *
34
+ * @module @claude-flow/cli/services/checkpoint-gate
35
+ */
36
+ /**
37
+ * Env var pointing at the `.rvf` memory file that a long-horizon loop mutates.
38
+ * When set, the autopilot loop opts in to checkpoint/rollback around each tick.
39
+ * When unset, the gate is a transparent pass-through.
40
+ */
41
+ export declare const CHECKPOINT_MEM_ENV = "CLAUDE_FLOW_AUTOPILOT_CHECKPOINT_MEM";
42
+ /**
43
+ * Kill switch. When truthy (`1`/`true`/`yes`), the gate never touches agenticow
44
+ * and every guard() runs the tick unguarded. Lets an operator disable the
45
+ * feature without changing config or code.
46
+ */
47
+ export declare const KILL_SWITCH_ENV = "CLAUDE_FLOW_AGENTICOW_DISABLE";
48
+ /** Outcome of a guarded tick. `result` is undefined only when the tick threw
49
+ * AND `rethrow` was disabled. */
50
+ export interface CheckpointGuardResult<T> {
51
+ /** The value fn() returned (undefined if it threw with rethrow:false). */
52
+ result: T | undefined;
53
+ /** True when a checkpoint was actually taken before running fn. */
54
+ checkpointed: boolean;
55
+ /** True when memory was rolled back to the checkpoint (throw or regression). */
56
+ rolledBack: boolean;
57
+ /** True when the gate could not engage agenticow and ran fn unguarded. */
58
+ degraded: boolean;
59
+ /** Machine-readable reason for degraded / rollback. */
60
+ reason?: string;
61
+ /** The checkpoint label, when one was taken. */
62
+ checkpointLabel?: string;
63
+ /** The error fn() threw, when rethrow was disabled. */
64
+ error?: unknown;
65
+ }
66
+ export interface GuardOptions<T> {
67
+ /**
68
+ * Verdict function: return true when `result` represents a regression that
69
+ * should trigger a rollback. Defaults to treating `{success:false}` /
70
+ * `{ok:false}` / `{regressed:true}` as regressions.
71
+ */
72
+ isRegression?: (result: T) => boolean;
73
+ /**
74
+ * When the tick throws: roll back, then re-throw the original error
75
+ * (default true). Set false to swallow the error and return it on the
76
+ * result object instead — useful when the loop must never crash.
77
+ */
78
+ rethrow?: boolean;
79
+ /** Optional checkpoint id to roll back to (defaults to most recent). */
80
+ checkpointId?: string;
81
+ }
82
+ export declare class CheckpointGate {
83
+ /** True when the kill-switch env is set to a truthy value. */
84
+ static isKillSwitchSet(): boolean;
85
+ /** The configured loop memory path, or undefined when the feature is off. */
86
+ static configuredMemPath(): string | undefined;
87
+ /**
88
+ * Lazy-load agenticow. Returns null when the package is absent (optional dep)
89
+ * or the kill switch is set. Any *other* import error is re-thrown — a broken
90
+ * install should be loud, a missing optional dep should be silent.
91
+ */
92
+ private load;
93
+ /** True when agenticow can be engaged (present + not killed). */
94
+ available(): Promise<boolean>;
95
+ /**
96
+ * Take a checkpoint on the given `.rvf` memory file. Non-throwing except for
97
+ * validation errors (path traversal, bad label) — those are surfaced so a
98
+ * misconfiguration is caught early.
99
+ */
100
+ checkpoint(memPath: string, label: string): Promise<{
101
+ ok: boolean;
102
+ degraded: boolean;
103
+ reason?: string;
104
+ checkpoint?: unknown;
105
+ }>;
106
+ /**
107
+ * Roll the given `.rvf` memory file back to a checkpoint. Discards edits made
108
+ * since (O(edits-since-checkpoint)). Omit `checkpointId` to target the most
109
+ * recent checkpoint.
110
+ */
111
+ rollback(memPath: string, checkpointId?: string): Promise<{
112
+ ok: boolean;
113
+ degraded: boolean;
114
+ reason?: string;
115
+ result?: unknown;
116
+ }>;
117
+ /**
118
+ * Guard a risky loop tick with a checkpoint/rollback bracket.
119
+ *
120
+ * const outcome = await gate.guard(memPath, 'iter-7', async () => runTick());
121
+ *
122
+ * Behaviour:
123
+ * - No memPath / kill-switch / agenticow-absent → run fn unguarded, degraded:true.
124
+ * - Checkpoint fails to engage (degraded) → run fn unguarded, degraded:true.
125
+ * - fn throws → roll back to checkpoint, then re-throw (unless rethrow:false).
126
+ * - fn returns a regression verdict → roll back, return result with rolledBack:true.
127
+ * - fn succeeds → keep the checkpoint, return result with rolledBack:false.
128
+ *
129
+ * The gate is non-fatal: an internal agenticow failure never masks fn()'s own
130
+ * result — if checkpointing throws, fn still runs unguarded.
131
+ */
132
+ guard<T>(memPath: string | undefined, label: string, fn: () => Promise<T>, opts?: GuardOptions<T>): Promise<CheckpointGuardResult<T>>;
133
+ /** Roll back without throwing — used on the failure path so a rollback error
134
+ * never masks the original tick failure. */
135
+ private safeRollback;
136
+ }
137
+ export declare function getCheckpointGate(): CheckpointGate;
138
+ /** Reset the lazy-load cache — test-only. */
139
+ export declare function __resetCheckpointGateForTests(): void;
140
+ //# sourceMappingURL=checkpoint-gate.d.ts.map