Skip to content

Fix #2337: rebase fallback in _sync_worktree_with_remote + loud-fail populator/slice-gate - #2352

Merged
jwbron merged 4 commits into
mainfrom
egg/issue-2337-fix-sync-worktree-rebase
Apr 30, 2026
Merged

Fix #2337: rebase fallback in _sync_worktree_with_remote + loud-fail populator/slice-gate#2352
jwbron merged 4 commits into
mainfrom
egg/issue-2337-fix-sync-worktree-rebase

Conversation

@jwbron

@jwbron jwbron commented Apr 30, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes #2337. The orchestrator-side worktree sync used git merge --ff-only on divergence, which can't reconcile a worktree where local has unpushed commits AND origin has commits the local doesn't. It returned silently, leaving agents' plan-phase commits on origin while the populator ran against the stale local checkout, hit its plan_draft_missing early-return, and produced contract.slices=[]. The implement-phase slice-loop gate then silently routed to monolithic, demoting issue-2261's 15-slice plan to a slice-1-only PR (#2335) carrying program-level copy.

Three behavioural changes plus one structural fail-loud guard:

  • _sync_worktree_with_remote rebases on divergence. Reuses gateway_client's _rebase_with_agent_output_autoresolve — the same helper the gateway-side push-reject path uses. The --ff-only branch is gone.
  • Every return path emits worktree_sync_outcome. Single structured log line with a case discriminator (fetch_failed, detached_head, no_remote_tracking, local_ahead_pushed, divergence_rebased, divergence_rebase_failed, reset_succeeded, reset_failed, …). The pre-fix function had four no-log early returns — [no further sync log lines] was literally the only signal of which path fired.
  • _populate_contract_from_plan_safe takes a source parameter. source="plan_complete" (natural call site) probes origin/{branch}:{draft_rel} via git cat-file when local is missing the draft; if origin has it, raises PlanDraftMissingOnLocalError with an OVERSEER_ALERT log so _run_pipeline marks the pipeline FAILED. source="advance_phase_force" keeps the swallow-everything contract from Force-advance out of plan phase skips _populate_contract_from_plan, leaving contract.pr empty and PR metadata as fallback placeholders #1941.
  • Slice-loop gate refuses monolithic demotion. New _slice_gate_block_monolithic_demotion helper: when contract.slices is empty but the on-disk plan draft parses to N>1 slices, the implement phase is marked FAILED with OVERSEER_ALERT slice_gate_blocked_monolithic_demotion instead of routing through _run_concurrent_phase. Defensive belt-and-braces — given the loud-fail populator above, this should rarely fire, but it catches any future code path that bypasses the populator.

The decision-sync state_store mismatch noted in #2337 as "worth filing separately" is left for a follow-up issue.

Test plan

  • make test on changed files: 49 tests pass (sync_worktree, populate_contract_audit_events, slice_gate_recheck, advance_phase_populate_on_plan_exit).
  • Adjacent suites: test_advance_phase_thread, test_pipeline_failure_path, test_slice_run_loop_integration — 69 tests, all green.
  • ruff check and ruff format clean across all changed files.
  • Operator verification on the next multi-slice issue pipeline: confirm worktree_sync_outcome case=divergence_rebased appears in logs at plan→implement boundary; confirm contract.slices populates when origin had unpushed plan commits at sync time.

…populator/slice-gate

