Operant Studio
OPER-173

Stacked-PR parent-merge detection — trigger rescue immediately on parent squash-merge

Band
SMALL
Suggested
Justin Cooke

Body

---
feature_id: FEAT-studio-dispatcher
band: SMALL
target_repo: operant-health/operant-studio
---

## Context

Reconciliation of OPER-146 (2026.09.09) surfaced a recurring class of stall in the stacked-PR workflow. When a parent PR in a stacked series is squash-merged into main, GitHub does not automatically rebase the child PRs onto main; their `base` ref still points at the deleted parent branch (or at a stale SHA on a branch that lingered), and their diff-against-main balloons to include the parent's already-shipped commits.

The existing `stacked-pr-rescue.yml` workflow exists to handle exactly this, but during the OPER-146 drain it did not fire for hours after the parent (#437) merged. Every child in the stack (#438, #439, #463) had to be manually rebased onto main + `base` repointed + `[BLOCKED]` prefix removed + draft flipped to ready. That is a 4-step manual sequence per PR, run three times in one afternoon, on a single stack.

Concretely observed:
- PR #437 merged 21:14Z
- PR #438 sat with `base=agent/oper-146-pr2` (deleted) and mergeable_state=dirty until 00:10Z manual intervention
- stacked-pr-rescue.yml recent-runs history shows no invocation between 13:18Z and midnight

This is Class **Stacked-PR-Parent-Merge-Detection-Lag**. Distinct from Premature-DONE (OPER-448 territory, which reconciles Ledger status against writeSets coverage); this class blocks *shipping* the actual code, not just the record.

Related evidence: reconciliation comment on OPER-146 (Ledger cuid cmttws4zg0084jo04jevjmcxx).

## Acceptance Criteria

Gate specification:

1. **Trigger surface.** `stacked-pr-rescue.yml` MUST subscribe to `pull_request` closed+merged events, not only to schedule/workflow_dispatch. On every merge, it MUST scan all open PRs whose `base` matches either (a) the merged PR's head ref or (b) any branch whose head SHA equals the merged PR's head SHA at merge time.

2. **Detection semantics.** For each identified child PR, the workflow MUST:
   - Repoint `base` to the merged parent's target (usually `main`)
   - Fetch the parent's post-merge commit SHA on main
   - Rebase the child branch onto that SHA (drop parent's now-shipped commits)
   - Force-push the rebased child branch
   - Post a sticky comment on the child summarizing the parent-merge + rebase action taken

3. **Un-blocking.** If the child PR title starts with `[BLOCKED]` AND its only remaining blocker was the parent PR, the workflow MUST also:
   - Retitle to drop `[BLOCKED]`
   - Flip draft to ready (`markPullRequestReadyForReview`)
   Otherwise leave title/draft as-is; the fleet's other guards decide.

4. **Failure handling.** If rebase produces conflicts, the workflow MUST NOT force-push; instead post a sticky comment on the child requesting manual attention and label with `stacked-rescue-conflict`.

5. **Idempotency.** Multiple invocations on the same parent-merge event MUST NOT stack extra force-pushes or duplicate comments.

6. **Observability.** Every workflow run MUST log: (a) which parent PR triggered it, (b) which children matched, (c) which children were rebased successfully, (d) which children hit conflicts. Log format compatible with fleet's existing dispatch observability parser.

7. **Test.** Add an integration test in `.github/workflows-tests/` that simulates parent-merge with two stacked children, verifies both children are rebased + un-blocked, and verifies a conflict on the second child produces the manual-attention path without force-pushing.

## Non-goals

- Does NOT try to detect stacks whose parents were merged via non-squash strategies (rebase-merge is rare in this monorepo)
- Does NOT handle diamond-shaped dependencies (two PRs both stacking on one parent both stacked-on) beyond flat linear chains

## Effort

Single Ledger ticket family. Parallelizable with OPER-157 (auto-update-branch on ms=behind) but independent of it. Unblocking prerequisites: none — `stacked-pr-rescue.yml` already exists as scaffold.

Attachments

Loading attachments…

Comments

Loading comments…