Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions examples/gate-canary-2048.matrix
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# A control for the gate-eligible study. Pool it:
# npx tsx matrix.mts examples/gate-eligible-2048.matrix examples/gate-canary-2048.matrix --out runs/gate/cells.json
#
# Two cells, not twenty. haiku's gap against opus is roughly 1000x and needs no
# statistical power; what it buys is a floor. A harness fault that made every
# arm score alike would show here as haiku scoring like the others, and nothing
# in a two-arm comparison of opus and sonnet could reveal that.
profile.haiku harness=./harnesses/claude-code author=./harnesses/author-policy model=claude-haiku-4-5-20251001 buildMin=6 transport=persistent

game.puzzle adapter=native-2048 target=2048
objective.score goal=maximize:score horizon=20000 budgetUsd=4
protocol.det frameskip=1 sticky=0 seeds=1 queue=8 empty=noop pace=0
sensor.ascii pixels=off channels=-

reps 2
36 changes: 36 additions & 0 deletions examples/gate-eligible-2048.matrix
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# The only contrast the earlier studies left unresolved, run at the sample size
# the statistics actually require.
#
# `BOOTSTRAP_GATE_MIN_N` in @tangle-network/agent-eval is 20, and every study in
# this repo so far ran 1 to 3 replicates. `pairedBootstrap` returned
# `gateEligible: false` for all of them, which is the correct answer: opus over
# haiku cleared zero, opus over sonnet did not, and neither was admissible.
#
# haiku is not here at 20 replicates. It scored 4 and 60 against opus's 46,124
# and 46,216, and paying for power on a 1000x gap buys nothing. It runs as a
# two-cell CANARY in gate-canary-2048.matrix instead, pooled with this file: a
# control that costs almost nothing and would catch a harness fault that made
# every arm look equally good.
#
# THREAT TO VALIDITY, stated rather than hidden. Every replicate runs at ONE
# seed, so the tile sequence is identical and all the variance measured here is
# variance in what the agent BUILT. That is the right question for comparing
# profiles, and it means the answer does not generalise across boards. A seed
# sweep multiplies the cost by the number of seeds and is a separate study.
profile.opus harness=./harnesses/claude-code author=./harnesses/author-policy model=claude-opus-5 buildMin=6 transport=persistent
profile.sonnet harness=./harnesses/claude-code author=./harnesses/author-policy model=claude-sonnet-5 buildMin=6 transport=persistent

game.puzzle adapter=native-2048 target=2048

# Horizon 20000, not 2000. At 2000 an opus cell is TRUNCATED: it scored 46,170
# with the board still alive, and the same policy reaches 367,272 over 13,988
# decisions when allowed to finish. Sonnet's programs are not truncated at 2000
# because they die at 317 and 609 decisions. Comparing a truncated arm against
# an untruncated one biases the result toward the arm that dies early, which is
# exactly backwards.
objective.score goal=maximize:score horizon=20000 budgetUsd=4

protocol.det frameskip=1 sticky=0 seeds=1 queue=8 empty=noop pace=0
sensor.ascii pixels=off channels=-

reps 20
20 changes: 19 additions & 1 deletion release-gate.mts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,25 @@ export const requiredCiJobs: readonly RequiredJob[] = [
{ job: 'Gymnasium environment adapter on real environments', step: 'Adapter gate' },
{ job: 'stable-retro multi-console adapter on a real emulator', step: 'Adapter gate' },
{ job: 'ALE Atari adapter on a real emulator', step: 'Adapter gate' },
{ job: 'RetroArch black-box host adapter on a real emulator', step: 'Adapter gate' },
// QUARANTINED, deliberately, and this is a reduction in release coverage.
//
// `RetroArch black-box host adapter on a real emulator` is not here. Its
// same-process replay assertion failed 4 times before a fix to the frame
// advance barrier and twice after it, against 3 passes: roughly 40% on a job
// that gated every release. The divergence is real and unexplained. Two
// replays of ONE boot state and ONE input log, in ONE process, agree byte for
// byte to emuFrame 811 and then differ on channel values at IDENTICAL frame
// numbers, which rules out a miscounted advance.
//
// The alternative to quarantine was what actually happened twice on
// 2026-08-27: rerun the job until it went green and then tag. That launders a
// red into a green, which is the precise failure this gate was built in 0.9.0
// to stop, and a gate everyone reruns past is worse than one that states its
// own limits.
//
// The job still runs on every pull request and its result is still visible.
// What changed is that a release no longer CLAIMS RetroArch replay as a
// verified property. Restore this line when the divergence is root-caused.
]

// `success` is the only conclusion that releases. `cancelled`, `timed_out`,
Expand Down
37 changes: 29 additions & 8 deletions release-gate.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,30 @@ function gate(overrides: Partial<GateInput> = {}): GateInput {
assert.match(decision.reasons.join('\n'), /concluded failure/u)
}
{
// A red REQUIRED job still refuses. ALE stands in for the property; RetroArch
// used to and no longer can, for the reason asserted immediately below.
const jobs = greenJobs().map((job) =>
job.name.startsWith('RetroArch') ? { ...job, conclusion: 'failure' } : job,
job.name.startsWith('ALE') ? { ...job, conclusion: 'failure' } : job,
)
const decision = decideRelease(gate({ jobs }))
assert.equal(decision.verdict, 'refuse')
assert.match(decision.reasons.join('\n'), /"RetroArch black-box host adapter on a real emulator" concluded failure/u)
assert.match(decision.reasons.join('\n'), /"ALE Atari adapter on a real emulator" concluded failure/u)
}
{
// THE QUARANTINE, asserted rather than left implicit. A red RetroArch job now
// allows a release, because its same-process replay assertion failed roughly
// 40% of the time and a gate that is rerun until green launders a red into a
// green. This case fails the moment RetroArch is restored to the required
// set, which is what makes the restoration a conscious act.
const jobs = greenJobs().map((job) =>
job.name.startsWith('RetroArch') ? { ...job, conclusion: 'failure' } : job,
)
const decision = decideRelease(gate({ jobs }))
assert.equal(
decision.verdict,
'allow',
'RetroArch is quarantined; restoring it to requiredCiJobs must break this case on purpose',
)
}

// Case 3 of 3, and the dangerous one: ci has no result at all for this commit.
Expand Down Expand Up @@ -146,17 +164,20 @@ function gate(overrides: Partial<GateInput> = {}): GateInput {
assert.match(decision.reasons.join('\n'), /"Gymnasium environment adapter on real environments" concluded skipped/u)
}

// The measured shape of the RetroArch job: its asset install is
// `continue-on-error` and its adapter gate is conditional, so the job can
// conclude success with the emulator never driven. A green job alone does not
// release; the step inside it must have run.
// A green job alone does not release; the step inside it must have run.
//
// This shape was measured on the RetroArch job, whose asset install is
// `continue-on-error` and whose adapter gate is conditional, so it could
// conclude success with the emulator never driven. RetroArch is quarantined out
// of the required set, so the property is asserted here on stable-retro, which
// is still required and has the same conditional-gate shape.
{
const jobs = greenJobs().map((job) =>
job.name.startsWith('RetroArch')
job.name.startsWith('stable-retro')
? {
...job,
steps: [
{ name: 'Install RetroArch, the gambatte core, and the free ROM', conclusion: 'failure' },
{ name: 'Install the emulator and the free ROM', conclusion: 'failure' },
{ name: 'Adapter gate', conclusion: 'skipped' },
{ name: 'Report an unusable pool', conclusion: 'success' },
],
Expand Down