@claude-flow/cli 3.34.0 → 3.36.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.
Files changed (36) hide show
  1. package/.claude/.proven-config-version +1 -0
  2. package/.claude/helpers/.helpers-version +1 -1
  3. package/.claude/helpers/helpers.manifest.json +2 -2
  4. package/.claude/helpers/statusline.cjs +0 -0
  5. package/.claude/proven-config.json +42 -0
  6. package/catalog-manifest.json +2 -2
  7. package/dist/src/commands/daemon.js +12 -7
  8. package/dist/src/commands/doctor.js +72 -1
  9. package/dist/src/commands/metaharness.js +37 -2
  10. package/dist/src/commands/neural.js +1 -1
  11. package/dist/src/log-filters.d.ts +3 -3
  12. package/dist/src/mcp-tools/metaharness-tools.js +35 -2
  13. package/dist/src/memory/memory-bridge.d.ts +25 -0
  14. package/dist/src/memory/memory-bridge.js +61 -8
  15. package/dist/src/memory/memory-initializer.d.ts +9 -3
  16. package/dist/src/memory/memory-initializer.js +344 -277
  17. package/dist/src/services/daemon-autostart.d.ts +31 -3
  18. package/dist/src/services/daemon-autostart.js +45 -3
  19. package/dist/src/services/distill-oracle.d.ts +1 -1
  20. package/dist/src/services/distill-oracle.js +2 -2
  21. package/dist/src/services/evolve-proof.d.ts +40 -1
  22. package/dist/src/services/evolve-proof.js +76 -14
  23. package/dist/src/services/flywheel-receipt.d.ts +15 -0
  24. package/dist/src/services/flywheel-receipt.js +22 -0
  25. package/dist/src/services/flywheel-sequential-evidence.d.ts +102 -0
  26. package/dist/src/services/flywheel-sequential-evidence.js +148 -0
  27. package/dist/src/services/flywheel-transaction.d.ts +71 -0
  28. package/dist/src/services/flywheel-transaction.js +121 -0
  29. package/dist/src/services/harness-flywheel-generations.d.ts +14 -0
  30. package/dist/src/services/harness-flywheel-generations.js +76 -4
  31. package/dist/src/services/harness-flywheel.d.ts +13 -0
  32. package/dist/src/services/harness-flywheel.js +31 -1
  33. package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
  34. package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
  35. package/package.json +6 -10
  36. package/plugins/ruflo-metaharness/scripts/smoke.sh +4 -2
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Sequential promotion evidence — family-wise error control for an ADAPTIVE
3
+ * candidate stream (report item 2 / upstream @metaharness/flywheel 0.1.10
4
+ * `withSequentialEvidence` interop, implemented in-house so ruflo keeps the
5
+ * property with every MetaHarness package removed).
6
+ *
7
+ * THE GAP THIS CLOSES: every promotion gate ruflo runs (accept/v1+sig, the
8
+ * ruflo.flywheel-gate/v1 bootstrap) spends a FRESH alpha per candidate. A
9
+ * flywheel proposes candidates adaptively — each new candidate is chosen after
10
+ * looking at the last one's scores — so per-candidate alpha does not bound the
11
+ * probability that ANY promotion in the stream is false. Published
12
+ * measurements of greedy accept-if-improved loops put the false-commit rate at
13
+ * 30-42% under exactly this regime.
14
+ *
15
+ * TWO COMPOSED MECHANISMS, both required by the promotion authority:
16
+ *
17
+ * 1. Anytime-valid e-process per candidate (testing-by-betting). Per paired
18
+ * task, a discordant pair multiplies the e-value by (1+lambda) when the
19
+ * candidate wins and (1-lambda) when the baseline wins; concordant pairs
20
+ * carry no information (McNemar). Under the null the e-value is a
21
+ * non-negative martingale with expectation 1, so by Ville's inequality
22
+ * P(e ever reaches 1/alpha) <= alpha — no penalty for peeking mid-stream.
23
+ *
24
+ * 2. Alpha allocation ACROSS candidates. Candidate k in the lineage's test
25
+ * stream must clear 1/alpha_k where alpha_k = alphaTotal * 6/(pi^2 * k^2),
26
+ * so sum(alpha_k) = alphaTotal for arbitrarily many adaptively-chosen
27
+ * candidates. The allocation index is persisted per receipt in the
28
+ * transaction state — looking spends alpha whether or not the candidate
29
+ * promotes, and retrying the same receipt reuses its index (no double
30
+ * spend, no index shopping).
31
+ *
32
+ * Family-wise guarantee: P(any false promotion, ever, in the stream)
33
+ * <= sum_k alpha_k = alphaTotal. The acceptance test for this module is the
34
+ * 1,000-null-simulation in flywheel-sequential-evidence.test.ts.
35
+ *
36
+ * Pure, $0, deterministic. Never throws on well-typed input.
37
+ */
38
+ export const SEQUENTIAL_EVIDENCE_VERSION = 'ruflo.sequential-evidence/v1';
39
+ export const DEFAULT_ALPHA_TOTAL = 0.05;
40
+ export const DEFAULT_LAMBDA = 0.5;
41
+ /** Score tie-band: |candidate - baseline| <= epsilon is a concordant (uninformative) pair. */
42
+ export const DEFAULT_SCORE_EPSILON = 1e-9;
43
+ /**
44
+ * Alpha share for the k-th test in the stream: alphaTotal * 6/(pi^2 k^2).
45
+ * Chosen over 2^-k because it decays polynomially — test 10 still gets a
46
+ * workable ~0.6% of a 5% budget instead of ~0.005%.
47
+ */
48
+ export function alphaForTest(testIndex, alphaTotal = DEFAULT_ALPHA_TOTAL) {
49
+ if (!Number.isInteger(testIndex) || testIndex < 1)
50
+ throw new RangeError('testIndex must be a positive integer');
51
+ if (!(alphaTotal > 0 && alphaTotal < 1))
52
+ throw new RangeError('alphaTotal must be in (0, 1)');
53
+ return (alphaTotal * 6) / (Math.PI * Math.PI * testIndex * testIndex);
54
+ }
55
+ /**
56
+ * Fold paired outcomes into an anytime-valid e-value and judge it against the
57
+ * k-th test's allocated alpha. Deterministic; order of outcomes does not
58
+ * change the final e-value (the product commutes).
59
+ */
60
+ export function sequentialEvidenceVerdict(outcomes, testIndex, config = {}) {
61
+ const alphaTotal = config.alphaTotal ?? DEFAULT_ALPHA_TOTAL;
62
+ const lambda = config.lambda ?? DEFAULT_LAMBDA;
63
+ const epsilon = config.epsilon ?? DEFAULT_SCORE_EPSILON;
64
+ if (!(lambda > 0 && lambda < 1))
65
+ throw new RangeError('lambda must be in (0, 1)');
66
+ const alphaAllocated = alphaForTest(testIndex, alphaTotal);
67
+ const threshold = 1 / alphaAllocated;
68
+ let eValue = 1;
69
+ let informativePairs = 0;
70
+ for (const o of outcomes) {
71
+ const delta = o.candidateScore - o.baselineScore;
72
+ if (Math.abs(delta) <= epsilon)
73
+ continue; // concordant: no information
74
+ informativePairs++;
75
+ // Under the null a discordant pair favors either arm with probability 1/2,
76
+ // so E[multiplier] = 1 and the running product is a martingale.
77
+ eValue *= delta > 0 ? 1 + lambda : 1 - lambda;
78
+ }
79
+ return {
80
+ significant: eValue >= threshold,
81
+ eValue,
82
+ threshold,
83
+ alphaAllocated,
84
+ testIndex,
85
+ informativePairs,
86
+ totalPairs: outcomes.length,
87
+ version: SEQUENTIAL_EVIDENCE_VERSION,
88
+ };
89
+ }
90
+ /**
91
+ * Minimum number of INFORMATIVE (discordant) pairs a candidate must win —
92
+ * with zero losses — to clear the k-th test's threshold: the smallest n with
93
+ * (1+lambda)^n >= 1/alpha_k. The pre-flight power check (ADR-381 §4): an
94
+ * evaluation whose promotion holdout is smaller than this cannot promote even
95
+ * on a perfect sweep, so it should be refused BEFORE compute is spent and
96
+ * before a doomed receipt can be presented to the gate (spending alpha).
97
+ */
98
+ export function minInformativePairsToClear(testIndex, config = {}) {
99
+ const alphaTotal = config.alphaTotal ?? DEFAULT_ALPHA_TOTAL;
100
+ const lambda = config.lambda ?? DEFAULT_LAMBDA;
101
+ if (!(lambda > 0 && lambda < 1))
102
+ throw new RangeError('lambda must be in (0, 1)');
103
+ const threshold = 1 / alphaForTest(testIndex, alphaTotal);
104
+ return Math.ceil(Math.log(threshold) / Math.log(1 + lambda));
105
+ }
106
+ /** Family-wise budget left after `testsRun` allocated tests: alphaTotal - Σ alpha_k. */
107
+ export function remainingAlphaBudget(testsRun, alphaTotal = DEFAULT_ALPHA_TOTAL) {
108
+ if (!Number.isInteger(testsRun) || testsRun < 0)
109
+ throw new RangeError('testsRun must be a non-negative integer');
110
+ let spent = 0;
111
+ for (let k = 1; k <= testsRun; k++)
112
+ spent += alphaForTest(k, alphaTotal);
113
+ return Math.max(0, alphaTotal - spent);
114
+ }
115
+ /**
116
+ * Structural consistency between a receipt's paired outcomes and its
117
+ * aggregate heldOutDeltas: same length and order, unique non-empty task IDs,
118
+ * and each delta must equal candidateScore - baselineScore. This is what makes
119
+ * paired outcomes EVIDENCE rather than decoration — an aggregate that cannot
120
+ * be reproduced from its own per-task rows is refused.
121
+ */
122
+ export function checkPairedOutcomesConsistency(pairedOutcomes, heldOutDeltas, tolerance = 1e-9) {
123
+ const reasons = [];
124
+ if (pairedOutcomes.length === 0)
125
+ reasons.push('pairedOutcomes is empty');
126
+ if (pairedOutcomes.length !== heldOutDeltas.length) {
127
+ reasons.push(`pairedOutcomes length ${pairedOutcomes.length} != heldOutDeltas length ${heldOutDeltas.length}`);
128
+ }
129
+ const ids = new Set();
130
+ for (const [i, o] of pairedOutcomes.entries()) {
131
+ if (!o.taskId || typeof o.taskId !== 'string')
132
+ reasons.push(`pairedOutcomes[${i}] has an empty taskId`);
133
+ else if (ids.has(o.taskId))
134
+ reasons.push(`duplicate taskId '${o.taskId}'`);
135
+ else
136
+ ids.add(o.taskId);
137
+ if (!Number.isFinite(o.baselineScore) || !Number.isFinite(o.candidateScore)) {
138
+ reasons.push(`pairedOutcomes[${i}] has a non-finite score`);
139
+ continue;
140
+ }
141
+ const delta = heldOutDeltas[i];
142
+ if (delta !== undefined && Math.abs((o.candidateScore - o.baselineScore) - delta) > tolerance) {
143
+ reasons.push(`pairedOutcomes[${i}] delta ${(o.candidateScore - o.baselineScore).toFixed(12)} != heldOutDeltas[${i}] ${delta}`);
144
+ }
145
+ }
146
+ return { ok: reasons.length === 0, reasons };
147
+ }
148
+ //# sourceMappingURL=flywheel-sequential-evidence.js.map
@@ -35,6 +35,51 @@ export interface FlywheelTransactionState {
35
35
  servedChampionRef: string | null;
36
36
  receiptStates: Record<string, ReceiptState>;
37
37
  commits: PromotionCommit[];
38
+ /**
39
+ * Sequential-evidence alpha ledger (ADR-381): receiptId → 1-based test
40
+ * index in this ledger's promotion-test stream for the CURRENT evidence
41
+ * epoch. The stream is scoped to the transaction state itself (one ledger,
42
+ * one champion chain, one stream) — NOT to receipt lineageIds, which
43
+ * default to a fresh UUID per run and would void the control. An index is
44
+ * allocated the first time a receipt is presented to the promotion gate
45
+ * and persists whether or not the candidate promoted — looking spends
46
+ * alpha, and retrying the same receipt reuses its index (no double spend,
47
+ * no index shopping). Absent in pre-upgrade state files; readers treat
48
+ * missing as empty.
49
+ */
50
+ sequentialTests?: Record<string, number>;
51
+ /** Current evidence epoch (ADR-381 §2). Incremented only by an explicit governed reset. */
52
+ evidenceEpoch?: number;
53
+ /**
54
+ * Wall-clock boundary (ms) of the current evidence epoch — the `now` an
55
+ * explicit reset ran at. A receipt whose OWN evidence (payload.issuedAt)
56
+ * predates this boundary belongs to a prior epoch even if it happens to be
57
+ * registered/promoted after the reset — the exact "index shopping" ADR-381
58
+ * §2 requires the epoch mechanism to prevent. Undefined for the genesis
59
+ * epoch (no lower bound) and for state files written before this field
60
+ * existed.
61
+ */
62
+ evidenceEpochStartedAt?: number;
63
+ /** Append-only audit trail of evidence resets. Nothing is ever deleted from it. */
64
+ sequentialResets?: SequentialResetRecord[];
65
+ }
66
+ export interface SequentialResetRecord {
67
+ /** The epoch that was CLOSED by this reset. */
68
+ epoch: number;
69
+ at: number;
70
+ reason: string;
71
+ /** Alpha spend archived from the closed epoch (receiptId → test index). */
72
+ testsSpent: Record<string, number>;
73
+ /** Outstanding 'evaluated' receipts expired by the reset (fresh-data enforcement). */
74
+ expiredReceipts: string[];
75
+ }
76
+ export interface EvidenceResetResult {
77
+ success: boolean;
78
+ reason: string;
79
+ closedEpoch?: number;
80
+ newEpoch?: number;
81
+ testsArchived?: number;
82
+ receiptsExpired?: number;
38
83
  }
