Skip to content

dream(flywheel-promotion): #287 wire withSequentialEvidence into run.ts + replay.ts (evaluated) - #288

Draft
ruvnet wants to merge 2 commits into
mainfrom
dream/2026-09-06-flywheel-promotion
Draft

dream(flywheel-promotion): #287 wire withSequentialEvidence into run.ts + replay.ts (evaluated)#288
ruvnet wants to merge 2 commits into
mainfrom
dream/2026-09-06-flywheel-promotion

Conversation

@ruvnet

@ruvnet ruvnet commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Closes/tracks #287.

Hypothesis

Given a FlywheelConfig whose Evaluator reports per-item itemWins for both baseline and candidate on the
same holdout, when promotionRule: withSequentialEvidence(meetsPromotionRule) is supplied to
runFlywheelGenerations, then a thin-margin win (few discordant items, e-value below the 1/alpha
threshold) should be REJECTED even though the frozen base gate alone would promote it, and a strong-margin
win should be PROMOTED identically to the base gate β€” subject to: zero behavioral change for any Evaluator
that omits itemWins; meetsPromotionRule itself untouched; the same guarantee holding under
verifyReplayBundle's independent replay, not just the live run. Frozen before implementation.

Candidate

packages/flywheel/src/sequential.ts's withSequentialEvidence() β€” an anytime-valid e-process gate
against repeated-peeking false-commits β€” was fully built, exported, and unit-tested, but had zero
production call site
: runFlywheelGenerations never populated PromotionEvidence.pairedOutcomes (the
type didn't even have the field), so wiring it into a real FlywheelConfig always silently degraded to the
plain frozen gate. Same bug class as ADR-278 (Darwin Mode's Tier-2 sandbox never calling its own safety
gate), different domain.

Changes (4 source files, ~50 net lines + 1 new test file):

  • types.ts: Score.itemWins?: boolean[] (optional, additive) + PromotionEvidence.pairedOutcomes?: PairedOutcome[] (optional, additive; PairedOutcome relocated here from sequential.ts, re-exported
    unchanged for API compatibility).
  • sequential.ts: new shared helper pairedOutcomesFromItemWins(baseline, candidate).
  • run.ts: the live promotion loop now builds pairedOutcomes from the evaluated baseline/candidate
    Scores and includes it on every gate call.
  • replay.ts: an independent adversarial critic (fresh subagent, not self-review) found the SAME gap at a
    sibling call site β€” verifyReplayBundle's ADR-235 gate re-execution never reconstructed
    pairedOutcomes either, so replaying a sequential-gated promotion silently re-verified only the wrapped
    base rule. Closed in this diff rather than deferred (same bug class, same package, ~10 line fix with its
    own regression test).

meetsPromotionRule (the frozen default gate) is byte-for-byte untouched.

Evaluation Receipt

8 new tests (packages/flywheel/__tests__/sequential-wiring.test.ts), each confirmed non-vacuous by
independently reverting the corresponding sub-fix (git stash on run.ts alone, then on replay.ts
alone) and re-running:

  • THIN evidence (3/20 discordant items, e=1.5Β³=3.375<20): the frozen gate alone promotes through a REAL
    runFlywheelGenerations call (1 promotion, sanity-checked); the sequential-wrapped rule does not (0).
    Pre-fix (run.ts reverted): both promote β€” test fails exactly as predicted (expected 0, got 1).
  • STRONG evidence (20/20 discordant, e=1.5²⁰≫20): both promote.
  • Backward-compat: an Evaluator that never sets itemWins still degrades to the base rule through the
    real run path (not just a direct unit-level function call).
  • Same THIN/STRONG pair re-verified through verifyReplayBundle's gate re-execution. Pre-fix (replay.ts
    reverted, run.ts fix kept): THIN sealed evidence incorrectly re-passes (gateReExecutes: true where
    false expected) β€” confirmed live.

