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.
- package/package.json +1 -1
- 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/doctor.js +56 -0
- package/v3/@claude-flow/cli/dist/src/commands/neural.js +309 -1
- package/v3/@claude-flow/cli/dist/src/init/executor.js +17 -0
- package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +26 -2
- package/v3/@claude-flow/cli/dist/src/init/memory-package-resolver.d.ts +53 -0
- package/v3/@claude-flow/cli/dist/src/init/memory-package-resolver.js +118 -0
- 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 +9 -82
- 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/memory/memory-bridge.js +49 -8
- 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/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
|
@@ -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
|
-
//
|
|
774
|
-
//
|
|
775
|
-
//
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
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)}+
|
|
783
|
-
: `
|
|
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
|