39
84
  export interface PromotionResult {
40
85
  success: boolean;
@@ -55,11 +100,37 @@ export interface PromoteOptions {
55
100
  approvedAttestors?: Set<string>;
56
101
  allowedProposerSubstitutions?: Set<string>;
57
102
  applyFn?: (root: string, policy: Record<string, unknown>, championRef: string, previous: string | null, now: number) => ApplyResult;
103
+ /**
104
+ * Strict promotion evidence (default true): the receipt must carry
105
+ * task-level pairedOutcomes AND clear the sequential-evidence e-process at
106
+ * this test's allocated share of the family-wise alpha budget. Set false
107
+ * ONLY as an explicit migration escape hatch for pre-upgrade receipts —
108
+ * aggregate-only evidence is otherwise refused, never silently accepted.
109
+ */
110
+ requirePairedEvidence?: boolean;
111
+ /** Family-wise type-I budget across the whole candidate stream (default 0.05). */
112
+ sequentialAlphaTotal?: number;
113
+ /** e-process betting fraction in (0,1) (default 0.5). */
114
+ sequentialLambda?: number;
58
115
  /** Test-only crash hook; production callers leave unset. */
59
116
  faultAt?: 'before-commit' | 'after-commit-before-materialize';
60
117
  }
61
118
  export declare function readFlywheelTransactionState(root: string): FlywheelTransactionState;
62
119
  export declare function readFlywheelReceipt(root: string, receiptId: string): FlywheelEvaluationReceipt | null;
120
+ /**
121
+ * Start a new evidence epoch (ADR-381 §2): archive the current alpha spend
122
+ * into the append-only reset audit trail, EXPIRE every outstanding
123
+ * 'evaluated' receipt (the new epoch may only promote evidence produced
124
+ * after the reset — structurally enforcing fresh data), clear the spend
125
+ * ledger, and increment the epoch. Requires explicit confirmation and a
126
+ * non-empty human reason; the CLI/MCP surfaces additionally gate this
127
+ * through the same policy engine as promotion.
128
+ */
129
+ export declare function resetSequentialEvidence(root: string, options: {
130
+ confirm: boolean;
131
+ reason: string;
132
+ now?: number;
133
+ }): Promise<EvidenceResetResult>;
63
134
  export declare function registerFlywheelReceipt(root: string, receipt: FlywheelEvaluationReceipt, now?: number): Promise<ReceiptState>;
64
135
  export declare function recoverFlywheelMaterialization(root: string, opts?: Pick<PromoteOptions, 'now' | 'applyFn'>): Promise<PromotionResult>;
65
136
  export declare function promoteFlywheelCandidate(root: string, receiptId: string, options: PromoteOptions): Promise<PromotionResult>;
@@ -10,6 +10,7 @@ import * as fs from 'node:fs';
10
10
  import * as path from 'node:path';
11
11
  import { applyChampionParams } from '../config/harness-feedback-applier.js';
12
12
  import { GENESIS_LEDGER_HEAD, canonicalizeJcs, sha256Ref, uuidV7, verifyFlywheelReceipt, } from './flywheel-receipt.js';
13
+ import { DEFAULT_ALPHA_TOTAL, DEFAULT_LAMBDA, minInformativePairsToClear, sequentialEvidenceVerdict, } from './flywheel-sequential-evidence.js';
13
14
  const STATE_VERSION = 1;
14
15
  const STATE_DIR = ['.claude-flow', 'flywheel-v1'];
15
16
  const STATE_FILE = 'transaction-state.json';
@@ -147,6 +148,50 @@ export function readFlywheelReceipt(root, receiptId) {
147
148
  return null;
148
149
  }
149
150
  }
151
+ /**
152
+ * Start a new evidence epoch (ADR-381 §2): archive the current alpha spend
153
+ * into the append-only reset audit trail, EXPIRE every outstanding
154
+ * 'evaluated' receipt (the new epoch may only promote evidence produced
155
+ * after the reset — structurally enforcing fresh data), clear the spend
156
+ * ledger, and increment the epoch. Requires explicit confirmation and a
157
+ * non-empty human reason; the CLI/MCP surfaces additionally gate this
158
+ * through the same policy engine as promotion.
159
+ */
160
+ export async function resetSequentialEvidence(root, options) {
161
+ if (!options.confirm)
162
+ return { success: false, reason: 'explicit confirmation required' };
163
+ const reason = (options.reason ?? '').trim();
164
+ if (!reason)
165
+ return { success: false, reason: 'a non-empty reason is required — the reset audit trail records intent' };
166
+ const now = options.now ?? Date.now();
167
+ return withStateLock(root, () => {
168
+ const state = readFlywheelTransactionState(root);
169
+ const closedEpoch = state.evidenceEpoch ?? 0;
170
+ const testsSpent = { ...(state.sequentialTests ?? {}) };
171
+ const expiredReceipts = [];
172
+ for (const record of Object.values(state.receiptStates)) {
173
+ if (record.status === 'evaluated') {
174
+ record.status = 'expired';
175
+ expiredReceipts.push(record.receiptId);
176
+ }
177
+ }
178
+ const resets = state.sequentialResets ?? [];
179
+ resets.push({ epoch: closedEpoch, at: now, reason, testsSpent, expiredReceipts: [...expiredReceipts].sort() });
180
+ state.sequentialResets = resets;
181
+ state.sequentialTests = {};
182
+ state.evidenceEpoch = closedEpoch + 1;
183
+ state.evidenceEpochStartedAt = now;
184
+ atomicWriteJson(statePath(root), state);
185
+ return {
186
+ success: true,
187
+ reason: `evidence epoch ${closedEpoch} closed — ${Object.keys(testsSpent).length} test(s) archived, ${expiredReceipts.length} outstanding receipt(s) expired`,
188
+ closedEpoch,
189
+ newEpoch: state.evidenceEpoch,
190
+ testsArchived: Object.keys(testsSpent).length,
191
+ receiptsExpired: expiredReceipts.length,
192
+ };
193
+ });
194
+ }
150
195
  export async function registerFlywheelReceipt(root, receipt, now = Date.now()) {
151
196
  ensureDir(root);
152
197
  validateReceiptId(receipt.payload.receiptId);
@@ -287,6 +332,82 @@ export async function promoteFlywheelCandidate(root, receiptId, options) {
287
332
  && (!evidence.attestor || !options.approvedAttestors?.has(evidence.attestor)))
288
333
  return { success: false, idempotent: false, reason: `unapproved evidence attestor for gate: ${term}` };
289
334
  }
