[issue-3393][slice-5/7] Cross-repo merge-sequencing hold (cq-1... - #3430
Merged
Conversation
Pin the cq-1 two-tier merge-gate semantics with always-green reference- logic tests over the slice-1 model API, plus skip-guarded integration tests on the coder-owned seams (mark_pr_ready verb + merge-poll classifier) that activate at convergence: - cross-repo edge detection (resolve_slice_repo endpoints differ); same-repo + N=1 take no gate - draft->ready keys off merged-state (mergedAt/merged), NOT head-SHA (squash-merge SHA!=head case) - CLOSED-unmerged and never-merging (poll>=bound) escalate to HITL hold - Tier-B beyond-merge edges are HITL-held, never auto-released - holds gate PR ready-state only, never development 38 passed, 3 skipped (coder seams); ruff clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…sk-5-1, task-5-2) Two-tier merge-sequencing gate for multi-repo pipelines. A dependency edge B->A is cross-repo iff resolve_slice_repo(A)!=resolve_slice_repo(B); such a dependent slice's PR opens DRAFT and only its ready-transition waits (development is not serialized). Same-repo deps keep stacked-PR behavior; N=1 untouched. task-5-1 (Tier A automated merge-state hold): new launcher-auth gateway verbs POST /api/v1/gh/pr/merge_state (wraps 'gh pr view --json state,mergedAt') and /api/v1/gh/pr/ready (wraps 'gh pr ready'); both gh cmds already allowlisted so github_client.py unchanged. Client methods GatewayClient.get_pr_merge_state/mark_pr_ready. New pure-logic cross_repo_merge_gate.py (find_cross_repo_gates/classify_hold_kind/poll_once) driven on the EXISTING stacked-PR reconciler cadence (no new scheduler). Merge detection keys off mergedAt/state, NOT head-SHA. All-upstreams-merged -> auto mark_pr_ready. Pinned terminals: CLOSED-not-merged and bounded-poll timeout both escalate to a HITL hold surfaced on status, not auto-ready / indefinite draft. task-5-2 (Tier B HITL beyond-merge-state hold): distinct hold kind for edges the plan declares via the [hold:beyond-merge-state] marker in the dependent slice goal/task (architect arch-q1 optional per-slice marker; default absent => Tier A). Registered up front, released ONLY by human decision -- two distinct release paths. Reuses contract HITL Decision (_register_cross_repo_hold, idempotent per gate via stable marker). Env knob EGG_ORCH_CROSS_REPO_MERGE_GATE_MAX_ATTEMPTS (default 240). Validation: py_compile + ruff clean; pure-logic sanity harness covers auto draft->ready (incl mergedAt-only squash), closed-unmerged hold+release, timeout hold, Tier-B up-front hold+release, N=1/same-repo no-op. Full pytest deferred to tester (task-5-3; no venv - cert-blocked). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…into egg/issue-3393-slice-5-coder/work
Add public classify_upstream_merge(pr_state) on cross_repo_merge_gate returning mark_ready/hitl_hold/wait (shared vocabulary with the task-5-3 tester reference logic; the bounded-timeout terminal stays in poll_once which tracks cross-tick attempts). _is_merged also accepts the raw 'merged' boolean shape. Make pr_number keyword-only on GatewayClient.get_pr_merge_state/mark_pr_ready to match the interface handed via the task-5-3 gap (mark_pr_ready(self, pipeline_id, repo, *, pr_number)); update the reconciler poll call sites to pass pr_number=. Activates the tester's skip-guarded TestMarkPrReadyGatewayVerb + TestMergePollClassifierIntegration at convergence. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…de_holistic v1 NACK)
The cross-repo hold release path ignored Decision.resolution and readied the PR on the bare resolved boolean, so selecting 'Keep the PR held for manual handling' (opt-2) still readied it on the next tick -- opt-2 was a lie. Fix: poll_once now takes hold_resolution(gate)->RELEASE|KEEP|None (was hold_is_resolved->bool); RELEASE marks ready, KEEP is terminal WITHOUT readying (PR stays draft; PollResult.kept_held), None keeps waiting. New cross_repo_merge_gate.RELEASE/KEEP consts. pipelines._cross_repo_hold_resolved -> _cross_repo_hold_resolution: finds the gate Decision, unwraps the {action:select,selected:<label>} envelope, maps to RELEASE iff the release option id/label/keyword matches else KEEP (fail-safe: an ambiguous resolved value never auto-readies). Named option consts opt-release/opt-keep; question text explains both outcomes. Added always-green regression test TestCrossRepoHoldReleaseSelection (RELEASE readies / KEEP does NOT ready + kept_held==1 / None waits).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
jwbron
approved these changes
Jul 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implement the operator-resolved (HITL cq-1) two-tier merge-sequencing hold. Tier A (automated): a cross-repo dependent slice is developed in parallel, its PR opens as draft, and the orchestrator polls the upstream slice's PR merge state and auto-marks the dependent PR ready once the upstream merges — no HITL for plain merge ordering. Tier B (HITL): a distinct hold type for beyond-merge-state conditions (release/publish of the upstream repo, a version-pin choice, or a genuine cannot-continue development block) is held and released by a HITL decision, never programmatic detection. Dependencies gate merge-readiness, not development.
Base PR: #3418
What's in this PR
Commits (5):
This slice
Cross-repo merge-sequencing hold (cq-1 two-tier)
Files affected:
orchestrator/routes/pipelines.pyorchestrator/stacked_pr_reconciler.pyorchestrator/gateway_client/_pr.pygateway/gateway.pygateway/github_client.pyorchestrator/tests/test_pipelines.pyTasks (3) + acceptance criteria
resolve_slice_repo(A) != resolve_slice_repo(B). For such an edge: open B's PR as a DRAFT (already supported) while upstream A's PR is unmerged, and register an upstream-merge poll on the EXISTING reconcile cadence (extend orchestrator/stacked_pr_reconciler.py, or add a small cross_repo_merge_gate.py invoked from it — do not invent a new scheduler subsystem). PIN THREE SEMANTICS: (a) MERGE DETECTION uses the PR merged boolean /mergedAt(gh pr view --json state,mergedAtvia a small gateway read verb), NOT head-SHA equality — a squash/rebase merge yields a merge-commit SHA ≠ the PR head, so SHA-equality would misfire. On merged⇒ call a NEW gateway verbmark_pr_ready(repo, pr_number)(wraps the existinggh pr ready, gateway/github_client.py:124) exposed via gateway/gateway.py + orchestrator/gateway_client/_pr.py, transitioning B draft→ready. (b) CLOSED-UNMERGED TERMINAL: if upstream A's PR reaches state=CLOSED with nomergedAt(closed, not merged), do NOT auto-ready B — escalate to a HITL hold (reuse the Tier-B hold mechanism / decision queue) and surface it on status. This is distinct from Tier B's beyond-merge-state conditions; it is the Tier-A failure terminal. (c) BOUND / TIMEOUT: bound the poll (a max wait / attempt budget); a never-merging upstream that exceeds the bound escalates to the same HITL hold rather than leaving B draft indefinitely. "If the auto path cannot resolve, fall through to the HITL hold rather than hanging" (architect layer_6). Development is NOT serialized — B is worked in parallel; only B's PR ready-state waits. Do NOT implement release/publish or version detection here (cq-1 forbids automating those signals).mark_pr_ready) only when the upstream PR's merged boolean /mergedAtis set — detection uses merged-state, NOT head-SHA equality. - Upstream PR CLOSED-not-merged does NOT auto-ready B; it escalates to a HITL hold and is surfaced on status. - The poll is bounded; exceeding the bound (never-merging upstream) escalates to the HITL hold, not an indefinite draft. - A newmark_pr_ready(repo, pr_number)gateway verb (wrappinggh pr ready) exists and is used for the transition. - Development is not serialized by the dependency; no release/publish/version auto-detection is added; same-repo dependencies and N=1 pipelines are unaffected.state,mergedAt) andmark_pr_ready; do not hit the network. Cases: (a) upstream PR mergedAt set ⇒ dependent PR draft→ready viamark_pr_ready; assert detection keys off merged-state, NOT head-SHA (include a squash-merge case where the merge commit SHA ≠ PR head and the transition still fires). (b) CLOSED-UNMERGED: upstream state=CLOSED, mergedAt=null ⇒ NO auto-ready; a HITL hold is registered and surfaced on status. (c) NEVER-MERGING: poll exceeds the bound/timeout ⇒ HITL hold, dependent PR not left draft indefinitely. (d) development is not blocked while the hold is active. (e) Tier-B beyond-merge-state edge routes to a HITL decision and stays held until resolved (no auto-release). (f) same-repo dependency and N=1 pipeline take neither hold.make test(narrowed) andmake test-allgreen.Stack
issue-3393egg/issue-3393/slice-4