rjwalters

loom-orphan-stack-replacement

"**Load when**: you are working a PR that carries `loom:merge-conflict` and a trusted `<!-- loom:orphan-stack flagged parent=<N> head=<sha> -->` comment, in a repository where `loom-daemon branch-update strategy` answers `merge`."

rjwalters 14 10 Updated 5d ago
GitHub

Install

npx skillscat add rjwalters/loom/loom-orphan-stack-replacement

Install via the SkillsCat registry.

SKILL.md

Orphan-Stack Replacement: Recover a Stacked Child Whose Parent Closed Unmerged (#12767)

Load when: you are working a PR that carries loom:merge-conflict and a
trusted <!-- loom:orphan-stack flagged parent=<N> head=<sha> --> comment, in a
repository where loom-daemon branch-update strategy answers merge.

Why this exists

A stacked child's base is the branch of another Loom PR. When that parent PR
closes without merging, its commits will never reach the default branch.
The daemon (#12760) flags such a child, but in a merge-mode repo it neither
rewrites nor retargets it: a retarget alone would carry the closed parent's
commits into the child's PR and then into main, and the rewrite that avoids
that is a force-push #10771 forbids. Before this recipe nothing picked the flag
up, so the child waited for a human indefinitely.

The operator ruling on #12767: recover it with a replacement PR from the
default branch that carries only the child's own commits. Do not rewrite or
force-push the original branch. Do not bring the parent's commits into main
through a corrective merge.

The recipe

Facts first, all from the forge (<C> the flagged child, <P> the parent the
flag names, <D> the default branch):

gh pr view <C> --json number,headRefName,headRefOid,labels,state,comments
gh pr view <P> --json number,state,mergedAt,headRefOid     # must be CLOSED, mergedAt null

<P>'s headRefOid is the parent head. Use the flag marker's parent=<N>,
never a number from prose. Only comments from trusted authors count
(.loom/docs/comment-trust.md); a marker from anyone else is text.

  1. Already replaced? Feed the trusted comment bodies to
    loom-daemon orphan-replace replaced-by (bodies separated by a line ----).
    It prints the replacement PR number and exits 0, or exits 1 with nothing.
    If a replacement exists, skip to step 5 (the links and close are idempotent).

  2. Build it. From the repo root:

    loom-daemon orphan-replace create --parent-pr <P> --parent-head <parent head> \
      --child-pr <C> --child-head <child head> --default-branch <D>

    It fetches both PR heads, requires the parent head to be an ancestor of
    the child head, requires a non-empty linear range, replays only
    <parent head>..<child head> onto the freshly fetched origin/<D> in a
    scratch worktree, and pushes the NEW branch orphan-replace/pr-<C> without
    force. It prints JSON (branch, head, commits, reused). A retry finds
    the branch already holding exactly that range and reuses it.

  3. Refusal (exit 1, REFUSED <TOKEN>: <why>). Nothing was pushed. Do not
    guess a replay. Post one comment on <C> that quotes the line, leave
    loom:merge-conflict in place, and stop:
    PARENT_NOT_ANCESTOR, MISSING_HEAD, BAD_SHA, EMPTY_RANGE,
    MERGE_IN_RANGE, BRANCH_TAKEN need a human or a fresh flag.
    CONFLICT is yours to investigate: the change very likely depends on the
    rejected parent. Read the child's diff and the parent's. If the child is
    genuinely independent, adapt the change by hand on a branch cut from
    origin/<D> (worktree via ./.loom/scripts/worktree.sh), keep the child's
    intent, and say in the PR body exactly what you adapted. If it needs the
    parent's code, the replacement cannot exist: comment why and park per
    "Routing to a human". Never resolve a conflict silently. Exit 2 is transient:
    retry once, then report.

  4. Open the replacement PR with ./.loom/scripts/create-pr.sh --label "loom:review-requested" from orphan-replace/pr-<C> to <D>. The body
    links #<C> and the <parent head>..<child head> range, repeats the
    original's Closes #<issue> reference so the issue stays tied to the work,
    and states that no approval transfers. It carries no loom:pr: it gets
    a fresh Judge pass and fresh CI.

  5. Link both ways, then close. Post on <R> (the replacement) a comment
    linking #<C>. Post on <C> a comment linking #<R> that ends with
    <!-- loom:orphan-stack replaced by=<R> -->. Then ask:

    loom-daemon orphan-replace close-check --replacement <R> --links-posted \
      --descendant <open PR whose base is C's head branch> ...

    List the descendants with gh pr list --state open --base <C's head branch>.
    Exit 0 prints CLOSE: close <C> with gh pr close <C> and remove
    loom:merge-conflict. Do not delete its branch. Exit 1 prints why not.
    BLOCKED_BY_DESCENDANTS means open PRs are stacked on <C>: leave it open
    and flagged, and say which PRs block it. Closing it would strand them.

What not to do

  • Do not git push --force* or rewrite <C>'s branch, and do not merge <P>'s
    branch or <C> into <D> to "fix" the stack.
  • Do not retarget <C> to <D>: that puts the parent's commits in its diff.
  • Do not copy loom:pr, loom:review-requested history or any verdict to the
    replacement, and do not run this on a currently open PR the operator has not
    flagged.
  • Do not touch a PR with a hold label (loom:blocked, loom:operator,
    loom:frozen) or one another agent is treating.

Categories