Files
OwnCord/.claude/workflows/bughunt.harness.mjs
T
J3vbandClaude Opus 5 3af3489f71 feat(bughunt): add a fix-run circuit breaker and panel finder attribution (#1362)
* feat(bughunt-fix): add a circuit breaker for systematically failing runs

A fix run had no abort condition. If something was systematically wrong - the
operator on the wrong branch, a broken test runner, ledger coordinates gone
stale after a rebase - it worked through every cluster, spending a high-effort
agent on each, and only reported the wreckage at the end.

Two trip points, because there are two distinct failure signals:

- after the fix stage, a high blocked rate means the fixing itself is failing.
  Proving each of those costs a serial agent per cluster and cannot succeed, so
  phase 3 is skipped entirely.
- inside the prove loop, a high revert-proof failure rate means the proving is
  failing. Break rather than attempt the rest.

`declined` never counts as a failure - it is a judgement the fix prompt
explicitly invites, and a run where several findings are correctly declined is a
good run. Both points require a minimum number of attempts first, because "50%
of two" is noise. Clusters never reached are marked blocked with a rationale
naming the breaker, so nothing is left reported as fixed with no commit behind
it, and the gate still runs over whatever committed before the trip.

proveAttempts is incremented before the ok check so successes land in the
denominator; inside the failure branch the ratio would be failures-over-failures
and trip on the first failed cluster at any threshold.

Verified with 6 new harness scenarios (21 -> 27, all green, bughunt.harness.mjs
untouched at 21). The guard was also proved load-bearing: with the threshold
temporarily raised to an unreachable 1.1, f16 runs all four clusters instead of
stopping at three and f20 produces no breaker report - both fail for the reason
the guard exists.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(bughunt): attribute confirmed findings to the finder that produced them

The dual-model panel unions its two finders rather than voting between them, so
the second model's entire value is what it finds alone - and the union threw
that away, leaving no way to tell whether sonnet earns its cost.

Tag each finding with its panel slot. Because dedupe keeps the first occurrence
and opus is slot 0, a confirmed finding tagged sonnet is one opus missed, which
is exactly the number that decides the question. The run logs the split.

Three details worth naming:

- the tag is taken from the panel slot, not from the position in the surviving
  list. Filtering the nulls out before reading the index shifts sonnet into slot
  0 whenever opus dies and mislabels its finds as opus - precisely when the
  attribution matters most.
- the tag is stripped in verifyPrompt, not at its two call sites, so every
  caller routes through the guard. The verifier prompt says "another model" on
  purpose; naming it is an authority cue that erodes refute-by-default.
- dropping to a single finder would also weaken convergence, since a round only
  counts as dry when the full panel reported. The skill records this next to the
  count so the decision is made with both halves in view.

Verified with 4 new harness scenarios (21 -> 25). The dead-opus case is the
load-bearing one: it fails against the naive filter-then-index form.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 21:03:15 +02:00

657 lines
28 KiB
JavaScript

// Offline harness for bughunt.js - mimics the workflow runtime: wraps the script
// body in an AsyncFunction with stubbed agent/parallel/pipeline/phase/log/args/budget.
// Run: node .claude/workflows/bughunt.harness.mjs [nameFilter]
import { readFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import assert from 'node:assert/strict'
const here = dirname(fileURLToPath(import.meta.url))
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor
export async function run({ agentStub, args = undefined, budget = undefined }) {
const src = readFileSync(join(here, 'bughunt.js'), 'utf8')
const body = src.replace('export const meta', 'const meta')
const calls = []
const logs = []
const agent = async (prompt, opts = {}) => {
calls.push({ prompt, opts })
return agentStub(prompt, opts)
}
const parallel = (thunks) =>
Promise.all(thunks.map((t) => Promise.resolve().then(t).catch(() => null)))
const pipeline = (items, ...stages) =>
Promise.all(
items.map(async (item, i) => {
let v = item
for (const stage of stages) {
try {
v = await stage(v, item, i)
} catch {
return null
}
}
return v
}),
)
const log = (m) => logs.push(String(m))
const phase = () => {}
const budgetImpl = budget || { total: null, spent: () => 0, remaining: () => Infinity }
const fn = new AsyncFunction('agent', 'parallel', 'pipeline', 'phase', 'log', 'args', 'budget', body)
const result = await fn(agent, parallel, pipeline, phase, log, args, budgetImpl)
return { result, calls, logs }
}
// ---------- stub kit (used from Task 2 onward; harmless now) ----------
export function makeStub({ hunt, verify, report = () => 'REPORT_MD', recon = defaultRecon }) {
return (prompt, opts) => {
const label = opts.label || ''
if (label.startsWith('recon:')) return recon(label)
let m = /^r(\d+):hunt:([a-z0-9-]+):(opus|sonnet)$/.exec(label)
if (m) return hunt(Number(m[1]), m[2], m[3], prompt)
m = /^r(\d+):verify:([a-z0-9-]+?)(:retry)?$/.exec(label)
if (m) {
const candidates = JSON.parse(prompt.split('--- CANDIDATES ---')[1])
return verify(Number(m[1]), m[2], candidates, Boolean(m[3]), prompt)
}
if (label === 'report') return report(prompt)
throw new Error(`unexpected agent label: ${label}`)
}
}
export function defaultRecon() {
return 'Server/ws/hub.go 12\nServer/api/user.go 9\nClient/tauri-client/src/lib/dispatcher.ts 8'
}
export const none = { findings: [] }
export const finding = (n, over = {}) => ({
title: `distinct bug alpha${n} omega${n}`,
file: 'Server/ws/hub.go',
line: 100 + n * 40,
severity: 'high',
why: 'w',
repro: 'r',
evidence: 'e',
...over,
})
export const confirmAll = (cands) => ({
verdicts: cands.map((c) => ({
title: c.title, file: c.file, line: c.line,
refuted: false, reason: 'confirmed', confidence: 'high',
severity: c.severity || 'high', fix: 'fix',
})),
})
export const refuteAll = (cands) => ({
verdicts: cands.map((c) => ({
title: c.title, file: c.file, line: c.line,
refuted: true, reason: 'refuted', confidence: 'high',
severity: c.severity || 'high',
})),
})
// ---------- scenarios ----------
const scenarios = {}
// S1: happy convergence - one bug in round 1, rounds 2-3 dry -> converged.
scenarios.s1_convergence = async () => {
const reportPrompts = []
const { result, calls } = await run({
agentStub: makeStub({
hunt: (round, key, model) =>
round === 1 && key === 'ws-hub' && model === 'opus' ? { findings: [finding(1)] } : none,
verify: (round, key, cands) => confirmAll(cands),
report: (prompt) => {
reportPrompts.push(prompt)
return 'REPORT_MD'
},
}),
})
for (const k of ['converged', 'stoppedOnBudget', 'rounds', 'confirmed', 'unverified', 'report'])
assert.ok(k in result, `missing key ${k}`)
assert.equal(result.converged, true)
assert.equal(result.rounds.length, 3)
assert.deepEqual(result.rounds.map((r) => r.dryAfter), [0, 1, 2])
assert.deepEqual(result.rounds.map((r) => r.family), ['surfaces', 'bug-classes', 'flows'])
assert.equal(result.confirmed.length, 1)
assert.equal(result.report, 'REPORT_MD')
assert.ok(!calls.some((c) => (c.opts.label || '').startsWith('r4:')), 'no round 4 after convergence')
assert.match(reportPrompts[0], /CONVERGED after 3 round\(s\)/)
assert.match(reportPrompts[0], /\| 1 \| surfaces \|/)
}
// S2: panel dedupe - opus and sonnet report the same bug -> one candidate, one verify call.
scenarios.s2_panel_dedupe = async () => {
const verifyBatches = []
const { result } = await run({
agentStub: makeStub({
hunt: (round, key, model) => {
if (round !== 1 || key !== 'ws-hub') return none
return model === 'opus'
? { findings: [finding(1, { line: 100 })] }
: { findings: [finding(1, { line: 105, title: 'distinct bug alpha1 omega1 variant' })] }
},
verify: (round, key, cands) => {
verifyBatches.push(cands)
return confirmAll(cands)
},
}),
})
assert.equal(verifyBatches.length, 1)
assert.equal(verifyBatches[0].length, 1)
assert.equal(result.confirmed.length, 1)
}
// S3: refuted findings stay dead - re-reported next round, never re-verified; refutes count toward dry.
scenarios.s3_refuted_permanence = async () => {
const { result, calls } = await run({
agentStub: makeStub({
hunt: (round, key, model) => {
if (round === 1 && key === 'ws-hub' && model === 'opus') return { findings: [finding(2)] }
if (round === 2 && key === 'state-desync' && model === 'opus') return { findings: [finding(2)] }
return none
},
verify: (round, key, cands) => refuteAll(cands),
}),
})
const verifyRounds = calls
.map((c) => /^r(\d+):verify:/.exec(c.opts.label || ''))
.filter(Boolean)
.map((m) => Number(m[1]))
assert.deepEqual(verifyRounds, [1], 'refuted candidate must not be re-verified in round 2')
assert.equal(result.rounds[0].refuted, 1)
assert.equal(result.confirmed.length, 0)
assert.equal(result.rounds.length, 2) // refute-only r1 is dry -> converged after r2
assert.equal(result.converged, true)
assert.ok(!calls.some((c) => c.opts.label === 'report'), 'zero confirmed -> code-built report')
assert.match(result.report, /Converged/i)
}
// S4: backstop - fresh confirmed bug every round with maxRounds=3 -> stops, NOT converged.
scenarios.s4_backstop = async () => {
const firstLens = { 1: 'ws-hub', 2: 'concurrency', 3: 'flow-reconnect' }
const { result } = await run({
args: { maxRounds: 3 },
agentStub: makeStub({
hunt: (round, key, model) =>
model === 'opus' && key === firstLens[round]
? { findings: [finding(round, { file: `Server/ws/f${round}.go` })] }
: none,
verify: (round, key, cands) => confirmAll(cands),
}),
})
assert.equal(result.rounds.length, 3)
assert.equal(result.converged, false)
assert.equal(result.stoppedOnBudget, false)
assert.equal(result.confirmed.length, 3)
assert.deepEqual(result.rounds.map((r) => r.dryAfter), [0, 0, 0])
}
// S5: failed finder -> round dry-ineligible; dry counter neither increments nor resets.
scenarios.s5_finder_failure_ineligible = async () => {
const { result } = await run({
args: { maxRounds: 3 },
agentStub: makeStub({
hunt: (round, key, model) => {
if (round === 1 && key === 'ws-hub' && model === 'opus') return { findings: [finding(1)] }
if (round === 2 && key === 'concurrency' && model === 'opus') return null // dead finder
return none
},
verify: (round, key, cands) => confirmAll(cands),
}),
})
assert.equal(result.rounds[1].dryEligible, false)
assert.deepEqual(result.rounds.map((r) => r.dryAfter), [0, 0, 1])
assert.equal(result.converged, false)
}
// S6: failed verifier retried once, retry succeeds.
scenarios.s6_verifier_retry = async () => {
const { result, calls } = await run({
agentStub: makeStub({
hunt: (round, key, model) =>
round === 1 && key === 'ws-hub' && model === 'opus' ? { findings: [finding(1)] } : none,
verify: (round, key, cands, isRetry) => (isRetry ? confirmAll(cands) : null),
}),
})
assert.ok(calls.some((c) => (c.opts.label || '').endsWith(':retry')))
assert.equal(result.confirmed.length, 1)
assert.equal(result.converged, true)
}
// S6b: verifier fails twice -> candidate dropped unconfirmed, round ineligible;
// re-reported later, verified then, and scrubbed from the unverified list.
scenarios.s6b_verifier_double_failure = async () => {
const { result } = await run({
args: { maxRounds: 3 },
agentStub: makeStub({
hunt: (round, key, model) => {
if (round === 1 && key === 'ws-hub' && model === 'opus') return { findings: [finding(3)] }
if (round === 2 && key === 'state-desync' && model === 'opus') return { findings: [finding(3)] }
return none
},
verify: (round, key, cands) => (round === 1 ? null : confirmAll(cands)),
}),
})
assert.equal(result.rounds[0].dryEligible, false)
assert.equal(result.rounds[0].confirmed, 0)
assert.equal(result.confirmed.length, 1)
assert.equal(result.confirmed[0].round, 2)
assert.equal(result.unverified.length, 0, 'later-confirmed candidate must leave the unverified list')
}
// S7: rounds 1-3 each confirm a bug -> round 4 runs adaptive lenses built from the stats.
scenarios.s7_adaptive_lenses = async () => {
const A = finding(1, { file: 'Server/ws/hub.go', line: 120, title: 'alpha race window one' })
const B = finding(2, { file: 'Server/ws/pubsub.go', line: 60, title: 'beta subscription leak two' })
const C = finding(3, { file: 'Client/tauri-client/src/lib/livekitE2EE.ts', line: 200, title: 'gamma epoch desync three' })
const { result, calls } = await run({
agentStub: makeStub({
hunt: (round, key, model) => {
if (model !== 'opus') return none
if (round === 1 && key === 'ws-hub') return { findings: [A] }
if (round === 2 && key === 'concurrency') return { findings: [B] }
if (round === 3 && key === 'flow-voice') return { findings: [C] }
return none
},
verify: (round, key, cands) => confirmAll(cands),
}),
})
assert.equal(result.converged, true)
assert.equal(result.rounds.length, 5) // r4, r5 adaptive + dry
assert.equal(result.rounds[3].family, 'adaptive')
const r4Hunts = calls.filter((c) => /^r4:hunt:/.test(c.opts.label || ''))
const r4Keys = [...new Set(r4Hunts.map((c) => c.opts.label.split(':')[2]))]
assert.ok(r4Keys.includes('hotspot-server-ws'), `r4 keys: ${r4Keys}`)
assert.ok(r4Keys.includes('fresh-eyes'), `r4 keys: ${r4Keys}`)
const hotspot = r4Hunts.find((c) => c.opts.label.includes('hotspot-server-ws'))
assert.match(hotspot.prompt, /Server\/ws\/hub\.go/)
assert.match(hotspot.prompt, /alpha race window one/)
const freshEyes = r4Hunts.find((c) => c.opts.label.includes('fresh-eyes'))
assert.match(freshEyes.prompt, /Server\/api\/user\.go/) // churned, never a finding
assert.equal(result.confirmed.length, 3)
}
// S7b: a lens with 2 consecutive clean rounds is demoted from later rounds.
scenarios.s7b_demotion = async () => {
const { result, calls } = await run({
args: { maxRounds: 6 },
agentStub: makeStub({
hunt: (round, key, model) => {
if (model !== 'opus') return none
const src = { 1: 'ws-hub', 2: 'concurrency', 3: 'flow-reconnect', 4: 'hotspot-server-ws', 5: 'hotspot-server-ws' }
if (key === src[round])
return { findings: [finding(round, { file: `Server/ws/a${round}.go`, title: `unique bug number${round} zeta${round}` })] }
return none
},
verify: (round, key, cands) => confirmAll(cands),
}),
})
const labels = calls.map((c) => c.opts.label || '')
assert.ok(labels.some((l) => /^r5:hunt:fresh-eyes:/.test(l)), 'fresh-eyes still runs in r5 (streak 1)')
assert.ok(!labels.some((l) => /^r6:hunt:fresh-eyes:/.test(l)), 'fresh-eyes demoted in r6 (streak 2)')
assert.ok(labels.some((l) => /^r6:hunt:hotspot-server-ws:/.test(l)), 'producing hotspot keeps running')
assert.equal(result.converged, false)
assert.equal(result.confirmed.length, 5)
}
// S7c: a lens whose VERIFIER died is not demoted; a zero-candidate lens still is.
scenarios.s7c_verifier_failure_not_demoted = async () => {
const early = { 1: 'ws-hub', 2: 'concurrency', 3: 'flow-reconnect' }
const { result, calls } = await run({
args: { maxRounds: 6 },
agentStub: makeStub({
hunt: (round, key, model) => {
if (model !== 'opus') return none
if (round <= 3 && key === early[round])
return { findings: [finding(round, { file: `Server/ws/a${round}.go`, title: `early bug item${round} kappa${round}` })] }
if (round >= 4 && key === 'hotspot-server-ws')
return { findings: [finding(round + 10, { file: `Server/ws/b${round}.go`, title: `late bug item${round} sigma${round}` })] }
return none
},
verify: (round, key, cands) => (round <= 3 ? confirmAll(cands) : null),
}),
})
const labels = calls.map((c) => c.opts.label || '')
assert.ok(labels.some((l) => /^r6:hunt:hotspot-server-ws:/.test(l)), 'verifier-dead lens must NOT be demoted')
assert.ok(!labels.some((l) => /^r6:hunt:fresh-eyes:/.test(l)), 'zero-candidate lens still accrues streak and demotes')
assert.equal(result.confirmed.length, 3)
assert.equal(result.unverified.length, 3)
assert.equal(result.converged, false)
assert.ok(result.rounds.slice(3).every((r) => r.dryEligible === false))
}
// S12: empty adaptive family (no confirms, no churn) must break honestly, not count dry rounds.
scenarios.s12_empty_adaptive_family = async () => {
const { result } = await run({
agentStub: makeStub({
recon: () => 'no parseable churn output',
hunt: (round, key, model) => {
if (round <= 2 && key === (round === 1 ? 'ws-hub' : 'concurrency') && model === 'opus')
return { findings: [finding(round, { file: `Server/ws/c${round}.go`, title: `verifierless bug delta${round} theta${round}` })] }
return none
},
verify: () => null,
}),
})
assert.equal(result.rounds.length, 3)
assert.equal(result.converged, false)
assert.equal(result.confirmed.length, 0)
assert.equal(result.unverified.length, 2)
}
// S8: budget below the round floor before round 1 -> zero rounds, honest non-convergence.
scenarios.s8_budget_floor = async () => {
const { result, calls } = await run({
budget: { total: 1000000, spent: () => 900000, remaining: () => 100000 },
agentStub: makeStub({ hunt: () => none, verify: (r, k, c) => confirmAll(c) }),
})
assert.equal(result.rounds.length, 0)
assert.equal(result.stoppedOnBudget, true)
assert.equal(result.converged, false)
assert.ok(!calls.some((c) => /:hunt:/.test(c.opts.label || '')))
assert.match(result.report, /budget/i)
}
// S8b: budget runs low mid-hunt -> finishes the round it started, stops before the next.
scenarios.s8b_budget_midrun = async () => {
let n = 0
const { result } = await run({
budget: { total: 1000000, spent: () => 0, remaining: () => (n++ === 0 ? 200000 : 100000) },
agentStub: makeStub({
hunt: (round, key, model) =>
round === 1 && key === 'ws-hub' && model === 'opus' ? { findings: [finding(1)] } : none,
verify: (round, key, cands) => confirmAll(cands),
}),
})
assert.equal(result.rounds.length, 1)
assert.equal(result.stoppedOnBudget, true)
assert.equal(result.converged, false)
assert.equal(result.confirmed.length, 1)
}
// S14: the title-word dedupe branch applies only near the prior's location.
// Dedupe is permanent, so merging two distinct same-file bugs that happen to
// share half their title words loses the second one forever.
scenarios.s14_title_dedupe_window = async () => {
const near = { file: 'Server/ws/hub.go', line: 140, title: 'hub client map race on register path' }
const far = { file: 'Server/ws/hub.go', line: 900, title: 'hub client map race on unregister' }
const verifyBatches = []
const { result } = await run({
agentStub: makeStub({
hunt: (round, key, model) => {
if (model !== 'opus') return none
if (round === 1 && key === 'ws-hub')
return { findings: [finding(1, { file: 'Server/ws/hub.go', line: 100, title: 'hub client map race on register' })] }
if (round === 2 && key === 'concurrency')
return { findings: [finding(2, far), finding(3, near)] }
return none
},
verify: (round, key, cands) => {
verifyBatches.push(cands.map((c) => c.line))
return confirmAll(cands)
},
}),
})
assert.deepEqual(verifyBatches, [[100], [900]], 'near-duplicate dropped, distant same-file bug kept')
assert.equal(result.confirmed.length, 2)
assert.ok(result.confirmed.some((c) => c.line === 900), 'the distant bug must survive dedupe')
}
// S13: JSON-stringified args must behave identically to object args (observed live: the
// runtime can deliver args as a string; maxRounds:1 silently fell back to 8 before the coercion).
scenarios.s13_string_args = async () => {
const { result, calls } = await run({
args: '{"maxRounds": 1}',
agentStub: makeStub({
hunt: (round, key, model) =>
round === 1 && key === 'ws-hub' && model === 'opus' ? { findings: [finding(1)] } : none,
verify: (round, key, cands) => confirmAll(cands),
}),
})
assert.equal(result.rounds.length, 1, 'string maxRounds:1 must cap the loop at one round')
assert.equal(result.converged, false)
assert.equal(result.confirmed.length, 1)
assert.ok(!calls.some((c) => (c.opts.label || '').startsWith('r2:')), 'no round 2 under the cap')
}
// S10: verifier returns truncated (empty) verdict lists on both attempts ->
// candidates land in unverified, round ineligible, dry counter untouched.
scenarios.s10_truncated_verdicts = async () => {
const { result, calls } = await run({
args: { maxRounds: 2 },
agentStub: makeStub({
hunt: (round, key, model) =>
round === 1 && key === 'ws-hub' && model === 'opus' ? { findings: [finding(1)] } : none,
verify: () => ({ verdicts: [] }),
}),
})
assert.ok(calls.some((c) => (c.opts.label || '').endsWith(':retry')), 'short verdict list must trigger the retry')
assert.equal(result.rounds[0].dryEligible, false)
assert.deepEqual(result.rounds.map((r) => r.dryAfter), [0, 1])
assert.equal(result.confirmed.length, 0)
assert.equal(result.unverified.length, 1)
assert.equal(result.converged, false)
}
// S11: verdict coordinates drift from the candidate's -> still pairs, confirms once,
// nothing listed unverified, and a round-2 re-report of the ORIGINAL coords is deduped.
scenarios.s11_drifted_verdict = async () => {
const orig = finding(4) // file Server/ws/hub.go, line 260
const { result, calls } = await run({
agentStub: makeStub({
hunt: (round, key, model) => {
if (round === 1 && key === 'ws-hub' && model === 'opus') return { findings: [orig] }
if (round === 2 && key === 'state-desync' && model === 'opus') return { findings: [orig] }
return none
},
verify: (round, key, cands) => ({
verdicts: cands.map((c) => ({
title: c.title, file: c.file, line: c.line + 5,
refuted: false, reason: 'confirmed', confidence: 'high', severity: 'high', fix: 'fix',
})),
}),
}),
})
const verifyRounds = calls
.map((c) => /^r(\d+):verify:/.exec(c.opts.label || ''))
.filter(Boolean)
.map((m) => Number(m[1]))
assert.deepEqual(verifyRounds, [1], 'drifted-but-paired verdict must still suppress the original coords')
assert.equal(result.confirmed.length, 1)
assert.equal(result.unverified.length, 0)
assert.equal(result.converged, true)
}
// S-known: a finding already in the ledger is suppressed - never verified, never re-confirmed,
// and its text appears in the finder prompt so the model does not spend effort re-deriving it.
scenarios.s_known_ledger_suppresses = async () => {
const known = [
{ file: 'Server/ws/hub.go', line: 140, title: 'distinct bug alpha1 omega1', status: 'declined' },
]
const { result, calls } = await run({
args: { known, maxRounds: 1, dryThreshold: 9 },
agentStub: makeStub({
hunt: (round, key, model) =>
round === 1 && key === 'ws-hub' && model === 'opus' ? { findings: [finding(1)] } : none,
verify: (round, key, cands) => confirmAll(cands),
}),
})
const huntPrompts = calls.filter((c) => /:hunt:/.test(c.opts.label || '')).map((c) => c.prompt)
assert.ok(huntPrompts.length > 0, 'expected at least one finder call')
assert.match(huntPrompts[0], /KNOWN FINDINGS/, 'ledger entries must reach the finder prompt')
assert.match(huntPrompts[0], /\[declined\] distinct bug alpha1 omega1/)
assert.ok(
!calls.some((c) => /:verify:/.test(c.opts.label || '')),
'a ledger-known candidate must not reach verification',
)
assert.equal(result.confirmed.length, 0)
}
// S-lenses: args.lenses replaces the round-1 family entirely, and the round label reflects it.
scenarios.s_custom_lenses = async () => {
const lenses = [
{ key: 'voice-e2ee-keyholder', prompt: 'Hunt the key-holder election.' },
{ key: 'voice-e2ee-rotation', prompt: 'Hunt the rotation paths.' },
]
const { result, calls } = await run({
args: { lenses, maxRounds: 1, dryThreshold: 9 },
agentStub: makeStub({ hunt: () => none, verify: (r, k, c) => confirmAll(c) }),
})
const keys = calls
.map((c) => /^r1:hunt:([a-z0-9-]+):(opus|sonnet)$/.exec(c.opts.label || ''))
.filter(Boolean)
.map((m) => m[1])
assert.deepEqual([...new Set(keys)].sort(), ['voice-e2ee-keyholder', 'voice-e2ee-rotation'])
assert.ok(!keys.includes('ws-hub'), 'the default surface family must not run when lenses are supplied')
assert.equal(result.rounds[0].family, 'custom')
assert.equal(result.rounds[0].lenses, 2)
}
// S-lenses-default: omitting args.lenses leaves the rotation untouched.
scenarios.s_custom_lenses_absent = async () => {
const { result } = await run({
args: { maxRounds: 1, dryThreshold: 9 },
agentStub: makeStub({ hunt: () => none, verify: (r, k, c) => confirmAll(c) }),
})
assert.equal(result.rounds[0].family, 'surfaces')
}
// S-ledger-fields: a confirmed record must carry finder detail (why/repro/evidence) as well as
// verifier detail (severity/fix), because the ledger needs both.
scenarios.s_confirmed_carries_finder_detail = async () => {
const cand = {
title: 'distinct bug alpha1 omega1',
file: 'Server/ws/hub.go',
line: 140,
severity: 'low',
why: 'WHY_TEXT',
repro: 'REPRO_TEXT',
evidence: 'EVIDENCE_TEXT',
}
const { result } = await run({
args: { maxRounds: 1, dryThreshold: 9 },
agentStub: makeStub({
hunt: (round, key, model) =>
round === 1 && key === 'ws-hub' && model === 'opus' ? { findings: [cand] } : none,
verify: (round, key, cands) => ({
verdicts: cands.map((c) => ({
title: c.title, file: c.file, line: c.line,
refuted: false, reason: 'confirmed', confidence: 'high',
severity: 'high', fix: 'FIX_TEXT',
})),
}),
}),
})
assert.equal(result.confirmed.length, 1)
const r = result.confirmed[0]
assert.equal(r.why, 'WHY_TEXT')
assert.equal(r.repro, 'REPRO_TEXT')
assert.equal(r.evidence, 'EVIDENCE_TEXT')
assert.equal(r.severity, 'high', 'verifier severity must win over the finder rating')
assert.equal(r.fix, 'FIX_TEXT')
assert.equal(r.lens, 'ws-hub')
assert.equal(r.round, 1)
}
// S_FINDER_ATTRIBUTION: confirmed records name which model found them. The panel unions rather
// than votes, so this is the only way to tell whether the second finder earns its cost. Because
// dedupe keeps the first occurrence and opus is index 0, `finder: 'sonnet'` means opus did NOT
// report it - i.e. a sonnet-unique find. `finder: 'opus'` says nothing about sonnet either way.
scenarios.s_finder_attribution = async () => {
const shared = finding(1)
// Different file, not just a different line: isDup treats same-file findings within
// TITLE_MATCH_WINDOW as duplicates on title-word overlap, and the finding() fixtures share
// enough words to collapse into one another.
const sonnetOnly = finding(2, { file: 'Server/api/user.go' })
const { result } = await run({
args: { maxRounds: 1, dryThreshold: 9 },
agentStub: makeStub({
hunt: (round, key, model) => {
if (round !== 1 || key !== 'ws-hub') return none
return model === 'opus' ? { findings: [shared] } : { findings: [shared, sonnetOnly] }
},
verify: (round, key, cands) => confirmAll(cands),
}),
})
const byTitle = Object.fromEntries(result.confirmed.map((r) => [r.title, r.finder]))
assert.equal(byTitle[shared.title], 'opus', 'a find both models made keeps the opus-first record')
assert.equal(byTitle[sonnetOnly.title], 'sonnet', 'a find only sonnet made must be attributed to sonnet')
}
// S_FINDER_ATTRIBUTION_SURVIVES_DEAD_OPUS: the tag must be taken from the panel slot, not from the
// position in the surviving list. Filtering the nulls out BEFORE reading the index shifts sonnet
// into slot 0 and mislabels every one of its finds as opus - exactly when attribution matters most.
scenarios.s_finder_attribution_survives_dead_opus = async () => {
const only = finding(3)
const { result } = await run({
args: { maxRounds: 1, dryThreshold: 9 },
agentStub: makeStub({
hunt: (round, key, model) => {
if (round !== 1 || key !== 'ws-hub') return none
return model === 'opus' ? null : { findings: [only] }
},
verify: (round, key, cands) => confirmAll(cands),
}),
})
assert.equal(result.confirmed.length, 1)
assert.equal(result.confirmed[0].finder, 'sonnet', 'a dead opus must not relabel sonnet finds as opus')
}
// S_VERIFIER_IS_NOT_TOLD_THE_FINDER: the verifier prompt deliberately says "another model" and
// never names it. Leaking the attribution tag would tell a refute-by-default verifier that opus
// found something, which is exactly the kind of authority cue that erodes refute-by-default.
scenarios.s_verifier_is_not_told_the_finder = async () => {
let verifyPromptText = ''
await run({
args: { maxRounds: 1, dryThreshold: 9 },
agentStub: makeStub({
hunt: (round, key, model) =>
round === 1 && key === 'ws-hub' && model === 'opus' ? { findings: [finding(1)] } : none,
verify: (round, key, cands, retry, prompt) => {
verifyPromptText = prompt
return confirmAll(cands)
},
}),
})
assert.ok(verifyPromptText, 'the verifier must have been called')
assert.doesNotMatch(verifyPromptText, /finder/i, 'the verifier must not be told which model found the candidate')
// Guard against over-stripping: the fields the verifier actually needs must survive.
for (const field of ['title', 'file', 'line', 'why', 'repro', 'evidence']) {
assert.match(verifyPromptText, new RegExp(`"${field}"`), `candidates must still carry ${field}`)
}
}
// S_PANEL_SPLIT_IS_REPORTED: a tag nobody reads is not a measurement. The run must say out loud
// how many confirmed findings only the second finder produced, which is the number that decides
// whether the second model is worth its cost.
scenarios.s_panel_split_is_reported = async () => {
const { logs } = await run({
args: { maxRounds: 1, dryThreshold: 9 },
agentStub: makeStub({
hunt: (round, key, model) => {
if (round !== 1 || key !== 'ws-hub') return none
return model === 'opus'
? { findings: [finding(1)] }
: { findings: [finding(1), finding(2, { file: 'Server/api/user.go' })] }
},
verify: (round, key, cands) => confirmAll(cands),
}),
})
const line = logs.find((l) => /panel:/.test(l))
assert.ok(line, 'the run must report the panel split')
assert.match(line, /sonnet-only/, 'the split must name the sonnet-only count explicitly')
assert.match(line, /\b1\b/, 'exactly one confirmed finding here was sonnet-only')
}
// ---------- runner ----------
const only = process.argv[2]
for (const [name, fn] of Object.entries(scenarios)) {
if (only && !name.includes(only)) continue
try {
await fn()
} catch (e) {
console.error(`FAIL ${name}`)
throw e
}
console.log(`PASS ${name}`)
}
console.log('all scenarios pass')