The orchestrator-side worktree sync used `git merge --ff-only` on
divergence, which by definition cannot reconcile a `local-ahead AND
remote-ahead` worktree. The function returned silently, leaving agents'
plan-phase commits on origin while the populator ran against a stale
local checkout, hit `if not plan_path.exists(): return`, and produced
an empty `contract.slices`. The implement phase's slice-loop gate then
silently routed to monolithic, demoting issue-2261's 15-slice plan to
a slice-1-only PR (#2335) carrying program-level title/description.

Three behavioural changes, one structural change:

1. `_sync_worktree_with_remote` reuses gateway_client's
   `_rebase_with_agent_output_autoresolve` on divergence — the same
   helper the gateway-side push-reject path already uses. The merge
   --ff-only branch is gone.

2. Every return path emits a single `worktree_sync_outcome` log line
   with a `case` discriminator (fetch_failed, detached_head,
   no_remote_tracking, local_ahead_pushed, divergence_rebased,
   divergence_rebase_failed, reset_succeeded, reset_failed, ...).
   The pre-fix function had four no-log early returns, which made
   `[no further sync log lines]` the only signal of which path fired.

3. `_populate_contract_from_plan_safe` takes a `source` parameter.
   `source="plan_complete"` (natural call site) now probes
   `origin/{branch}:{draft_rel}` via git cat-file when the local
   draft is missing — if origin has it, raises
   `PlanDraftMissingOnLocalError` with an OVERSEER_ALERT log so the
   pipeline can fail loudly. `source="advance_phase_force"` keeps
   the swallow-everything behaviour from #1941.

4. The implement-phase slice-loop gate calls
   `_slice_gate_block_monolithic_demotion`: when contract.slices is
   empty but the on-disk plan draft parses to N>1 slices, the phase
   is marked FAILED with an OVERSEER_ALERT instead of routing to
   _run_concurrent_phase. Defensive belt-and-braces — given the
   loud-fail populator above, this gate should rarely fire, but it
   catches any future code path that bypasses the populator.

Tests cover all four paths: rebase happy/error, every outcome log
case, source="plan_complete" vs "advance_phase_force" branches with
mocked origin probe, and the slice-gate helper across multi/single/
missing/parse-fail plan drafts.
@james-in-a-box

This comment has been minimized.

@egg-reviewer egg-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: ACK / APPROVE — non-blocking suggestions

The core fix is sound and correctly addresses #2337. Replacing git merge --ff-only with _rebase_with_agent_output_autoresolve lets the worktree reconcile the bidirectional divergence (local ahead and origin ahead) that previously stranded plan-phase commits on origin. The defense-in-depth is well-layered:

  1. Sync rebases divergence instead of bailing out (orchestrator/routes/pipelines.py:5232-5511)
  2. Populator natural-source loud-fail (source="plan_complete") raises PlanDraftMissingOnLocalError when local is missing the draft but origin has it (orchestrator/routes/pipelines.py:13008+)
  3. Slice gate recheck in _run_pipeline re-parses the on-disk plan and refuses to demote N>1 plans to monolithic implement (orchestrator/routes/pipelines.py:12906-12957, 14949-14998)

The worktree_sync_outcome case-discriminator logging gives ops a clean signal-per-path for future post-mortems. The _populate_contract_from_plan_safe Literal["plan_complete", "advance_phase_force"] split correctly preserves #1941 swallow-everything semantics for force-advance while strengthening the natural plan→implement transition. Tests cover the new helpers in isolation reasonably well.

I traced the full data flow end-to-end (sync → populator → slice gate → _run_pipeline outer loop) and verified the multi-slice test fixture parses to 2 slices via the production parse_plan. Nothing I found rises to blocking; below are non-blocking suggestions.

Non-blocking suggestions

S1 — PR description over-claims single-emit invariant

The PR body says "Every return path emits a single worktree_sync_outcome." The push-fail-fallthrough branch (pipelines.py:~5380-5400) emits local_ahead_push_failed_falling_through_to_reset and then a second outcome from the subsequent reset path. Either reword the description or note explicitly that fall-through paths emit a sequence (which is actually nicer for ops than a single conflated event).

S2 — Missing worktree_sync_outcome on prior-phase-failed discard

The "Prior phase failed — discarding local-ahead commits" branch around pipelines.py:5414-5420 doesn't emit a worktree_sync_outcome, breaking the case-discriminator parity. Add case="local_ahead_discarded" (or similar) so log-based dashboards see every exit path.

S3 — PlanDraftMissingOnLocalError handler bypasses graceful cleanup

The new try/except at pipelines.py:15224-15250 marks the pipeline FAILED and breaks out of the outer loop directly, skipping the if phase_failed: cleanup block (which would have called _teardown_phase_overseer and report_pipeline_status gracefully). The finally: at pipelines.py:15989+ does eventually tear down the overseer container so it's not a leak — but it's inconsistent with the slice-gate failure handler at 14949-14998, which does set phase_failed = True and reuses the existing cleanup path. Consider restructuring the plan-draft-missing path to do the same; it makes the cleanup story uniform and keeps the next reader from wondering whether the divergence was intentional.

S4 — _origin_has_plan_draft has no direct test

All current callers mock it. A subprocess-arg regression (e.g. swapping git cat-file -e for git rev-parse, dropping origin/, mis-quoting the path) would slip through. A minimal test against a tmp_path git repo with a real git init && git fetch setup would cost little and protect a load-bearing branch.

S5 — Reset paths double-log

Both reset_failed and reset_exception emit the legacy log line and the new worktree_sync_outcome. Not harmful — slightly redundant. Could collapse to a single emission once the dashboards are migrated to the new case field.

S6 — Integration test gap on the new failure paths

The new tests cover the helper functions in isolation. There's no integration coverage of (a) PlanDraftMissingOnLocalError raising into _run_pipeline and exiting cleanly, or (b) the slice-gate state transition with a real contract+plan mismatch. Both paths are now load-bearing for plan→implement correctness; an integration test against a tmp pipeline would meaningfully harden the change.

S7 — pipeline.base_branch is None inherits #2222 contamination

When pipeline.base_branch is None, _rebase_with_agent_output_autoresolve falls through _build_rebase_cmd (gateway_client.py:2222) to plain git rebase origin/{branch} — the same form that triggered #2222 main-contamination. This is not a regression (it matches the gateway-side push-reject rebase behavior), but worktree-sync now inherits the same risk surface. Worth a code comment at the call site or a defensive log noting that a None base_branch falls back to the bare-rebase form, so the next person debugging a contamination bug knows to look here.

— Authored by egg

@james-in-a-box

This comment has been minimized.

@james-in-a-box

This comment has been minimized.

S2: Emit worktree_sync_outcome with case=local_ahead_discarded_falling_through_to_reset
on the prior-phase-failed branch so log-based dashboards see a uniform
exit-path event for every branch.

S3: Mirror the slice-gate failure handler in the PlanDraftMissingOnLocalError
catch — set phase_failed=True and run the full cleanup sequence (overseer
teardown, status report, best-effort push) inline so both load-bearing
plan-phase failure paths have a uniform cleanup story.

S4: Direct subprocess-argv tests for _origin_has_plan_draft. The sandbox
blocks git init so we can't build a real-clone fixture; instead the tests
mock subprocess.run and assert the exact argv (cat-file -e
origin/{branch}:{path} with -C {repo}) so any subprocess-arg regression
the reviewer named — swapping cat-file for rev-parse, dropping the -e flag,
dropping the origin/ prefix, mis-quoting the path — trips a test failure.

S7: Comment + warning log at the divergence-rebase call site flagging
that base_branch=None falls back to the bare-rebase form (the #2222
contamination vector) so the next person debugging contamination has a
breadcrumb. Add tests verifying the warning fires with base_branch=None
and stays silent with base_branch set.

S5 (deferred to #2359): collapse double-log on reset paths once dashboards
migrate to the new case discriminator.

S6 (deferred to #2360): integration tests for the new failure paths.
@james-in-a-box

Copy link
Copy Markdown
Contributor

Thanks for the thorough review. Per-item disposition:

  • S1 — PR description over-claims single-emit invariant. fixed-in-PR (commit 4948912) for the code path; the prose-only PR-body edit was blocked by the gateway (PR is owned by jwbron, not egg). The body should be reworded to: "Most return paths emit one outcome; the two intentional fall-through paths (push-fail-fallthrough-to-reset, prior-phase-failed-discard-then-reset) emit a sequence — first a discriminator naming WHY we fell through, then the terminal reset_succeeded / reset_failed / reset_exception event." Operator can apply this when they next touch the PR.

  • S2 — Missing worktree_sync_outcome on prior-phase-failed discard. fixed-in-PR (commit 4948912). Added case="local_ahead_discarded_falling_through_to_reset" at pipelines.py:5414-5429 plus test_local_ahead_discarded_emits_outcome covering the new emission AND the subsequent reset_succeeded to lock in the documented sequence behaviour.

  • S3 — PlanDraftMissingOnLocalError handler bypasses graceful cleanup. fixed-in-PR (commit 4948912). The handler at pipelines.py:15224-15291 now mirrors the slice-gate path: sets phase_failed = True, then runs the same cleanup sequence as the existing if phase_failed: block — overseer teardown under overseer_lock, report_pipeline_status, best-effort push_worktree_branch for backup. Cleanup story is uniform across both load-bearing plan-phase failure paths now.

  • S4 — _origin_has_plan_draft has no direct test. fixed-in-PR (commit 4948912). Five direct tests in TestOriginHasPlanDraft. The sandbox blocks git init so a real-clone fixture wasn't viable; instead the tests mock subprocess.run and assert the exact argv (cat-file -e origin/{branch}:{path} with -C {repo_path} ordering checked), which catches each named regression — swapping cat-file for rev-parse, dropping -e, dropping the origin/ prefix, mis-quoting the path, or misrouting -C. Plus tests for return-True on zero exit, return-False on non-zero exit, and exception-swallow.

  • S5 — Reset paths double-log. deferred-to Collapse legacy log + worktree_sync_outcome on reset paths once dashboards migrate #2359. Reviewer explicitly conditioned the cleanup on dashboard migration ("Could collapse to a single emission once the dashboards are migrated to the new case field"). Collapsing prematurely would break consumers still keyed off the legacy literal — outside this PR's scope. Issue Collapse legacy log + worktree_sync_outcome on reset paths once dashboards migrate #2359 captures the trigger condition and the exact lines to delete.

  • S6 — Integration test gap on the new failure paths. deferred-to Integration tests for PlanDraftMissingOnLocalError + slice-gate failure paths #2360. Both paths (PlanDraftMissingOnLocalError raising into _run_pipeline, slice-gate state transition with a real contract+plan mismatch) need a tmp-pipeline fixture that doesn't currently exist for the _run_pipeline outer-loop path — building it is larger than the original review-feedback scope and would balloon this PR. Justification (i): scope creep. Issue Integration tests for PlanDraftMissingOnLocalError + slice-gate failure paths #2360 names the two specific tests to add and points at the existing failure-path fixture base.

  • S7 — pipeline.base_branch is None inherits Investigate why pipeline branch ate main commits + carried stale prior-run history #2222 contamination. fixed-in-PR (commit 4948912). Block comment at pipelines.py:5436-5448 documents the contamination risk; warning log fires when base_branch_for_reconcile is None so the next person debugging duplicate-by-content commits in a worktree-sync rebase has a breadcrumb. Two tests cover both states (warns with None, silent with main).

Post-fix: 90 unit tests pass across test_sync_worktree, test_populate_contract_audit_events, test_slice_gate_recheck, test_advance_phase_thread, test_pipeline_failure_path. Ruff clean.

— Authored by egg

@james-in-a-box

This comment has been minimized.

@james-in-a-box

This comment has been minimized.

Resolves conflicts between this branch's rebase-fallback divergence-handling
work and main's worktree-sync outcome-taxonomy refactor (#2349). Both PRs
touched the same `_sync_worktree_with_remote` exit-path logging surface.

Resolution: kept this branch's rebase implementation (replaces the old
`--ff-only` reconcile, which couldn't fix true divergence), and adopted
main's case-discriminator taxonomy and assertion style (`logger.info(...,
case=...)`, strict `_outcome_cases(mock) == [...]` assertions). The two
ff-only-only tests from main (`test_case_diverged_ff_*`) were dropped —
the code path they covered no longer exists on this branch. Renamed
this branch's `local_ahead_discarded_falling_through_to_reset` to main's
shorter `local_ahead_discarded` for taxonomy consistency.
@james-in-a-box

Copy link
Copy Markdown
Contributor

Merge conflict resolution — origin/main → PR #2352

Merged origin/main into this branch via git merge --no-commit (no rebase, history preserved). Conflicts arose because PR #2349 landed on main while this PR was open; both touched _sync_worktree_with_remote's exit-path logging surface with overlapping intent.

Resolution summary

File Region Resolution
orchestrator/routes/pipelines.py fetch_failed, detached_head, branch_detect_failed, no_remote_tracking, rev_parse_failed cases Took main — adopted main's logger.info("worktree_sync_outcome", pipeline_id=..., branch=..., case=...) taxonomy and field ordering.
orchestrator/routes/pipelines.py rev_list_failed (rc!=0/parts!=2/exception) Took main — main's branch unified the two failure modes under a single case=rev_list_failed.
orchestrator/routes/pipelines.py local_ahead_pushed Combined — kept this PR's PushResult variable + main's category=push_result.category, error=push_result.detail propagation.
orchestrator/routes/pipelines.py local_ahead_push_failed, local_ahead_discarded Combined — kept the case logic from this PR, renamed the _falling_through_to_reset suffix away to match main's shorter taxonomy names.
orchestrator/routes/pipelines.py Divergence (local_ahead>0 AND remote_ahead>0) Took HEAD — kept this PR's _rebase_with_agent_output_autoresolve call (the whole point of #2337) and the S7 base_branch=None contamination warning. Replaces main's --ff-only merge attempt, which can't reconcile true divergence by definition.
orchestrator/routes/pipelines.py Step 4 reset exception block Took main — collapsed this PR's separate reset_exception log into main's single case=reset_failed for both rc-failure and exception paths.
orchestrator/tests/test_sync_worktree.py _outcome_cases helper, TestSyncWorktreeOutcomeTaxonomy class Took main — adopted mock_logger.method_calls-based collector (preserves call order across info/warning/error) and the strict == [...] assertion style.
orchestrator/tests/test_sync_worktree.py test_case_diverged_ff_succeeded, test_case_diverged_ff_failed_returncode, test_case_diverged_ff_failed_exception (main) Deleted — these test the --ff-only code path that this PR replaces with rebase.
orchestrator/tests/test_sync_worktree.py test_divergence_rebased_emits_outcome, test_divergence_rebase_failed_emits_outcome, S7 contamination-warning tests (HEAD) Took HEAD — renamed to test_case_diverged_rebased / test_case_diverged_rebase_failed for taxonomy consistency, kept the S7 tests verbatim.
orchestrator/tests/test_sync_worktree.py test_local_ahead_discarded_emits_outcome (HEAD) Dropped — main's test_case_local_ahead_discarded already covers this with the correct (shorter) case name.
orchestrator/tests/test_sync_worktree.py test_case_reset_succeeded, test_case_reset_failed_returncode, test_case_reset_failed_exception, test_counters_present_when_known (main) Took main — kept verbatim.

Verification

  • make lint — passes (ruff check, ruff format, mypy, custom checks)
  • pytest orchestrator/tests/test_sync_worktree.py — 37/37 pass
  • pytest orchestrator/tests/ -k "sync_worktree or rebase or worktree" — 297 pass, 2 environmental failures unrelated to the merge (test_rebase_does_not_replay_main_commits_when_base_branch_set and test_rebase_does_not_contaminate_when_base_fetch_silently_failed both fail on git init being blocked in this container; same failures reproduce on the pre-merge tip)

Pushed as 1a58797ef to egg/issue-2337-fix-sync-worktree-rebase (no force).

— Authored by egg

@james-in-a-box

This comment has been minimized.

@egg-reviewer egg-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: ACK / APPROVE — non-blocking suggestions

Re-review at 1a58797ef. All four substantive items from the prior review (S2, S3, S4, S7) are correctly addressed in commit 4948912, and the subsequent origin/main merge cleanly composes this PR's rebase implementation with #2349's case-discriminator taxonomy. 61 tests across test_sync_worktree, test_populate_contract_audit_events, and test_slice_gate_recheck pass.

Verification of prior review items

  • S2 — local_ahead_discarded discriminator emitted ✓ Code at orchestrator/routes/pipelines.py:5444-5451 emits case="local_ahead_discarded" before falling through to step-4 reset. The merge correctly renamed away the _falling_through_to_reset suffix to match main's shorter taxonomy. Test test_case_local_ahead_discarded (test_sync_worktree.py:581-604) asserts the strict sequence ["local_ahead_discarded", "reset_succeeded"].

  • S3 — PlanDraftMissingOnLocalError cleanup parity ✓ Handler at orchestrator/routes/pipelines.py:15437-15496 mirrors the if phase_failed: cleanup block at 15314-15357: same overseer-teardown-under-lock, same report_pipeline_status + _emit_pipeline_event, same best-effort push_worktree_branch with try/except, same break. Both load-bearing failure paths now have a uniform cleanup story.

  • S4 — Direct argv tests for _origin_has_plan_draft ✓ Five tests in TestOriginHasPlanDraft (test_populate_contract_audit_events.py:432-555) assert the exact argv structure (cat-file -e origin/{branch}:{path} with correct -C ordering), zero/non-zero exit handling, and exception swallow. Each named regression in the prior review (cat-file→rev-parse swap, dropping -e, dropping origin/, mis-quoted path, mis-routed -C) has a dedicated assertion.

  • S7 — base_branch=None contamination breadcrumb ✓ Block comment at 5462-5474 and warning log at 5475-5481 flag the #2222 contamination vector. Tests test_divergence_with_base_branch_none_logs_contamination_warning and test_divergence_with_base_branch_set_does_not_log_contamination_warning (test_sync_worktree.py:644-695) verify both states.

Merge resolution review

The merge of origin/main into the PR branch (1a58797ef) had to reconcile this PR's rebase-fallback work with main's worktree_sync_outcome outcome-taxonomy refactor (#2349). Both touched the same _sync_worktree_with_remote exit-path logging surface. The resolution is correct:

  • Kept this PR's _rebase_with_agent_output_autoresolve call (line 5489-5494); main's --ff-only retry path is gone, as it should be — by definition it cannot reconcile true divergence.
  • Adopted main's case taxonomy (kwarg ordering pipeline_id, branch, case=, strict _outcome_cases(mock) == [...] assertions), correctly renaming this PR's local_ahead_discarded_falling_through_to_reset to local_ahead_discarded.
  • Adopted main's already_in_sync early return (5359-5371) — this PR didn't need it, but composes cleanly.
  • Adopted main's collapsed reset_failed (subsumes prior reset_exception) and rev_list_failed (subsumes the rc!=0 / parts!=2 / exception triad).
  • Dropped main's test_case_diverged_ff_* tests (the --ff-only code path no longer exists) and dropped this PR's test_local_ahead_discarded_emits_outcome in favor of main's pre-existing test_case_local_ahead_discarded covering the same path under the shorter case name.
  • Kept this PR's S7 contamination-warning tests verbatim.

End-to-end data flow re-verified: sync rebases on divergence → populator natural call site (source="plan_complete") raises PlanDraftMissingOnLocalError if local missing draft + origin has it → _run_pipeline catches and runs the inline cleanup → slice-gate recheck at 15171 is the belt-and-braces backstop. Three independent layers, no silent demotion path.

Non-blocking suggestions

N1 — Dead phase_failed = True at pipelines.py:15459

The PlanDraftMissingOnLocalError handler does inline cleanup and then breaks out of the outer while True: at _run_pipeline:14674. The break at 15496 exits that outer loop directly — it does NOT route through the if phase_failed: block at 15314 (which is at column 12, inside the same outer while). So phase_failed = True set at 15459 is never re-read and the assignment is dead. It's harmless (and could be confusing for the next reader). Either drop it, or add a one-line comment that it's set for parity with the slice-gate handler even though the inline cleanup makes it unused on this branch.

N2 — Function docstring overclaims single-emit invariant (same root cause as S1)

pipelines.py:5262-5264 reads:

"Every return path emits a single worktree_sync_outcome log line with a case discriminator so production logs name which path fired."

This claim was inherited from main (PR #2349's docstring) and survived the merge unchanged, but the same prose-vs-behaviour gap S1 flagged on the PR body applies here: the three fall-through paths (local_ahead_push_failed, local_ahead_discarded, rev_list_failed) intentionally emit a sequence — first the discriminator, then reset_* from step 4. The tests in TestSyncWorktreeOutcomeTaxonomy correctly assert sequences (e.g. ["local_ahead_discarded", "reset_succeeded"]), so the contract-as-tested is sequence-on-fall-through, single-on-direct-return. Reword to match. Suggested:

"Every return path emits at least one worktree_sync_outcome log line with a case discriminator. Paths that fall through to the step-4 reset emit a sequence — first a discriminator naming WHY we fell through, then the terminal reset_succeeded/reset_failed event."

The prose-only PR-body edit S1 flagged is the same fix and is still pending operator action (gateway blocks egg from editing PRs owned by jwbron).

N3 — _origin_has_plan_draft docstring references rev-parse

pipelines.py:13165-13174 docstring says:

"A False return means either origin really doesn't have the draft, or the rev-parse query itself failed — caller should treat both as ..."

The helper uses git cat-file -e, not git rev-parse. Stale wording from a previous version. Replace rev-parse with cat-file so the docstring matches the implementation it's documenting (and matches the test class that explicitly asserts the cat-file -e argv).

N4 — S5 / S6 deferral tracking issues

The author correctly deferred S5 (collapse double-log on reset paths) to #2359 and S6 (integration tests on the new failure paths) to #2360 with conditioning rationale. No action here — flagging for the merger so the issues stay on the radar.

— Authored by egg

@james-in-a-box

This comment has been minimized.

@james-in-a-box

This comment has been minimized.

N1: Drop dead 'phase_failed = True' in the PlanDraftMissingOnLocalError
handler at _run_pipeline. The handler runs inline cleanup and then
breaks out of the outer 'while True:' loop directly, so the assignment
is never re-read by the 'if phase_failed:' block above. Cleanup parity
with the slice-gate handler is preserved by the existing block comment
naming the mirrored cleanup sequence.

N2: Reword the _sync_worktree_with_remote docstring to match the
sequence-on-fall-through behaviour the tests in
TestSyncWorktreeOutcomeTaxonomy already assert. The three fall-through
paths (local_ahead_push_failed, local_ahead_discarded,
rev_list_failed) emit a discriminator naming WHY we fell through, then
the terminal reset_succeeded / reset_failed event from step 4. Keeps
prose and behaviour in sync.

N3: Replace 'rev-parse' with 'cat-file' in the _origin_has_plan_draft
docstring. Stale wording from a prior version — the helper uses
git cat-file -e (the test class TestOriginHasPlanDraft asserts the
exact argv).
@james-in-a-box

Copy link
Copy Markdown
Contributor

Thanks for the re-review. Per-item disposition:

  • N1 — Dead phase_failed = True at pipelines.py:15459. fixed-in-PR (commit 85c2b62). Dropped the assignment. The existing block comment at the handler entry already documents the slice-gate-mirroring cleanup sequence, so removing the line cleans up the dead write without losing the parity rationale for the next reader.

  • N2 — Docstring overclaims single-emit invariant. fixed-in-PR (commit 85c2b62). Reworded the _sync_worktree_with_remote docstring (pipelines.py:5262-5268) to match the sequence-on-fall-through behaviour TestSyncWorktreeOutcomeTaxonomy already asserts: every return path emits at least one outcome with a case discriminator; the three fall-through paths (local_ahead_push_failed, local_ahead_discarded, rev_list_failed) emit a sequence — first the discriminator naming WHY we fell through, then the terminal reset_succeeded / reset_failed. Prose-and-behaviour are now in sync. (Same root-cause prose-only edit S1 flagged on the PR body is still pending operator action — gateway blocks egg from editing PRs owned by jwbron.)

  • N3 — _origin_has_plan_draft docstring references rev-parse. fixed-in-PR (commit 85c2b62). Replaced rev-parse with cat-file at pipelines.py:13176. Now matches the implementation (git cat-file -e origin/{branch}:{path}) and the TestOriginHasPlanDraft argv assertions.

  • N4 — S5 / S6 deferral tracking issues. No code action — flagged for the merger. Collapse legacy log + worktree_sync_outcome on reset paths once dashboards migrate #2359 (S5: collapse double-log on reset paths once dashboards migrate) and Integration tests for PlanDraftMissingOnLocalError + slice-gate failure paths #2360 (S6: integration tests on the new failure paths) remain open with conditioning rationale; the merger keeps them on the radar.

Post-fix verification: make lint clean (only pre-existing soft-cap warnings unrelated to this PR). pytest orchestrator/tests/test_sync_worktree.py orchestrator/tests/test_populate_contract_audit_events.py orchestrator/tests/test_slice_gate_recheck.py — 61/61 pass. Adjacent test_advance_phase_thread.py + test_pipeline_failure_path.py — 38/38 pass.

— Authored by egg

@james-in-a-box

This comment has been minimized.

@egg-reviewer egg-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: ACK / APPROVE

Re-review at 85c2b62f1. The delta since 1a58797ef is a single-file, 8-line change to orchestrator/routes/pipelines.py that addresses N1, N2, and N3 from the prior review. All three are correctly fixed; nothing else moves in this iteration.

Verification of prior review items

  • N1 — Dead phase_failed = True removed. ✓ The assignment in the PlanDraftMissingOnLocalError handler is gone (pipelines.py:15441-15499). I traced control flow to confirm the removal is safe: the inline cleanup mirrors the if phase_failed: block at 15318-15361 (overseer teardown under lock, report_pipeline_status + _emit_pipeline_event, best-effort push_worktree_branch, then break), and the break at column 20 exits the outer while True: at _run_pipeline:14678 (column 8). No other reader of phase_failed exists between the handler exit and the outer except/finally (grep confirms zero references in lines 16000–16500), so the assignment was genuinely dead. Three other phase_failed = True sites at 14900, 15068, 15204, 15253, 15294 are untouched and still feed the shared cleanup block correctly.

  • N2 — _sync_worktree_with_remote docstring matches behaviour.pipelines.py:5262-5268 now says "at least one" outcome and explicitly enumerates the three fall-through cases (local_ahead_push_failed, local_ahead_discarded, rev_list_failed). I cross-checked the enumeration against the implementation: those are exactly the three case-discriminators that emit before return is skipped and control reaches the step-4 reset:

    • rev_list_failed (5369/5379): emits, leaves rev_list_ok=False, all three Step 3c branches (already_in_sync, local_ahead, divergence) evaluate False → falls through to Step 4.
    • local_ahead_push_failed (5437): emits, if push_result: block doesn't return → falls through.
    • local_ahead_discarded (5452): emits, the surrounding else: doesn't return → falls through.
      All other case-discriminators (fetch_failed, detached_head, branch_detect_failed, no_remote_tracking, rev_parse_failed, already_in_sync, local_ahead_pushed, divergence_rebased, divergence_rebase_failed) return immediately. The behind-only case (local_ahead==0 and remote_ahead>0) reaches Step 4 with no preceding emission, so it produces a single outcome (reset_succeeded/reset_failed) — correctly excluded from the sequence list. The docstring is now an accurate contract.
  • N3 — _origin_has_plan_draft docstring references cat-file.pipelines.py:13176 now reads cat-file instead of rev-parse, matching the implementation at 13190-13192 (["cat-file", "-e", f"origin/{branch}:{draft_rel}"]) and the argv assertions in TestOriginHasPlanDraft.

Other observations

  • The S1 PR-body wording from the original review is still pending operator action (gateway blocks egg from editing PRs owned by jwbron). Not blocking; flagged for the merger.
  • Deferred items #2359 (S5 — collapse double-log on reset paths once dashboards migrate) and #2360 (S6 — integration coverage of the new failure paths) remain open with conditioning rationale. Still on the radar.

No new issues in the delta.

— Authored by egg

@james-in-a-box

Copy link
Copy Markdown
Contributor

egg review completed. View run logs

9 previous review(s) hidden.

@jwbron
jwbron merged commit 69b2a18 into main Apr 30, 2026
21 checks passed
jwbron added a commit that referenced this pull request Apr 30, 2026
* Fix #2367: pass pipeline_branch into _sync_worktree_with_remote

The orchestrator-side worktree runs on egg/<pid>/work but the agent-facing
remote branch is egg/<pid>.  Reading the local branch via
`git branch --show-current` and looking up `origin/<that>` always missed,
exiting at case=no_remote_tracking before the rebase fallback (#2352) or
the step-4 reset could run — stranding plan-phase commits on origin while
the loud-fail populator marked the pipeline FAILED.

Add a keyword-only pipeline_branch arg and use it for every remote-side
ref (rev-parse, rev-list, gateway push target, divergence-rebase target,
reset target).  The local branch name is still used for detached-HEAD
detection and is logged alongside remote_branch in worktree_sync_outcome
so operators can grep both names.  Both pipelines.py call sites now pass
pipeline.branch.

Also fixes the latent companion bug at the local-ahead push: the gateway
builds HEAD:refs/heads/{branch} from its `branch` argument, so passing
the /work-suffixed local name would have pushed to origin/egg/<pid>/work.
Masked today by the step-3 early-out, but the regression guard test pins
the correct target.

* Tighten #2367 regression guard with argv-aware rev-parse mock

The reviewer flagged that test_no_remote_tracking_does_not_fire_when_
pipeline_branch_resolves accepted any rev-parse target as success — so
under the buggy code (which would query origin/<local_branch>) the mock
would still return 0 and the test would still pass. The named regression
guard provided zero regression value.

Replace the linear side_effect with an argv-aware callable: rev-parse
returns 0 only when origin/egg/issue-42 is in the argv, and 128 otherwise.
Under the pre-fix code path (querying origin/egg/issue-42/work) the mock
now returns 128 → no_remote_tracking fires → the test fails. The fix
path (querying origin/egg/issue-42) still passes.

---------

Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com>
james-in-a-box Bot pushed a commit that referenced this pull request Apr 30, 2026
* Fix #2367: pass pipeline_branch into _sync_worktree_with_remote

The orchestrator-side worktree runs on egg/<pid>/work but the agent-facing
remote branch is egg/<pid>.  Reading the local branch via
`git branch --show-current` and looking up `origin/<that>` always missed,
exiting at case=no_remote_tracking before the rebase fallback (#2352) or
the step-4 reset could run — stranding plan-phase commits on origin while
the loud-fail populator marked the pipeline FAILED.

Add a keyword-only pipeline_branch arg and use it for every remote-side
ref (rev-parse, rev-list, gateway push target, divergence-rebase target,
reset target).  The local branch name is still used for detached-HEAD
detection and is logged alongside remote_branch in worktree_sync_outcome
so operators can grep both names.  Both pipelines.py call sites now pass
pipeline.branch.

Also fixes the latent companion bug at the local-ahead push: the gateway
builds HEAD:refs/heads/{branch} from its `branch` argument, so passing
the /work-suffixed local name would have pushed to origin/egg/<pid>/work.
Masked today by the step-3 early-out, but the regression guard test pins
the correct target.

* Tighten #2367 regression guard with argv-aware rev-parse mock

The reviewer flagged that test_no_remote_tracking_does_not_fire_when_
pipeline_branch_resolves accepted any rev-parse target as success — so
under the buggy code (which would query origin/<local_branch>) the mock
would still return 0 and the test would still pass. The named regression
guard provided zero regression value.

Replace the linear side_effect with an argv-aware callable: rev-parse
returns 0 only when origin/egg/issue-42 is in the argv, and 128 otherwise.
Under the pre-fix code path (querying origin/egg/issue-42/work) the mock
now returns 128 → no_remote_tracking fires → the test fails. The fix
path (querying origin/egg/issue-42) still passes.

---------

Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com>
jwbron added a commit that referenced this pull request May 19, 2026
…orktree (#2714) (#2719)

* fix: rebase with --autostash so divergence reconcile survives dirty worktree (#2714)

The orchestrator writes statefile / agent-output deltas continuously
without committing them eagerly, so the worktree is routinely dirty
at sync time.  PR #2352 added a divergence-rebase fallback inside
`_sync_worktree_with_remote`, but the underlying `git rebase` refuses
on any unstaged changes — making the rebase abort 100% of the time at
plan-complete sync, leaving the populator's draft missing on local
and halting the pipeline at plan_complete.

Set `--autostash` on both forms returned by `_build_rebase_cmd`
(plain `git rebase origin/{branch}` and `--onto origin/{branch}
origin/{base_branch}`).  Git stashes unstaged changes before the
rebase and pops them on success; on abort the stash entry is kept
in the stash list for recovery.

Adds an end-to-end regression test that reproduces the production
shape (divergent remote + dirty worktree, both rebase forms) and
asserts the unstaged statefile delta is restored after the rebase.

* fix: detect autostash-pop conflicts + reproduce dirty-worktree bug in test (#2714 review)

Two review-driven fixes on top of the original --autostash one-liner:

1. Reproduce the bug in the regression test.  The old setup created an
   untracked file under .egg-state/, but git rebase only refuses for
   *modified tracked* files; an untracked-only worktree does not block
   rebase, so the test passed with or without --autostash.  Seed a tracked
   .egg-state/contract.json in the initial commit and modify it in the
   work tree so the precondition that produces the bug actually fires.

2. Detect the autostash-pop-conflict failure mode.  git rebase --autostash
   exits 0 even when the final stash pop conflicts: rebase succeeded, but
   UU entries land in the worktree and the autostash entry stays in
   git stash list.  Without a check the orchestrator would treat a
   half-merged worktree as a successful sync.  New helper
   _autostash_pop_conflict_result probes _list_unmerged_paths after every
   rebase-success path and surfaces reconcile_autostash_pop_conflict so
   the caller can react.  New regression test exercises the failure path
   end-to-end.

Also corrects the abort-behavior wording in both docstrings: git rebase
--abort *reapplies* the autostash to the working tree automatically; it
only preserves the stash list entry as a fallback when the reapply itself
conflicts.

* test: tighten dirty-worktree precondition to '' M'' (unstaged only)

The reviewer flagged that the precondition assertion in
test_rebase_succeeds_against_dirty_worktree accepted ''M '' (staged-only)
and ''MM'' (both staged and unstaged) in addition to '' M'' (unstaged).
The autostash bug fires only on *unstaged* modifications --
staged-only would trip ''Your index contains uncommitted changes''
instead. The current test setup always lands at '' M'', so the broader
pattern doesn't currently false-positive, but a future drift to a
staged-only setup could silently regress the test back to not
exercising the bug.

Pin the assertion to startswith('' M'') and update the comment to spell
out why the other shapes don''t exercise the autostash path.

---------

Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant