Fix #1941: populate contract.pr on advance_phase out of plan - #1949
Conversation
advance_phase (especially force=true, the recovery hammer used to unstick plan-stuck pipelines) spawned a fresh _run_pipeline thread directly on the target phase, so the plan-phase populate step that writes contract.pr from the plan draft's yaml-tasks appendix never ran. The PR phase's auto-PR path then fell back to orchestrator placeholders — observed on PR #1937 during the #1938 recovery session. - Extract _populate_contract_from_plan_safe as the shared entrypoint used by _run_pipeline's post-complete block and advance_phase. - In advance_phase, when previous_phase == PLAN, run the helper and commit the populated contract so _sync_worktree_with_remote in the newly-spawned thread pushes (local-ahead path) rather than resets. - Failures warn and continue — blocking the advance hammer on a populate crash would defeat its purpose. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Clean, well-targeted fix for #1941. The approach is sound: extracting _populate_contract_from_plan_safe as a shared entry point ensures both advance_phase and _run_pipeline's post-complete path run the same populate logic, and the commit-before-thread-spawn guarantees _sync_worktree_with_remote pushes the change rather than resetting it.
No blocking issues. A few non-blocking observations:
1. Commit proceeds after silently failed populate (non-blocking)
phases.py:419-433 — If _populate_contract_from_plan_safe catches an exception internally (which it's designed to do), execution falls through to _commit_statefiles_to_worktree with the message "Populate contract from plan on plan-phase exit." In practice this is a no-op commit (nothing changed on disk), but the message could be misleading in git logs if someone investigates. Consider either:
- Tracking whether populate succeeded and skipping the commit when it didn't, or
- Accepting this as harmless (the commit is idempotent/no-op when nothing was staged)
I lean toward accepting it — it matches how _persist_phase_brc_history works (unconditional commit after best-effort write), and the commit helper already skips when nothing is staged.
2. Test call-order assertion gap (non-blocking)
test_populate_is_followed_by_commit verifies that both populate and commit were called with the right arguments, but doesn't assert that populate was called before commit. Since these are mocks on different objects, ordering verification would require a shared Mock manager. Minor, but the test name implies it checks ordering.
3. Inconsistent worktree resolution in the same code path (pre-existing, non-blocking)
advance_phase now calls both _persist_phase_brc_history (which uses _resolve_pipeline_worktree_path from pipelines.py:5348) and the new populate block (which uses resolve_worktree_path from routes/__init__.py:217). These are functionally equivalent but different implementations. Not introduced by this PR, just worth noting since both run in the same request now.
4. Test coverage is solid
The regression suite covers the key scenarios well:
- Force advance from plan calls populate with correct args
- Populate is followed by a scoped commit
- Non-plan advances skip populate
- Populate failure doesn't block the advance
- Integration test verifies the end-to-end yaml-tasks → contract.pr transformation
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Address review feedback: the test name implied it verified that populate was called before commit, but it only checked both were called. Use a shared call tracker to assert ordering across the two mocks.
There was a problem hiding this comment.
Thanks for the thorough review. Addressing the observations:
1. Commit after silently failed populate — Agreed this is harmless. The commit helper already skips when nothing is staged, and this matches the existing _persist_phase_brc_history pattern. Leaving as-is.
2. Test call-order assertion gap — Agreed. Added a shared call tracker (call_order list) to test_populate_is_followed_by_commit that records invocations across both mocks and asserts populate precedes the plan-exit commit. Fixed in a80fd94.
3. Inconsistent worktree resolution — Acknowledged as pre-existing. Worth a follow-up to consolidate but out of scope here.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Autofix tracking{"Lint/Python": 1} |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review of changes since 434aeae (commits a80fd94 and f0beaea).
Previous feedback status
Item 1 (commit after failed populate): Author accepted as harmless. Agreed — no action needed.
Item 2 (call-order assertion gap): Fixed in a80fd94. The call_order list + cross-mock side effects correctly verify that populate is invoked before the plan-exit commit. The approach is sound:
mock_populate.side_effectappends"populate"to the shared tracker_track_commitwraps the commit mock to append"commit"and delegates to the original side_effect- The assertion finds the populate index and confirms at least one commit index follows it
- The pattern correctly handles the BRC-history commit that precedes the populate block (it appears earlier in
call_orderand doesn't confuse the assertion)
Item 3 (inconsistent worktree resolution): Acknowledged as pre-existing/out-of-scope. Fine.
New changes review
f0beaea — formatting fix
Trivial ruff-format adjustment to the assert line wrapping. No behavioral change.
a80fd94 — call-order assertion
No issues. The implementation directly addresses the feedback: it records invocation order across two independently-mocked functions and asserts the constraint the test name promises. One minor observation:
orig_commit_side_effect will always be None here (fresh MagicMock), so the if orig_commit_side_effect: guard is dead code — but it's a reasonable defensive pattern if someone later adds a default side_effect to the fixture, so it's fine to keep.
Verdict
No blocking or non-blocking issues in the new changes. Previous feedback has been properly addressed.
— Authored by egg
|
egg review completed. View run logs 5 previous review(s) hidden. |
* Fix #1941: populate contract.pr on advance_phase out of plan advance_phase (especially force=true, the recovery hammer used to unstick plan-stuck pipelines) spawned a fresh _run_pipeline thread directly on the target phase, so the plan-phase populate step that writes contract.pr from the plan draft's yaml-tasks appendix never ran. The PR phase's auto-PR path then fell back to orchestrator placeholders — observed on PR #1937 during the #1938 recovery session. - Extract _populate_contract_from_plan_safe as the shared entrypoint used by _run_pipeline's post-complete block and advance_phase. - In advance_phase, when previous_phase == PLAN, run the helper and commit the populated contract so _sync_worktree_with_remote in the newly-spawned thread pushes (local-ahead path) rather than resets. - Failures warn and continue — blocking the advance hammer on a populate crash would defeat its purpose. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Add call-order assertion to test_populate_is_followed_by_commit Address review feedback: the test name implied it verified that populate was called before commit, but it only checked both were called. Use a shared call tracker to assert ordering across the two mocks. * Fix checks: apply automated formatting fixes --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com> Co-authored-by: egg <egg@localhost>
Summary
_populate_contract_from_plan_safeas a shared entry point for the two sites that run the populate step when leaving theplanphase.advance_phase(especiallyforce=true) now invokes that helper + commits the result whenprevious_phase == PLAN, so_sync_worktree_with_remotein the newly-spawned_run_pipelinethread pushes the contract update via the local-ahead path instead of resetting it._run_pipeline's existing post-complete call routes through the same helper; happy-path behavior is unchanged.Fixes #1941. Before this, a
force=trueadvance out ofplan(the recovery hammer used in #1938) replaced the plan-phase_run_pipelinethread before it reached the populate step, leavingcontract.prempty and the PR phase's auto-PR path falling back to orchestrator placeholders (observed on PR #1937).Test plan
pytest orchestrator/tests/test_advance_phase_populate_on_plan_exit.py— new regression suite:force=trueadvance fromplan → prcalls the populate helper with the resolved worktree pathpipeline_idplanskip both populate and commityaml-tasksplan draft and empty contract, the helper writescontract.pr.titleandcontract.pr.descriptionpytest orchestrator/tests/— full orchestrator suite passes (4314 passed, 1 skipped)ruff check+ruff format --checkclean on all touched files🤖 Generated with Claude Code