claude-flow 3.23.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 -1
- package/.claude/helpers/helpers.manifest.json +2 -2
- package/package.json +1 -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/worker-daemon.d.ts +8 -1
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +42 -0
- package/v3/@claude-flow/cli/package.json +1 -1
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export type ProvenanceTier = 'oracle:test-exec' | 'judge:fable' | 'proxy:structural';
|
|
2
|
+
export interface TrajectoryStep {
|
|
3
|
+
action: string;
|
|
4
|
+
tier: ProvenanceTier;
|
|
5
|
+
}
|
|
6
|
+
export interface Trajectory {
|
|
7
|
+
id: string;
|
|
8
|
+
steps: TrajectoryStep[];
|
|
9
|
+
outcome: 'success' | 'failure';
|
|
10
|
+
benchmarkTaskId?: string;
|
|
11
|
+
inputs: unknown;
|
|
12
|
+
recordedOutputs: unknown;
|
|
13
|
+
}
|
|
14
|
+
/** Re-execute a trajectory's recorded inputs and return the outputs. */
|
|
15
|
+
export type ReplayFn = (t: Trajectory) => unknown;
|
|
16
|
+
export interface QualificationResult {
|
|
17
|
+
qualified: boolean;
|
|
18
|
+
reasons: string[];
|
|
19
|
+
}
|
|
20
|
+
/** Fingerprint a trajectory by its shape (step actions + outcome) so identical failures dedupe. */
|
|
21
|
+
export declare function fingerprintTrajectory(t: Trajectory): string;
|
|
22
|
+
/**
|
|
23
|
+
* Invariant Q. `replay` is required — determinism cannot be asserted without
|
|
24
|
+
* re-executing; a trajectory with no verifiable replay is REJECTED (fail-closed).
|
|
25
|
+
*/
|
|
26
|
+
export declare function qualifyTrajectory(t: Trajectory, replay?: ReplayFn): QualificationResult;
|
|
27
|
+
export interface AntiPattern {
|
|
28
|
+
fingerprint: string;
|
|
29
|
+
stage: string;
|
|
30
|
+
reasons: string[];
|
|
31
|
+
ts: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* File-backed avoid-list of rejected trajectories/mutations. JSONL, append-only
|
|
35
|
+
* with a bounded cap: once the file exceeds `maxEntries` it is rewritten to the
|
|
36
|
+
* newest `maxEntries` (rotation), so the archive can never grow unbounded —
|
|
37
|
+
* runaway-storage guard. Deduped by fingerprint at the call site.
|
|
38
|
+
*/
|
|
39
|
+
export declare class AntiPatternArchive {
|
|
40
|
+
private readonly filePath;
|
|
41
|
+
private readonly maxEntries;
|
|
42
|
+
constructor(filePath: string, maxEntries?: number);
|
|
43
|
+
private load;
|
|
44
|
+
record(entry: AntiPattern): void;
|
|
45
|
+
has(fingerprint: string): boolean;
|
|
46
|
+
list(): AntiPattern[];
|
|
47
|
+
}
|
|
48
|
+
export interface AdmissionReport {
|
|
49
|
+
qualified: Trajectory[];
|
|
50
|
+
rejected: Array<{
|
|
51
|
+
id: string;
|
|
52
|
+
reasons: string[];
|
|
53
|
+
}>;
|
|
54
|
+
admittedCount: number;
|
|
55
|
+
rejectedCount: number;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Admit a batch of observed trajectories into the candidate dataset. Rejects are
|
|
59
|
+
* recorded to the anti-pattern archive (deduped by fingerprint).
|
|
60
|
+
*/
|
|
61
|
+
export declare function admitTrajectories(trajectories: Trajectory[], opts?: {
|
|
62
|
+
replay?: ReplayFn;
|
|
63
|
+
archive?: AntiPatternArchive;
|
|
64
|
+
ts?: number;
|
|
65
|
+
}): AdmissionReport;
|
|
66
|
+
//# sourceMappingURL=harness-qualification.d.ts.map
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Qualification + anti-pattern archive (ADR-176 phase 1).
|
|
3
|
+
*
|
|
4
|
+
* Separates OBSERVATION from TRAINING DATA. Raw observed trajectories are NOT
|
|
5
|
+
* training data — they enter optimization only through the Qualification gate,
|
|
6
|
+
* which enforces:
|
|
7
|
+
*
|
|
8
|
+
* Invariant Q: a trajectory is admitted iff it has
|
|
9
|
+
* (a) complete provenance — every step attributed, tier >= oracle/judge
|
|
10
|
+
* (ADR-171; proxy:structural is triage-only),
|
|
11
|
+
* (b) deterministic replay — re-running its recorded inputs reproduces its
|
|
12
|
+
* recorded outputs, and
|
|
13
|
+
* (c) benchmark attribution — it maps to a task in the versioned corpus.
|
|
14
|
+
*
|
|
15
|
+
* Rejected trajectories are not discarded — they are recorded to the
|
|
16
|
+
* ANTI-PATTERN ARCHIVE (negative learning), so future optimization runs avoid
|
|
17
|
+
* re-discovering identical failures. Zero dependencies, $0.
|
|
18
|
+
*/
|
|
19
|
+
import * as fs from 'fs';
|
|
20
|
+
import * as path from 'path';
|
|
21
|
+
import { createHash } from 'crypto';
|
|
22
|
+
/** Deterministic JSON for the replay comparison (recursively key-sorted). */
|
|
23
|
+
function stable(v) {
|
|
24
|
+
const c = (x) => {
|
|
25
|
+
if (Array.isArray(x))
|
|
26
|
+
return x.map(c);
|
|
27
|
+
if (x && typeof x === 'object') {
|
|
28
|
+
const o = {};
|
|
29
|
+
for (const k of Object.keys(x).sort())
|
|
30
|
+
o[k] = c(x[k]);
|
|
31
|
+
return o;
|
|
32
|
+
}
|
|
33
|
+
return x;
|
|
34
|
+
};
|
|
35
|
+
return JSON.stringify(c(v));
|
|
36
|
+
}
|
|
37
|
+
/** Fingerprint a trajectory by its shape (step actions + outcome) so identical failures dedupe. */
|
|
38
|
+
export function fingerprintTrajectory(t) {
|
|
39
|
+
const shape = t.steps.map(s => s.action).join('→') + '#' + t.outcome + '#' + (t.benchmarkTaskId ?? '');
|
|
40
|
+
return createHash('sha256').update(shape).digest('hex').slice(0, 32);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Invariant Q. `replay` is required — determinism cannot be asserted without
|
|
44
|
+
* re-executing; a trajectory with no verifiable replay is REJECTED (fail-closed).
|
|
45
|
+
*/
|
|
46
|
+
export function qualifyTrajectory(t, replay) {
|
|
47
|
+
const reasons = [];
|
|
48
|
+
// (a) complete provenance
|
|
49
|
+
if (!t.steps || t.steps.length === 0)
|
|
50
|
+
reasons.push('incomplete: no steps');
|
|
51
|
+
else {
|
|
52
|
+
if (t.steps.some(s => !s.action))
|
|
53
|
+
reasons.push('incomplete provenance: a step has no action');
|
|
54
|
+
if (t.steps.some(s => s.tier === 'proxy:structural'))
|
|
55
|
+
reasons.push('proxy-tier step present (below oracle/judge; ADR-171)');
|
|
56
|
+
}
|
|
57
|
+
// (c) benchmark attribution
|
|
58
|
+
if (!t.benchmarkTaskId)
|
|
59
|
+
reasons.push('no benchmark attribution');
|
|
60
|
+
// (b) deterministic replay
|
|
61
|
+
if (!replay) {
|
|
62
|
+
reasons.push('deterministic replay not verified (no replay fn)');
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
let out;
|
|
66
|
+
try {
|
|
67
|
+
out = replay(t);
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
reasons.push(`replay threw: ${e?.message ?? e}`);
|
|
71
|
+
out = undefined;
|
|
72
|
+
}
|
|
73
|
+
if (reasons.every(r => !r.startsWith('replay threw')) && stable(out) !== stable(t.recordedOutputs)) {
|
|
74
|
+
reasons.push('replay non-deterministic (outputs differ from recorded)');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { qualified: reasons.length === 0, reasons };
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* File-backed avoid-list of rejected trajectories/mutations. JSONL, append-only
|
|
81
|
+
* with a bounded cap: once the file exceeds `maxEntries` it is rewritten to the
|
|
82
|
+
* newest `maxEntries` (rotation), so the archive can never grow unbounded —
|
|
83
|
+
* runaway-storage guard. Deduped by fingerprint at the call site.
|
|
84
|
+
*/
|
|
85
|
+
export class AntiPatternArchive {
|
|
86
|
+
filePath;
|
|
87
|
+
maxEntries;
|
|
88
|
+
constructor(filePath, maxEntries = 10000) {
|
|
89
|
+
this.filePath = filePath;
|
|
90
|
+
this.maxEntries = maxEntries;
|
|
91
|
+
}
|
|
92
|
+
load() {
|
|
93
|
+
try {
|
|
94
|
+
return fs.readFileSync(this.filePath, 'utf-8').split('\n').filter(Boolean).map(l => JSON.parse(l));
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
record(entry) {
|
|
101
|
+
try {
|
|
102
|
+
fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
|
|
103
|
+
const existing = this.load();
|
|
104
|
+
if (existing.length >= this.maxEntries) {
|
|
105
|
+
// Rotate: keep the newest (maxEntries-1) + the new entry.
|
|
106
|
+
const kept = existing.slice(existing.length - (this.maxEntries - 1));
|
|
107
|
+
fs.writeFileSync(this.filePath, kept.concat(entry).map(e => JSON.stringify(e)).join('\n') + '\n', 'utf-8');
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
fs.appendFileSync(this.filePath, JSON.stringify(entry) + '\n', 'utf-8');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch { /* best-effort */ }
|
|
114
|
+
}
|
|
115
|
+
has(fingerprint) {
|
|
116
|
+
return this.load().some(a => a.fingerprint === fingerprint);
|
|
117
|
+
}
|
|
118
|
+
list() { return this.load(); }
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Admit a batch of observed trajectories into the candidate dataset. Rejects are
|
|
122
|
+
* recorded to the anti-pattern archive (deduped by fingerprint).
|
|
123
|
+
*/
|
|
124
|
+
export function admitTrajectories(trajectories, opts = {}) {
|
|
125
|
+
const qualified = [];
|
|
126
|
+
const rejected = [];
|
|
127
|
+
const ts = opts.ts ?? Date.now();
|
|
128
|
+
for (const t of trajectories) {
|
|
129
|
+
const r = qualifyTrajectory(t, opts.replay);
|
|
130
|
+
if (r.qualified) {
|
|
131
|
+
qualified.push(t);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
rejected.push({ id: t.id, reasons: r.reasons });
|
|
135
|
+
const fp = fingerprintTrajectory(t);
|
|
136
|
+
if (opts.archive && !opts.archive.has(fp)) {
|
|
137
|
+
opts.archive.record({ fingerprint: fp, stage: 'qualification', reasons: r.reasons, ts });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return { qualified, rejected, admittedCount: qualified.length, rejectedCount: rejected.length };
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=harness-qualification.js.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Content digest of a value (canonical, order-independent). */
|
|
2
|
+
export declare function digest(v: unknown): string;
|
|
3
|
+
export interface RecordedRun {
|
|
4
|
+
id: string;
|
|
5
|
+
inputs: unknown;
|
|
6
|
+
outputDigest: string;
|
|
7
|
+
recordedAt?: number;
|
|
8
|
+
}
|
|
9
|
+
/** Re-executable unit: given inputs, produce outputs. Must be pure for determinism. */
|
|
10
|
+
export type RunFn = (inputs: unknown) => unknown;
|
|
11
|
+
/** Capture a run: execute once and record the output digest. */
|
|
12
|
+
export declare function recordRun(id: string, inputs: unknown, run: RunFn, ts?: number): RecordedRun;
|
|
13
|
+
export interface ReplayResult {
|
|
14
|
+
deterministic: boolean;
|
|
15
|
+
expectedDigest: string;
|
|
16
|
+
actualDigest: string;
|
|
17
|
+
}
|
|
18
|
+
/** Replay a recorded run and check it reproduces the recorded output digest. */
|
|
19
|
+
export declare function verifyReplay(recorded: RecordedRun, run: RunFn): ReplayResult;
|
|
20
|
+
/** Batch predicate for accept(): all recorded runs replay deterministically. */
|
|
21
|
+
export declare function allDeterministic(recorded: RecordedRun[], run: RunFn): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* File-backed store of recorded runs (JSONL). Append-only with a bounded cap:
|
|
24
|
+
* once it exceeds `maxEntries` it rotates to the newest `maxEntries` — so the
|
|
25
|
+
* store can never grow unbounded (runaway-storage guard). `get` returns the
|
|
26
|
+
* latest for an id.
|
|
27
|
+
*/
|
|
28
|
+
export declare class ReplayStore {
|
|
29
|
+
private readonly filePath;
|
|
30
|
+
private readonly maxEntries;
|
|
31
|
+
constructor(filePath: string, maxEntries?: number);
|
|
32
|
+
private load;
|
|
33
|
+
record(r: RecordedRun): void;
|
|
34
|
+
get(id: string): RecordedRun | undefined;
|
|
35
|
+
all(): RecordedRun[];
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=harness-replay.d.ts.map
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic replay engine (ADR-176 phase 3).
|
|
3
|
+
*
|
|
4
|
+
* The `replay == deterministic` predicate in Invariant Q (qualification) and in
|
|
5
|
+
* accept() requires re-executing a recorded run's inputs and confirming they
|
|
6
|
+
* reproduce the recorded outputs. This module records an output DIGEST at
|
|
7
|
+
* capture time and verifies a replay reproduces it — so a trajectory only
|
|
8
|
+
* counts as training data / promotion evidence if its behavior is reproducible.
|
|
9
|
+
* Zero deps, $0.
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import { createHash } from 'crypto';
|
|
14
|
+
function stable(v) {
|
|
15
|
+
const c = (x) => {
|
|
16
|
+
if (Array.isArray(x))
|
|
17
|
+
return x.map(c);
|
|
18
|
+
if (x && typeof x === 'object') {
|
|
19
|
+
const o = {};
|
|
20
|
+
for (const k of Object.keys(x).sort())
|
|
21
|
+
o[k] = c(x[k]);
|
|
22
|
+
return o;
|
|
23
|
+
}
|
|
24
|
+
return x;
|
|
25
|
+
};
|
|
26
|
+
return JSON.stringify(c(v));
|
|
27
|
+
}
|
|
28
|
+
/** Content digest of a value (canonical, order-independent). */
|
|
29
|
+
export function digest(v) {
|
|
30
|
+
return createHash('sha256').update(stable(v)).digest('hex');
|
|
31
|
+
}
|
|
32
|
+
/** Capture a run: execute once and record the output digest. */
|
|
33
|
+
export function recordRun(id, inputs, run, ts) {
|
|
34
|
+
return { id, inputs, outputDigest: digest(run(inputs)), recordedAt: ts };
|
|
35
|
+
}
|
|
36
|
+
/** Replay a recorded run and check it reproduces the recorded output digest. */
|
|
37
|
+
export function verifyReplay(recorded, run) {
|
|
38
|
+
let actual;
|
|
39
|
+
try {
|
|
40
|
+
actual = digest(run(recorded.inputs));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
actual = 'THREW';
|
|
44
|
+
}
|
|
45
|
+
return { deterministic: actual === recorded.outputDigest, expectedDigest: recorded.outputDigest, actualDigest: actual };
|
|
46
|
+
}
|
|
47
|
+
/** Batch predicate for accept(): all recorded runs replay deterministically. */
|
|
48
|
+
export function allDeterministic(recorded, run) {
|
|
49
|
+
return recorded.every(r => verifyReplay(r, run).deterministic);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* File-backed store of recorded runs (JSONL). Append-only with a bounded cap:
|
|
53
|
+
* once it exceeds `maxEntries` it rotates to the newest `maxEntries` — so the
|
|
54
|
+
* store can never grow unbounded (runaway-storage guard). `get` returns the
|
|
55
|
+
* latest for an id.
|
|
56
|
+
*/
|
|
57
|
+
export class ReplayStore {
|
|
58
|
+
filePath;
|
|
59
|
+
maxEntries;
|
|
60
|
+
constructor(filePath, maxEntries = 10000) {
|
|
61
|
+
this.filePath = filePath;
|
|
62
|
+
this.maxEntries = maxEntries;
|
|
63
|
+
}
|
|
64
|
+
load() {
|
|
65
|
+
try {
|
|
66
|
+
return fs.readFileSync(this.filePath, 'utf-8').split('\n').filter(Boolean).map(l => JSON.parse(l));
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
record(r) {
|
|
73
|
+
try {
|
|
74
|
+
fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
|
|
75
|
+
const existing = this.load();
|
|
76
|
+
if (existing.length >= this.maxEntries) {
|
|
77
|
+
const kept = existing.slice(existing.length - (this.maxEntries - 1));
|
|
78
|
+
fs.writeFileSync(this.filePath, kept.concat(r).map(e => JSON.stringify(e)).join('\n') + '\n', 'utf-8');
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
fs.appendFileSync(this.filePath, JSON.stringify(r) + '\n', 'utf-8');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch { /* best-effort */ }
|
|
85
|
+
}
|
|
86
|
+
get(id) {
|
|
87
|
+
const all = this.load().filter(r => r.id === id);
|
|
88
|
+
return all.length ? all[all.length - 1] : undefined; // latest wins
|
|
89
|
+
}
|
|
90
|
+
all() { return this.load(); }
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=harness-replay.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adversarial + drift verify gate (ADR-176 phase 4).
|
|
3
|
+
*
|
|
4
|
+
* Produces the two verdicts accept() consumes beyond held-out benchmarking:
|
|
5
|
+
* - redblue: an adversarial red-team pass (@metaharness/redblue, mock-judge/$0
|
|
6
|
+
* by default, loopback-only, cost-capped),
|
|
7
|
+
* - drift: distance from the current champion (@metaharness/drift_from_history).
|
|
8
|
+
*
|
|
9
|
+
* FAIL-CLOSED: if the adversarial verifier is unavailable (metaharness is an
|
|
10
|
+
* optional dependency — ADR-150), redblue is SKIPPED, and since accept() requires
|
|
11
|
+
* redblue === 'PASS', a candidate CANNOT be promoted without real adversarial
|
|
12
|
+
* evidence. The loop then safely degrades to "observe + benchmark, don't
|
|
13
|
+
* promote" and the current signed champion stands. Runners are injectable so the
|
|
14
|
+
* orchestrator supplies the real metaharness bridge; defaults degrade. $0.
|
|
15
|
+
*/
|
|
16
|
+
export type RedblueVerdict = 'PASS' | 'FAIL' | 'SKIPPED';
|
|
17
|
+
export type RedblueRunner = () => Promise<RedblueVerdict>;
|
|
18
|
+
/** Returns the drift distance from the champion (>=0), or a negative value = unavailable. */
|
|
19
|
+
export type DriftRunner = () => Promise<number>;
|
|
20
|
+
export interface VerifyResult {
|
|
21
|
+
redblue: RedblueVerdict;
|
|
22
|
+
drift: number;
|
|
23
|
+
driftThreshold: number;
|
|
24
|
+
driftVerdict: 'ok' | 'regressed' | 'skipped';
|
|
25
|
+
adversarialPass: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface VerifyOptions {
|
|
28
|
+
redblue?: RedblueRunner;
|
|
29
|
+
drift?: DriftRunner;
|
|
30
|
+
driftThreshold?: number;
|
|
31
|
+
}
|
|
32
|
+
export declare function runVerify(opts?: VerifyOptions): Promise<VerifyResult>;
|
|
33
|
+
//# sourceMappingURL=harness-verify.d.ts.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Default redblue: no verifier wired in this module → SKIPPED (fail-closed). */
|
|
2
|
+
const defaultRedblue = async () => 'SKIPPED';
|
|
3
|
+
/** Default drift: no baseline → 0. */
|
|
4
|
+
const defaultDrift = async () => 0;
|
|
5
|
+
export async function runVerify(opts = {}) {
|
|
6
|
+
const driftThreshold = opts.driftThreshold ?? 0.05;
|
|
7
|
+
let redblue;
|
|
8
|
+
try {
|
|
9
|
+
redblue = await (opts.redblue ?? defaultRedblue)();
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
redblue = 'SKIPPED';
|
|
13
|
+
} // a throwing verifier is not a PASS
|
|
14
|
+
let drift;
|
|
15
|
+
try {
|
|
16
|
+
drift = await (opts.drift ?? defaultDrift)();
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
drift = -1;
|
|
20
|
+
}
|
|
21
|
+
const driftVerdict = drift < 0 ? 'skipped' : (drift <= driftThreshold ? 'ok' : 'regressed');
|
|
22
|
+
// Fail-closed: promotion requires a real PASS and a non-regressing, non-skipped drift.
|
|
23
|
+
const adversarialPass = redblue === 'PASS' && driftVerdict === 'ok';
|
|
24
|
+
return { redblue, drift, driftThreshold, driftVerdict, adversarialPass };
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=harness-verify.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type HarnessLoopOptions } from './harness-loop.js';
|
|
2
|
+
export declare const STAGED_CHAMPION_FILE = "harness-champion.manifest.json";
|
|
3
|
+
export type HarnessWorkerInput = HarnessLoopOptions<any>;
|
|
4
|
+
export type LoadHarnessInput = (projectRoot: string) => HarnessWorkerInput | null;
|
|
5
|
+
export interface HarnessWorkerResult {
|
|
6
|
+
ran: boolean;
|
|
7
|
+
reason: string;
|
|
8
|
+
accepted?: boolean;
|
|
9
|
+
staged?: boolean;
|
|
10
|
+
admitted?: number;
|
|
11
|
+
rejected?: number;
|
|
12
|
+
}
|
|
13
|
+
export declare function harnessLoopOptedIn(): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Execute one bounded, opt-in harness-loop tick for `projectRoot`. Injectable
|
|
16
|
+
* `loadInput`/`optInOverride` for tests. Best-effort; never throws.
|
|
17
|
+
*/
|
|
18
|
+
export declare function runHarnessLoopWorker(projectRoot: string, opts?: {
|
|
19
|
+
loadInput?: LoadHarnessInput;
|
|
20
|
+
maxTrajectories?: number;
|
|
21
|
+
optInOverride?: boolean;
|
|
22
|
+
}): Promise<HarnessWorkerResult>;
|
|
23
|
+
//# sourceMappingURL=harness-worker.d.ts.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness loop daemon worker (ADR-176 phase 8, part 2).
|
|
3
|
+
*
|
|
4
|
+
* Runs runHarnessLoop() on the daemon's schedule — but strictly bounded, given
|
|
5
|
+
* the runaway-resource posture:
|
|
6
|
+
* - OPT-IN: no-op unless RUFLO_HARNESS_LOOP is truthy. The self-optimizing
|
|
7
|
+
* loop never runs autonomously without an explicit opt-in.
|
|
8
|
+
* - $0: with no optimizer/verifier/canary wired (the default), runHarnessLoop
|
|
9
|
+
* qualifies + benchmarks but promotes nothing (fail-closed). No LLM, no
|
|
10
|
+
* metaharness subprocess, no spend unless the operator opts those in too.
|
|
11
|
+
* - bounded: trajectories capped per tick; the daemon adds single-flight
|
|
12
|
+
* (maxConcurrent) + a 16-min worker timeout + TTL/idle shutdown.
|
|
13
|
+
* - never throws: a worker must not crash the daemon.
|
|
14
|
+
*
|
|
15
|
+
* On acceptance it STAGES the (unsigned) champion manifest for the separate
|
|
16
|
+
* publish/sign step — the worker never signs (no key material in the daemon).
|
|
17
|
+
*/
|
|
18
|
+
import * as fs from 'fs';
|
|
19
|
+
import * as path from 'path';
|
|
20
|
+
import { runHarnessLoop } from './harness-loop.js';
|
|
21
|
+
export const STAGED_CHAMPION_FILE = 'harness-champion.manifest.json';
|
|
22
|
+
/** No built-in input source yet — the operator wires a corpus + optimizer. */
|
|
23
|
+
const defaultLoadInput = () => null;
|
|
24
|
+
export function harnessLoopOptedIn() {
|
|
25
|
+
return /^(1|true|yes|on)$/i.test(process.env.RUFLO_HARNESS_LOOP ?? '');
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Execute one bounded, opt-in harness-loop tick for `projectRoot`. Injectable
|
|
29
|
+
* `loadInput`/`optInOverride` for tests. Best-effort; never throws.
|
|
30
|
+
*/
|
|
31
|
+
export async function runHarnessLoopWorker(projectRoot, opts = {}) {
|
|
32
|
+
try {
|
|
33
|
+
const optedIn = opts.optInOverride ?? harnessLoopOptedIn();
|
|
34
|
+
if (!optedIn)
|
|
35
|
+
return { ran: false, reason: 'opt-in required (RUFLO_HARNESS_LOOP=1)' };
|
|
36
|
+
const input = (opts.loadInput ?? defaultLoadInput)(projectRoot);
|
|
37
|
+
if (!input)
|
|
38
|
+
return { ran: false, reason: 'no harness input (corpus/optimizer) configured' };
|
|
39
|
+
const cap = opts.maxTrajectories ?? 2000;
|
|
40
|
+
const bounded = { ...input, trajectories: input.trajectories.slice(0, cap) };
|
|
41
|
+
const result = await runHarnessLoop(bounded);
|
|
42
|
+
let staged = false;
|
|
43
|
+
if (result.accepted && result.manifest) {
|
|
44
|
+
// Stage the UNSIGNED champion for the separate publish/sign step.
|
|
45
|
+
try {
|
|
46
|
+
const dir = path.join(projectRoot, '.claude-flow');
|
|
47
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
48
|
+
fs.writeFileSync(path.join(dir, STAGED_CHAMPION_FILE), JSON.stringify(result.manifest, null, 2), 'utf-8');
|
|
49
|
+
staged = true;
|
|
50
|
+
}
|
|
51
|
+
catch { /* non-fatal */ }
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
ran: true,
|
|
55
|
+
reason: result.reason,
|
|
56
|
+
accepted: result.accepted,
|
|
57
|
+
staged,
|
|
58
|
+
admitted: result.admitted,
|
|
59
|
+
rejected: result.rejected,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
return { ran: false, reason: `error: ${e?.message ?? e}` };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=harness-worker.js.map
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { EventEmitter } from 'events';
|
|
14
14
|
import { HeadlessWorkerExecutor } from './headless-worker-executor.js';
|
|
15
|
-
export type WorkerType = 'ultralearn' | 'optimize' | 'consolidate' | 'predict' | 'audit' | 'map' | 'preload' | 'deepdive' | 'document' | 'refactor' | 'benchmark' | 'testgaps' | 'backup';
|
|
15
|
+
export type WorkerType = 'ultralearn' | 'optimize' | 'consolidate' | 'predict' | 'audit' | 'map' | 'preload' | 'deepdive' | 'document' | 'refactor' | 'benchmark' | 'testgaps' | 'backup' | 'harness';
|
|
16
16
|
interface WorkerConfig {
|
|
17
17
|
type: WorkerType;
|
|
18
18
|
intervalMs: number;
|
|
@@ -327,6 +327,13 @@ export declare class WorkerDaemon extends EventEmitter {
|
|
|
327
327
|
* RUFLO_BACKUP_GCS (a gs:// prefix), retention via RUFLO_BACKUP_KEEP.
|
|
328
328
|
*/
|
|
329
329
|
private runBackupWorker;
|
|
330
|
+
/**
|
|
331
|
+
* Self-optimizing harness loop worker (ADR-176 phase 8). Strictly bounded:
|
|
332
|
+
* OPT-IN (RUFLO_HARNESS_LOOP), $0-default (no optimizer/verifier wired => no
|
|
333
|
+
* promotion), trajectory-capped, single-flight via the daemon, never throws.
|
|
334
|
+
* On acceptance the (unsigned) champion is staged for the separate sign step.
|
|
335
|
+
*/
|
|
336
|
+
private runHarnessWorker;
|
|
330
337
|
/**
|
|
331
338
|
* Local testgaps worker (fallback when headless unavailable)
|
|
332
339
|
*/
|
|
@@ -30,6 +30,7 @@ const DEFAULT_WORKERS = [
|
|
|
30
30
|
{ type: 'consolidate', intervalMs: 30 * 60 * 1000, offsetMs: 6 * 60 * 1000, priority: 'low', description: 'Memory distillation (ADR-174)', enabled: true },
|
|
31
31
|
{ type: 'testgaps', intervalMs: 20 * 60 * 1000, offsetMs: 8 * 60 * 1000, priority: 'normal', description: 'Test coverage analysis', enabled: true },
|
|
32
32
|
{ type: 'backup', intervalMs: 24 * 60 * 60 * 1000, offsetMs: 10 * 60 * 1000, priority: 'low', description: 'Nightly memory DB backup (WAL-safe, rotated)', enabled: true },
|
|
33
|
+
{ type: 'harness', intervalMs: 6 * 60 * 60 * 1000, offsetMs: 12 * 60 * 1000, priority: 'low', description: 'Self-optimizing harness loop (opt-in RUFLO_HARNESS_LOOP, $0-default)', enabled: true },
|
|
33
34
|
{ type: 'predict', intervalMs: 10 * 60 * 1000, offsetMs: 0, priority: 'low', description: 'Predictive preloading', enabled: false },
|
|
34
35
|
{ type: 'document', intervalMs: 60 * 60 * 1000, offsetMs: 0, priority: 'low', description: 'Auto-documentation', enabled: false },
|
|
35
36
|
];
|
|
@@ -1162,6 +1163,8 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
1162
1163
|
return this.runConsolidateWorker();
|
|
1163
1164
|
case 'backup':
|
|
1164
1165
|
return this.runBackupWorker();
|
|
1166
|
+
case 'harness':
|
|
1167
|
+
return this.runHarnessWorker();
|
|
1165
1168
|
case 'testgaps':
|
|
1166
1169
|
return this.runTestGapsWorkerLocal();
|
|
1167
1170
|
case 'predict':
|
|
@@ -1437,6 +1440,45 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
1437
1440
|
writeFileSync(backupFile, JSON.stringify(result, null, 2));
|
|
1438
1441
|
return result;
|
|
1439
1442
|
}
|
|
1443
|
+
/**
|
|
1444
|
+
* Self-optimizing harness loop worker (ADR-176 phase 8). Strictly bounded:
|
|
1445
|
+
* OPT-IN (RUFLO_HARNESS_LOOP), $0-default (no optimizer/verifier wired => no
|
|
1446
|
+
* promotion), trajectory-capped, single-flight via the daemon, never throws.
|
|
1447
|
+
* On acceptance the (unsigned) champion is staged for the separate sign step.
|
|
1448
|
+
*/
|
|
1449
|
+
async runHarnessWorker() {
|
|
1450
|
+
const metricsDir = join(this.projectRoot, '.claude-flow', 'metrics');
|
|
1451
|
+
if (!existsSync(metricsDir))
|
|
1452
|
+
mkdirSync(metricsDir, { recursive: true });
|
|
1453
|
+
const harnessFile = join(metricsDir, 'harness-loop.json');
|
|
1454
|
+
let result;
|
|
1455
|
+
try {
|
|
1456
|
+
// ADR-176 flywheel (A-P3b autonomy loop): run ONE COMPOUNDING generation
|
|
1457
|
+
// on the install's REAL data — read the persisted champion as baseline,
|
|
1458
|
+
// gate a constrained candidate on the frozen self-supervised held-out with
|
|
1459
|
+
// the human-anchor guard + separate canary, and on a verified promotion
|
|
1460
|
+
// advance the champion so the NEXT tick compounds. Shadow-first (serve lags
|
|
1461
|
+
// one tick). Opt-in ($0 no-op without RUFLO_HARNESS_LOOP).
|
|
1462
|
+
const { runFlywheelGenerationWorker } = await import('./harness-flywheel-runtime.js');
|
|
1463
|
+
const gen = await runFlywheelGenerationWorker(this.projectRoot, { sample: 120 });
|
|
1464
|
+
let status = null;
|
|
1465
|
+
try {
|
|
1466
|
+
const { flywheelStatus } = await import('./harness-flywheel-generations.js');
|
|
1467
|
+
status = flywheelStatus(this.projectRoot);
|
|
1468
|
+
}
|
|
1469
|
+
catch { /* no lineage yet */ }
|
|
1470
|
+
if (gen.promoted)
|
|
1471
|
+
this.log('info', `Flywheel gen ${gen.generation}: PROMOTED (+${(gen.delta ?? 0).toFixed(4)} held-out, significant=${gen.significant}) — champion advanced`);
|
|
1472
|
+
result = { timestamp: new Date().toISOString(), flywheel: gen, lineage: status };
|
|
1473
|
+
}
|
|
1474
|
+
catch (error) {
|
|
1475
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1476
|
+
this.log('warn', `Harness worker failed: ${message}`);
|
|
1477
|
+
result = { timestamp: new Date().toISOString(), ran: false, error: message };
|
|
1478
|
+
}
|
|
1479
|
+
writeFileSync(harnessFile, JSON.stringify(result, null, 2));
|
|
1480
|
+
return result;
|
|
1481
|
+
}
|
|
1440
1482
|
/**
|
|
1441
1483
|
* Local testgaps worker (fallback when headless unavailable)
|
|
1442
1484
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-flow/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.24.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
|
|
6
6
|
"main": "dist/src/index.js",
|