335
+ // Strict sequential evidence (default ON). Two refusals, both explicit:
336
+ // 1. aggregate-only receipts (no pairedOutcomes) — the upstream-style
337
+ // "degrade to the base gate" fallback is exactly the hole this closes;
338
+ // 2. paired evidence that cannot clear the e-process at this test's
339
+ // allocated share of the family-wise alpha budget.
340
+ // Alpha is spent by LOOKING: the allocation is persisted even when the
341
+ // verdict refuses, and a retried receipt reuses its index.
342
+ if (options.requirePairedEvidence !== false) {
343
+ const rawOutcomes = receipt.payload.pairedOutcomes;
344
+ if (!rawOutcomes || rawOutcomes.length === 0) {
345
+ return {
346
+ success: false,
347
+ idempotent: false,
348
+ reason: 'receipt carries aggregate-only evidence (no task-level pairedOutcomes) — re-evaluate the candidate with a current ruflo, or pass the explicit aggregate-evidence override',
349
+ };
350
+ }
351
+ // Epoch-boundary enforcement (ADR-381 §2): a receipt whose OWN evidence
352
+ // predates the current epoch's start must be refused even if it was
353
+ // registered after the reset — registration time is not evidence time.
354
+ // Without this check a receipt built from a stale/cached evaluation (or
355
+ // any future path that decouples evaluation from immediate
356
+ // registration) could be presented into the fresh, low-alpha epoch —
357
+ // exactly the index-shopping the reset mechanism exists to prevent.
358
+ if (state.evidenceEpochStartedAt !== undefined) {
359
+ const issuedAtMs = Date.parse(receipt.payload.issuedAt);
360
+ if (!Number.isNaN(issuedAtMs) && issuedAtMs < state.evidenceEpochStartedAt) {
361
+ return {
362
+ success: false,
363
+ idempotent: false,
364
+ reason: `receipt evidence (issued ${receipt.payload.issuedAt}) predates the current evidence epoch ${state.evidenceEpoch ?? 0} (started ${new Date(state.evidenceEpochStartedAt).toISOString()}) — re-evaluate the candidate to produce fresh evidence`,
365
+ };
366
+ }
367
+ }
368
+ // Consistency with heldOutDeltas was already enforced by verifyFlywheelReceipt.
369
+ const outcomes = rawOutcomes.map((o) => ({
370
+ taskId: o.taskId,
371
+ baselineScore: Number(o.baselineScore),
372
+ candidateScore: Number(o.candidateScore),
373
+ }));
374
+ if (!state.sequentialTests)
375
+ state.sequentialTests = {};
376
+ const priorIndex = state.sequentialTests[receiptId];
377
+ const testIndex = priorIndex ?? (Object.keys(state.sequentialTests).length + 1);
378
+ // Size pre-flight (ADR-381 §4): refuse BEFORE allocating an alpha index
379
+ // when the receipt cannot clear this test's threshold even on a perfect
380
+ // all-win sweep. Sample size is ancillary — independent of the outcomes
381
+ // — so this refusal looks at no evidence and spends no alpha.
382
+ if (priorIndex === undefined) {
383
+ const minPairs = minInformativePairsToClear(testIndex, {
384
+ alphaTotal: options.sequentialAlphaTotal ?? DEFAULT_ALPHA_TOTAL,
385
+ lambda: options.sequentialLambda ?? DEFAULT_LAMBDA,
386
+ });
387
+ if (outcomes.length < minPairs) {
388
+ return {
389
+ success: false,
390
+ idempotent: false,
391
+ reason: `receipt cannot clear sequential evidence at test ${testIndex}: ${outcomes.length} paired task(s) < ${minPairs} required even on a perfect sweep — no alpha spent; re-evaluate with a larger promotion holdout (ADR-381)`,
392
+ };
393
+ }
394
+ }
395
+ const verdict = sequentialEvidenceVerdict(outcomes, testIndex, {
396
+ alphaTotal: options.sequentialAlphaTotal ?? DEFAULT_ALPHA_TOTAL,
397
+ lambda: options.sequentialLambda ?? DEFAULT_LAMBDA,
398
+ });
399
+ if (priorIndex === undefined)
400
+ state.sequentialTests[receiptId] = testIndex;
401
+ if (!verdict.significant) {
402
+ if (priorIndex === undefined)
403
+ atomicWriteJson(statePath(root), state); // record the alpha spend
404
+ return {
405
+ success: false,
406
+ idempotent: false,
407
+ reason: `insufficient sequential evidence: e-value ${verdict.eValue.toFixed(3)} < ${verdict.threshold.toFixed(1)} at test ${verdict.testIndex} (alpha ${verdict.alphaAllocated.toFixed(5)} of family-wise ${(options.sequentialAlphaTotal ?? DEFAULT_ALPHA_TOTAL).toFixed(2)}; ${verdict.informativePairs}/${verdict.totalPairs} informative pairs)`,
408
+ };
409
+ }
410
+ }
290
411
  if (options.faultAt === 'before-commit')
