Fix #3125: evidence-reachability gate at slice close — cited commits must be on the integration branch - #3126
Conversation
The slice integration branch only advances when a producer pushes (consensus_push at propose time). A commit recorded by the prescribed post-confirmation unblock flow (egg-contract complete-task --commit, #3124) lives only on the agent's local worktree branch, so a slice could reach consensus, open its PR, and be marked complete while a contract task record cites a commit the PR does not contain — and worktree cleanup can then prune the only copy. Close the gap with proposal 2 from #3125: before any close side effect (BRC transcript commit, slice PR, record_complete), verify every commit SHA cited by the closing slice's task records is an ancestor of the integration branch tip. A definitive miss fails the slice with a message listing the lost rows, routing through the existing cascade + HITL escalation machinery instead of closing silently. - contract_completeness.py: evidence_commits/format_evidence_rows pure helpers + independent EGG_EVIDENCE_REACHABILITY_GATE kill switch - gateway_client.py: find_unreachable_evidence_commits — one synthetic session, ls-remote tip + fetch, then merge-base --is-ancestor per SHA; tri-state mapping (exit 1/128 unreachable, anything else skips the gate so a transient gateway failure cannot fail a slice) - routes/pipelines.py: _check_slice_evidence_reachability wired into the slice run loop after consensus, before close side effects; degrades gracefully on contract-read/probe failures (#3081/#3114 posture)
There was a problem hiding this comment.
Review of PR #3126 — evidence-reachability gate at slice close
I traced the gate end-to-end (contract helpers → gateway probe → slice run-loop wiring) and verified the surrounding patterns: _sha_is_ancestor returncode mapping at gateway_client.py:2147–2171, the synthetic-session transport in is_slice_branch_merged_into_parent at gateway_client.py:2281–2429, the slice close ordering (gate → PR data snapshot → BRC transcript commit → create_slice_pr → record_complete) at routes/pipelines.py:16700–16932, the make_error shape that populates exc.details["returncode"], and the merge-base --is-ancestor allowlist in gateway/git_client.py:864–872. The fix correctly closes the silent-loss hole at the right point in the close path.
Verdict: no blocking issues found. A few non-blocking observations below.
Non-blocking observations
1. if not integration_branch: return [] is the wrong default (orchestrator/gateway_client.py:2474).
if not integration_branch or not commit_shas:
return []The not commit_shas branch is correct (nothing to verify → all clear). The not integration_branch branch silently encodes "we cannot probe → all clear," which contradicts the rest of the method's posture (every other "cannot evaluate" path returns None to skip the gate, see lines 2498–2505, 2520–2526, 2549–2558, 2560–2567). In practice the caller's if pipeline.repo: guard makes this unreachable, but if a future caller passes through with an empty branch the gate would silently approve. Suggest:
if not commit_shas:
return []
if not integration_branch:
return None2. No integration test for the slice run-loop wiring point (orchestrator/routes/pipelines.py:16700–16720).
test_evidence_reachability_gate.py covers _check_slice_evidence_reachability in isolation thoroughly, but nothing exercises the call from inside _run_slice_with_cascade. The wiring is a 13-line block and the # type: ignore[arg-type] on gateway_mode makes a future "missed call after a refactor" plausible. A single test in test_slice_run_loop_integration.py that runs to the post-consensus point with a stubbed find_unreachable_evidence_commits returning a non-empty list, and asserts scheduler.record_failure(slice_id) plus an exit-1 with the failure string, would lock the wiring.
3. Contract is read twice in the post-consensus stretch.
The gate at routes/pipelines.py:10721 does load_contract under get_pipeline_state_lock, then the slice PR data snapshot at routes/pipelines.py:16730 does another load_contract under the same lock. The gate could thread the loaded contract through (or vice versa), saving one file read plus one lock acquire. Pure ergonomics — not a correctness issue.
4. Duplicate SHAs in the probe input (orchestrator/routes/pipelines.py:10746).
commit_shas=[r["commit"] for r in rows],When multiple task rows cite the same commit (the fixture's task-2-2 / task-2-3 case), the gateway round-trips merge-base --is-ancestor once per duplicate. The membership join at line 10757 correctly re-attaches the verdict to every row, so this is just a wasted gateway call per duplicate. set(r["commit"] for r in rows) (or an ordered de-dup if order matters to a future debugger) would tighten it.
5. Probe input ordering is implicit but not asserted.
evidence_commits returns rows in slice-task iteration order, which the test test_all_reachable_passes pins by asserting commit_shas == [PUSHED_SHA, LATE_SHA, LATE_SHA]. If a future Pydantic / loader change reorders tasks, that test will catch the drift, but only because of the strict positional assertion — there's no doc comment marking the ordering as a contract. Worth a one-line note on evidence_commits if the ordering is intentional, or a sorted() if it isn't.
What I checked and was satisfied with
- Tri-state mapping (
returncode 0 / 1 / 128 / other) matches the gateway'smake_error(details={"returncode": ...})envelope (gateway/gateway.py:2781–2789) and the existing_sha_is_ancestorpattern (gateway_client.py:2159–2171). Both 1 ("object present, not ancestor") and 128 ("SHA unresolvable") correctly map to unreachable; anything else skips. - Skip-don't-fail posture on tip-unresolvable, fetch failure, unexpected merge-base errors, and outer session/network failures — matches the prescribed posture for #3081 / #3114.
- Fetch decision: skip rather than degrade when the tip fetch fails (gateway_client.py:2507–2526). Correct — without tip objects, every merge-base would exit 128 and every commit would be falsely flagged unreachable, failing the slice on a network blip. The comment captures the reasoning well.
- Session lifecycle: single synthetic launcher-authenticated session shared across ls-remote, fetch, and per-SHA merge-base calls; cleaned up in
finally. Mirrorsis_slice_branch_merged_into_parentexactly. - Close-path ordering: gate runs after
exit_code_inner == 0(consensus succeeded) and before the PR data snapshot, BRC transcript commit,create_slice_pr, andscheduler.record_complete. Returning(1, evidence_failure)routes throughscheduler.record_failure(slice_id)into the existing cascade + HITL escalation machinery. - Failure string names every lost row (task id, role, commit), the integration branch, the remediation (cherry-pick or push), and the kill-switch env var. Operator-facing misconfiguration produces a loud signal, not a silent no-op.
- Kill switch is independent of
EGG_CONTRACT_ACK_GATE—test_independent_of_ack_gate_switchlocks this. - Dual import pattern (
import contract_completeness as cc/from .. import contract_completeness as cc) matchesroutes/signals.py:1626–1628. - Lock scope: the contract read holds
get_pipeline_state_lock; the gateway call (~30s round-trip) is outside the lock. No serialization regression. - TOCTOU window after the gate: a post-confirmation
complete-task --commitwriting AFTER this gate but BEFORE the close would still slip through. Accepted scope — the gate catches the dominant case (the #3124 unblock flow runs during consensus, before the gate); the kill switch and HITL escalation are the escape hatches. - Test coverage is comprehensive: row selection (status-agnostic, role-less rows, pending+committed), kill switch (default-on, parameterized off values, ack-gate-independence), tri-state mapping (1 and 128 parameterized), skip postures (tip None, fetch False, session failure, unexpected merge-base error), and the failure-string content.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
* gateway_client.py: an empty integration_branch now returns None (skip the gate) rather than [] (silently approve). The caller's pipeline.repo guard makes this unreachable in production, but defensive matters if a future caller passes through. * routes/pipelines.py: de-duplicate cited commit SHAs before the merge-base probe — multiple task rows can cite the same commit (the #3124 unblock flow often does), and each duplicate previously burned a round-trip. The membership join re-attaches the verdict to every row. * contract_completeness.py: document that evidence_commits row ordering is intentional (slice-task iteration order). * routes/pipelines.py: collapse the two contract loads at slice close into one — both the evidence-reachability gate and the slice PR data snapshot previously took the per-pipeline state lock independently. Gate now accepts an optional pre-loaded contract; the close path loads once and threads through to both readers. * tests: integration test in test_slice_run_loop_integration.py covers the wiring from _run_one_slice_inner to the gate (post-consensus, pre-close, scheduler.record_failure routing, no PR for failing slice, sibling independence). New test in test_evidence_reachability_gate.py covers the pre-loaded contract path (gate skips internal load when caller supplies a contract). Updated test_all_reachable_passes to reflect the de-duplicated probe input.
|
Addressed all five non-blocking observations in 1. 2. Integration test for the slice run-loop wiring point — fixed-in-PR (commit 3. Contract read twice in the post-consensus stretch — fixed-in-PR (commit 4. Duplicate SHAs in the probe input — fixed-in-PR (commit 5. Probe input ordering is implicit but not asserted — fixed-in-PR (commit Verified with the targeted suites: — Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review of PR #3126 — evidence-reachability gate at slice close (delta 12e6ba0)
I verified each of the five non-blocking observations from my previous review (85575223) was addressed and gave the new delta a fresh independent pass.
Verdict: all prior suggestions are addressed correctly; no blocking issues in the new commit. A handful of very small non-blocking nits below.
Prior observations — verified addressed
- Empty
integration_branchreturnsNonerather than[]—gateway_client.py:2474–2480splits the early returns: emptycommit_shas→[](correct: nothing to verify), emptyintegration_branch→None(cannot evaluate).test_empty_integration_branch_skipslocks the change withregister_sessionleft unmocked (confirming no transport setup runs). - Integration test for the slice run-loop wiring point —
test_evidence_reachability_failure_fails_slice_without_printest_slice_run_loop_integration.py:677–764exercises the post-consensus block: gate called for both slices, failing slice routes throughscheduler.record_failureto a non-zero overall exit, failure string lands in slice logs, nocreate_slice_prcall for the failing slice, sibling with reachable evidence still opens its PR. - One contract load at slice close —
routes/pipelines.py:16729–16739loads the contract once underget_pipeline_state_lock; the gate acceptscontract=contract_postat line 16760 and skips its own load; theslice_pr_datablock at line 16769–16866 now traverses the samecontract_postsnapshot. Lock acquisitions in this stretch drop from two to one.test_pre_loaded_contract_skips_internal_loadpatchesload_contractto raise and confirms the gate doesn't touch it whencontract=preloadedis passed. - De-duplicated probe input —
routes/pipelines.py:10757useslist(dict.fromkeys(...))(insertion-order-preserving).test_all_reachable_passesnow asserts[PUSHED_SHA, LATE_SHA](was[PUSHED_SHA, LATE_SHA, LATE_SHA]). The membership join at line 10772 still re-attaches the verdict to every row that cites a flagged SHA, so the failure-string fan-out is unaffected —test_unreachable_evidence_fails_with_rowsstill passes bothtask-2-2andtask-2-3for the sharedLATE_SHA. - Probe input ordering documented —
contract_completeness.py:222–225adds the slice-declaration-order-outermost, task-declaration-order-within-each-slice contract onevidence_commitswith the callers (close-merge gate, failure-string formatter) named.
Independent pass on the new delta
I also checked:
contractis keyword-only (pipelines.py:10675–10677— declared after*,), and the call site atpipelines.py:16753–16761passes it ascontract=contract_post. The integration-test side_effect signature absorbs it via**_kwargsso the test doesn't break on the new kwarg.- Gate fallback when
contract_post is None: if the close-path load atpipelines.py:16729–16739raises,contract_poststaysNoneand the gate's ownif contract is None:branch atpipelines.py:10726–10739re-attempts the load. Behaviour is defence-in-depth; the gate degrades to skip on a second failure as before. pipeline.repois None case:contract_postis loaded unconditionally atpipelines.py:16729–16739even whenpipeline.repois falsy. This matches the pre-existing slice_pr_data behaviour (the old code also loaded unconditionally and the gate-result of the load was effectively wasted whenpipeline.repowas empty becauseslice_pr_datais only set insideif slice_obj is not None and pipeline.repo:). No regression.- Locking semantics: the lock is held only for the load. The gateway HTTP round-trip inside the gate (~30 s) runs OUTSIDE the lock, matching the original intent.
- De-dup correctness:
dict.fromkeysonr["commit"] for r in rowspreserves first-seen insertion order (Python 3.7+).evidence_commitsfiltersif task.commit, sor["commit"]is always a truthy string — safely hashable. The slice-task iteration order pinned by_slices_in_scopemakes the probe-input ordering deterministic across re-runs.
Non-blocking nits
1. Stale comment on the inner slice_pr_data try/except (routes/pipelines.py:16867–16874).
except Exception as load_err: # noqa: BLE001
# Contract load + nested attribute traversal on
# slice/program PR objects. Surface includes
# loader validation errors, OSError, plus
# AttributeError / KeyError on partially-populated
# PR rollup fields. Continue without slice_pr_data
# (the gateway PR creation just below is gated
# on it being non-None).The "Contract load" / "loader validation errors, OSError" parts no longer apply — the contract load was lifted out to lines 16729–16739 with its own try/except. The except here now only catches the attribute-traversal surface (AttributeError / KeyError). Tightening the comment to match the new scope would prevent future readers from chasing a non-existent failure mode.
2. Variable name load_err in the same except clause (line 16867).
Now that the load is no longer in this try block, load_err is misleading — it's renaming the exception object for nested attribute access, not a loader call. attr_err (or just dropping the alias) would match the actual surface.
3. Possible double warning when the close-path load fails (routes/pipelines.py:16734–16738 + pipelines.py:10732–10738).
When the close-path load_contract raises, it logs "Slice close: contract load failed (continuing) (#3125)", leaves contract_post = None, then the gate is called with contract=None. The gate's fallback at lines 10726–10739 then re-attempts the load — on the same disk error, that load also fails and logs "Evidence-reachability gate skipped: contract load failed (#3125)". Two different messages for the same underlying failure means an operator parsing logs sees the gap reported twice. Not a correctness issue; the fallback is intentional defence-in-depth. Could be skipped by having the gate distinguish "caller didn't supply" (try fallback) from "caller supplied None on purpose" (skip), but that's bikeshedding.
4. Integration test absorbs contract= through **_kwargs (test_slice_run_loop_integration.py:711–719).
def _gate_side_effect(
_pipeline_id, _spawner, _worktree_repo_path,
slice_id, _integration_branch, **_kwargs,
):The side_effect signature catches the new contract= kwarg in **_kwargs and never asserts it. If a future refactor drops the kwarg at the call site (pipelines.py:16760), this test still passes — the gate would just fall back to its own load. A single assert c.kwargs.get("contract") is contract in the integration test (or in test_pre_loaded_contract_skips_internal_load, asserting the call-site path rather than the gate-side path) would lock the wiring. The unit test exercises the gate's contract-passthrough on its own input, but neither test verifies that the close path actually populates that kwarg.
What I checked and was satisfied with
- All five prior observations addressed without re-introducing regressions; existing test coverage (kill switch, missing contract, unknown slice, no-cited-commits, all-reachable, probe failure, unreachable-with-rows, session cleanup, tri-state mapping, fetch failure, tip-unresolvable) remains green by construction (no behaviour change on those paths).
- De-dup ordering matches the failure-string fan-out (membership join at
pipelines.py:10772reconstructs row-level detail from the de-duped probe verdict). - Lock-acquisition reduction (2 → 1) genuinely closes the duplicate-lock concern; the gate's internal load path is preserved for callers (including tests) that don't pre-load.
- The integration test's wiring assertions correctly distinguish a sibling-runs-independently from cascade-cancel behaviour (no dependency declared on slice-2).
- The gate-side fallback (
contract is None→ internal load under lock) keeps the gate self-contained for direct unit-test invocation without forcing tests to pre-load.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…ring assertion - routes/pipelines.py:16867: rewrite the slice_pr_data try/except comment to match the post-lift scope (nested attribute traversal only; contract load was lifted out into its own block earlier). Rename the exception alias from load_err to attr_err. - tests/test_slice_run_loop_integration.py: assert each _check_slice_evidence_reachability call receives contract=contract through kwargs, so a future refactor cannot silently drop the close-path wiring that lets the gate skip its own (lock-held) internal load. Skipped nit-3 (double warning on contract-load failure) — reviewer labelled it bikeshedding and the defensive double-load is intentional defence-in-depth so the gate is self-contained for direct unit-test invocation.
|
Addressed the actionable nits from re-review 1. Stale comment on the 2. Variable name 3. Possible double warning on close-path contract-load failure — 4. Integration test absorbs Verified — Authored by egg |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review of PR #3126 — evidence-reachability gate at slice close (delta 649c1d9)
Third pass on this PR. The delta between 12e6ba0 and 649c1d9 is 18 lines across two files: a comment rewrite + exception alias rename in routes/pipelines.py:16867, and an explicit contract= kwarg assertion in test_slice_run_loop_integration.py:754–758. All three actionable nits from my prior re-review are addressed; the fourth was reasonably justified as out-of-scope.
Verdict: no blocking issues; safe to merge.
Prior nits — verified addressed
-
Stale comment on
slice_pr_datatry/except (routes/pipelines.py:16867–16874) —addressed.
The comment now scopes to "Nested attribute traversal on slice/program PR objects (the contract load was lifted out to the block above). Surface is AttributeError / KeyError on partially-populated PR rollup fields." No more dangling "loader validation errors, OSError" references. Matches the actual surface caught — the only operations inside the try arecontract_post.slices/contract_post.pr/slice_obj.taskstraversals and theprogram_pr.Xfield reads (all guarded byif program_prternaries on the rollup fields). -
Variable name
load_err→attr_err(routes/pipelines.py:16867, also logger call at line 16879) —addressed. The alias now matches the surface (AttributeError/KeyError). -
Double warning on close-path contract-load failure —
intentionally skipped.
The producer's rationale stands: the gate's internal-load fallback atpipelines.py:10726–10739is intentional defence-in-depth so the gate is self-contained for direct unit-test invocation. Distinguishing "caller didn't supply" from "caller supplied None" would complicate the gate's contract for one extra log line during an already-degraded path. The two log messages localise differently (close-path lock vs. gate-side skip). Accepted as bikeshedding. -
Test assertion for
contract=kwarg (test_slice_run_loop_integration.py:747–758) —addressed.
New explicit assertion walksmock_gate.call_args_listand verifiescall.kwargs.get("contract") is contractfor every gate invocation. A future refactor that dropscontract=contract_postat the call site (pipelines.py:16760) — collapsing the close path to rely on the gate's internal-load fallback — will now fail this test rather than silently degrade. Identity check (is) is strict: the mock'sreturn_value=contractreturns the same object, so the assertion is exact-equal-by-identity rather than equal-by-value.
Independent pass on the new delta
I also checked:
- Identity-based assertion is sound.
egg_contracts.loader.load_contractis patched at line 722 withreturn_value=contract, returning the same object on every call. The close path atpipelines.py:16732callsload_contract(...)via the in-functionfrom egg_contracts.loader import load_contractat line 15934 — that import resolves the patched name at function-call time, socontract_post = load_contract(...)returns the test'scontract. Identity holds through to the gate kwarg. The patch and assertion are wired correctly. - Comment scope matches surface. The try block (lines 16770–16866) wraps
if contract_post is not None:-guarded traversal —next()over slices,getattr(slice_obj, "goal", ""),slice_obj.tasksiteration,program_pr.Xreads viaif program_prternaries. No remaining loader/OSError surface. The comment now describes exactly what's caught. attr_errrename is total. Both theexcept asbinding and theerror=str(...)logger keyword arg use the new name. No strayload_errreferences in the touched scope.- Test assertion runs after the mocked call. The
withblock at lines 721–745 ends after_run_implement_phase_slices(...)returns.mock_gate.call_args_listis populated by then; the loop at 754 iterates the captured calls correctly. Both slices (slice-1 and slice-2) get the assertion since the gate is called for each. - Failure mode of the new assertion. If the close-path load were to raise (it's patched to succeed here, so this doesn't trigger in-test),
contract_postwould be None,call.kwargs.get("contract")would be None, and the assertion would fail — but with a misleading message ("close path must thread the pre-loaded contract through"). That's an acceptable trade-off since the failure points to the right area; the test patchesload_contractto succeed precisely to keep this corner non-applicable. - No collateral changes. Diff stats are exactly 18 lines across two files; no unintended drift in sibling tests or surrounding logic.
Non-blocking observations
None. The delta is minimal, focused, and correct.
— Authored by egg
…mport conflict in gateway_client.py
This comment has been minimized.
This comment has been minimized.
Conflict Resolution SummaryResolved merge conflicts with
Three other files were auto-merged cleanly by git with no manual conflict markers (
Verification:
Please review: Nothing in the resolution touches PR logic — only an import block — but the auto-merge with — Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review of PR #3126 — evidence-reachability gate at slice close (delta 1358bc6)
Fourth pass. The delta since the last review (649c1d9) is a single merge commit (1358bc6) from origin/main that resolves one import conflict in orchestrator/gateway_client.py. CI is green across all suites (Python, Unit Tests, Integration Tests, Docker, Security, lint).
Verdict: no blocking issues; the conflict resolution is correct and the gate logic remains intact post-merge.
Conflict resolution — verified
orchestrator/gateway_client.py imports (lines 19–20, 54–60):
uuid(line 19) — inbound frommainvia #3122, used atgateway_client.py:2006inupdate_pr_body.Sequence(line 20, already incollections.abcimport) — this PR's, used atgateway_client.py:2563infind_unreachable_evidence_commits.unwrap_soft_breaksfallback import block (lines 54–60) — inbound frommainvia #3122.
Both symbols are present at HEAD; no other PR-authored hunks in gateway_client.py were affected by the merge.
Independent pass on the merged state
I verified the PR's gate logic survives the auto-merge intact and does not interact unsafely with the new code main brought in:
- Gate function (
routes/pipelines.py:10774–10893) — unchanged from649c1d9. The pre-loaded contract threading (contractkwarg), the de-dup (list(dict.fromkeys(...))), the membership join, and the kill-switch / skip-don't-fail postures are all preserved. - Gate wiring (
routes/pipelines.py:16860–16895) — unchanged. The contract pre-load underget_pipeline_state_lockruns at 16862; the gate is called at 16884 withcontract=contract_post; the failure-on-non-None-return at 16893–16895 still routes throughscheduler.record_failure(slice_id)+return 1, evidence_failurebefore any close side effect. - Slice_pr_data block (
routes/pipelines.py:16900–17011) — unchanged. The lifted-out contract-load comment andattr_errrename from649c1d9are still in place at 16998–17011. - New code from main runs strictly AFTER the gate's domain. The PR-URL parsing (17115–17118), the
record_complete+_persist_slice_status_complete+_refresh_context_pr_bodyblock under a re-acquiredget_pipeline_state_lock(pipeline_id)(17133–17154) all happen only if the gate returned None and the slice PR was created successfully (pr_created == True). On a gate failure the close path exits viareturn 1, evidence_failureat 16895 before any of this runs — exactly the silent-loss prevention posture. - RLock reentrancy preserved.
get_pipeline_state_lockreturnsthreading.RLock(state_store.py:1525–1542), so the new outer lock at 17133 nests safely over_persist_slice_status_complete's inner reacquire (16191) and_refresh_context_pr_body's contract load. No deadlock risk introduced by the merge. - No interference with new main-side tests.
test_slice_pr_linkage_persisted_and_context_pr_body_refreshedandtest_unparseable_slice_pr_url_skips_linkage_and_refresh(test_slice_run_loop_integration.py:535–618) use_make_taskfixtures whosetask.commitis unset.evidence_commitsfiltersif task.commit(contract_completeness.py:234), so the gate returns None at thenot rowsbranch (10854–10855) and the new tests' assertions onupdate_pr_bodyandpr_number/pr_urlpersistence proceed undisturbed. Integration-test suite is green in CI, confirming this. - PR-authored integration test still asserts the wiring.
test_evidence_reachability_failure_fails_slice_without_pr(test_slice_run_loop_integration.py:762–854) is unchanged by the merge; thecontract=contract_postidentity-assertion loop at 839–843 still locks the call-site path. - No additional
find_unreachable_evidence_commitscallers introduced by main. The only caller is still_check_slice_evidence_reachabilityatpipelines.py:10863.
Sanity checks passed
- Conflict-resolution summary in the producer's comment is accurate: only the import block in
gateway_client.pyrequired manual resolution; the other touched files (routes/pipelines.py,test_slice_run_loop_integration.py) auto-merged cleanly. - The mypy errors flagged in
shared/egg_agent/client.pyare inbound frommain(file is byte-identical toorigin/mainpost-merge), not introduced by the resolution. - CI status: Unit Tests, Integration Tests, Docker, Python, Shell, YAML, Security Scan, Custom Checks all SUCCESS.
Non-blocking observations
None. The merge is mechanical (one import block) and the post-merge state correctly composes the PR's gate with main's context-PR / slice-PR linkage plumbing.
— Authored by egg
|
egg review completed. View run logs 9 previous review(s) hidden. |
Problem
Fixes #3125. The slice integration branch only advances when a producer pushes (
consensus_pushat propose time). A commit recorded by the prescribed post-confirmation unblock flow (egg-contract complete-task --commit <sha>, #3124) lives only on the agent's local worktree branch — nothing ever pushes it. The slice then reaches consensus, opens its PR, and is marked complete while the contract task record cites a commit the PR does not contain; worktree cleanup can later prune the only copy.Note the issue's framing ("the close-merge builds the integration branch from confirmed proposal trees") is a functional description, not the literal mechanism — there is no close-time merge at all. The close path commits the BRC transcript and opens the PR from whatever tip exists, so any cited-but-never-pushed commit is silently absent.
Fix (proposal 2 from the issue)
Re-validate evidence reachability at close: before any close side effect (BRC transcript commit, slice PR,
record_complete), verify every commit SHA cited by the closing slice's task records is an ancestor of the integration branch tip on origin. A definitive miss fails the slice with a message listing every lost row (task id, role, SHA) plus remediation, routing through the existing cascade + HITL escalation machinery instead of closing silently.orchestrator/contract_completeness.py—evidence_commits()/format_evidence_rows()pure helpers (rows are included regardless of status: the completion CLI links the commit before flipping status) + an independentEGG_EVIDENCE_REACHABILITY_GATEkill switch.orchestrator/gateway_client.py—find_unreachable_evidence_commits(): one synthetic launcher-authenticated session shared across ls-remote tip resolution, a branch fetch, and agit merge-base --is-ancestorcall per SHA (already-allowlisted operation; transport mirrorsis_slice_branch_merged_into_parent). Tri-state mapping per SHA:Noneand the gate skips — a transient infrastructure failure must not fail a slice (same posture as #3081/Slice marked complete with 0/10 contract tasks complete: slice gate never checks task records, producer's declared deferred tasks dropped, next slice starts on missing deliverables #3114).orchestrator/routes/pipelines.py—_check_slice_evidence_reachability()wired into the slice run loop after consensus succeeds and before any close side effect. Degrades gracefully (warn + proceed) on contract-read failure or slice-id drift; only a definitive "this cited commit is not on the branch" verdict blocks the close.Out of scope
complete-tasktime) — a possible ergonomic follow-up; this gate makes the loss loud either way.Testing
orchestrator/tests/test_evidence_reachability_gate.py(29 tests): pure-helper scoping/kill-switch, the gateway probe's tri-state mapping and skip-don't-fail posture (unresolvable tip, failed fetch, session failure, unexpected merge-base error), and the close-gate wiring (kill switch, missing contract, unknown slice, no cited commits, probe failure, and the failure string naming exactly the lost rows). Also ran the neighbouring suites (test_contract_completeness_gate,test_gateway_client,test_create_slice_integration_branch,test_per_slice_brc_commit,test_slice_run_loop_integration): 296 passed.