@claude-flow/cli 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/evolve-proof/generation-0.json +211 -0
- package/.claude/evolve-proof/real-generation-0.json +406 -0
- package/.claude/evolve-proof/real-generation-1.json +406 -0
- package/.claude/helpers/.helpers-version +1 -0
- package/.claude/helpers/helpers.manifest.json +2 -2
- package/.claude/helpers/hook-handler.cjs +0 -0
- package/.claude/helpers/intelligence.cjs +10 -0
- package/.claude/proven-config.manifest.json +37 -0
- package/.claude/proven-config.signed.json +41 -0
- package/.claude/proven-config.signed.rvf +0 -0
- package/dist/src/commands/memory-backup.d.ts +11 -0
- package/dist/src/commands/memory-backup.js +46 -0
- package/dist/src/commands/memory.js +2 -1
- package/dist/src/config/harness-feedback-applier.d.ts +50 -0
- package/dist/src/config/harness-feedback-applier.js +122 -0
- package/dist/src/config/proven-config-refresh.d.ts +39 -0
- package/dist/src/config/proven-config-refresh.js +154 -0
- package/dist/src/config/proven-config-rvfa.d.ts +23 -0
- package/dist/src/config/proven-config-rvfa.js +73 -0
- package/dist/src/config/proven-config.d.ts +86 -0
- package/dist/src/config/proven-config.js +176 -0
- package/dist/src/index.js +35 -0
- package/dist/src/mcp-tools/neural-tools.d.ts +10 -0
- package/dist/src/mcp-tools/neural-tools.js +107 -18
- package/dist/src/services/daemon-autostart.d.ts +17 -0
- package/dist/src/services/daemon-autostart.js +79 -0
- package/dist/src/services/evolve-proof.d.ts +247 -0
- package/dist/src/services/evolve-proof.js +341 -0
- package/dist/src/services/harness-benchmark.d.ts +68 -0
- package/dist/src/services/harness-benchmark.js +94 -0
- package/dist/src/services/harness-canary.d.ts +60 -0
- package/dist/src/services/harness-canary.js +69 -0
- package/dist/src/services/harness-corpus-harvester.d.ts +51 -0
- package/dist/src/services/harness-corpus-harvester.js +74 -0
- package/dist/src/services/harness-flywheel-generations.d.ts +111 -0
- package/dist/src/services/harness-flywheel-generations.js +354 -0
- package/dist/src/services/harness-flywheel-runtime.d.ts +25 -0
- package/dist/src/services/harness-flywheel-runtime.js +101 -0
- package/dist/src/services/harness-flywheel.d.ts +48 -0
- package/dist/src/services/harness-flywheel.js +207 -0
- package/dist/src/services/harness-hosts.d.ts +40 -0
- package/dist/src/services/harness-hosts.js +88 -0
- package/dist/src/services/harness-improvement-ledger.d.ts +63 -0
- package/dist/src/services/harness-improvement-ledger.js +101 -0
- package/dist/src/services/harness-loop.d.ts +53 -0
- package/dist/src/services/harness-loop.js +85 -0
- package/dist/src/services/harness-qualification.d.ts +66 -0
- package/dist/src/services/harness-qualification.js +143 -0
- package/dist/src/services/harness-replay.d.ts +37 -0
- package/dist/src/services/harness-replay.js +92 -0
- package/dist/src/services/harness-verify.d.ts +33 -0
- package/dist/src/services/harness-verify.js +26 -0
- package/dist/src/services/harness-worker.d.ts +23 -0
- package/dist/src/services/harness-worker.js +66 -0
- package/dist/src/services/memory-backup.d.ts +24 -0
- package/dist/src/services/memory-backup.js +94 -0
- package/dist/src/services/worker-daemon.d.ts +16 -1
- package/dist/src/services/worker-daemon.js +84 -0
- package/package.json +1 -1
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export interface HarvestPattern {
|
|
2
|
+
id: string;
|
|
3
|
+
name?: string;
|
|
4
|
+
content?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface HarvestedTask {
|
|
7
|
+
id: string;
|
|
8
|
+
input: {
|
|
9
|
+
id: string;
|
|
10
|
+
q: string;
|
|
11
|
+
targetId: string;
|
|
12
|
+
};
|
|
13
|
+
expected: string;
|
|
14
|
+
provenanceTier: 'oracle:self-identity';
|
|
15
|
+
}
|
|
16
|
+
export interface BlendedCorpus {
|
|
17
|
+
version: string;
|
|
18
|
+
corpusHash: string;
|
|
19
|
+
anchorIds: string[];
|
|
20
|
+
tasks: Array<{
|
|
21
|
+
id: string;
|
|
22
|
+
input: unknown;
|
|
23
|
+
expected: unknown;
|
|
24
|
+
}>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Harvest up to `sample` self-retrieval tasks from real patterns. Deterministic
|
|
28
|
+
* stride sampling (stable across runs); skips docs whose body is too thin to
|
|
29
|
+
* form a discriminative query.
|
|
30
|
+
*/
|
|
31
|
+
export declare function harvestSelfSupervisedTasks(patterns: HarvestPattern[], opts?: {
|
|
32
|
+
sample?: number;
|
|
33
|
+
minQueryTerms?: number;
|
|
34
|
+
}): HarvestedTask[];
|
|
35
|
+
/** Content hash over the task ids+queries — changes iff the corpus changes. */
|
|
36
|
+
export declare function hashBlend(tasks: Array<{
|
|
37
|
+
id: string;
|
|
38
|
+
input: unknown;
|
|
39
|
+
expected: unknown;
|
|
40
|
+
}>): string;
|
|
41
|
+
/**
|
|
42
|
+
* Blend a human-labeled anchor set with harvested tasks into one versioned,
|
|
43
|
+
* hashed corpus. The version encodes the sizes + hash so it visibly grows as the
|
|
44
|
+
* store does; `anchorIds` marks the never-regress subset.
|
|
45
|
+
*/
|
|
46
|
+
export declare function blendCorpus(anchor: Array<{
|
|
47
|
+
id: string;
|
|
48
|
+
input: unknown;
|
|
49
|
+
expected: unknown;
|
|
50
|
+
}>, harvested: HarvestedTask[]): BlendedCorpus;
|
|
51
|
+
//# sourceMappingURL=harness-corpus-harvester.d.ts.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Corpus harvester — grows the benchmark yardstick from REAL store data so the
|
|
3
|
+
* flywheel gets a bigger, fresher test set as ruflo is used (ADR-176).
|
|
4
|
+
*
|
|
5
|
+
* Self-supervised self-retrieval: a stored doc is unambiguous ground truth for a
|
|
6
|
+
* query derived from its OWN body. To make it discriminative (so different
|
|
7
|
+
* retrieval configs actually score differently — a usable gradient) the query is
|
|
8
|
+
* built from body keywords with the doc's SUBJECT tokens removed: "can retrieval
|
|
9
|
+
* find this doc from its content when the obvious title words are withheld?"
|
|
10
|
+
* The label is the doc's identity — oracle-grade, not a proxy guess.
|
|
11
|
+
*
|
|
12
|
+
* This grows the auto signal; callers keep the human-labeled seed as a separate
|
|
13
|
+
* NON-REGRESSION anchor (Goodhart guard) so optimizing self-retrieval can never
|
|
14
|
+
* silently drift away from human relevance. Pure + deterministic (no RNG): the
|
|
15
|
+
* sample is a fixed stride, so the same store yields the same corpus.
|
|
16
|
+
*/
|
|
17
|
+
import { createHash } from 'node:crypto';
|
|
18
|
+
const STOP = new Set(['the', 'a', 'an', 'of', 'to', 'in', 'on', 'for', 'and', 'or', 'is', 'was', 'with', 'by', 'at', 'as', 'it', 'this', 'that', 'from', 'be', 'are', 'so', 'if', 'we', 'i', 'you', 'not', 'no', 'do', 'via', 'per']);
|
|
19
|
+
function tokenize(s) {
|
|
20
|
+
return (s.toLowerCase().match(/[a-z0-9][a-z0-9_-]{1,}/g) ?? []).filter((t) => !STOP.has(t));
|
|
21
|
+
}
|
|
22
|
+
/** Top-K body keywords with the subject tokens removed, most-frequent first. */
|
|
23
|
+
function deriveQuery(name, content, k = 6) {
|
|
24
|
+
const nameToks = new Set(tokenize(name));
|
|
25
|
+
const freq = new Map();
|
|
26
|
+
for (const t of tokenize(content)) {
|
|
27
|
+
if (nameToks.has(t))
|
|
28
|
+
continue; // withhold the obvious title words
|
|
29
|
+
freq.set(t, (freq.get(t) ?? 0) + 1);
|
|
30
|
+
}
|
|
31
|
+
return [...freq.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, k).map(([t]) => t).join(' ');
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Harvest up to `sample` self-retrieval tasks from real patterns. Deterministic
|
|
35
|
+
* stride sampling (stable across runs); skips docs whose body is too thin to
|
|
36
|
+
* form a discriminative query.
|
|
37
|
+
*/
|
|
38
|
+
export function harvestSelfSupervisedTasks(patterns, opts = {}) {
|
|
39
|
+
const sample = Math.max(1, opts.sample ?? 40);
|
|
40
|
+
const minTerms = opts.minQueryTerms ?? 3;
|
|
41
|
+
// Deterministic order: sort by id, then take an even stride to cover the store.
|
|
42
|
+
const ordered = [...patterns].sort((a, b) => a.id.localeCompare(b.id));
|
|
43
|
+
const stride = Math.max(1, Math.floor(ordered.length / sample));
|
|
44
|
+
const out = [];
|
|
45
|
+
for (let i = 0; i < ordered.length && out.length < sample; i += stride) {
|
|
46
|
+
const p = ordered[i];
|
|
47
|
+
const q = deriveQuery(p.name ?? '', p.content ?? '', 6);
|
|
48
|
+
if (q.split(' ').filter(Boolean).length < minTerms)
|
|
49
|
+
continue; // too thin → skip
|
|
50
|
+
out.push({ id: `hv-${p.id}`, input: { id: `hv-${p.id}`, q, targetId: p.id }, expected: p.id, provenanceTier: 'oracle:self-identity' });
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
/** Content hash over the task ids+queries — changes iff the corpus changes. */
|
|
55
|
+
export function hashBlend(tasks) {
|
|
56
|
+
const canon = tasks.map((t) => `${t.id}|${JSON.stringify(t.input)}|${JSON.stringify(t.expected)}`).sort().join('\n');
|
|
57
|
+
return 'sha256:' + createHash('sha256').update(canon).digest('hex');
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Blend a human-labeled anchor set with harvested tasks into one versioned,
|
|
61
|
+
* hashed corpus. The version encodes the sizes + hash so it visibly grows as the
|
|
62
|
+
* store does; `anchorIds` marks the never-regress subset.
|
|
63
|
+
*/
|
|
64
|
+
export function blendCorpus(anchor, harvested) {
|
|
65
|
+
const tasks = [...anchor, ...harvested.map((h) => ({ id: h.id, input: h.input, expected: h.expected }))];
|
|
66
|
+
const corpusHash = hashBlend(tasks);
|
|
67
|
+
return {
|
|
68
|
+
version: `flywheel-a${anchor.length}-h${harvested.length}-${corpusHash.slice(7, 19)}`,
|
|
69
|
+
corpusHash,
|
|
70
|
+
anchorIds: anchor.map((a) => a.id),
|
|
71
|
+
tasks,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=harness-corpus-harvester.js.map
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { type EvolveReceiptBundle, type LineageTelemetry, type PlateauReport, type MutationStat } from './evolve-proof.js';
|
|
2
|
+
import { type HarvestPattern } from './harness-corpus-harvester.js';
|
|
3
|
+
import { type RetrievalConfig, type RankedItem } from './harness-flywheel.js';
|
|
4
|
+
export declare const FLYWHEEL_DIR: string[];
|
|
5
|
+
export declare const FROZEN_CORPUS = "harvested-selfsup-frozen-v1";
|
|
6
|
+
export interface AnchorTask {
|
|
7
|
+
id: string;
|
|
8
|
+
q: string;
|
|
9
|
+
labels: string[];
|
|
10
|
+
}
|
|
11
|
+
export interface GenerationDeps {
|
|
12
|
+
getPatterns: () => HarvestPattern[] | Promise<HarvestPattern[]>;
|
|
13
|
+
search: (q: string, cfg: RetrievalConfig) => Promise<RankedItem[]> | RankedItem[];
|
|
14
|
+
anchorTasks: AnchorTask[];
|
|
15
|
+
sample?: number;
|
|
16
|
+
now: number;
|
|
17
|
+
applyFn?: (cfg: Record<string, number>, hash: string, generation: number) => void;
|
|
18
|
+
}
|
|
19
|
+
export interface GenerationResult {
|
|
20
|
+
ran: boolean;
|
|
21
|
+
reason: string;
|
|
22
|
+
generation: number;
|
|
23
|
+
promoted?: boolean;
|
|
24
|
+
delta?: number;
|
|
25
|
+
significant?: boolean;
|
|
26
|
+
championConfig?: Record<string, number>;
|
|
27
|
+
servedChampion?: string | null;
|
|
28
|
+
anchorRegressed?: boolean;
|
|
29
|
+
}
|
|
30
|
+
/** Promotions only — the champion chain (generation-N.json), sorted by generation. */
|
|
31
|
+
export declare function loadPromotions(root: string): EvolveReceiptBundle[];
|
|
32
|
+
/** Every attempt (promoted + rejected) — for telemetry + mutation-effectiveness. */
|
|
33
|
+
export declare function loadAttempts(root: string): EvolveReceiptBundle[];
|
|
34
|
+
/** The current operating champion (last promotion's config), or defaults. */
|
|
35
|
+
export declare function currentChampion(root: string): {
|
|
36
|
+
config: Record<string, number>;
|
|
37
|
+
hash: string | null;
|
|
38
|
+
generation: number;
|
|
39
|
+
};
|
|
40
|
+
export interface ServedState {
|
|
41
|
+
championHash: string | null;
|
|
42
|
+
config: Record<string, number> | null;
|
|
43
|
+
servedAt: number | null;
|
|
44
|
+
fromGeneration: number | null;
|
|
45
|
+
}
|
|
46
|
+
export declare function servedChampion(root: string): ServedState;
|
|
47
|
+
/**
|
|
48
|
+
* Shadow→serve gate: apply the latest promoted champion to the ACTIVE policy iff
|
|
49
|
+
* it is newer than what is currently served. Called at tick START, so a champion
|
|
50
|
+
* promoted last tick is served this tick (a 1-generation shadow delay) — never
|
|
51
|
+
* auto-served the instant it is promoted.
|
|
52
|
+
*/
|
|
53
|
+
export declare function serveCurrentChampionIfPending(root: string, now: number, applyFn?: GenerationDeps['applyFn']): string | null;
|
|
54
|
+
export interface AxisStat {
|
|
55
|
+
axis: string;
|
|
56
|
+
promotions: number;
|
|
57
|
+
meanDelta: number;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Which policy AXES have historically paid off — attribute each promotion's
|
|
61
|
+
* held-out Δ to the axes that changed in it. Turns the lineage into a
|
|
62
|
+
* knowledge base the optimizer can act on (not just audit).
|
|
63
|
+
*/
|
|
64
|
+
export declare function axisEffectiveness(promotions: EvolveReceiptBundle[]): AxisStat[];
|
|
65
|
+
/**
|
|
66
|
+
* Candidate set biased by measured axis payoff (meta-learning). Every axis keeps
|
|
67
|
+
* a ±1 exploration floor (never abandon a dimension), but axes with a positive
|
|
68
|
+
* historical Δ get EXPANDED range (±2, ±3) and PAIRWISE joint moves with other
|
|
69
|
+
* productive axes — so compute concentrates on the dimensions that have actually
|
|
70
|
+
* produced gains, instead of a uniform grid. Deterministic; bounded.
|
|
71
|
+
*/
|
|
72
|
+
export declare function biasedGrid(c: RetrievalConfig, ranking: AxisStat[]): RetrievalConfig[];
|
|
73
|
+
/**
|
|
74
|
+
* Run ONE generation against `root`, compounding on the persisted champion.
|
|
75
|
+
* Serves the prior champion first (shadow delay), then evaluates a new candidate.
|
|
76
|
+
*/
|
|
77
|
+
export declare function runFlywheelGeneration(root: string, deps: GenerationDeps): Promise<GenerationResult>;
|
|
78
|
+
export interface DriftCheck {
|
|
79
|
+
checked: boolean;
|
|
80
|
+
rolledBack: boolean;
|
|
81
|
+
reason: string;
|
|
82
|
+
servedScore?: number;
|
|
83
|
+
predecessorScore?: number;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* A real deployment-safety check on real (not fabricated) data: the store keeps
|
|
87
|
+
* changing as ruflo is used, so a champion benchmarked at promotion time can
|
|
88
|
+
* DRIFT. Each tick, re-score the currently-SERVED champion against its
|
|
89
|
+
* predecessor on a FRESH harvest of the current store; if it now regresses
|
|
90
|
+
* (self-retrieval OR the human anchor), automatically ROLL BACK the active policy
|
|
91
|
+
* to the predecessor. This is the honest analogue of a live-traffic canary —
|
|
92
|
+
* genuine ongoing measurement + genuine rollback — without fabricating traffic.
|
|
93
|
+
* $0; never throws.
|
|
94
|
+
*/
|
|
95
|
+
export declare function checkServedChampionDrift(root: string, deps: GenerationDeps): Promise<DriftCheck>;
|
|
96
|
+
export interface FlywheelStatus {
|
|
97
|
+
generations: number;
|
|
98
|
+
attempts: number;
|
|
99
|
+
lineage: LineageTelemetry;
|
|
100
|
+
plateau: PlateauReport;
|
|
101
|
+
mutation: MutationStat[];
|
|
102
|
+
axisEffectiveness: AxisStat[];
|
|
103
|
+
served: ServedState;
|
|
104
|
+
champion: {
|
|
105
|
+
config: Record<string, number>;
|
|
106
|
+
hash: string | null;
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/** Reconstruct the persisted lineage + telemetry for a status endpoint / CLI. */
|
|
110
|
+
export declare function flywheelStatus(root: string): FlywheelStatus;
|
|
111
|
+
//# sourceMappingURL=harness-flywheel-generations.d.ts.map
|
|
@@ -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
|