Skip to content

[issue-3393][slice-5/7] Cross-repo merge-sequencing hold (cq-1... - #3430

Merged
jwbron merged 6 commits into
mainfrom
egg/issue-3393/slice-5
Jul 2, 2026
Merged

[issue-3393][slice-5/7] Cross-repo merge-sequencing hold (cq-1...#3430
jwbron merged 6 commits into
mainfrom
egg/issue-3393/slice-5

Conversation

@james-in-a-box

@james-in-a-box james-in-a-box Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

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):

.egg-state/brc-history/3393-implement-slice-5.json | 2290 +++++++++++++++++++++++++++++++++++++
 .egg-state/brc-history/3393-implement-slice-5.md   | 2465 ++++++++++++++++++++++++++++++++++++++++
 gateway/gateway.py                                 |  170 +++
 orchestrator/cross_repo_merge_gate.py              |  423 +++++++
 orchestrator/env_config.py                         |   13 +
 orchestrator/gateway_client/__init__.py            |    2 +
 orchestrator/gateway_client/_pr.py                 |   98 ++
 orchestrator/routes/pipelines.py                   |  320 ++++++
 orchestrator/tests/test_pipelines.py               |  431 +++++++
 9 files changed, 6212 insertions(+)

This slice

Cross-repo merge-sequencing hold (cq-1 two-tier)

Files affected:

  • orchestrator/routes/pipelines.py
  • orchestrator/stacked_pr_reconciler.py
  • orchestrator/gateway_client/_pr.py
  • gateway/gateway.py
  • gateway/github_client.py
  • orchestrator/tests/test_pipelines.py
Tasks (3) + acceptance criteria
  • task-5-1: Implement Tier A (automated merge-ordering hold) with FULLY PINNED failure semantics (reviewer_plan R3 / architect aeb3528 layer_6). A dependency edge B→A is cross-repo iff 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,mergedAt via 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 verb mark_pr_ready(repo, pr_number) (wraps the existing gh 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 no mergedAt (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).
    • Acceptance criteria: - A cross-repo dependent slice's PR opens draft and is auto-marked ready (via mark_pr_ready) only when the upstream PR's merged boolean / mergedAt is 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 new mark_pr_ready(repo, pr_number) gateway verb (wrapping gh 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.
  • task-5-2: Implement Tier B (HITL beyond-merge-state hold). Add a distinct hold type that, for a dependency the plan marks as a beyond-merge-state condition (release/publish of the upstream repo, version-pin choice, or a genuine cannot-continue development block), holds the dependent slice's PR and releases it only via a HITL decision — never programmatic detection. Reuse the existing HITL decision mechanism (register_open_question / contract decision gate); default a cross-repo dependency to the Tier-A merge-state hold and escalate to Tier B only where the plan identifies such a condition. Wire it in orchestrator/routes/pipelines.py alongside Tier A.
    • Acceptance criteria: - A beyond-merge-state hold routes to a HITL decision and is released by human resolution, not auto-detection. - The default for a plain cross-repo dependency is the Tier-A merge-state hold; Tier B is opt-in per the plan. - The two hold kinds have two distinct release paths.
  • task-5-3: Add tests in orchestrator/tests covering Tier A's happy path AND both failure terminals (reviewer_plan R3), plus Tier B. Mock the gateway PR-state read (state, mergedAt) and mark_pr_ready; do not hit the network. Cases: (a) upstream PR mergedAt set ⇒ dependent PR draft→ready via mark_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.
    • Acceptance criteria: - Tests assert Tier-A auto draft→ready on merged-state (incl. a squash-merge SHA≠head case), the CLOSED-unmerged⇒HITL terminal, and the never-merging⇒HITL bound/timeout terminal. - Tests assert non-blocking development, Tier-B HITL routing (no auto-release), and the same-repo / N=1 no-hold paths. - make test (narrowed) and make test-all green.

Stack

egg and others added 6 commits July 2, 2026 18:06
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>
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
jwbron changed the base branch from egg/issue-3393/slice-4 to main July 2, 2026 20:58
@jwbron
jwbron merged commit 4eaa3ce into main Jul 2, 2026
25 of 30 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant