verify merge result fails when the gate is stale against the merge result, so carry-through cannot complete a run - #154
Merged
Merged
Conversation
…result, so carry-through cannot complete a run Closes #153
…the merge result, so carry-through cannot complete a run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Plan
Plan: Fix stale gate binary in verify-merge (issue #153)
Context
Carry-through resolutions fail unconditionally when
mainhas taken anycommit touching
tools/build/since the arena last built.stepVerifyMergematerialises the merge result via
PrepareMergeResult(a local merge oforigin/maininto the worktree), then runsbin/gate. If that mergebrought newer tool source, the compiled gate is stale against the tree it is
asked to measure. The gate's staleness check (
tools/build/common/stale.go:StaleReason)exits without printing an envelope; the runner classifies that as
OutcomeDied;runIntegrationGatereturns a plain error; the step failsand the resolution stops.
Nothing is misreported: the runner correctly says "died" and the step
correctly says "this is not the change failing." The fault is upstream: a
stale binary was asked to measure a tree it cannot, and nothing rebuilt it
first. Separately, the non-measured outcome and a verdict refusal both land
as a plain step failure, discarding a distinction the runner already made.
Normative check
Consistent.
docs/backend.md(Optional capabilities table, line 115)documents
MergeResultPrepareras an optional worktree capability; the newToolsRebuilderfollows the same pattern.docs/gates-and-commands.md(lines 26-41) requires that the gate measures the merge result, and
docs/resolution-standalone.md(lines 113-124) requires that carry-throughstill verifies against what will actually land. No normative document speaks
to tool rebuilding or to the transient classification of non-measured
outcomes; the documents are silent on both, and nothing in them contradicts
either change. The plan includes a doc update to
docs/backend.mdaddingToolsRebuilderto the optional capabilities table.Changes
1. Add
ToolsRebuilderoptional interfaceFile:
backend.go(afterMergeResultPreparer, ~line 794)2. Implement on github worktree
File:
pkg/backend/github/worktree.go(afterRevertMergePrep, ~line 164)Uses the existing
w.run()method (line 267), which executes a command incfg.WorktreeDirviaexec.Command../makeis at the repo root(=
cfg.WorktreeDir), has a shebang, and short-circuits if up-to-date.bin/is gitignored, so the rebuild does not affectStatusPorcelainchecks.3. Rebuild tools in
stepVerifyMergeafter merge-prepFile:
issue/steps_integration.go(inside theif prep, okblock,after
PrepareMergeResultsucceeds, beforerunIntegrationGate)Insert after the defer block (~line 47), before line 50:
The rebuild is inside the merge-prep guard: it runs only when the tree was
actually changed by a merge simulation. A worktree that does not implement
MergeResultPreparerwill not reach it. A rebuild failure is a plain error(not
ErrTransient) because it signals a broken build environment, not atransient condition.
4. Non-measured gate outcomes wrap
ErrTransientFile:
issue/steps.go, functionrunIntegrationGate(lines 721-746)Three branches currently return plain errors for infrastructure failures.
Wrap each with
flow.ErrTransientso the orchestrator parks without burningbudget:
a)
RunGateerrored (line 724-728): no gate ran at all.b) Non-measured outcome (line 729-733): died, timed out, etc.
c) Judge errored (line 734-738): no verdict exists.
NOT changed: the verdict-refusal branch (line 740-744). A refusal is
the gate's answer about the change -- deterministic, not transient.
Both call sites of
runIntegrationGatebenefit:stepVerifyMerge(line 50) and the contributor-mode gate in
stepOpenPR(line 630).5. Update normative doc
File:
docs/backend.md(line 116, optional capabilities table)Add a row after
MergeResultPreparer:6. Tests
File:
issue/steps_integration_test.goa) Extend
integrationWorktreewithToolsRebuilder:toolsRebuilt bool,rebuildToolsErr errorRebuildToolsthat records the call, setstoolsRebuilt, returnsrebuildToolsErrb) Update
TestStepVerifyMerge_GatePassesMergeResultAccepted(line 125):wt.toolsRebuilt == truemerge-prepbeforerebuild-toolsbeforegate:integrationc) Update
TestStepVerifyMerge_GateNotMeasured(line 190):errors.Is(err, flow.ErrTransient)d) Update
TestStepVerifyMerge_GateError(line 176):errors.Is(err, flow.ErrTransient)e) Update
TestStepVerifyMerge_JudgeError(line 206):errors.Is(err, flow.ErrTransient)f) Add
TestStepVerifyMerge_RebuildToolsFails:rebuildToolsErr, verify step returns error, verify!errors.Is(err, flow.ErrTransient)(plain error, not transient)g) Add
TestStepVerifyMerge_RebuildToolsCallOrder:callsslice hasmerge-prepthenrebuild-toolsthengate:integrationh) Check whether
TestStepVerifyMerge_WithoutMergeResultPreparer(line 248)needs an assertion that
rebuild-toolswas NOT called (it should alreadybe skipped since the rebuild is inside the
MergeResultPreparerguard).File:
issue/steps_test.go(contributor-mode gate tests)i) Any tests asserting on
runIntegrationGateerrors for non-measured outcomesor gate/judge errors need
errors.Is(err, flow.ErrTransient)assertions added.Search for uses of
gateOutcomeandgateErrinstepOpenPRtests.Deliberately not done
No rebuild in the contributor-mode gate (
stepOpenPR, line 630).That step measures the branch as-is, not a merge result. The tree has not
changed from under the gate, so staleness there is a different cause with a
different fix.
No
ErrRefusedforOutcomeCouldNotStart. All non-measured outcomesshare the same transient treatment. Refining them into separate sentinels
is future work if needed, not part of this fix.
No rebuild-revert in the defer. After
RevertMergePrepresets thetree, the rebuilt tools are stale against the reverted tree. This is
harmless: the next steps (
stepMerge,stepRecordMerge) do not run thegate, and the claim ends after them.
Verification
go build ./...-- compiles.go test ./issue/... -run TestStepVerifyMerge-- all verify-merge tests pass,including the new ones.
go test ./pkg/backend/github/... -run TestGate-- existing gate runner testsunaffected.
go test ./...-- full suite green.bin/verify-- the project's own gate passes.Review
bin/verifypasses clean. Here is the review.Review
The change does both things the issue asks for, in the right order:
Rebuild tools against the merge result (
ToolsRebuilderinterface +RebuildToolscall instepVerifyMerge, afterPrepareMergeResultand before the gate). This is the primary fix — stale binaries are rebuilt before they can refuse to measure.Classify infrastructure failures as transient (
ErrTransientwrapping inrunIntegrationGatefor all three non-code-failure error paths). This is the fallback — if for any reason the gate still can't run, the orchestrator parks transient instead of burning invocation budget and reporting a failed step.Correctness
MergeResultPreparerblock — it only runs when the tree was actually changed by a merge simulation. Without merge prep there's no staleness.ErrTransientwrappings target exactly the right cases: (a) gate couldn't start, (b) gate ran but didn't measure (died), (c) judge couldn't answer. The actual-failure path (verdict not acceptable) is left as a hard error — correctly, since that is the change failing.RebuildToolsfailure is not transient (test atsteps_integration_test.go:320), which is correct: a broken build isn't going to fix itself on retry../makeis the project's standard build trampoline; hardcoding it here matches every other reference intools/build/.Scope
Every file in the diff is load-bearing for the issue. No unrelated changes.
Duplication
No existing rebuild mechanism was available on
Worktree; the new interface is the first one.Tests
TestStepVerifyMerge_GatePassesMergeResultAccepted).ErrTransientwrapping.WithoutMergeResultPreparerasserts neither merge-prep nor rebuild-tools fires on a worktree that doesn't support them.stepOpenPRtests that already exercised these samerunIntegrationGateerror paths now additionally assertErrTransient. These tests would fail if the wrapping were reverted.Normative documents
docs/step-handler.mddefinesErrTransientas "infrastructure failures the handler observed" — a gate that can't start or didn't measure is exactly that. The one potential tension isErrRefused's description ("a required tool is out of date"), but the issue's own analysis distinguishes:ErrRefusedis deterministic and would strand the item, whereas stale tools are transient because a rebuild between runs clears them.docs/backend.mdis updated with the newToolsRebuildercapability row, consistent with the existing table format.Nothing found to fix.
Coverage
bin/verifypasses clean.Already covered by the existing tests in the diff:
runIntegrationGatewrapErrTransient(gate error, non-measured outcome, judge error) — tested from bothstepVerifyMergeandstepOpenPRcallers.RebuildToolscalled after merge prep, before the gate, in the correct order.RebuildToolsfailure stops the step with a non-transient error.RebuildToolsnot called when the worktree is not aMergeResultPreparer.RevertMergePrepcalled even when the gate fails.Added:
TestStepVerifyMerge_GateRefuses(issue/steps_integration_test.go): asserts that a gate refusal does NOT wrapErrTransient. This is the central invariant of the change — infrastructure failures are transient, real verdicts are not. Without this assertion, a regression that made refusals transient would pass all tests.TestStepOpenPR_DoesNotProposeWhatTheJudgeRefuses(issue/steps_test.go): same not-transient assertion for the open-PR caller ofrunIntegrationGate.TestStepVerifyMerge_RebuildToolsFails(expanded,issue/steps_integration_test.go): two new assertions on the failure path — (a) the gate must not have been called (short-circuit), and (b)RevertMergePrepmust still be called (the defer was set before rebuild ran). The original test only checked that the step errored and wasn't transient.Not added, and why:
worktree.RebuildToolsin the GitHub backend — it delegates tow.run(ctx, "rebuild tools", []string{"./make"}), which is the samerunhelper every other worktree method uses. Testing it would restate the implementation.TestStepVerifyMerge_MergeConflictsis not transient — that path was not changed by this diff, and merge conflicts are a genuinely ambiguous category (they can resolve after main changes).Gate
integrationmeasuredtruemeasurement:
thresholds:
Closes #153