claude-flow 3.22.0 → 3.24.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/.claude/helpers/.helpers-version +1 -0
- package/.claude/helpers/auto-memory-hook.mjs +59 -274
- package/.claude/helpers/helpers.manifest.json +12 -0
- package/.claude/helpers/hook-handler.cjs +132 -112
- package/.claude/helpers/intelligence.cjs +992 -196
- package/package.json +1 -1
- package/v3/@claude-flow/cli/dist/src/commands/memory-backup.d.ts +11 -0
- package/v3/@claude-flow/cli/dist/src/commands/memory-backup.js +46 -0
- package/v3/@claude-flow/cli/dist/src/commands/memory.js +2 -1
- package/v3/@claude-flow/cli/dist/src/config/harness-feedback-applier.d.ts +50 -0
- package/v3/@claude-flow/cli/dist/src/config/harness-feedback-applier.js +122 -0
- package/v3/@claude-flow/cli/dist/src/config/proven-config-refresh.d.ts +39 -0
- package/v3/@claude-flow/cli/dist/src/config/proven-config-refresh.js +154 -0
- package/v3/@claude-flow/cli/dist/src/config/proven-config-rvfa.d.ts +23 -0
- package/v3/@claude-flow/cli/dist/src/config/proven-config-rvfa.js +73 -0
- package/v3/@claude-flow/cli/dist/src/config/proven-config.d.ts +86 -0
- package/v3/@claude-flow/cli/dist/src/config/proven-config.js +176 -0
- package/v3/@claude-flow/cli/dist/src/index.js +35 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/neural-tools.d.ts +10 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/neural-tools.js +107 -18
- package/v3/@claude-flow/cli/dist/src/services/daemon-autostart.d.ts +17 -0
- package/v3/@claude-flow/cli/dist/src/services/daemon-autostart.js +79 -0
- package/v3/@claude-flow/cli/dist/src/services/evolve-proof.d.ts +247 -0
- package/v3/@claude-flow/cli/dist/src/services/evolve-proof.js +341 -0
- package/v3/@claude-flow/cli/dist/src/services/harness-benchmark.d.ts +68 -0
- package/v3/@claude-flow/cli/dist/src/services/harness-benchmark.js +94 -0
- package/v3/@claude-flow/cli/dist/src/services/harness-canary.d.ts +60 -0
- package/v3/@claude-flow/cli/dist/src/services/harness-canary.js +69 -0
- package/v3/@claude-flow/cli/dist/src/services/harness-corpus-harvester.d.ts +51 -0
- package/v3/@claude-flow/cli/dist/src/services/harness-corpus-harvester.js +74 -0
- package/v3/@claude-flow/cli/dist/src/services/harness-flywheel-generations.d.ts +111 -0
- package/v3/@claude-flow/cli/dist/src/services/harness-flywheel-generations.js +354 -0
- package/v3/@claude-flow/cli/dist/src/services/harness-flywheel-runtime.d.ts +25 -0
- package/v3/@claude-flow/cli/dist/src/services/harness-flywheel-runtime.js +101 -0
- package/v3/@claude-flow/cli/dist/src/services/harness-flywheel.d.ts +48 -0
- package/v3/@claude-flow/cli/dist/src/services/harness-flywheel.js +207 -0
- package/v3/@claude-flow/cli/dist/src/services/harness-hosts.d.ts +40 -0
- package/v3/@claude-flow/cli/dist/src/services/harness-hosts.js +88 -0
- package/v3/@claude-flow/cli/dist/src/services/harness-improvement-ledger.d.ts +63 -0
- package/v3/@claude-flow/cli/dist/src/services/harness-improvement-ledger.js +101 -0
- package/v3/@claude-flow/cli/dist/src/services/harness-loop.d.ts +53 -0
- package/v3/@claude-flow/cli/dist/src/services/harness-loop.js +85 -0
- package/v3/@claude-flow/cli/dist/src/services/harness-qualification.d.ts +66 -0
- package/v3/@claude-flow/cli/dist/src/services/harness-qualification.js +143 -0
- package/v3/@claude-flow/cli/dist/src/services/harness-replay.d.ts +37 -0
- package/v3/@claude-flow/cli/dist/src/services/harness-replay.js +92 -0
- package/v3/@claude-flow/cli/dist/src/services/harness-verify.d.ts +33 -0
- package/v3/@claude-flow/cli/dist/src/services/harness-verify.js +26 -0
- package/v3/@claude-flow/cli/dist/src/services/harness-worker.d.ts +23 -0
- package/v3/@claude-flow/cli/dist/src/services/harness-worker.js +66 -0
- package/v3/@claude-flow/cli/dist/src/services/memory-backup.d.ts +24 -0
- package/v3/@claude-flow/cli/dist/src/services/memory-backup.js +94 -0
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.d.ts +16 -1
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +84 -0
- package/v3/@claude-flow/cli/package.json +1 -1
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stateful flywheel — close the autonomy loop (ADR-176 A-P3b).
|
|
3
|
+
*
|
|
4
|
+
* The milestone was demonstrated by a one-shot script. This makes it the daemon's
|
|
5
|
+
* ACTUAL behavior: each tick runs ONE generation, reads the persisted lineage to
|
|
6
|
+
* find the current champion, uses it as the baseline, and — on a verified
|
|
7
|
+
* promotion — advances the champion so the NEXT tick compounds on it. Winners
|
|
8
|
+
* accumulate into a persisted, replayable lineage instead of being rediscovered.
|
|
9
|
+
*
|
|
10
|
+
* Same honest gate as the milestone run: a large frozen self-supervised held-out
|
|
11
|
+
* (significance achievable), the human anchor as a no-regression guard, a
|
|
12
|
+
* SEPARATE canary slice, and constrained (Pareto) multi-axis selection.
|
|
13
|
+
*
|
|
14
|
+
* Shadow-first / no auto-serve: a promoted champion is registered but NOT served;
|
|
15
|
+
* it is applied to the active policy only at the START of a LATER tick, once it
|
|
16
|
+
* has been the operating baseline for a full generation (a 1-tick shadow delay).
|
|
17
|
+
*
|
|
18
|
+
* Pure-ish + $0: deps (store patterns + search) are injected → testable without
|
|
19
|
+
* ONNX. Never throws.
|
|
20
|
+
*/
|
|
21
|
+
import * as fs from 'fs';
|
|
22
|
+
import * as path from 'path';
|
|
23
|
+
import { runRealEvolveRound, reconstructLineage, detectPlateau, mutationEffectiveness, } from './evolve-proof.js';
|
|
24
|
+
import { harvestSelfSupervisedTasks } from './harness-corpus-harvester.js';
|
|
25
|
+
import { applyChampionParams, rollbackActivePolicy } from '../config/harness-feedback-applier.js';
|
|
26
|
+
import { DEFAULT_CONFIG } from './harness-flywheel.js';
|
|
27
|
+
export const FLYWHEEL_DIR = ['.claude-flow', 'flywheel'];
|
|
28
|
+
export const FROZEN_CORPUS = 'harvested-selfsup-frozen-v1';
|
|
29
|
+
const SERVED_FILE = 'served.json';
|
|
30
|
+
const ATTEMPTS_FILE = 'attempts.jsonl';
|
|
31
|
+
const ANCHOR_TOL = 0.02;
|
|
32
|
+
const CANARY_CATASTROPHE = 0.5;
|
|
33
|
+
// ── Lineage store ─────────────────────────────────────────────────────────────
|
|
34
|
+
function dir(root) { return path.join(root, ...FLYWHEEL_DIR); }
|
|
35
|
+
function readJson(p) { try {
|
|
36
|
+
return JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
} }
|
|
41
|
+
/** Promotions only — the champion chain (generation-N.json), sorted by generation. */
|
|
42
|
+
export function loadPromotions(root) {
|
|
43
|
+
try {
|
|
44
|
+
const d = dir(root);
|
|
45
|
+
return fs.readdirSync(d).filter((f) => /^generation-\d+\.json$/.test(f))
|
|
46
|
+
.map((f) => readJson(path.join(d, f))).filter((b) => !!b)
|
|
47
|
+
.sort((a, b) => a.generation - b.generation);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Every attempt (promoted + rejected) — for telemetry + mutation-effectiveness. */
|
|
54
|
+
export function loadAttempts(root) {
|
|
55
|
+
try {
|
|
56
|
+
return fs.readFileSync(path.join(dir(root), ATTEMPTS_FILE), 'utf-8').split('\n').filter(Boolean)
|
|
57
|
+
.map((l) => JSON.parse(l));
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function appendAttempt(root, b) {
|
|
64
|
+
try {
|
|
65
|
+
fs.mkdirSync(dir(root), { recursive: true });
|
|
66
|
+
fs.appendFileSync(path.join(dir(root), ATTEMPTS_FILE), JSON.stringify(b) + '\n', 'utf-8');
|
|
67
|
+
}
|
|
68
|
+
catch { /* */ }
|
|
69
|
+
}
|
|
70
|
+
function appendPromotion(root, b) {
|
|
71
|
+
try {
|
|
72
|
+
fs.mkdirSync(dir(root), { recursive: true });
|
|
73
|
+
fs.writeFileSync(path.join(dir(root), `generation-${b.generation}.json`), JSON.stringify(b, null, 2) + '\n', 'utf-8');
|
|
74
|
+
}
|
|
75
|
+
catch { /* */ }
|
|
76
|
+
}
|
|
77
|
+
/** The current operating champion (last promotion's config), or defaults. */
|
|
78
|
+
export function currentChampion(root) {
|
|
79
|
+
const p = loadPromotions(root);
|
|
80
|
+
if (!p.length)
|
|
81
|
+
return { config: { ...DEFAULT_CONFIG }, hash: null, generation: 0 };
|
|
82
|
+
const last = p[p.length - 1];
|
|
83
|
+
return { config: (last.candidateManifest.policy.value ?? { ...DEFAULT_CONFIG }), hash: last.candidateManifestHash, generation: p.length };
|
|
84
|
+
}
|
|
85
|
+
export function servedChampion(root) {
|
|
86
|
+
return readJson(path.join(dir(root), SERVED_FILE)) ?? { championHash: null, config: null, servedAt: null, fromGeneration: null };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Shadow→serve gate: apply the latest promoted champion to the ACTIVE policy iff
|
|
90
|
+
* it is newer than what is currently served. Called at tick START, so a champion
|
|
91
|
+
* promoted last tick is served this tick (a 1-generation shadow delay) — never
|
|
92
|
+
* auto-served the instant it is promoted.
|
|
93
|
+
*/
|
|
94
|
+
export function serveCurrentChampionIfPending(root, now, applyFn) {
|
|
95
|
+
const champ = currentChampion(root);
|
|
96
|
+
if (!champ.hash)
|
|
97
|
+
return null;
|
|
98
|
+
const served = servedChampion(root);
|
|
99
|
+
if (served.championHash === champ.hash)
|
|
100
|
+
return served.championHash;
|
|
101
|
+
const apply = applyFn ?? ((cfg, hash) => applyChampionParams(root, { championId: hash, params: cfg, layer: 'repo/local', previous: served.championHash, now }));
|
|
102
|
+
try {
|
|
103
|
+
apply(champ.config, champ.hash, champ.generation - 1);
|
|
104
|
+
}
|
|
105
|
+
catch { /* */ }
|
|
106
|
+
try {
|
|
107
|
+
fs.mkdirSync(dir(root), { recursive: true });
|
|
108
|
+
fs.writeFileSync(path.join(dir(root), SERVED_FILE), JSON.stringify({ championHash: champ.hash, config: champ.config, servedAt: now, fromGeneration: champ.generation - 1 }, null, 2), 'utf-8');
|
|
109
|
+
}
|
|
110
|
+
catch { /* */ }
|
|
111
|
+
return champ.hash;
|
|
112
|
+
}
|
|
113
|
+
// ── Grading + candidate generation ────────────────────────────────────────────
|
|
114
|
+
const key = (c) => `a${c.alpha}_sw${c.subjectWeight}_mmr${c.mmrLambda}_bw${c.bodyWeight}_tp${c.typePenaltyFactor}`;
|
|
115
|
+
const mean = (xs) => (xs.length ? xs.reduce((a, b) => a + b, 0) / xs.length : 0);
|
|
116
|
+
function ndcg3(names, labels) {
|
|
117
|
+
const rel = names.slice(0, 3).map((n) => !!n && labels.some((s) => n.toLowerCase().includes(s.toLowerCase())));
|
|
118
|
+
const dcg = rel.reduce((a, r, i) => a + (r ? 1 / Math.log2(i + 2) : 0), 0);
|
|
119
|
+
const num = rel.filter(Boolean).length;
|
|
120
|
+
if (!num)
|
|
121
|
+
return 0;
|
|
122
|
+
let idcg = 0;
|
|
123
|
+
for (let i = 0; i < num; i++)
|
|
124
|
+
idcg += 1 / Math.log2(i + 2);
|
|
125
|
+
return dcg / idcg;
|
|
126
|
+
}
|
|
127
|
+
const rr = (items, targetId) => { const i = items.findIndex((x) => x.id === targetId); return i >= 0 ? 1 / (i + 1) : 0; };
|
|
128
|
+
function coarseGrid() {
|
|
129
|
+
const g = [];
|
|
130
|
+
for (const alpha of [0.3, 0.5, 0.7])
|
|
131
|
+
for (const subjectWeight of [1, 2, 3])
|
|
132
|
+
for (const mmrLambda of [0.5, 0.7, 0.9])
|
|
133
|
+
for (const bodyWeight of [1, 1.5])
|
|
134
|
+
for (const typePenaltyFactor of [1, 0.5])
|
|
135
|
+
g.push({ alpha, subjectWeight, mmrLambda, bodyWeight, typePenaltyFactor });
|
|
136
|
+
return g;
|
|
137
|
+
}
|
|
138
|
+
// ── Evidence-grounded meta-learning: bias the search by axis payoff ───────────
|
|
139
|
+
const AXES = ['alpha', 'subjectWeight', 'mmrLambda', 'bodyWeight', 'typePenaltyFactor'];
|
|
140
|
+
const STEP = { alpha: 0.1, subjectWeight: 0.5, mmrLambda: 0.1, bodyWeight: 0.5, typePenaltyFactor: 0.25 };
|
|
141
|
+
function clampAxis(a, v) {
|
|
142
|
+
const r = +v.toFixed(3);
|
|
143
|
+
if (a === 'alpha')
|
|
144
|
+
return r > 0 && r < 1 ? r : null;
|
|
145
|
+
if (a === 'mmrLambda')
|
|
146
|
+
return r >= 0 && r <= 1 ? r : null;
|
|
147
|
+
return r > 0 ? r : null;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Which policy AXES have historically paid off — attribute each promotion's
|
|
151
|
+
* held-out Δ to the axes that changed in it. Turns the lineage into a
|
|
152
|
+
* knowledge base the optimizer can act on (not just audit).
|
|
153
|
+
*/
|
|
154
|
+
export function axisEffectiveness(promotions) {
|
|
155
|
+
const by = new Map();
|
|
156
|
+
for (const b of promotions) {
|
|
157
|
+
const base = (b.baselineManifest.policy.value ?? {});
|
|
158
|
+
const cand = (b.candidateManifest.policy.value ?? {});
|
|
159
|
+
for (const a of AXES)
|
|
160
|
+
if (base[a] !== cand[a]) {
|
|
161
|
+
const d = by.get(a) ?? [];
|
|
162
|
+
d.push(b.deltas.benchmark);
|
|
163
|
+
by.set(a, d);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return AXES.map((a) => ({ axis: a, promotions: (by.get(a) ?? []).length, meanDelta: mean(by.get(a) ?? []) }))
|
|
167
|
+
.sort((x, y) => y.meanDelta - x.meanDelta);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Candidate set biased by measured axis payoff (meta-learning). Every axis keeps
|
|
171
|
+
* a ±1 exploration floor (never abandon a dimension), but axes with a positive
|
|
172
|
+
* historical Δ get EXPANDED range (±2, ±3) and PAIRWISE joint moves with other
|
|
173
|
+
* productive axes — so compute concentrates on the dimensions that have actually
|
|
174
|
+
* produced gains, instead of a uniform grid. Deterministic; bounded.
|
|
175
|
+
*/
|
|
176
|
+
export function biasedGrid(c, ranking) {
|
|
177
|
+
const productive = ranking.filter((r) => r.meanDelta > 1e-9).map((r) => r.axis);
|
|
178
|
+
const out = [], seen = new Set();
|
|
179
|
+
const add = (cfg) => { const k = key(cfg); if (!seen.has(k)) {
|
|
180
|
+
seen.add(k);
|
|
181
|
+
out.push(cfg);
|
|
182
|
+
} };
|
|
183
|
+
// exploration floor — single-axis ±1 for every axis
|
|
184
|
+
for (const a of AXES)
|
|
185
|
+
for (const dir of [-1, 1]) {
|
|
186
|
+
const v = clampAxis(a, c[a] + dir * STEP[a]);
|
|
187
|
+
if (v != null)
|
|
188
|
+
add({ ...c, [a]: v });
|
|
189
|
+
}
|
|
190
|
+
// exploitation — productive axes get wider range
|
|
191
|
+
for (const a of productive)
|
|
192
|
+
for (const m of [2, 3])
|
|
193
|
+
for (const dir of [-1, 1]) {
|
|
194
|
+
const v = clampAxis(a, c[a] + dir * m * STEP[a]);
|
|
195
|
+
if (v != null)
|
|
196
|
+
add({ ...c, [a]: v });
|
|
197
|
+
}
|
|
198
|
+
// exploitation — joint moves among productive axis pairs
|
|
199
|
+
for (let i = 0; i < productive.length; i++)
|
|
200
|
+
for (let j = i + 1; j < productive.length; j++) {
|
|
201
|
+
const a = productive[i], b = productive[j];
|
|
202
|
+
for (const da of [-1, 1])
|
|
203
|
+
for (const db of [-1, 1]) {
|
|
204
|
+
const va = clampAxis(a, c[a] + da * STEP[a]), vb = clampAxis(b, c[b] + db * STEP[b]);
|
|
205
|
+
if (va != null && vb != null)
|
|
206
|
+
add({ ...c, [a]: va, [b]: vb });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Run ONE generation against `root`, compounding on the persisted champion.
|
|
213
|
+
* Serves the prior champion first (shadow delay), then evaluates a new candidate.
|
|
214
|
+
*/
|
|
215
|
+
export async function runFlywheelGeneration(root, deps) {
|
|
216
|
+
try {
|
|
217
|
+
// shadow→serve the prior champion (1-tick delay); never serve the just-promoted one.
|
|
218
|
+
const served = serveCurrentChampionIfPending(root, deps.now, deps.applyFn);
|
|
219
|
+
const patterns = await deps.getPatterns();
|
|
220
|
+
if (!patterns || patterns.length < 12)
|
|
221
|
+
return { ran: false, reason: 'store too small', generation: 0, servedChampion: served };
|
|
222
|
+
const harvested = harvestSelfSupervisedTasks(patterns, { sample: deps.sample ?? 120 });
|
|
223
|
+
const nT = Math.floor(harvested.length * 0.4), nH = Math.floor(harvested.length * 0.4);
|
|
224
|
+
const TRAIN = harvested.slice(0, nT), HELD = harvested.slice(nT, nT + nH), CANARY = harvested.slice(nT + nH);
|
|
225
|
+
if (HELD.length < 20)
|
|
226
|
+
return { ran: false, reason: 'held-out too small for significance', generation: 0, servedChampion: served };
|
|
227
|
+
const champ = currentChampion(root);
|
|
228
|
+
const baseline = champ.config;
|
|
229
|
+
const parent = champ.hash;
|
|
230
|
+
const generation = champ.generation;
|
|
231
|
+
const cache = new Map();
|
|
232
|
+
const ranked = async (id, q, cfg) => {
|
|
233
|
+
const ck = `${id}::${key(cfg)}`;
|
|
234
|
+
if (!cache.has(ck))
|
|
235
|
+
cache.set(ck, (await deps.search(q, cfg)) || []);
|
|
236
|
+
return cache.get(ck);
|
|
237
|
+
};
|
|
238
|
+
const selfRR = async (t, cfg) => rr(await ranked(t.id, t.input.q, cfg), t.expected);
|
|
239
|
+
const meanRR = async (tasks, cfg) => { let s = 0; for (const t of tasks)
|
|
240
|
+
s += await selfRR(t, cfg); return s / tasks.length; };
|
|
241
|
+
const anchorMean = async (cfg) => { let s = 0; for (const a of deps.anchorTasks)
|
|
242
|
+
s += ndcg3((await ranked(a.id, a.q, cfg)).map((x) => x.name), a.labels); return s / deps.anchorTasks.length; };
|
|
243
|
+
const baseAnchor = await anchorMean(baseline);
|
|
244
|
+
// Meta-learning: after gen 0, bias the search toward axes that have
|
|
245
|
+
// historically paid off (a uniform coarse grid only for the first generation).
|
|
246
|
+
const grid = generation === 0 ? coarseGrid() : biasedGrid(baseline, axisEffectiveness(loadPromotions(root)));
|
|
247
|
+
// constrained (Pareto) selection: best self-retrieval on TRAIN subject to no anchor regression.
|
|
248
|
+
let cand = baseline, candTrain = await meanRR(TRAIN, baseline);
|
|
249
|
+
for (const c of grid) {
|
|
250
|
+
if (key(c) === key(baseline))
|
|
251
|
+
continue;
|
|
252
|
+
if ((await anchorMean(c)) < baseAnchor - ANCHOR_TOL)
|
|
253
|
+
continue;
|
|
254
|
+
const s = await meanRR(TRAIN, c);
|
|
255
|
+
if (s > candTrain + 1e-9) {
|
|
256
|
+
candTrain = s;
|
|
257
|
+
cand = c;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const holdout = [];
|
|
261
|
+
for (const t of HELD)
|
|
262
|
+
holdout.push({ taskId: t.id, baselineScore: await selfRR(t, baseline), candidateScore: await selfRR(t, cand) });
|
|
263
|
+
let cRoll = 0;
|
|
264
|
+
for (const t of CANARY)
|
|
265
|
+
if ((await selfRR(t, cand)) < (await selfRR(t, baseline)) - CANARY_CATASTROPHE)
|
|
266
|
+
cRoll++;
|
|
267
|
+
const canaryRollbackRate = CANARY.length ? cRoll / CANARY.length : 0;
|
|
268
|
+
const candAnchor = await anchorMean(cand);
|
|
269
|
+
const redblue = candAnchor >= baseAnchor - ANCHOR_TOL ? 'PASS' : 'FAIL';
|
|
270
|
+
const bundle = runRealEvolveRound({ baseline: baseline, candidate: cand, holdout, generation, parent, branch: 'main', now: deps.now, redblue, canaryRollbackRate, corpus: FROZEN_CORPUS });
|
|
271
|
+
appendAttempt(root, bundle);
|
|
272
|
+
if (bundle.decisionReceipt.promoted)
|
|
273
|
+
appendPromotion(root, bundle);
|
|
274
|
+
return {
|
|
275
|
+
ran: true, reason: bundle.decisionReceipt.reason, generation, promoted: bundle.decisionReceipt.promoted,
|
|
276
|
+
delta: bundle.deltas.benchmark, significant: bundle.decisionReceipt.significant,
|
|
277
|
+
championConfig: (bundle.decisionReceipt.promoted ? cand : baseline),
|
|
278
|
+
servedChampion: served, anchorRegressed: redblue === 'FAIL',
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
catch (e) {
|
|
282
|
+
return { ran: false, reason: `error: ${e?.message ?? e}`, generation: 0 };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
// ── Deployment-safety canary: drift detection on the REAL evolving store ──────
|
|
286
|
+
const DRIFT_TOL = 0.02;
|
|
287
|
+
/**
|
|
288
|
+
* A real deployment-safety check on real (not fabricated) data: the store keeps
|
|
289
|
+
* changing as ruflo is used, so a champion benchmarked at promotion time can
|
|
290
|
+
* DRIFT. Each tick, re-score the currently-SERVED champion against its
|
|
291
|
+
* predecessor on a FRESH harvest of the current store; if it now regresses
|
|
292
|
+
* (self-retrieval OR the human anchor), automatically ROLL BACK the active policy
|
|
293
|
+
* to the predecessor. This is the honest analogue of a live-traffic canary —
|
|
294
|
+
* genuine ongoing measurement + genuine rollback — without fabricating traffic.
|
|
295
|
+
* $0; never throws.
|
|
296
|
+
*/
|
|
297
|
+
export async function checkServedChampionDrift(root, deps) {
|
|
298
|
+
try {
|
|
299
|
+
const served = servedChampion(root);
|
|
300
|
+
if (!served.championHash || served.fromGeneration == null || !served.config)
|
|
301
|
+
return { checked: false, rolledBack: false, reason: 'nothing served' };
|
|
302
|
+
const bundle = readJson(path.join(dir(root), `generation-${served.fromGeneration}.json`));
|
|
303
|
+
if (!bundle)
|
|
304
|
+
return { checked: false, rolledBack: false, reason: 'served bundle missing' };
|
|
305
|
+
const predecessor = (bundle.baselineManifest.policy.value ?? {});
|
|
306
|
+
const servedCfg = served.config;
|
|
307
|
+
const patterns = await deps.getPatterns();
|
|
308
|
+
if (!patterns || patterns.length < 12)
|
|
309
|
+
return { checked: false, rolledBack: false, reason: 'store too small' };
|
|
310
|
+
const harvested = harvestSelfSupervisedTasks(patterns, { sample: deps.sample ?? 120 });
|
|
311
|
+
const nT = Math.floor(harvested.length * 0.4), nH = Math.floor(harvested.length * 0.4);
|
|
312
|
+
const FRESH = harvested.slice(nT, nT + nH); // fresh held slice from the CURRENT store
|
|
313
|
+
if (FRESH.length < 12)
|
|
314
|
+
return { checked: false, rolledBack: false, reason: 'fresh slice too small' };
|
|
315
|
+
const cache = new Map();
|
|
316
|
+
const ranked = async (id, q, cfg) => { const ck = `${id}::${key(cfg)}`; if (!cache.has(ck))
|
|
317
|
+
cache.set(ck, (await deps.search(q, cfg)) || []); return cache.get(ck); };
|
|
318
|
+
const meanRR = async (cfg) => { let s = 0; for (const t of FRESH)
|
|
319
|
+
s += rr(await ranked(t.id, t.input.q, cfg), t.expected); return s / FRESH.length; };
|
|
320
|
+
const anchorMean = async (cfg) => { let s = 0; for (const a of deps.anchorTasks)
|
|
321
|
+
s += ndcg3((await ranked(a.id, a.q, cfg)).map((x) => x.name), a.labels); return s / deps.anchorTasks.length; };
|
|
322
|
+
const servedScore = await meanRR(servedCfg), predScore = await meanRR(predecessor);
|
|
323
|
+
const servedAnchor = await anchorMean(servedCfg), predAnchor = await anchorMean(predecessor);
|
|
324
|
+
const drifted = servedScore < predScore - DRIFT_TOL || servedAnchor < predAnchor - DRIFT_TOL;
|
|
325
|
+
if (drifted) {
|
|
326
|
+
rollbackActivePolicy(root, { now: deps.now });
|
|
327
|
+
try {
|
|
328
|
+
fs.writeFileSync(path.join(dir(root), SERVED_FILE), JSON.stringify({ championHash: null, config: null, servedAt: deps.now, fromGeneration: null }, null, 2), 'utf-8');
|
|
329
|
+
}
|
|
330
|
+
catch { /* */ }
|
|
331
|
+
}
|
|
332
|
+
return { checked: true, rolledBack: drifted, reason: drifted ? `drift → rolled back (served ${servedScore.toFixed(3)} < predecessor ${predScore.toFixed(3)})` : 'stable', servedScore, predecessorScore: predScore };
|
|
333
|
+
}
|
|
334
|
+
catch (e) {
|
|
335
|
+
return { checked: false, rolledBack: false, reason: `error: ${e?.message ?? e}` };
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/** Reconstruct the persisted lineage + telemetry for a status endpoint / CLI. */
|
|
339
|
+
export function flywheelStatus(root) {
|
|
340
|
+
const promotions = loadPromotions(root);
|
|
341
|
+
const attempts = loadAttempts(root);
|
|
342
|
+
const champ = currentChampion(root);
|
|
343
|
+
return {
|
|
344
|
+
generations: promotions.length,
|
|
345
|
+
attempts: attempts.length,
|
|
346
|
+
lineage: reconstructLineage(promotions),
|
|
347
|
+
plateau: detectPlateau(attempts.length ? attempts : promotions, { window: 5 }),
|
|
348
|
+
mutation: mutationEffectiveness(attempts.length ? attempts : promotions),
|
|
349
|
+
axisEffectiveness: axisEffectiveness(promotions),
|
|
350
|
+
served: servedChampion(root),
|
|
351
|
+
champion: { config: champ.config, hash: champ.hash },
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
//# sourceMappingURL=harness-flywheel-generations.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type FlywheelResult } from './harness-flywheel.js';
|
|
2
|
+
import { type GenerationResult } from './harness-flywheel-generations.js';
|
|
3
|
+
/**
|
|
4
|
+
* Run one live flywheel tick against `projectRoot`. Opt-in + $0 default: with
|
|
5
|
+
* RUFLO_HARNESS_LOOP unset it is a no-op. Best-effort; never throws.
|
|
6
|
+
*/
|
|
7
|
+
export declare function runFlywheelWorker(projectRoot: string, opts?: {
|
|
8
|
+
sample?: number;
|
|
9
|
+
optInOverride?: boolean;
|
|
10
|
+
now?: number;
|
|
11
|
+
}): Promise<FlywheelResult>;
|
|
12
|
+
/**
|
|
13
|
+
* Run ONE live COMPOUNDING generation against the persisted lineage (ADR-176
|
|
14
|
+
* A-P3b — the autonomy loop). Reads the current champion as baseline, evaluates
|
|
15
|
+
* a constrained candidate on the frozen self-supervised held-out with the human
|
|
16
|
+
* anchor guard, and — on a verified promotion — advances the champion so the
|
|
17
|
+
* NEXT daemon tick compounds on it. Shadow-first (serve lags one tick). Opt-in,
|
|
18
|
+
* $0 default; never throws.
|
|
19
|
+
*/
|
|
20
|
+
export declare function runFlywheelGenerationWorker(projectRoot: string, opts?: {
|
|
21
|
+
sample?: number;
|
|
22
|
+
optInOverride?: boolean;
|
|
23
|
+
now?: number;
|
|
24
|
+
}): Promise<GenerationResult>;
|
|
25
|
+
//# sourceMappingURL=harness-flywheel-runtime.d.ts.map
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flywheel runtime wiring (ADR-176) — binds runFlywheelTick to the LIVE neural
|
|
3
|
+
* store + retrieval so the daemon can self-optimize on real data. Opt-in
|
|
4
|
+
* (RUFLO_HARNESS_LOOP) and bounded; lazily imports neural-tools so the daemon
|
|
5
|
+
* pays the ONNX cost only when actually running a tick. Never throws.
|
|
6
|
+
*/
|
|
7
|
+
import { harnessLoopOptedIn } from './harness-worker.js';
|
|
8
|
+
import { runFlywheelTick } from './harness-flywheel.js';
|
|
9
|
+
import { runFlywheelGeneration, checkServedChampionDrift } from './harness-flywheel-generations.js';
|
|
10
|
+
/** The frozen ADR-081 human-relevance anchor in the generation runner's shape. */
|
|
11
|
+
const GEN_ANCHOR = [
|
|
12
|
+
['how was the Opus model alias fixed', ['opus 4.8', 'opus alias', 'opus model alias', '#2232']],
|
|
13
|
+
['self-learning wiring task-completed pretrain', ['self-learning', 'adr-074', '#2245', 'task-completed']],
|
|
14
|
+
['deterministic codemod engine var-to-const', ['deterministic tier-1 codemod', 'adr-143', 'codemod', 'var-to-const']],
|
|
15
|
+
['MCP server orphan leak parent-death', ['mcp orphan', 'parent-death', '#2234', 'orphan on every claude']],
|
|
16
|
+
['unified learning stats aggregator', ['unified learning-stats', 'adr-075']],
|
|
17
|
+
['structured distillation 4-field schema', ['structured distillation', 'adr-076', '4-field schema']],
|
|
18
|
+
['SQL injection migrate.ts table identifier', ['sql injection', 'migrate.ts', 'agentdb', 'cve']],
|
|
19
|
+
['recall@k HNSW benchmark harness', ['hnsw', 'recall@k', 'benchmark intelligence']],
|
|
20
|
+
['Q-learning encoder keyword block', ['q-state encoder', 'keyword block', '#2239', 'q-encoder']],
|
|
21
|
+
['security hardening crypto random IDs', ['cwe-347', 'crypto.randomuuid', 'random id', 'crypto random']],
|
|
22
|
+
].map(([q, labels], i) => ({ id: `anchor-${i}`, q: q, labels: labels }));
|
|
23
|
+
/** The human-labeled ADR-081 anchor — the never-regress relevance set. */
|
|
24
|
+
const ANCHOR = [
|
|
25
|
+
['how was the Opus model alias fixed', ['opus 4.8', 'opus alias', 'opus model alias', '#2232']],
|
|
26
|
+
['self-learning wiring task-completed pretrain', ['self-learning', 'adr-074', 'self learning', '#2245', 'task-completed']],
|
|
27
|
+
['deterministic codemod engine var-to-const', ['deterministic tier-1 codemod', 'adr-143', 'codemod', 'var-to-const']],
|
|
28
|
+
['MCP server orphan leak parent-death', ['mcp orphan', 'mcp servers orphan', 'parent-death', '#2234', 'orphan on every claude']],
|
|
29
|
+
['unified learning stats aggregator', ['unified learning-stats', 'adr-075', 'unified learning stats']],
|
|
30
|
+
['structured distillation 4-field schema', ['structured distillation', 'adr-076', '4-field schema']],
|
|
31
|
+
['SQL injection migrate.ts table identifier', ['sql injection', 'shell injection', 'migrate.ts', 'agentdb', 'cve']],
|
|
32
|
+
['recall@k HNSW benchmark harness', ['hnsw', 'memory-recall', 'benchmark suite', 'recall@k', 'benchmark intelligence']],
|
|
33
|
+
['Q-learning encoder keyword block', ['q-state encoder', 'route q-state', 'keyword block', '#2239', 'q-encoder']],
|
|
34
|
+
['security hardening crypto random IDs', ['cwe-347', 'crypto.randomuuid', 'security fix', 'random id', 'crypto random']],
|
|
35
|
+
].map(([q, labels], i) => ({ id: `q${String(i).padStart(2, '0')}`, input: { id: `q${String(i).padStart(2, '0')}`, q: q }, expected: labels }));
|
|
36
|
+
/**
|
|
37
|
+
* Run one live flywheel tick against `projectRoot`. Opt-in + $0 default: with
|
|
38
|
+
* RUFLO_HARNESS_LOOP unset it is a no-op. Best-effort; never throws.
|
|
39
|
+
*/
|
|
40
|
+
export async function runFlywheelWorker(projectRoot, opts = {}) {
|
|
41
|
+
try {
|
|
42
|
+
if (!(opts.optInOverride ?? harnessLoopOptedIn()))
|
|
43
|
+
return { ran: false, reason: 'opt-in required (RUFLO_HARNESS_LOOP=1)' };
|
|
44
|
+
const neural = await import('../mcp-tools/neural-tools.js');
|
|
45
|
+
const applier = await import('../config/harness-feedback-applier.js');
|
|
46
|
+
const tool = neural.neuralTools.find((t) => t.name === 'neural_patterns');
|
|
47
|
+
if (!tool)
|
|
48
|
+
return { ran: false, reason: 'neural_patterns tool unavailable' };
|
|
49
|
+
const deps = {
|
|
50
|
+
getPatterns: () => neural.getStorePatterns(),
|
|
51
|
+
search: async (query, cfg) => {
|
|
52
|
+
const r = await tool.handler({ action: 'search', query, mode: 'hybrid', limit: 5, rerank: false, ...cfg });
|
|
53
|
+
return (r.results || []).slice(0, 5).map((m) => ({ id: m?.id ?? '', name: m?.name ?? '' }));
|
|
54
|
+
},
|
|
55
|
+
anchorTasks: ANCHOR,
|
|
56
|
+
activeParams: () => applier.activeChampion(projectRoot)?.params ?? null,
|
|
57
|
+
sample: opts.sample ?? 40,
|
|
58
|
+
now: opts.now,
|
|
59
|
+
};
|
|
60
|
+
return await runFlywheelTick(projectRoot, deps);
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
return { ran: false, reason: `error: ${e?.message ?? e}` };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Run ONE live COMPOUNDING generation against the persisted lineage (ADR-176
|
|
68
|
+
* A-P3b — the autonomy loop). Reads the current champion as baseline, evaluates
|
|
69
|
+
* a constrained candidate on the frozen self-supervised held-out with the human
|
|
70
|
+
* anchor guard, and — on a verified promotion — advances the champion so the
|
|
71
|
+
* NEXT daemon tick compounds on it. Shadow-first (serve lags one tick). Opt-in,
|
|
72
|
+
* $0 default; never throws.
|
|
73
|
+
*/
|
|
74
|
+
export async function runFlywheelGenerationWorker(projectRoot, opts = {}) {
|
|
75
|
+
try {
|
|
76
|
+
if (!(opts.optInOverride ?? harnessLoopOptedIn()))
|
|
77
|
+
return { ran: false, reason: 'opt-in required (RUFLO_HARNESS_LOOP=1)', generation: 0 };
|
|
78
|
+
const neural = await import('../mcp-tools/neural-tools.js');
|
|
79
|
+
const tool = neural.neuralTools.find((t) => t.name === 'neural_patterns');
|
|
80
|
+
if (!tool)
|
|
81
|
+
return { ran: false, reason: 'neural_patterns tool unavailable', generation: 0 };
|
|
82
|
+
const deps = {
|
|
83
|
+
getPatterns: () => neural.getStorePatterns(),
|
|
84
|
+
search: async (query, cfg) => {
|
|
85
|
+
const r = await tool.handler({ action: 'search', query, mode: 'hybrid', limit: 5, rerank: false, ...cfg });
|
|
86
|
+
return (r.results || []).slice(0, 5).map((m) => ({ id: m?.id ?? '', name: m?.name ?? '' }));
|
|
87
|
+
},
|
|
88
|
+
anchorTasks: GEN_ANCHOR,
|
|
89
|
+
sample: opts.sample ?? 120,
|
|
90
|
+
now: opts.now ?? Date.now(),
|
|
91
|
+
};
|
|
92
|
+
// Deployment-safety canary first: roll back the served champion if the real
|
|
93
|
+
// store has drifted since it was promoted. Then run the next generation.
|
|
94
|
+
await checkServedChampionDrift(projectRoot, deps);
|
|
95
|
+
return await runFlywheelGeneration(projectRoot, deps);
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
return { ran: false, reason: `error: ${e?.message ?? e}`, generation: 0 };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=harness-flywheel-runtime.js.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type HarvestPattern } from './harness-corpus-harvester.js';
|
|
2
|
+
export interface RetrievalConfig {
|
|
3
|
+
alpha: number;
|
|
4
|
+
subjectWeight: number;
|
|
5
|
+
mmrLambda: number;
|
|
6
|
+
bodyWeight: number;
|
|
7
|
+
typePenaltyFactor: number;
|
|
8
|
+
}
|
|
9
|
+
export declare const DEFAULT_CONFIG: RetrievalConfig;
|
|
10
|
+
export interface RankedItem {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
}
|
|
14
|
+
export interface AnchorTask {
|
|
15
|
+
id: string;
|
|
16
|
+
input: {
|
|
17
|
+
id: string;
|
|
18
|
+
q: string;
|
|
19
|
+
};
|
|
20
|
+
expected: string[];
|
|
21
|
+
}
|
|
22
|
+
export interface FlywheelDeps {
|
|
23
|
+
getPatterns: () => HarvestPattern[] | Promise<HarvestPattern[]>;
|
|
24
|
+
search: (query: string, config: RetrievalConfig) => Promise<RankedItem[]> | RankedItem[];
|
|
25
|
+
anchorTasks: AnchorTask[];
|
|
26
|
+
activeParams?: () => Partial<RetrievalConfig> | null;
|
|
27
|
+
sample?: number;
|
|
28
|
+
now?: number;
|
|
29
|
+
}
|
|
30
|
+
export interface FlywheelResult {
|
|
31
|
+
ran: boolean;
|
|
32
|
+
reason: string;
|
|
33
|
+
accepted?: boolean;
|
|
34
|
+
applied?: boolean;
|
|
35
|
+
baselineScore?: number;
|
|
36
|
+
candidateScore?: number;
|
|
37
|
+
delta?: number;
|
|
38
|
+
anchorRegressed?: boolean;
|
|
39
|
+
championRef?: string;
|
|
40
|
+
corpusVersion?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Run one flywheel tick against `projectRoot`. Best-effort; never throws.
|
|
44
|
+
* Returns a rich result AND (as a side effect) appends to the improvement ledger
|
|
45
|
+
* and — on accept — applies the champion locally + chains it.
|
|
46
|
+
*/
|
|
47
|
+
export declare function runFlywheelTick(projectRoot: string, deps: FlywheelDeps): Promise<FlywheelResult>;
|
|
48
|
+
//# sourceMappingURL=harness-flywheel.d.ts.map
|