claude-flow 3.20.0 → 3.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +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/neural.js +309 -1
- 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/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
|
@@ -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
|
|
@@ -0,0 +1,223 @@
|
|
|
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
|
+
import { loadAgenticow, __resetAgenticowCache, resolveMemoryPath, manifestFor, validateLabel, openWithLineage, } from '../mcp-tools/agenticow-loader.js';
|
|
37
|
+
/**
|
|
38
|
+
* Env var pointing at the `.rvf` memory file that a long-horizon loop mutates.
|
|
39
|
+
* When set, the autopilot loop opts in to checkpoint/rollback around each tick.
|
|
40
|
+
* When unset, the gate is a transparent pass-through.
|
|
41
|
+
*/
|
|
42
|
+
export const CHECKPOINT_MEM_ENV = 'CLAUDE_FLOW_AUTOPILOT_CHECKPOINT_MEM';
|
|
43
|
+
/**
|
|
44
|
+
* Kill switch. When truthy (`1`/`true`/`yes`), the gate never touches agenticow
|
|
45
|
+
* and every guard() runs the tick unguarded. Lets an operator disable the
|
|
46
|
+
* feature without changing config or code.
|
|
47
|
+
*/
|
|
48
|
+
export const KILL_SWITCH_ENV = 'CLAUDE_FLOW_AGENTICOW_DISABLE';
|
|
49
|
+
function defaultIsRegression(result) {
|
|
50
|
+
if (!result || typeof result !== 'object')
|
|
51
|
+
return false;
|
|
52
|
+
const r = result;
|
|
53
|
+
return r.success === false || r.ok === false || r.regressed === true;
|
|
54
|
+
}
|
|
55
|
+
// ── The gate ────────────────────────────────────────────────────────────────
|
|
56
|
+
export class CheckpointGate {
|
|
57
|
+
/** True when the kill-switch env is set to a truthy value. */
|
|
58
|
+
static isKillSwitchSet() {
|
|
59
|
+
const v = (process.env[KILL_SWITCH_ENV] || '').trim().toLowerCase();
|
|
60
|
+
return v === '1' || v === 'true' || v === 'yes' || v === 'on';
|
|
61
|
+
}
|
|
62
|
+
/** The configured loop memory path, or undefined when the feature is off. */
|
|
63
|
+
static configuredMemPath() {
|
|
64
|
+
const p = (process.env[CHECKPOINT_MEM_ENV] || '').trim();
|
|
65
|
+
return p || undefined;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Lazy-load agenticow. Returns null when the package is absent (optional dep)
|
|
69
|
+
* or the kill switch is set. Any *other* import error is re-thrown — a broken
|
|
70
|
+
* install should be loud, a missing optional dep should be silent.
|
|
71
|
+
*/
|
|
72
|
+
async load() {
|
|
73
|
+
if (CheckpointGate.isKillSwitchSet())
|
|
74
|
+
return null;
|
|
75
|
+
// Delegates to the canonical loader (#22) — one cache, one degradation
|
|
76
|
+
// policy shared across every agenticow consumer.
|
|
77
|
+
return loadAgenticow();
|
|
78
|
+
}
|
|
79
|
+
/** True when agenticow can be engaged (present + not killed). */
|
|
80
|
+
async available() {
|
|
81
|
+
return (await this.load()) !== null;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Take a checkpoint on the given `.rvf` memory file. Non-throwing except for
|
|
85
|
+
* validation errors (path traversal, bad label) — those are surfaced so a
|
|
86
|
+
* misconfiguration is caught early.
|
|
87
|
+
*/
|
|
88
|
+
async checkpoint(memPath, label) {
|
|
89
|
+
const api = await this.load();
|
|
90
|
+
if (!api)
|
|
91
|
+
return { ok: false, degraded: true, reason: CheckpointGate.isKillSwitchSet() ? 'kill-switch' : 'agenticow-not-found' };
|
|
92
|
+
const path = resolveMemoryPath(String(memPath));
|
|
93
|
+
const lbl = validateLabel(String(label));
|
|
94
|
+
const mem = await openWithLineage(api, path);
|
|
95
|
+
try {
|
|
96
|
+
const cp = await mem.checkpoint(lbl);
|
|
97
|
+
await mem.save?.(manifestFor(path));
|
|
98
|
+
return { ok: true, degraded: false, checkpoint: cp };
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
await mem.close?.();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Roll the given `.rvf` memory file back to a checkpoint. Discards edits made
|
|
106
|
+
* since (O(edits-since-checkpoint)). Omit `checkpointId` to target the most
|
|
107
|
+
* recent checkpoint.
|
|
108
|
+
*/
|
|
109
|
+
async rollback(memPath, checkpointId) {
|
|
110
|
+
const api = await this.load();
|
|
111
|
+
if (!api)
|
|
112
|
+
return { ok: false, degraded: true, reason: CheckpointGate.isKillSwitchSet() ? 'kill-switch' : 'agenticow-not-found' };
|
|
113
|
+
const path = resolveMemoryPath(String(memPath));
|
|
114
|
+
const mem = await openWithLineage(api, path);
|
|
115
|
+
try {
|
|
116
|
+
const r = checkpointId ? await mem.rollback(checkpointId) : await mem.rollback();
|
|
117
|
+
await mem.save?.(manifestFor(path));
|
|
118
|
+
return { ok: true, degraded: false, result: r };
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
await mem.close?.();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Guard a risky loop tick with a checkpoint/rollback bracket.
|
|
126
|
+
*
|
|
127
|
+
* const outcome = await gate.guard(memPath, 'iter-7', async () => runTick());
|
|
128
|
+
*
|
|
129
|
+
* Behaviour:
|
|
130
|
+
* - No memPath / kill-switch / agenticow-absent → run fn unguarded, degraded:true.
|
|
131
|
+
* - Checkpoint fails to engage (degraded) → run fn unguarded, degraded:true.
|
|
132
|
+
* - fn throws → roll back to checkpoint, then re-throw (unless rethrow:false).
|
|
133
|
+
* - fn returns a regression verdict → roll back, return result with rolledBack:true.
|
|
134
|
+
* - fn succeeds → keep the checkpoint, return result with rolledBack:false.
|
|
135
|
+
*
|
|
136
|
+
* The gate is non-fatal: an internal agenticow failure never masks fn()'s own
|
|
137
|
+
* result — if checkpointing throws, fn still runs unguarded.
|
|
138
|
+
*/
|
|
139
|
+
async guard(memPath, label, fn, opts = {}) {
|
|
140
|
+
const rethrow = opts.rethrow !== false;
|
|
141
|
+
const isRegression = opts.isRegression ?? defaultIsRegression;
|
|
142
|
+
// Fast paths: feature off → transparent pass-through.
|
|
143
|
+
if (!memPath) {
|
|
144
|
+
const result = await fn();
|
|
145
|
+
return { result, checkpointed: false, rolledBack: false, degraded: true, reason: 'no-memory-path' };
|
|
146
|
+
}
|
|
147
|
+
if (CheckpointGate.isKillSwitchSet()) {
|
|
148
|
+
const result = await fn();
|
|
149
|
+
return { result, checkpointed: false, rolledBack: false, degraded: true, reason: 'kill-switch' };
|
|
150
|
+
}
|
|
151
|
+
// Try to checkpoint. Any failure here degrades to unguarded — the loop must
|
|
152
|
+
// never break because memory branching is unavailable.
|
|
153
|
+
let checkpointed = false;
|
|
154
|
+
try {
|
|
155
|
+
const cp = await this.checkpoint(memPath, label);
|
|
156
|
+
if (!cp.ok) {
|
|
157
|
+
const result = await fn();
|
|
158
|
+
return { result, checkpointed: false, rolledBack: false, degraded: true, reason: cp.reason };
|
|
159
|
+
}
|
|
160
|
+
checkpointed = true;
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
const result = await fn();
|
|
164
|
+
return {
|
|
165
|
+
result,
|
|
166
|
+
checkpointed: false,
|
|
167
|
+
rolledBack: false,
|
|
168
|
+
degraded: true,
|
|
169
|
+
reason: `checkpoint-error: ${err instanceof Error ? err.message : String(err)}`,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
// Run the tick under the checkpoint.
|
|
173
|
+
let result;
|
|
174
|
+
try {
|
|
175
|
+
result = await fn();
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
const rb = await this.safeRollback(memPath, opts.checkpointId);
|
|
179
|
+
if (rethrow)
|
|
180
|
+
throw err;
|
|
181
|
+
return {
|
|
182
|
+
result: undefined,
|
|
183
|
+
checkpointed,
|
|
184
|
+
rolledBack: rb,
|
|
185
|
+
degraded: false,
|
|
186
|
+
reason: 'tick-threw',
|
|
187
|
+
checkpointLabel: label,
|
|
188
|
+
error: err,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
// Verdict: regression → roll back, keep the returned result for the caller.
|
|
192
|
+
if (isRegression(result)) {
|
|
193
|
+
const rb = await this.safeRollback(memPath, opts.checkpointId);
|
|
194
|
+
return { result, checkpointed, rolledBack: rb, degraded: false, reason: 'regression-verdict', checkpointLabel: label };
|
|
195
|
+
}
|
|
196
|
+
// Success → keep the checkpoint as the new baseline.
|
|
197
|
+
return { result, checkpointed, rolledBack: false, degraded: false, checkpointLabel: label };
|
|
198
|
+
}
|
|
199
|
+
/** Roll back without throwing — used on the failure path so a rollback error
|
|
200
|
+
* never masks the original tick failure. */
|
|
201
|
+
async safeRollback(memPath, checkpointId) {
|
|
202
|
+
try {
|
|
203
|
+
const r = await this.rollback(memPath, checkpointId);
|
|
204
|
+
return r.ok;
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/** Shared process-wide gate instance. */
|
|
212
|
+
let _shared = null;
|
|
213
|
+
export function getCheckpointGate() {
|
|
214
|
+
if (!_shared)
|
|
215
|
+
_shared = new CheckpointGate();
|
|
216
|
+
return _shared;
|
|
217
|
+
}
|
|
218
|
+
/** Reset the lazy-load cache — test-only. */
|
|
219
|
+
export function __resetCheckpointGateForTests() {
|
|
220
|
+
__resetAgenticowCache();
|
|
221
|
+
_shared = null;
|
|
222
|
+
}
|
|
223
|
+
//# sourceMappingURL=checkpoint-gate.js.map
|