291
412
  throw new Error('fault injection: before-commit');
292
413
  const transactionId = uuidV7(now);
@@ -109,6 +109,20 @@ export interface FlywheelStatus {
109
109
  config: Record<string, number>;
110
110
  hash: string | null;
111
111
  };
112
+ /**
113
+ * ADR-381 §3 — visibility into the sequential alpha stream so budget
114
+ * exhaustion reads as a plateau signal, not a mystery: the next test's
115
+ * index, its allocated alpha and e-value threshold, the minimum all-win
116
+ * informative pairs needed to clear it, and the family-wise budget left.
117
+ */
118
+ sequential: {
119
+ nextTestIndex: number;
120
+ alphaAllocatedNext: number;
121
+ thresholdNext: number;
122
+ minInformativePairsNext: number;
123
+ remainingAlphaBudget: number;
124
+ alphaTotal: number;
125
+ };
112
126
  }
113
127
  /** Reconstruct the persisted lineage + telemetry for a status endpoint / CLI. */
114
128
  export declare function flywheelStatus(root: string): FlywheelStatus;
@@ -21,6 +21,7 @@
21
21
  import * as fs from 'fs';
22
22
  import * as path from 'path';
23
23
  import { runRealEvolveRound, reconstructLineage, detectPlateau, mutationEffectiveness, } from './evolve-proof.js';
24
+ import { DEFAULT_ALPHA_TOTAL, alphaForTest, minInformativePairsToClear, remainingAlphaBudget, } from './flywheel-sequential-evidence.js';
24
25
  import { harvestSelfSupervisedTasks } from './harness-corpus-harvester.js';
25
26
  import { applyChampionParams, rollbackActivePolicy } from '../config/harness-feedback-applier.js';
26
27
  import { DEFAULT_CONFIG } from './harness-flywheel.js';
@@ -28,8 +29,12 @@ export const FLYWHEEL_DIR = ['.claude-flow', 'flywheel'];
28
29
  export const FROZEN_CORPUS = 'harvested-selfsup-frozen-v1';
29
30
  const SERVED_FILE = 'served.json';
30
31
  const ATTEMPTS_FILE = 'attempts.jsonl';
32
+ const ATTEMPTS_LOCK_FILE = 'attempts.lock';
31
33
  const ANCHOR_TOL = 0.02;
32
34
  const CANARY_CATASTROPHE = 0.5;
35
+ const ATTEMPTS_LOCK_TIMEOUT_MS = 10_000;
36
+ const ATTEMPTS_LOCK_STALE_MS = 60_000;
37
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
33
38
  // ── Lineage store ─────────────────────────────────────────────────────────────
34
39
  function dir(root) { return path.join(root, ...FLYWHEEL_DIR); }
35
40
  function readJson(p) { try {
@@ -74,6 +79,51 @@ function appendPromotion(root, b) {
74
79
  }
75
80
  catch { /* */ }
76
81
  }
82
+ /**
83
+ * Serialize the read-testIndex → build-bundle → append critical section
84
+ * (ADR-381 §3): attempts.jsonl's length IS the sequential-evidence test
85
+ * stream position, so two concurrent runFlywheelGeneration calls reading it
86
+ * before either appends would be assigned the SAME test index and spend the
87
+ * SAME alpha_k twice — silently doubling the true false-promotion rate the
88
+ * e-process math is meant to bound. Mirrors flywheel-transaction.ts's
89
+ * withStateLock (O_EXCL lock file, stale-lock takeover, bounded retry).
90
+ */
91
+ async function withAttemptsLock(root, fn) {
92
+ fs.mkdirSync(dir(root), { recursive: true });
93
+ const lock = path.join(dir(root), ATTEMPTS_LOCK_FILE);
94
+ const deadline = Date.now() + ATTEMPTS_LOCK_TIMEOUT_MS;
95
+ for (;;) {
96
+ try {
97
+ const fd = fs.openSync(lock, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, 0o600);
98
+ fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, at: Date.now() }), 'utf-8');
99
+ fs.closeSync(fd);
100
+ try {
101
+ return fn();
102
+ }
103
+ finally {
104
+ try {
105
+ fs.unlinkSync(lock);
106
+ }
107
+ catch { /* lock already gone */ }
108
+ }
109
+ }
110
+ catch (error) {
111
+ if (error.code !== 'EEXIST')
112
+ throw error;
113
+ try {
114
+ const stat = fs.lstatSync(lock);
115
+ if (Date.now() - stat.mtimeMs > ATTEMPTS_LOCK_STALE_MS) {
116
+ fs.unlinkSync(lock);
117
+ continue;
118
+ }
119
+ }
120
+ catch { /* raced with owner */ }
121
+ if (Date.now() >= deadline)
122
+ throw new Error('timed out acquiring flywheel attempts lock');
123
+ await delay(5);
124
+ }
125
+ }
126
+ }
77
127
  /** The current operating champion (last promotion's config), or defaults. */