Full suite: flywheel 77/77 (baseline 72/72), tsc --noEmit clean. Downstream sweep, 0 regressions:
evals-math 14/14, evals-sql 7/7, evals-toolcall 14/14, evals-servedmodel 12/12, evals-hle 7/7, evals-extract
7/7, autogenous 7/7. Build: npm ci && npm run build clean for the affected packages (no wasm/NAPI steps
needed for this candidate).

Baseline Comparison

N/A in the model-eval sense β€” no live model calls, no benchmark corpus. "Baseline" here is the pre-fix
package (72/72 tests, withSequentialEvidence reachable only from its own unit tests); "candidate" is the
package after wiring (77/77 tests, the same function now reachable from runFlywheelGenerations and
verifyReplayBundle).

Darwin Lineage

Not applicable β€” this candidate is a structural fix to the flywheel's own infrastructure, not a tunable
policy genome. No Darwin generations run.

Flywheel Evidence

No flywheel receipt/lineage produced β€” the candidate IS flywheel infrastructure. See Evaluation Receipt
above for the direct test evidence instead.

Reward Hack Check

No test/benchmark weakened; meetsPromotionRule unchanged; no new caching or live model spend; fully
deterministic. Both new fields are additive. Independent critic explicitly checked for
cherry-picked/tautological tests and hand-verified the THIN-evidence math independently β€” see #287 for the
full critique summary (verdict: ACCEPT-WITH-CAVEATS, caveat = the replay.ts gap it found, which this PR
also closes).

Security Review

Not a security-sensitive surface change. No new trust boundary; pairedOutcomesFromItemWins only reads
already-sealed Score fields at both call sites; the receipt/signature model is untouched.

Regression Analysis

0 regressions (see Evaluation Receipt). Every consumer of @metaharness/flywheel in this repo re-tested
green. PairedOutcome relocation from sequential.ts to types.ts is a pure move + re-export β€” no
consumer imports it from a path this PR changes the export of.

ADR

ADR-279
(new).

Research Gist

No gist-hosting tool available in this session; published locally instead:
docs/dream-cycle/2026-09-06-gist.md.

Issue

#287

Witness

session_commit : d5833dc6512ac1adeeef91a331c29055cd8a4dbb
report_sha256   : 89298e5e8bf3fd853d1219fa9eba6e8cb4089cf3ce519330da111f6390c46d84
witness         : f88888a48df67ad3d317fa441b80d69e344d8d0e9662afc9617551c323e8291a

Merge Policy

Human review required. Do not self-merge. Do not autonomously promote any Flywheel state. This PR only
makes withSequentialEvidence reachable from the flywheel's live and replay paths β€” it does not enable
sequential-evidence gating anywhere by default (see Recommended Next Steps in #287 for the deliberately
deferred per-vertical rollout).


πŸ€– Generated with Claude Code

https://claude.ai/code/session_01EHW2MJiNYeYTb8nWHQYt89


Generated by Claude Code

…s unreachable from run.ts/replay.ts

`withSequentialEvidence` (sequential.ts) was fully built, exported, and
unit-tested but had zero production call site: `runFlywheelGenerations`
never populated `PromotionEvidence.pairedOutcomes` (the type didn't even
have the field), so wiring it into a real FlywheelConfig always silently
degraded to the plain frozen gate. Same gap independently found at
verifyReplayBundle's ADR-235 gate re-execution by a fresh-subagent critic,
closed in the same diff. Adds Score.itemWins (optional, additive) and
threads it through both call sites via a shared pairedOutcomesFromItemWins
helper. ADR-279 filed.

8 new tests, all confirmed non-vacuous by independently reverting each
sub-fix. flywheel 77/77 (was 72/72), tsc clean, 0 regressions across every
downstream @metaharness/flywheel consumer (evals-math/sql/toolcall/
servedmodel/hle/extract, autogenous).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EHW2MJiNYeYTb8nWHQYt89
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EHW2MJiNYeYTb8nWHQYt89
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants