Billy — branch-improvement validation
2026-09-09 — #1028: lazy cloud home, bank recovered after quota stop
- Status: bank recovered; delivery review interrupted by provider quota. This was not a completed end-to-end Billy run.
- Method:
/billyon PR #1028, scoped to Revi findingR8a91d4after review01a085f9-871b-7aa9-85d6-e8724258c04c. Run01a08605-1dd6-7907-9baa-86fae51f3e0bstarted at 11:54:37Z from82f881938; auto-merge was disabled during the pass. The owner made no concurrent edits to its branch. - Result:
f0c723e59defers CloudHome with React.lazy and the existing BootLoading fallback while keeping PublicTopBar eager;54a2c24d3adds a regression test;8fe07161frecords the initial bilan. The approved page content, appearance, local login fallback and marketplace flag stay intact. R8a91d4 is fixed. - Evidence: the bot measured entry JS 461.25 → 289.78 kB (gzip 127.43 → 88.30) and entry CSS 119.68 → 98.21 kB (gzip 21.62 → 17.03). Neither CloudHome nor its brand-icon payload remained in the eager preload graph. The new test was falsified by temporarily restoring the static import. Studio lint had zero errors, TypeScript and Vite passed, and all 1,352 tests in 152 files passed. The generated delivery script also passed, including the OpenAPI/client drift check.
- Stop and recovery: the final
reviewnode hit the Claude five-hour limit at 12:41:07Z (USAGE_LIMIT_BLOCKED, reset advertised for 13:50Z), before publishing a ledger or pushing the PR. The owner cancelled the parked run and confirmedcancelled, with no running execution, before recovering checkpointiterion/run-01a08605-1dd6-7907-9baa-86fae51f3e0b-checkpoint. FETCH_HEAD matched the recorded8fe07161f6a92587a3dc34c3bb6bf155365ccd06; its three commits were preserved by fast-forward rather than replaying the completed campaign after the reset. - Recovery validation: the owner reran Studio lint (0 errors), TypeScript, all 1,352 tests and the production build successfully. Browser checks on the built SPA confirm that
/loginneither requests nor preloads the CloudHome JS/CSS, while/loads both and still navigates to login. Local bundle sizes match the measurements above. GitHub CI and Revi must validate the published head before queue entry. - Frictions: planning and peer review took about 25 minutes for one import-boundary correction. A 30-second shell timeout cut off the peer's baseline lint; the owner supplied the already-green baseline and advised a longer timeout. Vitest cannot resolve the real page's lobehub UI import in this environment, so the boundary test mocks its far side; the production bundle check covers the actual dependency graph. Soft usage readings (
stopped=false, including 0%) did not predict the hard denial. - Lesson: validate the eager dependency graph, not just a chunk-size drop, and preserve banked work when the delivery review cannot obtain a model.
2026-09-08 — #964: quota stop after useful fixes, bank delivered locally
- Status: banked commits recovered and locally validated. The campaign did not reach its ledger or delivery gate, so this is not a successful end-to-end Billy run.
- Method:
/billyon our PR #964 at 11:26:52Z, after Revi's medium findingR565a2f. Run01a080c5-585f-7574-8394-bb28803d6f24completed planning, peer review and revision, then entered campaign at 12:06:51Z. The PR was removed from the merge queue while the fixer worked; the interactive owner made no concurrent edits to its branch. - Result: the campaign reached the Claude session limit at 12:43:31Z and stopped as
failed_resumable / USAGE_LIMIT_BLOCKED, withretry_armedfor the 15:00Z reset. That reset was after its advertised working window. The owner cancelled the parked run to disarm the retry, verifiedcancelledwith no running execution or continuation, then recovered its final bank. No campaign was relaunched. - Recovery:
iterion/run-01a080c5-585f-7574-8394-bb28803d6f24ate9b7e0576c9e7d1c9ab16ef9d3c51301c9666ed6held nine commits, preserved by fast-forward.0f9c8c0dfadds settling;fd59f26d9bounds the server join;c4393c6f3preserves the suite exit code;f913b4ea8groups imports;0d82611d9records the audit;5db295eecnarrows the fixture ownership check;cd143ff62fixes final forgiveness accounting;2a9c459fdsignals late descendants at the reclaim deadline;e9b7e0576reflows the audit. - Finding ledger: R565a2f fixed. A child gets a 500 ms settle window before a leak verdict, keyed by PID and process start time. True survivors still fail the suite and are killed through verified owned process handles. The server fixture cancels its process group and applies a ten-second WaitDelay for inherited output pipes. The final timeout sweep remains best effort: it fails the suite rather than claiming that every descendant was joined.
- Live steering: checkpoint review identified an interleaving the campaign had missed: a child can be seen alive and reaped in the same scan, then disappear through ECHILD before its forgiveness is counted. A
runs sendmessage at 12:34:29Z was consumed at 12:35:48Z. The campaign confirmed and fixed it. Independent local falsification used Go overlays to delay the per-PID reap by 1.1 seconds: the real settling-child fixture passed, then failed when only the ECHILD accounting call was removed. No checked-out source was modified by those canaries. - Validation after recovery: full Devbox
task checkpassed. Complete race suites passed for proctest, dispatcher, runview, runner, runtime, CLI and E2E, with only the previously reproduced unrelated ordering test from #960 excluded from that race invocation. The non-race full suite retained it. The proctest subprocess cases still cover a genuine surviving orphan, settling exit, clean success and preservation of an existing failure code. - Value: real fixes for a false leak verdict and an unbounded inherited-pipe join, plus a useful interaction between checkpoint review and live steering. The bank saved the work when quota prevented delivery; the owner supplied the missing final validation and ledger. Revi and CI still need to judge the published head before merge-queue entry.
- Frictions and boundaries: the final quota denial followed soft usage events reporting
stopped=false, including zero-percent readings just before the denial; those events did not establish remaining capacity. The remaining Git-maintenance gap in fixture repository config is tracked separately in #974 after verification against main. No shared infrastructure, historical orphan, other session's branch, or operator Git configuration was changed. - Independent review of the delivered bank found
Race656: a missing Linux child-scan/subreaper capability could fail or skip a whole package suite. The owner completed this bounded follow-up after the quota stop, checking first that no fixer was active on this PR. Startup now probes the scanner before enabling adoption, logs unavailability, and runs the suite normally; a post-activation scan failure still fails. A red-first capability-boundary regression covers both missing capabilities, exactly-once suite execution, preservation of success/failure results and later hard scan errors. The original process-group comment was also corrected: it is not a pidfd-backed group signal and does not prove PID reuse impossible.
The delivery tail's contract (bot 1.7.0)
Three properties push_back_tool and publish_verdict now hold, in the order they are decided. They exist because of two measured production defects (#863, #773); the tests that pin them execute the real command bodies against real git repositories (bots/push_back_banked_branch_test.go).
- A pull request that ended takes no more commits. Before pushing, the tool asks the iterion server whether the pull request is still open —
forge_pr_state_url, the read half of the run's publish grant. git in the workspace cannot answer it: a squash merge leaves the source branch present and its head no ancestor of the base, so a merged pull request reads locally as an open one. A merged or closed pull request routes the run topr_superseded(typedDECLINED): one comment naming where the commits are, no push onto the dead branch, no verdict on the head it left. No grant (a local run) means the question cannot be asked, which is never read as a closure; an endpoint that errors says so in the report and the push proceeds. - Work that cannot land is banked and NAMED. Every refusal — a merged pull request, a conflicting advance, a protected branch, a dead token — pushes the run's HEAD to
iterion/banked/<branch>-<head12>and the verdict carries the branch plus thegit fetch … && git cherry-pick …that takes it. A run whose work is reachable only by someone who already knows the convention has not delivered it. - "Nothing to push" names what it compared. The no-op reason carries both revisions (HEAD and
origin/<branch>), so a claim that contradicts the campaign's own commit count is falsifiable instead of merely surprising.
2026-09-08 — #961: orphaned campaign recovered from its workspace checkpoint
- Status: useful commits recovered and validated locally; the bot did not reach its delivery ledger or final gate. This is not a successful end-to-end Billy run.
- Method:
/billyon our PR #961 at 11:26:44Z, after Revi's medium findingR3d8dcc. Run01a080c5-3bf3-7657-a24f-d9a24d854cc4completed planning, peer review and revision, then entered campaign at 11:52:48Z. The PR was dequeued while the fixer worked; the interactive session did not edit its branch during that time. - Result: the runner lost its connection to the Kubernetes API at 12:05:43Z; delegate retries did not restore the campaign. The last persisted checkpoint event was at 12:08:35Z. Run inspection subsequently reported
failed_resumable / PROCESS_ORPHANED, continuationfinal, with no active execution or scheduled retry. We did not relaunch the campaign. - Recovery:
final_branchandfinal_commitwere null, and the commit endpoint saidavailable=false,reason=no_baseline. Nevertheless, the checkpoint refiterion/run-01a080c5-3bf3-7657-a24f-d9a24d854cc4-checkpointexisted at4e16f625d802e5a1353bbd7f4b5c7e86e52c7766. Fetching it recovered three commits on top of the PR head, which were fast-forwarded locally:bf9f8cac4caps one operation rather than consuming the package timeout;e2a91068dbounds service teardown;4e16f625drecords the race-oracle falsification under synctest. Local follow-up tightened the helper's edge cases, reports teardown timeout explicitly, and aligns the audit wording. - Finding ledger: R3d8dcc fixed. A real-process wait now takes the smaller of a three-minute operation ceiling and the remaining harness time minus 30 seconds. Explicit
-timeout=0remains unlimited. Service cleanup has its own 30-second context. The callerless oldwaitBudgethelper was removed. - Validation after recovery: full Devbox
task checkpassed; full runview and runner suites passed under-race. Independent Go-overlay canaries both failed as intended: restoring the old package-wide deadline broke the bound tests, and removing the retired-epoch guard producedDATA RACEinTestFanOutAbandonedBranchDoesNotRaceRunState. These overlays did not alter the checked-out production sources. The bot's additional unsynchronized probe is recorded in the wait audit; it was not independently rerun locally. - Value: the campaign supplied two real lifecycle fixes and measured evidence for the retained race oracle. The persisted checkpoint preserved that value despite a failed delivery. PR #961 receives the preserved commit chain and this local validation report; Revi and CI must judge the new head before it can return to the merge queue.
- Friction: recovery required discovering the checkpoint ref outside the normal final-bank and commits fields. Tracked separately in #972; this session does not change shared runner infrastructure.
- Coordination lesson:
gh pr merge --disable-autodid not remove a queued PR here. We useddequeuePullRequestwith the verified PR ID. InspectmergeQueueEntry: an emptyautoMergeRequestdoes not establish that the PR is absent from the queue.
2026-09-08 — the campaign delivered, the tail never ran: a provider cap killed the run at minute 53 and the banked branch was the only receipt (run 01a07f7a)
- Status: partial — the review work is complete and correct; the delivery tail was never exercised, so this run says nothing about it.
- Versions: bot resolved from the baked catalog on runner
iterion-runner-devbox@sha256:001a8431…(no platform override active) · server v3.115.1 (f47ff6e1) · repo manifest 1.7.0. - Method:
/billyon PR #851 (the override-staleness guard), after three rounds of hand-fixing on the same PR had produced six real Revi findings. The decision to hand it over was the point of the run. - Result: not converged —
failed_resumableat 06:18Z, 53 min in, $3.32, on the provider's own 5-hour session limit inside thecampaignnode (rate_limited (claude_code): You've hit your session limit · resets 10am). Outputs stop afterdelivery_reserve/delivery_deadline; neitherpush_back_toolnorpublish_verdictever ran. Six commits were banked onrefs/heads/iterion/run-01a07f7a-…@bf2aa2072— the campaign commits in stride, so the work survived the death of the run that made it. The pull request itself merged at 05:57Z, 21 min before he died: its gate had gone green at 05:34 on a pass that reported zero findings, and the last check cleared while he was still reading. So his final commits were written against a branch already squashed onto main, and none of them shipped with it. - Value: high, and it is the anti-hand-fix argument made concrete. Four defects, all inside code I had written and twice reviewed: a shadowed TEAM row was handed the PLATFORM remedy (
8ff4d37b3); an unreadable platform overlay was read as empty instead of unknown (9282bee42); a platform row with noversion:counted as absent rather than present (5c032a0a5); the shadow fields were promised on the team listing in prose but typed only on the admin one (8096529c0). Plus one finding he chose to document rather than fix (220ecbbf1: the catalog walk escapesplatformcfg's 3 s fetch timeout, and cancelling a blocked filesystem walk would trade bounded blocking for a goroutine leak per TTL) — the right call, argued in the commit body. - Findings / misses: the sharpest one is
bf2aa2072— two comments that my own commits had inverted.41c38d43cwrote "the dedup check runs BEFORE the catalog walk";6f08d55f1then moved theLoadOrStoreafter the comparison on purpose and left the claim standing, so the file documented the opposite of what it did. A reviewer reading one chunk at a time does not catch that; a campaign that re-reads the whole branch does. - Engine hardening: none from the bot. The run surfaced a diagnostics defect elsewhere — see the cap note below.
- Lessons for next run:
- The delivery tail is the fragile half, and a long campaign will keep dying before it. 53 min of campaign against a 5-hour provider window is a coin flip. The banked branch is what saved this run, and it is reachable only by someone who knows the ref convention (#773 again, from the other side: here there was no verdict at all to name it).
- When the tail cannot run, the work is still deliverable by hand, but only if it is verified independently:
go build ./..., the fullpkg/serversuite, andtask openapi:genproducing no diff. That is a better receipt than the bot's own verdict would have been. - Cancel the parked run before taking its commits. A
failed_resumablecampaign has a retry armed (1/5, here for ~10:10Z); left alone it resumes onto a branch that has moved. - Re-read the pull request's state immediately before pushing — taking a campaign's commits by hand means taking its guards too. #851 merged at 05:57Z, while the campaign was still running; the operator had last read
OPENat 05:36Z and pushedbf2aa2072onto the closed branch an hour later. Nothing was lost (a merged branch still accepts commits, they are simply orphaned) but the work needed a second pull request onto main. This is the exact casepush_back_toolinterrogates the server about, and it is worth restating why git alone cannot answer it: after a squash merge the source branch still exists and its head is no ancestor of the base, so a merged pull request reads locally as an open one. The bot's contract was written from this failure; the hand path has no such contract, so the check has to be deliberate.
The cap that stopped it — and the message that misdiagnoses it
Worth recording because it cost a wrong recommendation. Eight runs parked the same morning with:
usage cap: provider rejected on the seven_day window (week cap 85%, hard),
resets 2026-09-08T21:00:00ZThat reads as "your 85 % weekly cap fired". It did not. In pkg/usagecap/usagecap.go a provider refusal short-circuits the threshold test (if !rejected && pct < wp.MaxPercent), and the branch that prints this message is rejected && pct == 0 — the provider refused and gave no utilization number. The 85% is the configured cap printed beside a comparison that never happened. Raising it to 95 or 100 would change nothing: Enabled() is MaxPercent > 0, so only 0/off disarms the policy, and that would merely move the failure from pre-flight to the API call.
The real shape was two exhausted tiers, which the publisher log states plainly: the org forfait (fp=2b36a854…) refused on its five_hour window until 10:00Z, falling through to the platform credential (fp=53d82df9…), refused on its seven_day window until 21:00Z. Both empty means no claude_code capacity at all. The outage therefore ends at 10:00Z, not 21:00Z, and the retries armed for ~10:00–10:10Z target the right reopening.
Read the cloudpublisher: … SKIPPED … fp= lines before the cap record: they name the credential, the window and the reopening. The run error names none of the three.
2026-09-06 — 1.6.0 dogfooded live: three runs, the third validates the reserve and finds a real bug in the code it reviewed (runs 01a07804, 01a0782f, 01a07840)
- Status: validated (run 3) — after two runs that validated nothing about the bot and everything about how a bot reaches production.
- Versions: bot
branch-improve-loop1.6.0 · run 1 on runner v3.112.7 (engine WITHOUT the variadicmin/maxof #830) via a platform override · run 2 on runner v3.112.14 with the server still at v3.112.6 · run 3 on server v3.112.15 (98334d181) + runner v3.112.14 (ba3db5361), baked catalog. - Method:
/billyon a real PR each time, cloud, sandboxed, claude_codeclaude-opus-5on the anthropic forfait;plan_reviewpeer = claw /openai/gpt-5.6-sol(platform codex forfait). Run 1 on #850 (a stacked PR), runs 2–3 on #855 (one commit, Revi green, nothing blocking to fix). - Result:
- run 1 (
01a07804, 18:38Z, override pushed withiterion remote admin bots push): plan phase fine, thencompute "delivery_reserve"→expr: max() takes …— the runner's engine predates #830 — and the runner auto-resumed the same deterministic failure seven times in ten minutes (a fresh sandbox each time) until cancelled. Also:git … fix/board-dispatch-launch-refusal-is-transientfailed in the plan step — the per-run clone does not fetch a PR base that is not the default branch. - run 2 (
01a0782f, 19:25Z, override removed, runner bumped to an image carrying 1.6.0):plan_budget_gate → campaignwith nodelivery_reservenode — the run was 1.5.x. The SERVER resolves team → platform → baked from its own baked catalog; bumping the runner alone changes nothing. Cancelled; server restarted. - run 3 (
01a07840, 19:45Z):delivery_reserve→{cap 10800, reserve 1620, window 9180, left 8632},delivery_deadline→2026-09-06T22:19:53Z, campaign prompt carriesSTOP WORKING AT (UTC): 2026-09-06T22:19:53Z— the checklist's numbers exactly.plan_reviewhit429 usage limiton the codex forfait three times → ADR-091 skip route,plan_gatestampedprovenance: … SKIPPED mid-run. Campaign 64 min, $8.47,commits_this_pass: 6,stopped_on_reserve: false,declined: false;verify_runexit 0;gate {converged: true, ship_now: true};publish_verdictpostedrevi/review=success.
- run 1 (
- Value: high, and not the value expected. The expected outcome was a
DECLINED(nothing to fix on a green PR). Instead the campaign judged Revi's two sub-gate findings real, then found a residual race in the very fix under review (#855's early-accept was a READ two syscalls before the retire it exists to prevent, so #854 stayed reachable), reproduced it with a scripted three-party interleaving on the pre-fix code, fixed it (the rename becomes the test-and-set), added the first test of an untested arm, and corrected three comments that overstated the code — with a green gate (task test136 packages,-race -count=30, a 9,600-publisher stress). Six commits, all real. Re-based as PR #862. - Findings / misses:
- The campaign pushed those six commits at 21:04Z onto a branch whose PR had merged at 20:34Z, and its verdict landed on the pre-merge head: the stop-on-close cancel covers reviewers, not fixers, and the delivery tail never checks that the PR is still open. Real work, invisible (#863).
reserve_secondsand the absolute deadline are correct; whether the agent HONOURS the deadline is unproven — this run finished at 39 % of its window. The 40-minute-cap re-launch of the checklist is still owed.- The
DECLINEDpath is still unexercised live (the PR had things to fix). - The codex forfait was exhausted from ~18:47Z on (Saturday evening); every
plan_reviewof the evening was skipped — the plan review's value is zero on a shut window, by design and visibly (#647).
- Engine hardening: #857 (P1 — a deterministic node failure is auto-resumed in a loop), #858 (a platform override can outrun the runner's engine, no guard), #859 (a stacked PR's base ref is not fetched), #863 (a fixer keeps working after its PR merged), and
docs/platform-bots.md§ "Shipping a baked-catalog change — two halves" (#861). - Lessons for next run: bump the runner image AND restart the server before dogfooding a catalog change; never use a platform override for a bot that needs an engine feature newer than the deployed runner; pick a PR that will still be open for the whole run (or expect #863); run the 40-minute-cap variant next to prove the deadline is obeyed, and a truly-clean PR to prove
DECLINED.
2026-09-06 — the delivery reserve, the no-op terminal, and two gates that lied (no run; bot 1.6.0)
- Status: partial — validated by the engine and by shell-level tests of the real command bodies, NOT by a live run. Four issues closed in one branch (#705, #706, #779, #789); every one of them was measured on a production run, and three of the four change what the campaign is told, which only a dogfood can confirm actually lands. The live checklist is at the end of this entry.
- Versions: bot
branch-improve-loop1.5.0 → 1.6.0 · iterion at3d7c46c1c841(branchfix/billy-delivery-reserve-decline-and-gates). - Method: no LLM run. Each change is pinned by a test that was RED first: the drift/masking gates by executing the real
verify_runbody against fixture repositories (bots/verify_run_drift_test.go), the decline oracle by executing the realdecline_probebody (bots/decline_probe_test.go), the reserve arithmetic and the decline routing through the ENGINE with the scenario stub (e2e/branch_improve_delivery_reserve_test.go,e2e/branch_improve_decline_test.go), and the engine half of the decline through the real webhook/gate lanes (pkg/server/…_decline_notice_test.go).
#705 — the delivery reserve, and why the cap moved
Three runs died within 2 s of the same cap (9001/9000, 2h30m01s/2h30m, 9001.86/9000) having pushed nothing. Landing in the last two seconds three times is not luck: the campaign spent the whole window and the tail that ships — verify gate, in-loop review, push, verdict, merge-gate status — was never scheduled, so the work existed only in the run's context.
delivery_reserve is one deterministic compute, the single choke point every forward entry into campaign crosses (a TestBranchImproveLoop_…IsTheOnlyWayIn guard fails if a new entry bypasses it):
reserve = min( max(floor_minutes × 60, cap × ratio), cap / 2 )
read from run.max_duration_seconds — the cap IN FORCE after any --max-duration, the recipe and the platform ceiling — exactly like plan_budget_ratio, because a literal mirroring the budget: block drifts from it in silence (the mistake 1.5.0 already paid for). A cap of 0 stays UNBOUNDED, and the /2 clamp guarantees the campaign at least half the run whatever the knobs are set to.
The deadline is a constant of the run, so the continuation loop needs no second copy of the arithmetic on its back-edge: the campaign's prompt pairs {{outputs.delivery_reserve.work_deadline_seconds}} with {{run.elapsed_seconds}}, which resolves fresh at every pass.
max_duration 2h30m → 3h, deliberately. Those three runs measure the campaign's appetite at more than 2h30m of pure campaign, so carving the reserve out of 2h30m would have traded "ships nothing" for "does less work" — not a fix. At 3h the default reserve is 27 min (against a ~16 min tail measured on this repo: verify ~10, in-loop review ~5, push + verdict ~1) and the campaign still gets 2h33m, more working time than the dead runs had. max_cost_usd is untouched: all three died on duration, cost never binding.
New knobs, both overridable per run: delivery_reserve_ratio (0.15), delivery_reserve_floor_minutes (10).
#706 — the no-op terminal
The heal lane launched the fixer on a green PR a flaky test had ejected. The bot concluded "no code issue in the diff — this is a re-queue, not a fix", recorded that a queue build was in flight and that pushing would cancel it, and its mission said push anyway. declined/decline_reason make that an outcome; decline_probe makes it earned (HEAD unmoved since workspace_probe, tree clean) rather than asserted — a pass that committed anything ships through the ordinary tail instead of stranding its commits behind a terminal failure. Earned, the run ends fail campaign_declined: code: DECLINED, non-resumable.
The engine half is generic and keyed on the code alone: relaunchDeadGateRun (the one point every relaunch crosses) stands down, the auto-fix lane does too and posts the reason on the PR, and the auto-heal mission now grants the refusal in as many words. Full contract: docs/merge-gate.md § DECLINED.
#779 / #789 — two gates that lied, fixed fleet-wide
Both are in the verify_run body shared by ten catalog bots, so both were fixed at every site:
- #789 —
has_drift_gateread the script one line at a time and only counted a quiet diff when the failing exit was on the SAME line, so the commonest real shape (if ! git diff --quiet …; then … exit 1; fi, which is what this repo's ownopenapi:checkwrites) was rejected: run 01a072b5 delivered eight commits, loggedVERIFY OK, and still returned exit 3 /DRIFT GATE MISSING. Detection is now structural, comments are stripped first (a commented-out gate used to count — that is how prose gamed it), and a commit-if-changed block still counts for nothing. - #779 —
… 2>&1 | tail -10; echo "EXIT=$?"printedEXIT=0for a 127 (run 01a07283, a missingtsc).pipefailis not POSIX and these run under/bin/sh, so the fix is not a reminder:verify_runnow refuses a verify.sh that pipes a top-level command into an output filter with MASKED EXIT STATUS (exit 5) before running it and moves the script aside so it is regenerated, and every campaign contract carries the CAPTURE THE STATUS, THEN FILTER clause (guarded fleet-wide bybots/status_capture_clause_test.go).
Engine hardening found on the way
min/maxwere array-only, so a clamp had to be four nestedifs. They are now variadic (one array, or two or more values) —pkg/dsl/expr.- The ship path had to become the
elsefallback. Withcampaign -> verify_probe when not declined, an output that OMITSdeclinedmatches neither edge and the run diesNO_OUTGOING_EDGE— caught by the existing e2e stubs, and it would have hit any resume of an older checkpoint. Silence must mean ship, never decline. {{outputs.X}}is not substituted in a tool command body (onlyinput/vars/secrets/run.id).decline_probehad two, which would have made it refuse every decline for want of an entry head — a guard that looks like it works. Caught bybots/catalog_command_refs_test.go.DECLINEDmust NOT be astore.FailureCodeconstant: that block is the set a workflow may not mint (C248), so declaring it there would have made the bot's owncode: DECLINEDa compile error. Caught bypkg/store/lifecycle_reserved_test.go.
Review round on PR #830 (Revi, mono topology)
Three findings, all reproduced before fixing — each one a real defect the tests did not cover:
- R69a603 [high] — the decline notice was posted from
autofixForRun, which the reconciliation sweep re-enters once a minute for a 60-minute lookback. A declined run is terminal, so itsupdated_atnever moves and it stays in the window the whole hour: ~57 identical comments per replica. Reproduced at 3 comments from 3 offers. Fixed with the subsystem's own discriminator —autofixForRunID(ctx, runID, via), the shapereconcileGateForRunIDalready has, with the notice ongateTriggerEventonly. Revi suggestedev.Kind != ""; that works but is incidental (the sweep's event just happens to have no kind), so the discriminator is explicit instead. NB the review's premise that the sibling notices dedup by scanning their marker is wrong:noticeGateDLQParked/noticeGatePausedForRetrycarry no marker check — they are wrapped invia == gateTriggerEvent, andforgeIssueCommenterdeliberately cannot list comments. One mechanism, and it is that one. - R8f498c [high] —
bots/app-devwas the 11th verify.sh carrier and was in neither the port list nor the fleet guard's hardcoded map, so its skill promised a structural drift gate and MASKED EXIT STATUS its ownverify_rundid not implement. Ported, and the root cause fixed with it: the guard now discovers carriers (any bot whose tool command containssubprocess.run(['sh', script]— the executor, notverify_probe'ssh -n), so a 12th cannot be forgotten, and a second guard cross-checks eachskills/verify-build.mdpromise against its own bot. - Rce6c53 [medium] — the campaign was handed
work_deadline_seconds/elapsed_seconds, resolved once when the prompt was built.run.*has nostarted_atand a tool body substitutes onlyrun.id, so the agent had no way to convertdateinto run time: "check it again" had nothing to check. Relying on an agent's unaided sense of elapsed time is the exact failure the reserve exists to remove. Newdelivery_deadlinetool node stamps the window as a UTC instant (python3, not GNUdate -d), the contract tells the agent to compare it withdate -uafter every fix, and being absolute it stays correct across continuation passes without re-running.
Both open questions answered, with code:
- Does anything clamp the 3h cap on the pod? Yes, potentially:
pkg/runner/loop.go:2415applyCloudBudgetCeilingreadsITERION_CLOUD_MAX_DURATION(with_MAX_ITERATIONS/_MAX_TOKENS/_MAX_COST_USD/_MAX_PARALLEL_BRANCHES) and callsir.Budget.ClampToCeiling, which setsCapImposed— and an imposed cap refuses the budget exit grace. The actual values live in the infra repo, not here. PodactiveDeadlineSecondsis k8s, also out of tree; theITERION_CLOUD_RETRY_*ceiling only lowers a retry policy, never the duration cap. The reserve is unaffected by construction: the clamp mutateswf.Budgetatloop.go:1867, beforeruntime.Newbuilds the tracker from that same budget (engine.go:631newSharedBudget(e.workflow.Budget, …)), sorun.max_duration_secondsis the POST-clamp cap. Pinned byTestBranchImproveLoop_DeliveryReserveFollowsAPlatformClamp, which drives the realClampToCeiling: at a clamped 2h30m the reserve is 22.5 min, still over the ~16 min tail. On a clamped pod the reserve is in fact the only protection left, since the grace is refused there. - Does a
stopped_on_reservepass leave the loop? It did not — reproduced at 9 passes.branch_cleanis honestly false, so the back-edge re-entered and the next pass's verify+review tail came out of the reserve; the loop budget guard is a backstop (it declines a back-edge it cannot FUND, a different question) and is switchable off.gatenow carries a separateship_now= converged ∨stopped_on_reserve, and the exit edge reads it.convergedis deliberately NOT widened:publish_verdictposts it as the merge gate's build verdict, so a pass that ran out of time must not green it.
Also worth recording from the review's non-blocking questions, answered but not changed: entry_head is captured at RUN start, so a pass-3 decline after passes 1–2 committed is refused — run-level granularity is intended (the run as a whole did change the branch, and shipping it is the safe outcome); and the decline notice fires regardless of AutoFixOnGateFailure on purpose — it is not an unattended action, it is telling a developer what a bot they themselves triggered decided.
Lessons for next run
- Write the differential, not just the assertion. The first version of the "a declined run launches nothing" test passed without the change — the auto-fix lane already refuses to relaunch a fixer off its own verdict. The fix was to assert, on the same fixture, that a non-declined failure DOES launch.
- A fleet-shared body is a class:
verify_runis copy-pasted into ten bots, and the presence guard (TestVerifyRunDriftTailPresentInAllBots) is the only thing that keeps them from diverging. Both fixes were scripted across all ten rather than applied to Billy alone.
How to verify live (next dogfood)
- Reserve, on the happy path.
/billyon a real PR here. In the run view, check the campaign's task prompt carries three non-zero figures (cap 10800, window closes at 9180, elapsed) and thatoutputs.delivery_reserve.reserve_secondsis 1620. The run must reachpublish_verdict— the whole point is that the tail happens. - Reserve, under pressure. Re-launch with
--max-duration 40mon a substantial diff: the reserve should read 600 (the floor beats the 360 s proportional slice), the campaign should reportstopped_on_reserve: truewith a populatedissues_remaining, and it must still push and post its verdict. That is the case the three dead runs failed. - Decline. Comment
/billyon a PR with nothing to fix (or let the heal lane fire on a flaky-test eject). Expect: runfailedwithfailure_code: DECLINEDiniterion remote runs list, the reason verbatim inrun.error, a<!-- iterion:fixer-declined -->comment on the PR, and no relaunch and no push. Confirmgit logon the PR branch is unchanged. - Decline refused. Harder to stage; if a run ever reports
declinedafter committing, the run must finish normally and its commits must land. Check thedecline_probeoutput saysHEAD moved. - The two gates. On a pass whose
verify.shmirrorstask openapi:check, the gate must go green (before 1.6.0 it returned exit 3). If a pass ever returns exit 5, readverify.sh.rejectedin the scratch dir — that is the masked pipeline it was refused for. - Then: cost, duration and where the commits landed, as usual.
2026-09-05 — PR #769 follow-up: drain recovery and local credential destinations
Delivery update, 2026-09-07: this is the historical record from the closed, unmerged PR #769. #824 superseded that PR; these observations describe its branch, not the implementation on main. #896 restores this record and follows up the board lookup retry that the replacement still lacks.
- Status: fixes delivered; independent validation pending. Run 01a072b5-6010-70b2-8df0-3e78458c92ef finished at 19:51:47Z after 1 h 55 m 23 s active. Target: PR #769 / issue #702.
- Method: a focused
/billyfollow-up named Revi findingsRbdd8d5andR47ce28, requiring the local/team fork guard to remain intact. The interactive session reviewed the published diff after the run stopped; no interactive push overlapped the fixer. - Result: eight commits,
76fcea019through78358ec2460c, pushed and banked oniterion/run-01a072b5-6010-70b2-8df0-3e78458c92ef. Both finding IDs are recorded as fixed in the publisher's PR review. A pre-launch retry now outranks replica draining, while a post-launch drain still leaves the live run alone. Unpinned local tokens reach only the exact canonical HTTPS origins of GitHub, GitLab and Codeberg; other origins require the operator's host pin. - Additional value: a refused return-to-ready write retains the claim, rather than releasing an unfiled card with no run pointer; the local forge base canonicalises host case; and the shared head-verification function rejects repository path traversal before building a request. Tests include actual authenticated TLS requests, their zero-request negative control, recovery after draining, and failed final-state writes.
- Verification: the campaign reports
devbox run -- task checkpassing (136 packages, zero failures) and focused race tests passing. One earlier concurrent suite reported a failure, followed by three clean reruns; resource contention was the bot's inference, not a reproduced cause. The finalverify_runreturned 3: build, vet, server tests and OpenAPI generation succeeded, but the deterministic tail rejected the script's quiet-diff check as a missing drift gate. This is not a green bot gate. The detector only recognises a quiet diff when a failing exit appears on the same line; the repository itself also uses a multiline failing conditional. The friction is tracked in #789. - Delivery follow-up: merging current
mainconflicted only in this bilan file. Both histories were retained verbatim, and this second-run record was added. The merged tree passed whole-module build and the server PR-guard/dispatcher regressions under-race, the full server and dispatcher package suites, and the actualtask openapi:checkwith no generated-file drift. Required GitHub checks and independent Revi must validate the updated branch before protected merging; no status override is used. - Lessons: test the outbound request count, not only the HTTP refusal; preserve the stronger pre-launch fact during draining; and distinguish a test failure from a verifier that rejects the shape of its own script. Claim-retention recovery still needs the opt-in claim watchdog, as the forge integration documentation explicitly records.
2026-09-05 — PR-launch guard: local credentials and bounded board retry (#769 / #702)
Delivery update, 2026-09-07: this is the historical record from the closed, unmerged PR #769. #824 superseded that PR; these observations describe its branch, not the implementation on main. #896 restores this record and follows up the board lookup retry that the replacement still lacks.
- Status: completed and delivered; independent Revi re-review and GitHub CI remain pending at this entry. Auto-merge stays off until those checks validate the final branch.
- Run:
01a0723b-acfb-71a9-9ae1-0d9b217e6cbb, 15:41:48–17:23:49Z, 1 h 40 m 31 s active. Target: PR #769, issue #702. - Method: the red-gate lane launched Billy against Revi's R57f074 (local studio launches refused) and R48e8f3 (transient forge failures permanently block board cards). The interactive session monitored the run and sent two scope corrections; it did not edit the branch while Billy ran.
- Result: seven commits pushed, head
6acd74d17da6934730b882db307bbd243880dcdb, also banked oniterion/run-01a0723b-acfb-71a9-9ae1-0d9b217e6cbb. The publisher's PR review records both finding IDs as fixed and reports the seven-commit push. The post-campaignverify_runreturned exit code 0 (whole-module build, formatting, vet and the touched server package tests), followed by publication and the terminalfinishedstate. - Value: the local and team launch paths now share
verifyPRHeadInBaseRepo. Local studio resolvesforge_tokenfrom its layered store, respects its host pin, refuses when verification is unavailable, and explains the missing credential. A fork or unnamed head creates no run. The board returns a failed pre-launch lookup to the eligible state under the claim token, waits 30 seconds, and escalates after five attempts. This retry marker is attached before launch, so a later run failure cannot relaunch the card through this arm. - Validation: the campaign reports lint with zero issues, 135 unit-test packages, the e2e suite, coverage-matrix gate, and the new tests under
-racepassing. Both headline regressions were observed red before green. The session inspected the published code and tests: local same-repo success, fork/unnamed-head refusal, absent/off-host credentials, recovery after an outage, retry backoff/budget, and the real board-path marker. - Friction: the first fix reinstated a team-only bypass from Revi's proposed replacement. That contradicts #702: a base-repository checkout plus a fork's head branch is unsafe even without a publish grant. Steering corrected it in
e86bccaecea1; accepting a suggested patch mechanically would have reintroduced the defect. The proposederrCardContinuablealso would have stranded a never-launched card inin_progress; the implemented return-to-ready path addresses that instead. - Limits and lessons: retry counters are per replica, and the current forge client does not distinguish a permission-denied 403 from a secondary rate-limit 403. The guard proves the PR's head repository, not the local checkout's remote or HEAD SHA. Keep those limits explicit, verify the original issue against the final diff, and wait for independent Revi rather than treating the fixer's own green gate as the final review.
2026-09-05 — the plan-budget guard reads the run, and both refusals are typed (no run; bot 1.5.0)
Status: validated by the engine, not by a live run — this is a bot change made against two engine features that landed the same day (PR #764: the
run.*expr namespace, #738; the typedfail <name>:node, #739), issue #752/#762. The next dogfood should confirm the refusal reads right in the studio and that a realiterion remote runs resume --max-cost-usd …walks past the guard.Versions: bot
branch-improve-loop1.4.0 → 1.5.0 · iterion atc2898c08f(v3.108.1, the first build carryingrun.*and typed fails).Method: no LLM run. The guard is exercised through the ENGINE with the scenario stub against the bot's own shipped
budget:block (e2e/branch_improve_loop_test.go, five cases): the stubbed plan nodes bill a chosen amount, and the readout is which tail the run took.Result — what changed in the bot:
plan_budget_gateis now onecomputereadingrun.elapsed_seconds/run.cost_usdagainstrun.max_duration_seconds/run.max_cost_usd. It replaces the tool node that shelled out to python fortime.time()arithmetic.- The two mirror vars are gone (
budget_max_duration_minutes/budget_max_cost_usd). They existed only because no primitive exposed the run's caps, and they were kept in sync by hand:iterion run --max-cost-usd 200re-budgeted the run and never reached them, so the guard went on refusing against a literal75nobody had updated. Therun.max_*members are the caps IN FORCE — after the CLI flags, the recipe, the platform ceiling and any liveraise_budget— so the drift is structurally impossible now, not merely documented. (TestBranchImproveLoop_PlanBudgetFollowsTheCapInForceis that property: the same $37.50 spend refuses under the shipped $75 cap and passes under a re-budgeted $200 one.) plan_scope_probe'sstarted_epochis gone with it; the run's clock is monotonic and survives a resume, which atime.time()stamp did not.plan_cost_probeis gone: it existed to hand the tool node a nil-safe per-node cost SUM. Nothing but the plan phase has spent when the guard runs, sorun.cost_usdIS the phase's spend — and it also counts anything a hand-written sum would have forgotten. Its relay role moved onto the guard, which now receives the hand-off from all three upstream routes directly.- Both refusals are named fail nodes, so the code reaches the RUN (
failure_code/error) instead of only the guard's output:plan_exhausted(PLAN_BUDGET_EXHAUSTED, resumable) andworkspace_not_a_repo(WORKSPACE_NOT_A_REPO, terminal). The 09-05 dogfood below recorded both as debt: two production runs endedfailedreadingworkflow reached fail node, and the operator had to open the artifacts to learn which refusal fired.
How to resume a
PLAN_BUDGET_EXHAUSTEDrun. The refusal isfailed_resumableand the checkpoint anchors onplan_budget_gate, not on the fail node — so the resume re-evaluates the guard against the caps then in force:shiterion resume --run-id <id> --file bots/branch-improve-loop/main.bot \ --max-duration 5h --max-cost-usd 150 # or, same effect from the other side: iterion resume --run-id <id> --file … --var plan_budget_ratio=0.6The plan phase this run already paid for is NOT re-run (the checkpoint keeps
plan/plan_review/plan_revise's outputs); the campaign starts on the plan already in hand. Nothing picks the refusal up by itself — not--auto-resume, not the cloud runner's retry: a deliberate refusal only changes verdict when an operator changes an input. Widening the cap is the only thing that flips it, which is exactly why re-paying the plan phase would be the wrong cure.Value: the two workarounds the 09-05 bilan filed as debt (#738, #739) are both retired, and the guard's arithmetic can no longer disagree with the budget the run is actually under.
Findings / misses (engine, filed as observations for #762's PR):
{{run.*}}renders EMPTY inside afailnode'smessage:— the fail message resolves throughresolveMapping(scope), which carriesoutputs/varsbut not the run snapshot the expr evaluator and the prompt/tool templates read. Measured on a probe bot:run.elapsed= cap=. The bot works around it by putting the four figures on the guard's own output and referencing{{outputs.plan_budget_gate.…}}, which is arguably better anyway (the numbers are then also in the artifact) — but the authoring instinct is to write{{run.cost_usd}}there and get silence.- A tiny
max_durationcannot be the lever for a deterministic test of the duration axis: the engine refuses a new node at 90% of a cap, so with a small enough capBUDGET_EXCEEDEDfires before the guard ever runs. The e2e drives the COST axis instead (a figure the stub sets exactly); the duration axis is covered by the> 0unbounded case and by the guard's own expression. - A compute's output schema does not coerce a float into an
intfield (probed:used_pct: intkept0.0952…), and float refs render at full precision in a template. The gate therefore reports seconds and USD rather than a rounded percentage.
Lessons for next run: launch with
--var plan_budget_ratiosmall to force the guard as before, then checkiterion remote runs listshows PLAN_BUDGET_EXHAUSTED (not FAIL_NODE) and that the resume with a widened cap startscampaignwithout re-runningplan.
2026-09-05 — lock-delivery follow-up on PR #770: an "open question" was a two-part defect, and one comment cited a function that never existed
- Status: delivered. Run 01a07283-16f2-7b55-bf66-db10c5453fdf, a focused follow-up to
01a07243on the same PR #770 / issue #703. - Method: seeded with Revi's single new finding
R1dca02plus five open questions, and with a plan a cross-model peer had already critiqued. The peer's catch changed the shipped result — see below. - Result: nine commits,
38f2d98e2…eaa33d0ce.R1dca02fixed (the non-contention lock class logs at Error again, so a broken lock store raises a tracker event instead of a breadcrumb nobody ships); one open question promoted to a defect and closed; one phantom cross-reference corrected; two ratchets landed. - Verification — passed with the exit code captured before any pipe:
task test(rc 0),go test ./e2e/...(rc 0, 769s),task lint(rc 0),go test -raceon runner/queue-nats/server (rc 0), and the three studio targetsstudio:lint/studio:typecheck/studio:test(rc 0, 1308 tests). Unavailable locally, left to the GitHub checks: themongo-conformanceandnats-conformancejobs (each needs a service container) and the Playwright UI suite (opt-in browser download). - Verification gotcha worth keeping: the first attempt at each of these piped through
grep/tailand then read$?, which reports the LAST pipeline stage, not the test command. A studiotscinvocation that died onexit 127(nonode_modules) printedEXIT=0that way. Redirect to a log, save$?immediately, then filter — or setpipefail. - Value — the open question was the bigger finding. Registering LockTTL in
RedeliveryWindowis the obvious half, and on its own it is inert:cmd/iterion/server.go'snatsq.Connectliteral never passed LockTTL, soapplyDefaultspinned the sweeper's own connection to 60s and the widened formula would have read a value no deployment configured. Passing it also closes a second, pre-existing hazard this branch made load-bearing —EnsureSchemawrites the KV bucket TTL fromcfg.LockTTL, so server and runner disagreeing meant the effective lease lifetime flapped by restart order. Neither edit protects anITERION_LOCK_TTL=15mdeployment alone. - Findings the review missed:
archiveLockFailure's audit-deadline comment justified itself as "same hazard, same remedy as parkAdmissionMismatch's status flip" — a function that exists nowhere in the tree, and whose real sibling (parkOnDLQOnFinalDelivery) does the OPPOSITE, handing its spent publish context straight to the status flip. A citation asserting a settled pattern for a remedy no other site applies. - Ratchets: the log-level regression asserts the hook LEVEL, not the message (verified failing against the unfixed tree first); and a wrapped-
ErrLockHeldtest pins the classification against the shape production actually delivers — every other double returns the sentinel bare, so swappingerrors.Isfor==kept the whole suite green while the fleet would page on every sibling collision (verified: that one token turns the new test red and nothing else). - Lessons for next run: an "open question" in a review is not automatically out of scope — this one was a real defect whose fix needed a second edit in a file the reviewer never named. And a comment citing a precedent deserves the same grep a code reference gets; a phantom name reads as authority.
- One open question answered with evidence rather than left open: a foreign
run_delivery_exhausteddoes NOT disturb a live run's observers, and the reason is narrow enough to be worth a comment at the emission site.alert.Managertreats any event as liveness (it clearsstallAlertedand can fire a spuriousstall_recovered) — but it is fed only by the localevents.jsonltailer and in-process run observers, never by the Mongo store this path writes to; and the cloud twinalert.OpsDispatcherfilters the bus toKindRunFailed, which a store event never becomes. Wiring a cloud event source into the Manager would turn this row into a false liveness signal. - Left deliberately:
MaxAckPendingheadroom during a lock outage and sweeper-vs-operator DLQ-replay ownership — genuine open questions, not findings.parkOnDLQOnFinalDelivery's inherited publish context is a real smell but pre-existing and outside this branch.
2026-09-05 — lock-delivery hardening on PR #770: seven commits delivered; the publisher still says nothing was pushed
- Status: first pass delivered and verified; review follow-up pending. Run 01a07243-13f3-7229-8aea-801a2fc3569e finished on 05/09 at 16:46Z, with 54m42s of recorded active duration. Target: PR #770, issue #703.
- Method: a maintainer
/billycomment named Revi findingsRd41f5dandR1cae68; auto-merge stayed off. Plan, peer review and revision preceded the campaign. No interactive session edited the branch while Billy ran. - Result: seven commits reached the PR, from
b8166f730toeb58773074357201f544d11525090a262be732a3. The run's final commit equals that PR head; its storage branch isiterion/run-01a07243-13f3-7229-8aea-801a2fc3569e.fca63105egives the audit an independent deadline after the DLQ publish;9a2b771afdistinguishes confirmed contention from unconfirmed ownership. Further commits correct the lost-PubAck wording, document both unknowns, add the event to the studio union and update the coverage matrix. - Proof:
TestExhaustedPublishDeadlineStillRecordsTheAuditRowwaits until the publication context expires and uses a store that honours cancellation; the old implementation loses the audit row. The reason-classification regression rejects both an asserted owner and asserted absence when the lock service did not answer. Billy's verification gate returned exit 0 (format/build/vet, touched Go suites, race checks and lint); the new head's NATS conformance CI also passed. The full PR test check was still running when this record was written. - Value of the peer review: a failed lock acquisition does not prove absence of an owner, and a missing PubAck does not prove the DLQ copy is absent. The final code reports both as unknown instead of inviting an unsafe replay or discard. No lock-failure branch mutates the run outcome or checkpoint.
- Friction: despite the campaign having pushed all seven commits,
publish_verdictopened its review with “No commits pushed. nothing to push: HEAD not ahead of origin/codex/fix-703-lock-delivery-dlq” and reported a failure status. The same review then correctly listed the delivered commits and fixed findings. This is additional evidence for #773, not missing work in this run: the PR head and final commit were equal. Revi's independent review subsequently replaced that status with success. - Remaining work: the independent review found
R1dca02— infrastructure lock failures had lost their error-level log, suppressing the tracker event. Keep auto-merge off for the follow-up correction. Also check that a configured lock delay larger than AckWait is represented in the redelivery window used by the queued-run sweeper.
2026-09-05 — plan-budget guard dogfooded on prod: the gate fires typed, before campaign, for $0.67–$1.80 (runs 01a0714a, 01a07156)
Status: validated (the guard itself; the entry below, written before any live run, is superseded on its "unvalidated" point).
Versions: bot
branch-improve-loop1.3.0, pushed to the prod platform bot-override tier (iterion remote admin bots push bots/branch-improve-loop --slug branch-improve-loop) so the runners used it without an image rollout · iterion runners8727674c(v3.102.6).Method:
plan_budget_ratio=0.001forces the guard (2h30 × 0.001 ≈ 9 s of plan phase, so any real plan trips it); target SocialGouv/iterion#749;post_to_board=false, auto-merge off. Two launches:iterion remote runs launch --bot branch-improve-loop --var pr_url=…— which attaches NO repository, so the plan node authored a plan against an empty/tmp/iterion— thenPOST /api/runswithrepo_url+repo_ref+connection_id, the only shape that carries a checkout.Result:
run repo active cost LLM nodes served outcome 01a0714anone (empty workspace) 5 m 21 s $0.67 plan, plan_review, plan_revise plan_budget_gate→fail;campaignnever entered; 0 push01a07156#749 checkout 7 m 54 s $1.80 plan, plan_review, plan_revise same Both runs end
failedwith the engine's ownworkflow reached fail node: the typedPLAN_BUDGET_EXHAUSTEDlives on the gate node's output only, because afailnode cannot carry a code yet (#739), and afailterminal is non-resumable by design — the right call for a probe, the wrong one for an operator who would rather widen the plan budget and continue (#739's follow-up comment).Value: the class of death of the three runs below — 2h30 of planning, zero commits, $8.59 — is closed. The guard stops the run at the plan phase's own ceiling, having spent 1–2 % of the budget instead of all of it.
Findings / misses (each filed):
plan_review=offskips the WHOLE plan phase, not only the peer review, so a single-provider deployment never plans (#751); six of the seven campaign bots start an opus-class plan node without checking the workspace is a repository —01a0714apaid $0.67 to plan against nothing (#752); the guard has to self-measure wall-clock and mirror the budget through two hand-maintained vars because no expr primitive exposes the run's elapsed budget (#738); a bot-declared refusal reads as "workflow reached fail node" at the top level (#739).Engine hardening: #751/#752 (bot-side) and #738/#739 (engine) in flight.
Lessons for next run: attach the repository through
POST /api/runs(repo_url,repo_ref,connection_id) — a CLI--botlaunch carriespr_urland nothing to check out; force the guard withplan_budget_ratiorather than waiting on a real long plan. The 04/09 friction this closes: Billy01a06d80, launched by the zero-touch lane on #683's red gate, died 4 s overmax_duration(2h30m01s / 2h30m) with a plan and nothing pushed, and the deployed binary had no re-budget flag onremote runs resume(#689, since merged) — exactly the shape the guard now refuses in the first minutes.
2026-09-05 — plan-phase budget guard shipped (native:695); three production deaths never reached campaign (runs 01a06d80, 01a06e72, #705)
Status: failed (the three cited production runs, pre-fix) — the fix itself is unvalidated by a live dogfood as of this entry; see "What the session should dogfood" below.
Versions: bot
branch-improve-loop1.3.0 (this change) · the three cited runs were on 1.2.1.Method (the failures being fixed):
/billyon SocialGouv/iterion#683 (+3870/-273 across 55 files, 30 commits),review_modeauto, budgetmax_duration 2h30m/max_cost_usd 75(the shipped default).Result (the three cited runs, unchanged bot):
run started duration cost nodes executed stopped at commits 01a06d802026-09-04 17:38Z 9004.7s/9000 (+4.7s) $3.77 plan_topology, plan, plan_review, plan_gate, plan_revise campaign(never entered)0 01a06e722026-09-05 22:03Z 9001.9s/9000 (+1.9s) $4.82 (identical set) campaign(never entered)0 Same target, same node set, same stopping point, same failure, twice. ~5 h of runner pod + $8.59 of LLM spend produced two triage plans and zero code (#695). #705 catalogs a third death on the SAME cap (
01a0517a, 2026-08-30, +1s) and argues the complementary half: a run that dies with nothing banked has no recovery path, whereas the 2026-09-03 entry above survived three deaths only because it had banked 17 commits in stride. Neither ticket's underlying wall existed until the plan phase itself starvedcampaignof the time to make its own first commit.Value: N/A for the three historical runs (zero commits, two duplicated triage plans neither of which reached code).
Findings / misses: the failure was invisible in the run's own status — both runs closed
failed_resumablewith an ordinary "budget exceeded: duration" message, indistinguishable from a run that did real work and ran long. Only the executed-node list showedcampaignwas never entered. A resume restarts from a fresh clone (the planning cost is paid again), so retrying was not a fix.Engine hardening (this change, native:695): the planning chain (
plan→plan_review→plan_gate→plan_revise) had no ceiling of its own — it could (and did) spend the ENTIRE run budget beforecampaign, the node that writes code, ever started. Added:plan_scope_probe(deterministic tool, beforeplan): captures a cappedgit diff --statfootprint, alargeclassification (overplan_large_diff_lines, default 1500 added lines), and the chain's wall-clock start (started_epoch) — the ONLY way to measure the phase's own elapsed time, since no DSL primitive exposes a run's elapsed duration to a node (see below).plan_gatenow also bypassesplan_reviseon a large diff, not only on a skipped peer (skip_revise = skipped || large) — the peer's critique reachescampaignunrevised rather than paying a second full-diff read.plan_cost_probe(compute): a NIL-SAFE sum ofplan/plan_review/plan_revise's own_cost_usd— a skipped/never-run node's cost key is absent, not zero, and a naive sum errors on that;if(x, x, 0)is the nil-safe idiom (truthy(nil) == false).plan_budget_gate(deterministic tool, the SOLE choke point beforecampaign): compares the real elapsed minutes and the nil-safe cost sum againstplan_budget_ratio(default 0.3) of two new mirror vars (budget_max_duration_minutes/budget_max_cost_usd), and routes to a typed early failure instead of lettingcampaignstart on whatever the plan phase left behind — guaranteeingcampaignat least (1-ratio) of the budget whenever it does start.plan/plan_reviewread the diff-stat footprint and skip the full unified diff aboveplan_large_diff_lines.- DSL gap found and NOT worked around: no primitive exposes a run's actual elapsed duration/cost or its resolved budget caps to a compute/tool node (
pkg/dsl/expr'srunnamespace resolves onlyrun.id—pkg/runtime/expr_eval.go);plan_scope_probe/plan_budget_gateself-measure wall-clock viatime.time()instead. Also: the DSL's-> failterminal has no way to carry a customRuntimeErrorcode/message (pkg/runtime/engine_exec.gohardcodes "workflow reached fail node" + a fixedFailureFailNodecode) — the typedPLAN_BUDGET_EXHAUSTEDcode + comparison detail live onplan_budget_gate's own persisted output (readable viaiterion report/ the run's events), not on the run's top-level failure message. Both are flagged as follow-up engine work, not hacked around (out of this change's bot-only scope).
Lessons for next run: dogfood the fix on a large diff BEFORE trusting it in production — a stub-driven e2e proves the graph routes correctly on a controlled cost sum, not that
plan_budget_ratio's default (0.3) is the right split on a real 4000-line diff, nor thatplan_large_diff_lines(1500) is the right threshold for the diff-stat adaptation to actually keepplan/plan_reviewinside their share. If the guard still trips too early/late on iterion#683 itself, tune the ratio/threshold vars before touching the mechanism.
2026-09-04/05 — three zero-touch launches on the wave-1 PRs: two deaths at the duration cap, one correct refusal we had to cancel (runs 01a06d80, 01a06e72, 01a06e45)
- Status: failed on the two fixer runs; the third produced a correct diagnosis and had to be cancelled to protect the merge it was about to break.
- Versions: bot
branch-improve-loop1.2.1 · iterion cloud prod (server v3.101.x) · PRs SocialGouv/iterion#682 and #683. - Method: the zero-touch
auto_fix_on_gate_failurelane and the merge-queue auto-heal lane, both firing by themselves. Bot budgetmax_duration 2h30m/max_cost_usd 75,plan_review on,post_to_board false.
The two deaths, at the same cap, seconds over
| run | launched | died | over by | pushed |
|---|---|---|---|---|
01a06d80 (#683 red gate) | 04/09 17:38Z | budget exceeded: duration (2h30m01s / 2h30m) | 4 s | nothing |
01a06e72 (#683 red gate again) | 04/09 22:03Z | budget exceeded: duration (9001.86s / 9000s) | 1.86 s | nothing |
Two runs, the same PR, ~5 h of forfait, zero commits. Both died inside the last two seconds of their window, which is the shape that matters: this is not a bot that ran out of room to finish, it is a bot whose delivery tail never got scheduled. The work it had done existed only in its own context and died with the run — nothing was banked, so nothing survived to be fast-forwarded by hand the way the 2026-09-03 run's chain was.
Resuming with a raised cap was impossible at the time: the deployed binary had no budget flags on remote runs resume. That is exactly the re-budget half of #652 — which was sitting unreviewed in PR #689 while the run that would have used it died. It merged at 22:32Z, so the next occurrence is recoverable.
The third run: right diagnosis, wrong mission
01a06e45 was launched by the merge-queue auto-heal lane 3 seconds after a flaky test (#692) ejected the green PR #682. Its plan node (opus-5, $4.53) concluded, unprompted:
Verdict: no code issue in the diff. This is a re-queue, not a fix.
and even noted in its own step 0 that a queue build was already in flight and that pushing would cancel it. Its mission then told it to rebase and git push --force-with-lease — step 1. We cancelled it; the queue run finished and #682 merged.
The bot was right and its instructions were wrong. Worth keeping as the reference case for "let the fixer refuse the task": the reasoning to decline was already there, the contract had no way to express it.
Engine hardening this produced
- PR #693 (merged) — the auto-heal now stands down when the queue takes the PR back, so a heal can no longer force-push over the build that would have merged it. Keyed on the heal's own idempotency key at the current head, so a human's
/billysurvives and a heal that already pushed is not killed by the enqueue its own push caused. - #692 — the flaky
TestEngineRunner_SubbotChildHoldsRunLockthat caused the ejection: a stall (26.9 s package → 300 s timeout), not slowness, and widening its budget was already tried once in #658. - #690 — corroborated with production evidence: a stale usage-window reading blocked every LLM run for ~50 min, including the merge gate itself, with no path to recover on its own.
Lessons for next run
- The 2h30 default is the finding, not the accident. Two independent runs landing 4 s and 1.86 s over is not bad luck; it is a cap sized below the work being asked. Either raise the bot's
max_duration, or give the campaign a delivery reserve it cannot spend on analysis. - A fixer with nothing banked has nothing to salvage. The 2026-09-03 run died three times but banked 17 commits, which the operator fast-forwarded by hand. These two banked nothing — commit-in-stride is what makes a death survivable.
- The auto-heal lane cannot tell a flake from a real break and should not try to; #693 bounds the damage, the flake itself belongs to #692.
2026-09-03 — /billy on the watchdog PR: three deaths, the banked chain delivered by hand (run 01a06728)
Status: partial. The fixer's work landed (17 commits on SocialGouv/iterion#646, the ADR-096 claim-lease + watchdog PR) but never through his own delivery tail: the run died three times before
push_back, and the operator fast-forwarded the banked chain onto the PR branch. The PR merged the same evening (e194aebe0, v3.99.0, deployed).Versions: iterion cloud prod (runner v3.96.1 digest, server ~v3.97) · bot
branch-improve-loop1.2.1 · branch basefa9c8b1be.Method: the documented habit — Revi left 1 medium (
Ra74e4c) + 2 low + 7 questions on #646;/billyas maintainer;review_mode=mono; bot budgetmax_duration 2h30m/max_cost_usd 75. An Anthropic incident was in progress during the first attempt.Result:
attempt wall-clock outcome 1 — 12:05→14:37Z 2h32 BUDGET_EXCEEDED: duration (9004.7s/9000s)mid-campaign(iter 3/8), $34.47; 6 commits banked oniterion/run-01a06728…2 — bare remote runs resume15 min re-died at 9901.6 s = the 110 % exit-grace ceiling (the consumed duration axis rides the checkpoint) 3 — resume with sourceinline (max_duration: 6h) +force1h47 restarted the campaign FROM SCRATCH (the resume re-clones the branch head, the banked chain is not re-imported), 17 commits banked, died USAGE_LIMIT_BLOCKED(Claude session limit, usage_window retry armed)4 — auto retry 20:09Z — cancelled by the operator: it would have died on iterion's own weekly cap (95 %, hard) Operator delivery 17:43Z: fast-forward of the banked chain (17 commits, verbatim) + one test-only fix + merge of
origin/main→ PR head196b18966; the re-review fired by itself 3 s later (review_on_sync) and came back green (0 ≥high, 2 low + 7 questions → #660). A merge-queue ejection (#658 merged ahead, same flaky test touched) forced a second merge (385d8677e); that head's re-review then died on the weekly usage cap (seven_day window at 95% ≥ 95%, reset 2026-09-08) and parked the gate for five days —/revi approvefailed on the GitHub App integration (#662), the override status was posted by hand.Value: high on substance. Beyond Revi's findings (all addressed —
launchTicketNowCAS anchored on the read state, the reaper gate misspelling made loud, the CI mongo-gate guard scoped), the campaign found defects a 20-round adversarial loop had missed: a false "claim lost" when the owner's own release races an in-flight heartbeat (Releasing()latch),cmdClaimLostcancelling the NEXT run holding the card (run id on the message), a lost fence at launch that still launched (both twins), the mongo terminal sink as check-then-act (now a CAS with re-evaluation),SetStateFrom(x→x)disagreeing across twins, the reconciler's tokenless write overwriting an operator's drag (SetStateFromReasonCAS), "a release is the last act of a disposition",store.RunAbsentshared across the four run-pointer authorities, the FS renew blockingStop()on the actor.Findings / misses: his new
TestAdapterRenewClaim_HonoursCancelMidCallfailed deterministically (10/10, plain and-race: the detached renewal wrote intot.TempDir()during cleanup) — a banked chain is committed in stride but not gate-verified (verify_runonly runs at the end of a pass). He appliedReleasing()on the local twin and missed the cloudprocessCard(Revi'sRf238b1, #660). His delivery tail was never exercised: he never pushed himself, so by design he would have stamped nothing on a head the operator pushed.Engine hardening (GitHub board): #652 — resume re-clones the branch head and restarts the campaign, ignoring the banked chain (proposal: re-anchor on
FinalBranchwhen it fast-forwards from the clone base); the cloudPOST /api/runs/{id}/resumehas no budget overrides (workaround:sourceinline +force); the consumed duration axis rides the checkpoint; the exit grace does not protect a node cancelled in flight. #650 — the gate-paused notice says "Review paused … a new push restarts it sooner" for a FIXER (forge_gate_pause_notice.gofilters ongate_context, which a gating fixer carries). #662 —/revi approve→set commit status: forge: insufficient scope, webhook 502. #663 — the parked review revived 2 s afterstopRunsForDeadPR(redelivery race). Verified working: the death bank (pushBank, richer-chain supersede), the usage-window retry (run_retry_scheduled, reset parsed from the typed error),review_on_sync, stop-on-close for the auto-heal lane run.Lessons for next run: size the fixer's
max_durationfor this repo's verify gate (2h30 with ~1h of plan phases is too tight) or slim the plan phases whenconsumes: reviewcarries few findings; push in stride on the PR branch (the work branch IS the PR branch); never bare-resume a duration death on cloud; a banked chain is deliverable by hand only after the full validation; when the weekly cap parks the gate, the documented override is broken until #662 lands — budget a manual status or the admin bypass.
2026-08-30 — three launches, zero delivery: the duration cap and the weekly cap (runs 01a0517a, 01a051dd, 01a05216)
Status: failed.
/billyon SocialGouv/iterion#579 (the CHANGELOG feature) produced no commit and no push across three launches. Nothing was wrong with the hand-off —prior_reviewseeded, campaign working, tool calls flowing — the run simply ran out of wall-clock, and the retries hit a wall that was not Billy's.Versions: iterion cloud prod (edge ~v3.74/3.77) · bot
branch-improve-loopMethod: documented habit — Revi left 1 medium + 1 low on #579,
/billycomment as maintainer, noskip.Result:
run wall-clock outcome 01a0517a07:022h31 budget exceeded: duration (9001s/9000s)01a051dd08:51— failed_resumable01a0521609:533 min usage cap: seven_day window at 75% ≥ 70% (week, hard)Value: none delivered. The findings were fixed by hand instead, after the weekly cap made a fourth attempt impossible before its 2026-09-01 reset.
Lessons
- The 2h30 duration cap is not sized for this repo's verify gate. Each campaign pass re-runs the repo's real build+test, measured at ~10 min a pass (one tool call spanned 07:59:04 → 08:09:05). A handful of passes and the cap is spent before the delivery tail. The loop budget guard declines a further iteration when the budget cannot fund one, but nothing here shortened the first pass, and the run died mid-pass with nothing committed — the exact shape "commit in stride" exists to avoid. Either raise
max_durationfor targets whose gate is expensive, or make the gate cheaper (scope the test command to the touched packages). - A run burning its whole cap with zero commits should be cancellable on sight. Nothing in the run view said "2h in, nothing banked"; the operator reads
runningand waits.commits_this_passexists in the contract — surfacing it (or a zero-commit warning past N minutes) would have turned 2h31 of spend into a 20-minute decision. - The habit has a precondition worth stating: Billy must be able to run.
docs/revi-billy-loop.mdsays "don't hand-fix, comment /billy". When the weekly cap is hard-blocking until a reset two days out, that instruction has no path, and waiting is worse than fixing. The runbook should name the fallback rather than leave the operator to infer it.
2026-08-27 — first /billy of the Revi→Billy habit on iterion itself (runs 01a0428a, 01a042b9, 01a042d7)
- Status: partial. The habit's whole chain worked —
/billycomment → launch withprior_reviewseeded (3.6–6 KB, the kind-matched hand-off), campaign converged in ONE pass (7 commits, verify green, in-loop review clean), honest verdict posted — but the push died on an expired forge token after 1h11, so the fixes never reached the PR. Three engine/bot defects found, two fixed in this change, one filed (native:54412d84). This bilan inaugurates docs/revi-billy-loop.md. - Versions: iterion cloud prod (edge ~v3.65) · bot
branch-improve-loop1.2.0 - Method: the documented habit — Revi reviewed SocialGouv/iterion#541 (1 high: "OAuth meter key follows the access token"), then
/billycomments (maintainer). Weekly usage cap was at 95% → rotated the team + platform claude_code OAuth credential (fresh token), temporarily raised the runtime week cap 95→99 to get past the pre-#541 shared-meter stale reading, resumed the parked review. Three Billy attempts:- Run 1 (01a0428a): paused on
plan_review— the team's codex OAuth record was DEAD (401), andplan_review: autohad resolvedonfrom its mere existence. Uploaded a fresh codex auth.json → peer served on retry — thenplan_revisewedged forever (~2.6serror_during_execution× 9): the mid-plan-phase resume had replaced the sandbox container, the author session's files died with it, andinherit_if_availableonly tolerated a MISSING session id, not an unloadable one. Cancelled. - Run 2 (01a042b9):
plan_review400 —gpt-5.6-sol requires a newer codex-cli(the ChatGPT-wire version gate; runner lacks a recentITERION_CODEX_VERSION). Cancelled; pinnedplan_review: "off"in the iterion repo integration's launch_vars (operator directive: the fixer must never depend on a second family — Anthropic alone suffices). - Run 3 (01a042d7): clean campaign, pure Anthropic. Converged in one pass:
branch_clean, 7 commits, verify_run green, in-loop review clean, $9.13 / 90 K tokens / 1h11. Fixed Revi's finding the right way (refresh-invariant identity: fingerprint stamped at connect/seal,PledgeIDfor pool grants) — notable because the PR author had hand-fixed the finding mid-dogfood and Revi's re-review had STILL found 1 high; Billy's ledger disposed of every id with arguments. Thenpush_back_tool:Invalid username or token— the sealed GitHub App installation token (minted at launch, ~1h validity) had expired. The 7 commits died with the pod; the verdict honestly postedfailure — 3 fix(es) never reached this head.
- Run 1 (01a0428a): paused on
- Value: the habit is real — the hand-off, the one-pass convergence, the honest gate all worked; and the dogfood surfaced defects no test had.
- Findings / misses: (1) a DEAD second-family credential blocks the whole fixer via
plan_review: auto+policy: wait; (2)inherit_if_availablewedges forever when the session's backing state died with the container; (3)push_auth_probevalidates token PRESENCE, not validity (answeredavailablewith a 10-min-dead token); (4) the launch-minted forge push token cannot outlive the App-token validity, so any >1h fixer run loses its push. - Engine/bot hardening (this change):
plan_review_policydefaults toskipfor Billy (bot 1.2.1 — the peer is optional enrichment, never a blocker);Task.SessionOptional— the executor retries ONCE with a fresh session when an optional session fails to serve (red-first tests). Filed: mint-at-use forge push token + a real auth probe (native:54412d84). - Follow-up — both delivery runs landed the same day:
- Run 4 on #541 (01a0431d, 53 min): re-derived the lost fixes from the fresh prior_review, converged, pushed 6 commits (refresh-invariant OAuth meter identity + tests actually executing the refresh path + operator docs), verdict
success — 3 finding(s) fixed, re-review by the fixer clean, build greenon556d6f4e. - Run 5 on #544 (01a0431e, 1h53):
/billyon the habit PR itself for Revi's 3 findings on OUR fixes — pushed 11 commits that materially hardened the session-degrade change: loud degrade (event + output stamp, R051957), narrowed tounclassifiedfailures (R1486ff), claw node-session eviction, backend gate (only backends that resume by SessionID), the SessionOptional mapping pinned across all six session modes (R552e44), plus docs/ADR alignment — and it improved the habit runbook itself (thereview_on_syncdependency of step 3). - The 1h53 push succeeding contradicts the "App token dies at 1h" hypothesis for run 3's failure; the board card (native:54412d84) was corrected — the standing defect is
push_auth_probevalidating presence, not validity; run 3's root cause is unreproduced (plausible: the integration PATCH 2 min before launch re-provisioned the managed forge_token).
- Run 4 on #541 (01a0431d, 53 min): re-derived the lost fixes from the fresh prior_review, converged, pushed 6 commits (refresh-invariant OAuth meter identity + tests actually executing the refresh path + operator docs), verdict
- Lessons for next run: keep fixer runs under the App-token validity until mint-at-use lands (tighter
scope_notes, ormax_duration< 1h so the budget guard ships what is banked while the token is alive); the usage-cap runbook rotation is CLI-only now (iterion remote admin llm oauth set+ team/oauth/{kind}/credentials— no k8s secret edit, no runner restart); after any/billy,git pullbefore touching the branch.
2026-08-13 — the write path on a self-hosted GitLab, end to end (run 019ffb9e)
- Status: validated. Every link a fixer needs on a forge that had never hosted one: clone, commit in stride, push onto the MR branch, post the verdict, and gate the head it just produced.
- Versions: iterion cloud prod v3.40.6 · bot
branch-improve-loop1.1.1 - Method:
POST /api/runswithrepo_url+repo_ref+connection_id(no webhook — the target instance blocks outbound hooks),push_branch= the MR source branch,pr_urlset,gate_context: iterion/review,post_to_board=false. Scope notes named the defects Revi had reported on the same MR (run019ffadb); the credential is a group access token, so the push authenticates as the bot rather than as a human. - Result: converged in ~46 min. 18 commits pushed onto the MR branch, one concern each (
fix(fetch): handle http.Get error instead of dereferencing a nil response,fix(fetch): refuse destinations outside the public internet,fix(fetch): ignore HTTP_PROXY so the destination guard cannot be bypassed, …), interleaved with the tests that pin them. Verdict comment posted on the MR, anditerion/review=failure — 1 finding(s) unresolvedlanded on the new head under the bot identity. - Value: it did not stop at the reported list. Revi flagged the missing destination validation; Billy shipped the guard, then found the ways around its own guard — an IPv6 literal embedding a private IPv4 address, a redirect hop landing inside the perimeter,
HTTP_PROXYsteering the dialer past it — and pinned each with a test. It also noticed the handler was never routed and wired it, which is what made the rest reachable. - Notes for the next run: the sandbox image carries no
goonPATH; the agent recovered on its own (export PATH=$PATH:/usr/local/go/bin) but paid a couple of turns for it — worth adevbox.jsonif this becomes routine. Commits are authored asiterion-runner[bot], distinct from the group-token identity that performs the push; the loop guard keys on the pusher, so the two must not be conflated when reading a branch's history.
2026-08-01 (fin de journée) — the zero-touch lane, and the crash it took to prove it (run 019fbd98)
- Status: validated after a crash-level fix. The opt-in lane had never fired once since it was written; its first real firing panicked a server pod.
- Method:
auto_fix_on_gate_failureenabled on the e2e integration, a concurrency defect planted by hand (Snapshothanding the internal map out past the lock,RecordHitmutating it without one), pushed as a human. No/billycomment at any point.
First attempt: the lane crashed the server
The gate went red at 12:51:30 and no fixer launched. The reason was in the pod's pre-restart log:
panic: store/mongo: tenant-scoped query without tenant in ctx
← cloudpublisher.SubmitLaunch ← runview.Launch ← launchWebhookTarget
← autofixForRun ← eventbus NATSBus subscriberThe lane stamped the auth identity the admission gate reads and not the store identity every tenant-scoped query asserts on. The inbound-webhook middleware stamps both; the retry sweeper stamps both; this lane had copied half the precedent. Missing, the launch does not fail — it trips the tenancy guard deep inside SaveRun and takes the process down.
Its own tests could not see it: they stub webhookLaunchBot, which is the seam directly above where the launch reaches the store. Fixed in b68f8a39f, with the positive control now asserting on the context handed across that seam.
A second, wider fix rode along: the bus fans one event out to independent consumers that share nothing but the dispatch, so a panic there is the widest blast radius for the narrowest bug. It is now recovered per delivery, logged with its stack, and dropped exactly as a returned error already is.
Second attempt: it works, untouched
gate auto-fix: iterion/review red on SocialGouv/iterion-test-appy-e2e#2@de9df24
→ launched branch-improve-loop (run 019fbd98)restarts=0 on all three replicas. The launched run carried everything needed to close the loop back: head_sha exactly the head the gate was red on, gate_context: iterion/review (the repo's pin, not the bot's default), the publish grant, 7141 characters of prior review, and a scope_notes saying why it was launched.
Lessons for the next run
- An opt-in feature that has never fired is not shipped, it is written. This one passed a
/simplify, an adversarial review and a refusal table, and still had a process-killing bug on line one of its only untested path. - When a lane runs off the event bus rather than an HTTP request, it inherits none of the request's context. Copy the middleware's stamping in full, or copy the retry sweeper, which already had it right.
2026-08-01 (soir) — the loop closes: red gate → fix → push → independent supersede (run 019fbd1b)
- Status: validated — the four links of the Revi↔Billy loop exercised live, two of them for the first time ever.
- Versions: bot 1.1.0 · iterion
a9f32534b(v3.19.0, carryingdb2676c5b). - Method: a real defect planted by hand on
feat/cache-layer(aPurgeranging over the entry map and aLenreading it, both without the mutex, on a type documented "safe for concurrent use"), pushed as a human. Revi reviewed it, the gate went red,/billy. - Result: Revi found 4 findings (1 critical, 1 high, 2 medium) and the gate went
failure — 2 blocking finding(s) ≥high. Billy fixed all four, pushed, and postediterion/review=success on a3a734324f33. The independent re-review landed 14 minutes later and superseded it withsuccess — no blocking findings (≥high); 2 total.
The measurement that mattered
The same actor, the same event, the same repo — before and after db2676c5b:
| heure (UTC) | synchronize pushed by | delivery | sha |
|---|---|---|---|
| 10:29:33 | the fixer, before the fix | filtered | 6dd691c1b |
| 11:28:28 | a human | launched | e8ca8c0be |
| 11:56:49 | the fixer, after the fix | launched | a3a734324 |
And the two verdicts that then sat on that one head, in order:
| heure | iterion/review on a3a73432 | by |
|---|---|---|
| 11:56:50 | success — 4 finding(s) fixed, re-review by the fixer clean, build green | the fixer, about its own code |
| 12:10:57 | success — no blocking findings (≥high); 2 total | the reviewer, independent |
The return ledger, exercised for the first time
prior_pushback reached the re-review at 1920 characters, finding by finding with the commit that fixed each. The reviewer re-raised none of the four and instead found two new low ones — plus a question worth more than the findings: does CI run go test -race? I could not here, and I confirmed by mutation that removing Len's mutex leaves the whole suite green. A locking discipline with no automated guard, surfaced by the reviewer's own falsifiability channel.
Lessons for the next run
- A guard keyed on the sender treats "our bot pushed" and "our bot opened this PR" as the same thing. They are opposites: the second converged in its own loop, the first is exactly the moment an independent judgement is needed.
- Plant a defect and drive the whole loop rather than asserting each link separately. Both defects fixed today were compositions, not units.
2026-08-01 — the board lane could not publish, and the fixer's push was invisible to the gate (run 019fbcbb)
- Status: validated — first end-to-end run where Billy answers a review, pushes, posts its verdict AND closes the merge gate. Two engine defects found live, both fixed and deployed in-session.
- Versions: bot 1.1.0 · iterion
cdfedc124(server), the run itself onclaude_code/claude-opus-5. - Method:
/billycomment on iterion-test-appy-e2e#2 (feat/cache-layer, a real Go module with planted defects), cloud, board lane, GitHub App connection,gate_context: iterion/reviewpinned on the integration. - Result: converged in one pass — 38m12s active, 390 events, 152 tool calls, 11 nodes,
continuation_loopnever re-entered. Pushed 6 commits onto the PR branch;publish_verdictreturnedverdict posted; iterion/review=success on 6dd691c1be95. - Value: the hand-off paid for itself. Billy received 3388 characters of prior review carrying the stable id
Rcae144, reproduced the finding before fixing it (a 503 origin was memoized and served with a nil error, and a recovered origin was never re-contacted), fixed it in57c7a00, then found four more real issues the review had not — among them aWarmsemaphore allocated per call instead of per cache, so the fan-out cap was not a cap.
The two defects the run surfaced
1. A bot launched off the board could not publish anything (fixed, 515714482 + cdfedc124). The cloud coordinator launches a card from its BotArgs alone, so the forge-publish grant and the repo's launch policy — both composed inline by the webhook tail — never reached it. Measured side by side with the reviewer on the same PR:
forge_publish_url | forge_publish_token | gate_context | |
|---|---|---|---|
Revi (mode: direct) | oui | oui | iterion/review |
Billy (mode: board), avant | — | — | absent |
Billy (mode: board), après | oui | oui (64 car.) | iterion/review |
Both are now resolved at claim time, never carried on the card: a grant expires, so a card claimed hours later would hold a dead token, and a board document is the wrong place for a credential at rest.
A second defect rode along, found while preparing the fixture: the repo carried two integrations (a stale personal-token one from 2026-07-17 and the GitHub App it was re-provisioned onto), two live webhooks, and a double delivery on every comment. Resolving them by lowest id — deterministic but arbitrary — elected the stale one, which would have posted the verdict under the operator's own account, the identity the loop guard refuses. The latest provisioning now wins.
2. The fixer's push was the one delivery the gate never saw (fixed, db2676c5b). The iterion-bot guard skips a PR our own loop produced, keyed on the SENDER. On a merge-gate resync the sender is by construction the forge bot — the fixer that just pushed — so the resync on 6dd691c1 was recorded filtered — PR authored by iterion's forge bot. Two consequences, both the failures the merge gate exists to remove: the required check stayed on the pre-push revision (absent on the head that needs judging), and the fixer's own verdict about code it wrote was never superseded by the independent re-review docs/merge-gate.md promises. A PR the bot opens is still skipped — that one did converge in its own loop.
Lessons for the next run
- A
mode: boardbot gets none of the launch context the webhook lane composes. When a board-mode bot behaves as if a var were unset, read the RUN's inputs before reading the bot. - Both defects were invisible to every test because each test supplied by hand the thing production had to produce. The same shape as the
publish:defect in review-pr.md: when a contract crosses two components, at least one test must run the producer for real. - Deprovision a stale integration when re-provisioning a repo onto another connection. Two live webhooks on one repo double every delivery, and the extra one refuses everything with a misleading reason.
2026-07-10 — /billy command on a Dependabot PR: 4 engine gaps peeled live, then a clean push-back under the App identity (runs 019f4bd4 / 019f4c46 / 019f4c86 / 019f4ccb)
- Status: validated (cloud E2E, command path) — 5 engine fixes found live, all landed in-session. Mission: make Dependabot PR #80 mergeable (bump
x/cryptoto 0.52.0 past a CRITICAL advisory cluster Vetty flagged + fix the failing CI OpenAPI drift). Final run 019f4ccb: 22 min,push_back: true, 2 commits pushed onto the PR asiterion-forge-83fde406[bot](52a4072d9crypto bump + tidy/vendor,d766e1d03openapi regen) + verdict comment posted under the App. - Versions: bot v2 · iterion
499957c31→6dd452c2a· chart 0.37.2→0.37.4. - Method:
/billy <mission>PR comment (args →scope_notes) → board-mode card → dispatcher → cloud runner. ~$4/run. The mission text included the memory guidance (skip the vite build,go test -p 2) after an OOM. - The onion, one layer per run (each failure = a real engine fix):
- run 019f4bd4: campaign did PERFECT work (crypto bump + openapi regen, verified) but
mr_gate.push_back: false→ commits stranded on the runner's ephemeral storage branch. Fix642b1ba0d: the command path now stampspush_branch/open_mr(parity with the pull_request-event path). - Same run also exposed that
ensureBoardCardrebuilt BotArgs from scratch — cloud launches use card BotArgs ONLY, sopr_url/branch vars never reached the run (also why Billy never commented). Fixbc2918024. - run 019f4c46: OOMKilled at 4Gi during the verify suite (exit 137, pod restart, banked commits lost with the pod FS). Infra fix: runner limit 8Gi (infra-apps
44b7fb4). JetStream redelivered the run — recovery worked — but the re-attempt then pushed with a DEAD App token. - runs 019f4c46/019f4c86:
Invalid username or tokenon push + gh 401 on comment — the sealed bundle snapshots the 1h installation token at LAUNCH; long/redelivered runs outlive it. Fix6dd452c2a(#99): the publisher records generic-secret store IDs on the bundle and the runner re-reads them every 5 min, atomically rewriting/run/iterion/secrets/<name>; toolscatthe file per use.
- run 019f4bd4: campaign did PERFECT work (crypto bump + openapi regen, verified) but
- Engine hardening (this session, all deployed):
499957c31PR head/base resolution for PR-surface commands ·642b1ba0dpush-back vars ·bc2918024board-card PR context ·6031a357eexecutor agent-stream lines now persist to the run log (the studio per-node Logs tab was empty on every cloud run) ·6dd452c2amid-run file-secret refresh (#99). - Lessons for next run: a dep-bump branch is typically BEHIND main — the CI drift gate runs on the merge-ref, so regen-style fixes must happen on an updated branch (
gh pr update-branchfirst, or Billy should update it); keep verify memory-aware on 8Gi pods (skip frontend builds, cap-p).
2026-07-09 — first CLOUD runs: PR-webhook → Billy on the devbox runner (runs 019f43a3 / 019f43c7 / 019f4551)
- Status: validated (cloud E2E) — 3 engine/bot gaps found live, all fixed in-session. The full production trigger chain ran for the first time: real GitHub PR (#84,
Fixes #83) → forge webhook (selectForgePRBotroutes Billy, not Revi) → NATS queue → devbox runner pod (uid 1000) → campaign/verify/gate. - Versions: bot @
1d076a618(adds the push-back tail) · iterion:edge36fb786b5→56a5680f1 · runner imageiterion-runner-devbox:edge· webhookd291059c(bots review-pr + branch-improve-loop + feature-dev, block_fork_prs, author allowlist Viczei+devthejo). - Method: PR-open/reopen events on SocialGouv/iterion#84 (a real fix: the vv0.32.0/vmain version-injection bug). Re-triggers need a NEW head sha + close/reopen — the delivery idem key is (PR#, head sha) and launch-success rows are terminal;
/billycomments from the connection identity are loop-guard-filtered (self). - Result per run:
019f43a3failed in 17s: Billy's inlinesandbox:block → kubernetes driver →ITERION_POD_IP env var is empty(the chart deliberately doesn't provision sibling-pod sandboxing). → Fix 136fb786b5:ITERION_SANDBOX_OVERRIDE=none(CLI-strength, beats the workflow block; chart auto-sets it whenrunner.sandbox.enabled=false) — the runner pod IS the isolation boundary.019f43c7finished, 1-pass converged (~7 min): verify.sh settled ondevbox run -- go build ./...+ targeted tests — the repo's devbox-pinned Go toolchain, i.e. the devbox-first-class runner goal proven live. campaign caught a REAL defect (the PR's new test file wasn't gofmt-clean — CI lint would have rejected it) and committed the fix (ef540115)… which died with the pod-ephemeral worktree:mr_gateopen_mr=false went straight to done. → Fix 21d076a618: deterministic push-back — webhook passespush_branch(PR source branch);mr_gate → push_auth_probe → push_back_tool(no-LLM python3 push, rev-list oracle so a converged-no-commits pass no-ops, token redacted from failures).019f4551finished; new path routed (…gate>mr_gate>push_auth_probe>done) but the probe found NO credential:materializeFileSecretsNoSandboxgated on the STATICwf.Sandbox != nil— under the override the run executes in-pod and nobody materializedforge_token(the launch HAD sealed it into the bundle: the stored secret's last_used_at == launch instant). → Fix 356a5680f1: gate on the RESOLVED decision via newruntime.WorkflowSandboxActive(wf, override, default).
- Value: the run 019f43c7 catch (gofmt) was real and manually reapplied as
a2cf464a9; the three fixes harden the entire cloud-runner class of bots (any bot with a sandbox block + file secrets), not just Billy. - Findings / misses: secret-resolution failures are SILENT at several layers (
buildGenericResolutionok=false without a log; publisher skips empty plaintexts) — an erreurs-explicites hardening candidate. CI race job flake:TestReconcileStalled_ForceReapsCtxIgnoringWorker(pkg/dispatcher) failed on 56a5680f1, 5× green locally with -race — known concurrency-flake family. - Engine hardening:
36fb786b5(+ chart 0.33.0, umbrella3a20e24),1d076a618,56a5680f1; stale factory commentafbdd6be3. - Lessons for next run: consult the RESOLVED sandbox mode everywhere run inputs depend on it; self-triggering a webhook from the connection identity hits the loop-guard (use a fresh head sha + close/reopen, or another actor); first devbox run per pod re-downloads the Nix toolchain (~2-4 min — the PVC warm-store follow-on).
The issue → Featurly → PR half of the cycle (runs 019f4582 / 019f4590, fixes 4–6)
Labeling an issue implement is meant to route to the implementer (Featurly) and open a PR that then re-triggers Billy — the other half of the loop. Two more gaps surfaced, both fixed live:
- Routing (fix 4,
2281a2eff): a 3-bot webhook with no pinned default routedissues/labeledto review-pr (run 019f4582), which stopped atdiff_precheck— an issue has no diff to review.resolveReviewBot's SelectBot→review-pr fallback is right for a PR delivery, wrong for an issue. NewselectIssueLabeledBot(pinned default → feature-dev → fallback), the issue-path counterpart toselectForgePRBot, wired on GitHub+GitLab. Validated: issue #86 → run 019f4590 = feature_dev, and Featurly shipped a genuine, high-quality feature (threaded a*log.Loggerthrough the whole generic-secret resolution path so silent credential drops become greppable — the erreurs-explicites finding the debugging itself surfaced), build+test green. - Forge token on the board-launch path (fixes 5+6,
483e69a3f+9b339c999): Featurly implemented butforge_auth_probefound noforge_token, so it never opened its PR (secrets_refnull). Root cause: an issue-labeled delivery makes a board card, and the board coordinator (boarddispatch.go) launches viaruns.Launch(BotID)— which resolves generic secrets by (tenant, bot) binding, NOT the webhook secret override (that only reaches the direct webhook path). Forge provisioning set only the override; the tier-3 name fallback misses (storedforge_github_<conn>≠ workflowforge_token). Fix: the orchestrator now upserts a per-botforge_tokenbinding at provision (fix 5), reconciled even on an idempotent re-provision so an already-provisioned integration is backfilled (fix 6). This is a general cloud-platform fix: EVERY board-launched bot that pushes (Featurly, Billy on the board path) needed it, not just this cycle. - Standing gap (deferred): fully zero-touch issue handling still needs the cloud dispatcher (the webhook
issuespath is labeled-only by design); and carrying the webhook's secret_overrides/launch_vars onto the board card itself would make the board path robust without a binding (belt-and-braces).
2026-07-08 — re-dogfood post-improvement (P1-P4), same PR #72 target, $1.80 (run 019f41af)
- Status: validated — reliability + integration confirmed; production excellent. Improvements are RIGHT and SAFE; this run's shape (1 pass, no MR) did not trigger the two biggest savers (see "P1/P2 not exercised live" below — an honesty note, not a regression).
- Versions: bot branch-improve-loop @
b7ea4bd78(P1 forge_auth_probe + P4 tool_max_steps 40→20 indb812f0dc; P2 verify_probe + P3 verify_build effort inb7ea4bd78) · iterion static binaryv0.31.0+b7ea4bd78(built CGO_ENABLED=0, invoked directly soos.Executable()bind-mounts the fresh binary into the sandbox). - Method: apples-to-apples with the pilot below. Same target: independent clone
/tmp/iterion-pr72-clonereset to the RAW PR #72 tip4b2394b94(the pilot's added test removed, so Billy re-finds the gap).sandboxinline (iterion-sandbox-full:edge),worktree: autobases on the clone's HEAD (CWD = clone),--store-dir <main>/.iterionso the run is visible in the operator's studio while operating on the clone (setupWorktree bases on CWD's git root but writes the worktree under--store-dir).base_ref=main,open_mr=false,--merge-into none,--max-cost-usd 12. - Result: converged in ONE pass, 8.5 min, $1.80 (31.8k tok). Billy reviewed
main…4b2394b94(the per-nodetimeout:feature) across all layers and re-found the exact same gap the pilot did — the parser/AST/IR/C199 were tested but the RUNTIME enforcement path was not — and addedpkg/backend/model/executor_timeout_test.go(+115: ablockingBackendthat blocks on the ctx deadline +immediateBackendhappy path, asserting the bounded context cuts the node off AND that a context-deadline error is not retried). Commitf5f55d5on storage branchiterion/run/ash-pulse-starforge-1c89.verify_runre-rango build ./... && go test ./pkg/dsl/… ./pkg/backend/model/…→ green (so the added test compiles and passes). Graph flow: campaign → verify_probe → verify_build → verify_run → gate (converged) → mr_gate (open_mr=false) → done. - Cost story vs the pilot's $2.26 (honest breakdown): campaign ~$1.17 (18.9k tok, was $1.31/21.2k — run variance on a slightly different pass) + verify_build ~$0.68 (12.9k tok, P3 effort now
medium) + verify_run (deterministic tool, 9.8s) + no finalize_mr (open_mr=false avoided the pilot's $0.26). Net −$0.46 (~20%), of which ~$0.26 is the avoided MR finalize and ~$0.20 is campaign variance; P3's verify_build delta was only ~$0.01 — verify.sh authoring uses little thinking, sohigh→mediumbarely moves it here (it will matter more on repos with a heavier build-capture step). - P1/P2 not exercised live (by design of this run's shape) — structurally proven:
- P2 (verify_probe skips verify_build) only fires on pass 2+; this run converged in 1 pass, so verify_probe correctly ran at
iteration=0→fresh=false→ routed to verify_build (the new node integrates without breaking the happy path). The skip path rests onTestVerifyProbeLoopIterationWiring(proves the loop iteration reaches the tool via the campaign→verify_probe edge with-mapping into node input — {{loop.*}} does NOT resolve inside a tool command) + the python-logic cases (fresh=true only when iteration>0 AND verify.sh valid) +iterion validate. - P1 (forge_auth_probe short-circuits finalize_mr when no push credential) only fires when
open_mr=true; this run usedopen_mr=false(mr_gate → done), so the probe wasn't reached. Its logic is deterministic + unit-covered.
- P2 (verify_probe skips verify_build) only fires on pass 2+; this run converged in 1 pass, so verify_probe correctly ran at
- Reliability: HIGH. Same target → same high-value conclusion (add the runtime enforcement test), reproduced independently. The two new deterministic nodes (verify_probe, forge_auth_probe) slotted into the graph with zero runtime surprise.
- Engine hardening: none needed. The e2e scenario sweeps gained stubs for the two new nodes (
aad7bddce,test(e2e): stub verify_probe + forge_auth_probe) sogo test ./e2e/stays green. - Lessons for next run: to measure P2's $0.69/pass saving live, point Billy at a ≥2-pass target (a branch with 2+ real issues, or a synthetic branch with a planted build-affecting change so pass 2 re-runs). To measure P1's skip live, run
open_mr=truewith no forge_token and no hostghauth → forge_auth_probe → done. For a pure production/cost demonstration this 1-pass/no-MR run is the right shape and $1.80/8.5min is a good point; the structural savers show on longer/MR runs.
2026-07-08 — pilot on a real contributor PR (#72), converged in 1 pass (run 019f415c)
- Status: validated — high-quality single-commit improvement.
- Versions: bot branch-improve-loop (feat/agent-node-timeout tree) · iterion 1f5082f (static, F7 skills fix in the engine)
- Method: local run,
sandbox: auto(iterion-sandbox-full:edge, devbox toolchain so the build/test gate has Go — the cloud runner does NOT, cf. F6). Independent clone checked out on the PR branchfeat/agent-node-timeoutsoworktree: autobases on the PR (NOT main — a linked git worktree bases on the shared repo HEAD, which is main; an independent clone was required).base_ref=main,max_passes=2,open_mr=true,mr_branch=iterion/billy/pr-72-pilot. - Result: converged in ONE pass. Billy reviewed
main...feat/agent-node-timeout(the per-nodetimeout:feature), found the parser + C199 validation were tested but the RUNTIME enforcement path (executeBackendderiving the bounded context — the feature's behavioral heart) was not, and addedpkg/backend/model/executor_timeout_test.go(+108:TestNodeTimeout_Enforcedwith a ctx-blocking backend + 20ms timeout, plus a happy-path immediate backend). The deterministic build/test gate (verify_run) passed green. Exactly the test a good human reviewer would add. Pushed by hand toiterion/billy/pr-72-pilot→ draft PR #82 (base = the PR branch, so the diff is just Billy's addition). - Cost: campaign $1.31 (21.2k tok) · verify_build $0.69 (13.1k tok) · verify_run (deterministic tool, 9.7s) · finalize_mr $0.26 (6.8k tok) = ~$2.26, ~9 min.
- F7 (engine) re-validated here too:
finalize_mrsuccessfullyLaunching skill: forge-mr-create— the directory-form skills fix works on the local sandboxed path, not just cloud. - Efficiency/reliability misses to fix (operator asked to bring Billy to a Willy-grade production/cost ratio):
- finalize_mr burns budget discovering there are no credentials: with no forge_token mounted and no
gh auth, the agent ran 4 probe commands before abandoning the push. A deterministic pre-check should skip finalize_mr (or short-circuit it) when no push credential is present. - verify_build ($0.69) re-authors the build/test script each run — candidate for caching / lower effort.
- finalize_mr burns budget discovering there are no credentials: with no forge_token mounted and no
- Lessons for next run: base the worktree on the PR via an INDEPENDENT clone (a linked worktree bases on main). Provide a
forge_token(or run in agh-authed shell) if you want Billy to self-push; otherwise recover the commit from theiterion/run/<name>storage branch the engine always creates and push by hand.
2026-06-14 — re-validation on a clean clone + good dead-code judgment (run 019ec5bc)
- Status: validated. Re-ran in the C082 worktree studio (non-watchexec) on a clean iterion clone with a synthetic
billy-testbranch: one added filepkg/log/billy_demo.goexportingWriteMarker, which had no godoc AND swallowed theos.Createerror.base_ref=main,merge_into=none. - Result: cross-family review loop converged (review → fix → re-review → streak),
Run finished, committed to storage branchiterion/run/feral-crash-duskvane-127c(final_commit 9c5a5891), not merged (merge_into=none respected). - Value — correct judgment, not a cosmetic fix. Rather than just adding godoc
- handling the error, Billy recognized
WriteMarkeras unreferenced dead code that swallows an error and removed it (refactor(log): remove unused WriteMarker demo helper, "No code in the tree calls it, so remove the dead code."). That's the right call — exactly what a demanding reviewer should do.
- handling the error, Billy recognized
- Finding (minor, non-fatal):
fix_claude(claude_code) emitted oneTool error: StructuredOutput — No such tool available: StructuredOutputbefore recovering and producing its output normally. The agent appears to try aStructuredOutputtool that isn't registered in the claude_code delegate — a wasted step, same broad family as the Devy claude_code-structured-output gap. Worth wiring/silencing, but it did NOT block convergence. - Convergence machinery (shared with Willy) is reference-correct; this run re-confirms it + the asymptote (no oscillation) on a fresh target.
Status: validated end-to-end (2026-06). Scope of this report: the capability and the engineering hardening it drove. The target here is iterion's own repository (the feat/cloud-control-plane epic), so target details are included.
Summary
Billy (the branch-improve-loop bot) was exercised against a real, large branch — a ~7000-line / 42-chunk epic — and demonstrably:
- converges a cross-family review/fix loop on a big diff (monotonic decrease to zero blockers + a two-family approval streak), rather than oscillating or stalling;
- finds real, high-value issues the human author missed, and authors complete fixes for them — including an ADR for a design-level change;
- drives the full pipeline plan_chunks → alternating cross-family review (Claude ↔ GPT) → same-family fix →
streak_check→prepare_commit→ semanticcommit_changes, and stops at the asymptote.
The exercise also hardened the engine: one significant runtime bug was root-caused and fixed while driving real runs — without it the GPT family could not review a diff this size at all.
What was validated
- The loop:
plan_chunks (deterministic diff measure + chunking) → round_robin reviewer (claude-opus-4-8 ↔ openai/gpt-5.5) → family-matched fixer → streak_check (2 consecutive opposite-family approvals) → prepare_commit → commit_changes. - Chunked review at scale: a 42-chunk diff, each reviewer reading chunks one at a time then merging into one whole-diff verdict (cross-family approval is on the whole diff, never chunk-by-chunk).
- Convergence to an asymptote, not oscillation — the bot settled into a stable approved state and committed.
- Both backends working through the whole run, including the GPT family on the local ChatGPT-forfait path.
Method
A single end-to-end run (019eb168) on feat/cloud-control-plane, ~7000 lines of diff. Reviewers/fixers: claude-opus-4-8 (Claude Code) and openai/gpt-5.5 (claw, ChatGPT-forfait). Sandboxed (per-run container), --merge-into none so the result lands on a storage branch for review. Budget raised to 8h/250$ for this run (the 2h/60$ default is too small for a diff this size); convergence took ~2h of effective run time.
Result
Converged: status finished, commit 1ffc4bc — fix(secrets,memory): enforce bot-secret binding egress and isolate bot memory by id, 745 insertions / 84 deletions across 30 files, build + tests green. Fast-forward-merged into the epic, then merged to main.
Convergence trajectory (blockers per verdict):
claude 2 → gpt 2 → claude 1 → gpt 1 → claude 1 → gpt 1
→ claude 0 (approved) → cross-family streak → commitA slight oscillation near the end (GPT re-raised one blocker before settling) is within the accepted asymptote behaviour; the prior_pushback / previous_scanned_areas feedback kept verdicts from re-litigating resolved items in a loop.
What Billy found and fixed
Genuine issues in the reviewed epic, fixed with tests:
cloudpublisherdid not persistRepoURL/RepoSHA/BotID, breaking cloud/webhook resume and bot-bound secret resolution. Fixed across the publisher,queue/types, and the run store.secretguarddid not intersect a bot-secret binding's egress hosts. Fixed, with ADR 018.- Bot memory was not isolated by bot id. Fixed (
fsstore,scope). - Binding-route validation tightened; new tests added.
Engineering hardening (the enabler)
Before the fix, gpt-5.5 on the ChatGPT forfait died on the 42-chunk review with context_length_exceeded — not a fundamental limit but a bug: the forfait's effective context window is smaller than the model's advertised 1.05M, so claw's preemptive compaction (sized to the advertised window) never triggered in time, and nothing reacted to the backend's rejection.
Fix: reactive force-compaction — on a context-window rejection the tool loop force-compacts the running history to a shrinking target (256k→128k→64k→32k, independent of the advertised window) and retries. Surfaced as an llm_retry, reusing claw's existing pure compactor. With it, Billy ran for hours with both GPT nodes (reviewer and fixer) and zero context-overflow deaths.
Operational resilience
The run absorbed several transient infrastructure interruptions — network drop, ChatGPT-forfait cap, and an intermittent sandbox bootstrap flake — via delegate-level network retries and an auto-resume loop that relaunches from the checkpoint (no progress lost) until convergence. None were the context-overflow bug; all were absorbed without operator intervention.
