Fix #2685: skip PR-phase auto-PR in slice-DAG mode; include contract on context PR - #2694
Conversation
…on context PR Two related fixes to the slice-DAG PR topology. 1. Gate the legacy `<pipeline_branch> → main` auto-PR on slice-DAG mode. When the contract has more than one slice, per-slice PRs already stack on the context PR (#2548); opening another program-level PR from the PR phase creates a redundant surface and confuses reviewers. The new `_should_skip_pr_phase_auto_pr` helper parallels the implement-phase slice-loop gate (`_use_slice_loop = _slice_count > 1`) so the two decisions stay in sync. Babysit-pr keeps its existing skip; single-slice and zero-slice contracts keep the legacy auto-PR; contract-load failures fail safe to running the legacy path so a transient read hiccup doesn't drop the PR silently. 2. Add `.egg-state/contracts/<id>.json` to the context PR diff via the loader's `get_contract_path` (canonical) plus `_legacy_contract_path` (fallback for pre-key-unification pipelines). The static glob set can't express the canonical `issue-<N>.json` filename that integer issue identifiers route through, so contract resolution lives on the dynamic side of `_gather_context_pr_files` alongside agent-output resolution. Reviewers approving the context PR now see the structured slice DAG alongside the prose drafts and BRC transcripts that produced it. Tests - `TestShouldSkipPrPhaseAutoPr` (6 tests): pins the skip helper decision matrix — babysit short-circuit (loader untouched), slice-DAG > 1 → skip, single-slice → don't skip, zero-slice → don't skip, contract-load raise → fail-safe to don't-skip, and a defensive babysit-wins-over-slice-DAG case. - `TestOpenContextPRAdversarial`: three new contract-glob tests (canonical `issue-N.json` for issue-mode, legacy `<N>.json` fallback, and qualified `issue-N-v2.json` for CUSTOM-style pipeline IDs). - The pre-existing static-glob inventory test is updated to reflect that contracts are now intentionally included via the dynamic loader-resolved path; the static set itself still excludes `/contracts/` templates as the single source of truth.
There was a problem hiding this comment.
Summary
Reviewed thoroughly. Both fixes are correct, well-tested, and additive — they don't change behavior for monolithic or single-slice pipelines, and they correctly mirror the existing implement-phase slice-loop gate. Tests pass locally (pytest orchestrator/tests/test_auto_pr.py orchestrator/tests/test_context_pr.py → 98 passed; regression suites → 64 passed).
No blocking issues found. A handful of non-blocking observations below.
What I verified
Correctness of the gate (_should_skip_pr_phase_auto_pr)
- Babysit short-circuit returns before the contract loader is touched (
mcp_load.assert_not_called()in the test pins this). - The
slice_count > 1predicate matches the implement-phase gate at line 19921 exactly, so the two gates can never disagree about whether a pipeline is in slice-DAG mode (within a single tick). - Fail-safe semantics on contract-load errors are correct: implement-phase falls back to monolithic on the same error → no per-slice PRs created → legacy auto-PR is the right path. This matches the documented fail-safe shape.
- Boundary on
slice_count == 1(monolithic) andslice_count == 0(pre-slice-DAG) is covered by named tests.
Correctness of the gather change (_gather_context_pr_files)
- Identifier regex check at line 9945 runs before the new contract path resolution, so the path-traversal defense remains intact (verified by
test_gather_rejects_path_traversal_identifier). - Symlink check is preserved on the new contract paths (
cp.is_symlink(): continuebeforecp.is_file()). - All three loader path shapes are covered: canonical
issue-<N>.jsonforintidentifiers, legacy<N>.jsonfallback, and qualifiedissue-<N>-<qualifier>.jsonfor CUSTOM-mode pipelines. - The existing
test_files_copied_include_all_curated_artifactsusesmissing = expected - rel(one-sided), so adding the contract to the result set doesn't break it.
Cross-module wiring
skip_pr_creation = True(slice-DAG branch) leavesphase_failed = False, so the phase still completes normally and the run loop transitions out of PR. Housekeeping (statefile commit, BRC history rewrite, gateway push) still runs as the PR body claims.- The static-glob inventory test (
test_static_glob_inventory_matches_documented_artifact_set) was updated correctly to reflect that the contract is now dynamically resolved, with a defensive assertion that contract paths stay out of the static set.
Non-blocking observations
-
Unreachable defensive fallback at
orchestrator/routes/pipelines.py:19809:skip_reason=_skip_reason or "babysit_pr_already_exists",
skip_pr_creationis True only when the gate already populated_skip_reason(babysit →"babysit_pr_already_exists", slice-DAG →"slice_dag_mode_slice_count=N"). Theorfallback is dead. It does no harm, but consider justskip_reason=_skip_reason. -
Docstring wording at
_should_skip_pr_phase_auto_pr:(The head-move guard still runs upstream and may force the skip independently.)
The head-move guard at line 19610 runs downstream of this helper (the call site calls the gate first at 19575, then runs the head-move guard). "Upstream" reads as misleading. Suggest "still runs at the call site after this helper returns" or similar.
-
Private-API import. The gather change imports
_legacy_contract_pathfromegg_contracts.loaderwith# type: ignore[attr-defined]. There is no public equivalent because the loader normally only exposesload_contract(which hides the fallback internally). This is fine for now, but if the loader is refactored, the gather will silently lose its legacy-path fallback. Optionally, the loader could expose aget_contract_path_candidates(identifier, repo_root)helper that returns[canonical, legacy_or_none_filtered]and have both call sites consume that. Not necessary for this PR. -
Both-files-present edge case. If both
.egg-state/contracts/issue-2685.jsonand.egg-state/contracts/2685.jsonexist on disk (e.g., during a partial migration), the new code adds both to the context PR, whereas the loader'sload_contractpicks canonical only. In practice this is benign — the reviewer would just see the migration in the diff — but it's a slight semantic divergence from the loader. Not worth changing. -
Log-message specificity. The previous PR-phase skip log was
"Skipping PR creation (babysit-pr already has a PR)". The new generic"Skipping PR creation"plusskip_reason=is structurally cleaner but breaks log-grep alerts that may have keyed on the old literal. If any operator dashboards key on the old string, they'll need updating. Minor observability migration concern.
Pre-existing observation (not a regression)
The two contract-loader imports — one in _should_skip_pr_phase_auto_pr at line 8322, one in _gather_context_pr_files at line 9988 — both repeat the import-inside-function pattern from the existing slice-loop gate at line 19913. This is consistent and intentional (lazy import), but the codebase now has three near-identical "try import egg_contracts.loader; on failure, log and degrade" blocks for the same module. If a future refactor consolidates, this PR's gate and gather can be folded in. Not in 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.
- _should_skip_pr_phase_auto_pr docstring: "upstream" → "at the call site after this helper returns". The head-move guard runs downstream of the helper (gate at line 19575 returns, then guard at line 19610). - PR-phase skip log: drop the unreachable "or babysit_pr_already_exists" fallback on _skip_reason — when skip_pr_creation is True, the gate has already populated _skip_reason with either babysit_pr_already_exists or slice_dag_mode_slice_count=N.
|
Thanks for the thorough review. Per-item disposition:
Tests still pass (98/98 in — Authored by egg |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Check Failure: Integration Tests / Integration TestsWhat's failing: The Docker Root cause: Transient infrastructure failure — Excerpt from the failed log: Why this is not a code issue: This PR only modifies What needs to be done:
— Authored by egg |
Autofix tracking{"Test/Integration Tests / Integration Tests": 1} |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review (delta since f65d1dd)
Re-reviewed commit 0443c9b against my prior feedback. Both addressable items are fixed correctly; the rest of the disposition is sound.
Items addressed
-
Dead
or "babysit_pr_already_exists"fallback atorchestrator/routes/pipelines.py:20505— fixed. The fallback is now provably unreachable, not just unused:skip_pr_creation = Trueis reachable only viaif is_babysit_mode:(line 20305) orelif _skip_decision:(line 20342).- The babysit branch implies the helper was called with
is_babysit_mode=Trueand short-circuited to_skip_reason = "babysit_pr_already_exists"atpipelines.py:8376. - The
_skip_decisionbranch (withis_babysit_mode=False) is only entered when the helper went through the slice-count check and returned_skip_reason = "slice_dag_mode_slice_count=N"atpipelines.py:8402. - The log line at 20505 therefore always sees a non-None
_skip_reason. Good.
-
Docstring direction at
_should_skip_pr_phase_auto_pr(pipelines.py:8361-8362) — fixed. "still runs upstream" → "still runs at the call site after this helper returns". The head-move guard at 20296-20341 runs after the helper returns at 20271, so the new wording is accurate.
Items declined
The author appropriately declined the other four non-blocking observations (private-API _legacy_contract_path import, both-files-present edge case, log-string observability migration, three near-identical lazy-import blocks). My prior review explicitly tagged each as out-of-scope or not worth changing, so the disagreements are warranted.
Other delta
The merge of main (fcaf3d6) brings in unrelated refactors (LIVE_POD_STATUSES, integration regression tests, etc.) that do not touch the PR's logic surface. _should_skip_pr_phase_auto_pr, _gather_context_pr_files, and the call site at 20271 are intact. No conflicts.
Tests
The pinning tests in test_auto_pr.py:770-838 continue to cover the helper's contract directly (babysit short-circuit with mock_load.assert_not_called(), slice-count > 1 → skip with slice_dag_mode_slice_count=N reason, slice-count ∈ {0, 1} → don't skip, contract-load raise → don't skip, defensive babysit-wins-over-slice-DAG). The log-line change doesn't require new test coverage — the field-shape contract is unchanged (always-present skip_reason).
No new issues found.
— Authored by egg
|
egg review completed. View run logs 5 previous review(s) hidden. |
Summary
Two related fixes to slice-DAG PR topology, as called out in #2685:
_should_skip_pr_phase_auto_prhelper at the PR-phase entry mirrors the implement-phase slice-loop gate (_use_slice_loop = _slice_count > 1). When the contract has more than one slice, per-slice PRs already stack on the context PR (Slice PRs are missing analysis/plan docs and all BRC history; need a 'context' PR for refine+plan phases plus per-slice BRC in each slice PR #2548), so the legacy<pipeline_branch> → mainauto-PR would only duplicate the program-level surface. Babysit-pr keeps its existing skip; single-slice / zero-slice contracts keep the legacy auto-PR; contract-load failures fail safe to running the legacy path so a transient read hiccup doesn't drop the PR silently._gather_context_pr_filesnow resolves the contract path through the loader (get_contract_pathcanonical +_legacy_contract_pathfallback) rather than the static glob templates — the canonicalissue-<N>.jsonfilename can't be expressed by{identifier}substitution. Reviewers approving the context PR now see the structured slice DAG alongside the prose drafts and BRC transcripts that produced it.Both fixes are unblocked by #2690 (merged), which moved the context-PR / per-slice-BRC temp worktrees under
WORKTREE_BASE_DIRso their gateway pushes actually land — before that, the context PR was opening with no files on the branch.The PR-phase still records phase timing and runs its housekeeping (statefile commit, BRC history rewrite, gateway push) in slice-DAG mode; only the
gh pr createstep is skipped.Test plan
pytest orchestrator/tests/test_auto_pr.py orchestrator/tests/test_context_pr.py— 98 passedpytest orchestrator/tests/test_auto_pr.py::TestShouldSkipPrPhaseAutoPr— 6 new tests pass (babysit short-circuit, slice-DAG > 1, single-slice, zero-slice, fail-safe on load raise, babysit-wins-over-slice-DAG)pytest orchestrator/tests/test_context_pr.py::TestOpenContextPRAdversarial— 13 passed (3 new contract-glob tests: canonicalissue-N.json, legacy<N>.jsonfallback, qualifiedissue-N-v2.json)pytest orchestrator/tests/test_pipeline_failure_path.py orchestrator/tests/test_slice_run_loop_integration.py— 64 passed (regression-related modules)ruff check+ruff format --checkclean on changed filesegg/<id>/work → mainPR, and (b) the context PR diff includes.egg-state/contracts/issue-<N>.jsonCloses #2685.