78
128
  export function currentChampion(root) {
79
129
  const p = loadPromotions(root);
@@ -270,10 +320,20 @@ export async function runFlywheelGeneration(root, deps) {
270
320
  // per-generation HUMAN-RELEVANCE delta on the frozen eval set (anti-overfitting
271
321
  // visibility): if this stays ~0 while benchmark Δ > 0, the loop is overfitting.
272
322
  const humanRelevanceDelta = candAnchor - baseAnchor;
273
- const bundle = runRealEvolveRound({ baseline: baseline, candidate: cand, holdout, generation, parent, branch: 'main', now: deps.now, redblue, canaryRollbackRate, humanRelevanceDelta, humanEvalHash: deps.humanEvalHash, corpus: FROZEN_CORPUS });
274
- appendAttempt(root, bundle);
275
- if (bundle.decisionReceipt.promoted)
276
- appendPromotion(root, bundle);
323
+ // ADR-381 §3 every generation is one look at the same adaptive stream;
324
+ // attempts.jsonl IS the stream record, so this attempt's 1-based position
325
+ // is its sequential test index. Promoted or not, the next tick's index
326
+ // moves on (this bundle is appended below). Read-index, build, and append
327
+ // happen inside withAttemptsLock so two concurrent generations can never
328
+ // be assigned — and spend alpha on — the same test index.
329
+ const bundle = await withAttemptsLock(root, () => {
330
+ const testIndex = loadAttempts(root).length + 1;
331
+ const b = runRealEvolveRound({ baseline: baseline, candidate: cand, holdout, generation, parent, branch: 'main', now: deps.now, redblue, canaryRollbackRate, humanRelevanceDelta, humanEvalHash: deps.humanEvalHash, corpus: FROZEN_CORPUS, sequential: { testIndex } });
332
+ appendAttempt(root, b);
333
+ if (b.decisionReceipt.promoted)
334
+ appendPromotion(root, b);
335
+ return b;
336
+ });
277
337
  return {
278
338
  ran: true, reason: bundle.decisionReceipt.reason, generation, promoted: bundle.decisionReceipt.promoted,
279
339
  delta: bundle.deltas.benchmark, significant: bundle.decisionReceipt.significant,
@@ -355,6 +415,18 @@ export function flywheelStatus(root) {
355
415
  humanEvalHash: promotions.length ? (promotions[promotions.length - 1].humanEvalHash ?? null) : null,
356
416
  served: servedChampion(root),
357
417
  champion: { config: champ.config, hash: champ.hash },
418
+ sequential: (() => {
419
+ const nextTestIndex = attempts.length + 1;
420
+ const alphaAllocatedNext = alphaForTest(nextTestIndex);
421
+ return {
422
+ nextTestIndex,
423
+ alphaAllocatedNext,
424
+ thresholdNext: 1 / alphaAllocatedNext,
425
+ minInformativePairsNext: minInformativePairsToClear(nextTestIndex),
426
+ remainingAlphaBudget: remainingAlphaBudget(attempts.length),
427
+ alphaTotal: DEFAULT_ALPHA_TOTAL,
428
+ };
429
+ })(),
358
430
  };
359
431
  }
360
432
  //# sourceMappingURL=harness-flywheel-generations.js.map
@@ -60,6 +60,19 @@ export interface FlywheelResult {
60
60
  receipt?: FlywheelEvaluationReceipt;
61
61
  promotable?: boolean;
62
62
  legacyDeprecation?: boolean;
63
+ /**
64
+ * ADR-381 §4 soft pre-flight: whether this evaluation's promotion holdout
65
+ * can clear the sequential-evidence threshold at the ledger's next test
66
+ * index even on a perfect all-win sweep. viable:false does NOT block the
67
+ * evaluation (learn value stands; the gate refuses size-inviable receipts
68
+ * without alpha spend) — it tells the operator promotion is out of reach.
69
+ */
70
+ sequentialPreflight?: {
71
+ viable: boolean;
72
+ heldSize: number;
73
+ minPairsRequired: number;
74
+ nextTestIndex: number;
75
+ };
63
76
  }
64
77
  export declare function retrievalPolicyNeighbors(base: RetrievalConfig): RetrievalConfig[];
65
78
  /**
@@ -28,6 +28,7 @@ import { applyChampionParams } from '../config/harness-feedback-applier.js';
28
28
  import { appendLedger, bootstrapDeltaCILow } from './harness-improvement-ledger.js';
29
29
  import { createFlywheelReceipt, sha256Ref, } from './flywheel-receipt.js';
30
30
  import { readFlywheelTransactionState, registerFlywheelReceipt, } from './flywheel-transaction.js';
31
+ import { minInformativePairsToClear } from './flywheel-sequential-evidence.js';
31
32
  import { runBoundedPool } from './bounded-worker-pool.js';
32
33
  export const DEFAULT_CONFIG = { alpha: 0.5, subjectWeight: 2.0, mmrLambda: 0.7, bodyWeight: 1.0, typePenaltyFactor: 1.0 };
33
34
  const EPS = 1e-3;
@@ -193,6 +194,13 @@ export async function evaluateFlywheelCandidate(projectRoot, deps) {
193
194
  // resampling, not ride on one lucky task. FINAL accept = loop-accept AND
194
195
  // significant, so the ledger's accepted subsequence stays monotonic + real.
195
196
  const heldDeltas = held.map((t) => heldScoreFor(candidate, t) - heldScoreFor(baseline, t));
197
+ // Task-level paired outcomes — the evidence the promotion authority now
198
+ // requires; must stay in the exact order of heldDeltas.
199
+ const pairedOutcomes = held.map((t) => ({
200
+ taskId: t.id,
201
+ baselineScore: heldScoreFor(baseline, t),
202
+ candidateScore: heldScoreFor(candidate, t),
203
+ }));
196
204
  const deltaCILow = bootstrapDeltaCILow(heldDeltas);
197
205
  const significant = deltaCILow > 0;
198
206
  const provisionalGates = Object.fromEntries(Object.entries(result.verdict?.terms ?? {}).map(([k, v]) => [k, v.pass]));
@@ -219,6 +227,7 @@ export async function evaluateFlywheelCandidate(projectRoot, deps) {
219
227
  baselineScore,
220
228
  candidateScore,
221
229
  heldOutDeltas: heldDeltas,
230
+ pairedOutcomes,
222
231
  frozenAnchorRegression: guardRegressed ? 1 : 0,
223
232
  gates: provisionalGates,
224
233
  resourceEvidence: {
@@ -266,6 +275,25 @@ export async function evaluateFlywheelCandidate(projectRoot, deps) {
266
275
  });
267
276
  await registerFlywheelReceipt(projectRoot, receipt, deps.now ?? Date.now());
268
277
  const finalAccept = result.accepted && significant && receipt.payload.decision === 'accepted';
278
+ // Soft pre-flight (ADR-381 §4): can the held half of the objective clear
279
+ // the sequential-evidence threshold at the ledger's NEXT test index even
280
+ // on a perfect all-win sweep? Annotate — never block: evaluation has
281
+ // learn value regardless, anchors may legitimately be small (≥4), and
282
+ // the promote gate refuses size-inviable receipts without spending alpha.
283
+ // Read as LATE as possible (after registration, right before returning)
284
+ // to minimize — but not eliminate — the window in which a concurrent
285
+ // promoteFlywheelCandidate call can move the ledger's next test index.
286
+ // This is inherently ADVISORY, not authoritative: promoteFlywheelCandidate
287
+ // (under its own lock) is the sole authority for index allocation, so a
288
+ // promotion racing between this read and an operator's subsequent
289
+ // `flywheel promote` call can still flip the outcome. Callers must not
290
+ // treat `promotable: true` as a guarantee — only as "was viable as of
291
+ // this evaluation".
292
+ const postState = readFlywheelTransactionState(projectRoot);
293
+ const nextTestIndex = Object.keys(postState.sequentialTests ?? {}).length + 1;
294
+ const heldSize = Math.max(1, Math.round(objective.length * 0.5)); // held half per split(objective, 0.5)
295
+ const minPairsRequired = minInformativePairsToClear(nextTestIndex);
296
+ const sequentialPreflight = { viable: heldSize >= minPairsRequired, heldSize, minPairsRequired, nextTestIndex };
269
297
  const entry = {
270
298
  ts: deps.now ?? Date.now(),
271
299
  corpusVersion: blended.version, corpusHash: blended.corpusHash,
@@ -283,7 +311,9 @@ export async function evaluateFlywheelCandidate(projectRoot, deps) {
283
311
  baselineScore, candidateScore, delta: candidateScore - baselineScore,
284
312
  anchorRegressed, championRef: finalAccept ? refOf(candidate) : undefined,
285
313
  corpusVersion: blended.version, candidateConfig: candidate,
286
- receiptId: receipt.payload.receiptId, receipt, promotable: finalAccept && !!receipt.signature,
314
+ receiptId: receipt.payload.receiptId, receipt,
315
+ promotable: finalAccept && !!receipt.signature && sequentialPreflight.viable,
316
+ sequentialPreflight,
287
317
  };
288
318
  }
289
319
  catch (e) {
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-flow/cli",
3
- "version": "3.34.0",
3
+ "version": "3.36.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",
@@ -123,24 +123,20 @@
123
123
  "@agntcy/slim-bindings": "2.0.0-alpha.5",
124
124
  "@napi-rs/keyring": "1.3.0",
125
125
  "@claude-flow/memory": "^3.0.0-alpha.21",
126
+ "@metaharness/darwin": "~0.9.0",
127
+ "@metaharness/flywheel": "~0.1.10",
128
+ "@metaharness/radio": "~0.1.0",
129
+ "@metaharness/turn-credit": "~0.1.0",
126
130
  "agentdb": "^3.0.0-alpha.17",
127
131
  "agentic-flow": "^3.0.0-alpha.1",
128
132
  "better-sqlite3": "^12.9.0",
129
133
  "ruvector": "^0.2.27"
130
134
  },
131
135
  "peerDependencies": {
132
- "@metaharness/darwin": "^0.8.0",
133
- "@metaharness/flywheel": "^0.1.7",
134
- "@metaharness/router": "^0.3.2",
136
+ "@metaharness/router": "^0.4.0",
135
137
  "metaharness": "^0.4.1"
136
138
  },
137
139
  "peerDependenciesMeta": {
138
- "@metaharness/darwin": {
139
- "optional": true
140
- },
141
- "@metaharness/flywheel": {
142
- "optional": true
143
- },
144
140
  "@metaharness/router": {
145
141
  "optional": true
146
142
  },