Imported from jleechanorg/jleechanbrain (
skills/pr-cleanup-replay/SKILL.md). Install upstream withnpx skills add jleechanorg/jleechanbrain --skill pr-cleanup-replay. Copyright stays with the author.
pr-cleanup-replay
Trigger
Load when ANY of these signals fire:
| Signal | Action |
|---|---|
A PR is open > 30 days with comments / commits.totalCount > 5× the diff scope |
Suspect drift. Run git log --first-parent origin/main..HEAD to verify. |
CodeRabbit / review is APPROVED but the PR is mergeable: CONFLICTING and won't rebase |
Don't force-push more drift. Replay. |
| Agent-O escalation: "Backfill respawn cap reached for PR #N — 6 prior workers archived" | The cap is the result of contaminated history. Replay, don't spawn another worker. |
git diff origin/main..HEAD --stat shows files unrelated to the PR's stated scope (e.g. .claude/hooks/*, .claude/metadata-updater.sh, .beads/issues.jsonl, fix(ci):, chore(beads):) |
Drift. Replay. |
| User's actual PR scope is < 5 files but the PR diff shows > 30 files / +1000 lines | Definitely drift. Replay. |
| The PR's head branch tip is also the tip of an unrelated feature branch (shared head, multiple agents converged) | STRONG replay signal. See "Anti-pattern: shared head" below. |
PR commits are clean and reviewable but the branch point itself is stale (created from an origin/main SHA that's now far behind) — git log origin/main..origin/<head> shows "Merge remote-tracking branch 'origin/main'" commits OR git merge-base --is-ancestor origin/main <head-branch-base> is false AND the diff vs current origin/main is small |
See "Variant B — same-PR replay" below. Same author + clean commits → cherry-pick onto fresh origin/main and force-push onto the SAME head branch. PR keeps its number, review history, and CodeRabbit thread. |
PR targets classes / selectors / DOM hooks that do NOT exist in the served HTML — git diff origin/main..HEAD is non-empty, but grep'ing the new selectors (e.g. .composer-stack, .composer-send) against mvp_site/frontend_v1/index.html and the served bundle returns ZERO hits |
Dead-diff variant — the PR branched from stale origin/main BEFORE a refactor (e.g. PR #8952 composer refactor) merged. Diff looks plausible in isolation but matches no element. Close + reopen on fresh origin/main. See "Variant C — dead-diff replay" below. |
PR is state=OPEN, mergeable=MERGEABLE, all CI checks green, BUT git merge-base --is-ancestor origin/<head> origin/main returns NO (main has moved past the PR's branch point without producing a literal merge conflict) |
Silent-stale-base variant — CI ran against the PR's own branch (NOT against current main), so it passed vacuously; mergeable=MERGEABLE because git sees no conflict, only drift. The fix sits on a branch only; dev Cloud Run SHA == current main SHA == no fix. Always run git merge-base --is-ancestor BEFORE claiming "PR is ready." See "Variant F — silent-stale-base rebasable PR" below. |
| User reports a deployed-URL symptom and a prior PR with all-CI-green claims to fix that exact symptom, but the fix is NOT visible in dev | Run before anything else: gcloud run revisions list --service=<dev-service> --region=<r> --project=<p> --limit=1 --format='value(metadata.labels.commit-sha)' AND gh pr view <N> --json headRefOid,mergeable. If dev SHA != PR head SHA AND merge-base --is-ancestor returns NO → Variant F (silent-stale-base). The user re-reported PR #9869's bug 3 days later as #9905 because dev never had the fix. |
PR is state=CLOSED (not MERGED), mergedAt=null, and the head branch still exists locally — usually a prior agent's PR that an operator closed without merging because of CI redness / merge conflicts. gh pr reopen cannot reset Evidence Gate / Green Gate gists that reference the dead head SHA. |
Closed-not-merged variant — the head-branch code is still on disk in the prior worktree; the issue is the GitHub PR object's lifecycle state, not the code. Don't reopen. Open a FRESH branch off current origin/main and re-apply the same code with a clean diff. See "Variant E — closed-not-merged replay" below. |
Rule
A long-stale PR with contaminated history is not a "rebase and fix" job — it is a clean replay. Don't add follow-up commits to the polluted branch. Cherry-pick only the in-scope commits to a fresh branch from origin/main, close the old PR with a comment pointing to the new one, and open the new PR.
The original PR head is shared with other agents / unrelated feature work. Per the SOUL.md ## COMMIT: pr-clean-branch-from-main-no-history-bloat and ## COMMIT: never-push-onto-someone-elses-pr-head, never push onto a head branch that is not yours — and a long-stale polluted PR is always someone else's (or many-agent-shared) head.
Two variants — pick by author + commit shape
Before replaying, check two facts: (1) is the PR author the same as the currently-authenticated gh auth status user? (2) is the commit set itself clean (no merge commits, no out-of-scope fixes)?
| Variant | Author vs gh auth |
Commit shape | End-state |
|---|---|---|---|
| A — new-PR replay (default, this skill's main flow below) | Different OR same-but-shared-head | Polluted (merge commits, agento drift, .beads churn) | Cherry-pick into NEW branch → open NEW PR → close OLD PR |
| B — same-PR replay (new) | Same AND maintainer_can_modify=true |
Clean (every commit advances PR scope) — only branch base is stale | Cherry-pick onto fresh origin/main → force-push onto SAME head branch → PR keeps its number |
Variant B is the lesson from PR #8932 (2026-08-15). Both PR #8932 and its sibling #8936 were trying to fix progress_percent fallback on issue #8747. PR #8932 was authored by ${GITHUB_USER} (same account as gh auth status), 4 files / +362/-3, 2 commits where every commit advanced the PR's stated scope. The branch point was stale (54 days behind origin/main), but the DIFF was already clean — no pollution, no merge commits, no agento drift. Pushing a sibling PR would have created a duplicate of an identical fix. The correct answer was cherry-pick the 2 in-scope commits onto fresh origin/main and force-push onto fix/rewards-delivery-custom-curve (the PR's own head branch). The PR keeps its number, CodeRabbit review thread, and headRefName. CI re-runs against the new SHA.
Variant B safety gates (all three MUST pass before force-pushing onto the PR's head branch):
- Same author.
gh pr view <N> --json user.loginreturns a name that matchesgh api user --jq .login. If different, fall through to Variant A —never-push-onto-someone-elses-pr-headviolation. - Same account authenticated.
gh auth statusshows the PR author's account is the active one (Active account: true). A different active account means current operator does not own this PR. - Diff is clean.
git log origin/main..origin/<head> --no-mergesshows ONLY commits whose messages advance the PR's stated scope. ZeroMerge remote-tracking branch, zerofix(beads), zerochore:, zerofix(ci):. If ANY of these appear, the commit set IS polluted → use Variant A.
If any gate fails, STOP and pivot to Variant A. The Variant B path is a force-push onto a shared branch — never-push-onto-someone-elses-pr-head exists for a reason; same author alone is necessary but not sufficient.
Variant B — same-PR replay sequence
After all three safety gates pass, execute this 6-step recipe instead of Step 1+ in the main flow below:
-
Create fresh worktree from
origin/main(not from the PR's branch):cd /path/to/<repo> git fetch origin main git worktree add -b fix/<N>-<slug>-variant-b origin/main cd <new-worktree-dir> git rev-parse HEAD # MUST equal origin/main SHA git status --short # MUST be clean -
Cherry-pick the in-scope commits in order with
-x(same as Step 3 in main flow):for sha in <commit-A> <commit-B>; do git cherry-pick -x "$sha" doneThe
-xflag preserves original SHAs in commit bodies for auditable provenance. -
Validate locally (same as Step 4 in main flow): run the targeted test files. The local pytest is the only proof you have that the replay preserves behavior —
git diff origin/main..HEADmatching the old PR's diff is necessary but not sufficient. -
Force-push onto the SAME head branch (the only step that differs from Variant A):
git push origin HEAD:refs/heads/<pr-original-head-branch> --force-with-lease--force-with-leasenot--force— protects against concurrent pushes to the same ref. Verify the new SHA is what GitHub now reports on the PR:gh pr view <N> --json headRefName,headShaPR head SHA MUST match
git rev-parse HEADin the worktree. If they diverge, the push landed somewhere else and the PR is in an inconsistent state — STOP and investigate. -
Comment on the PR with a one-line note explaining the replay (do NOT close the PR — that's Variant A):
gh pr comment <N> --body "Clean replay of this PR's commits onto fresh origin/main@<sha>. Original head 5cbff868 → new head <new-sha>. Diff vs main unchanged. Cherry-picks used \`-x\` for provenance." -
Verify CI re-runs against new SHA:
gh pr checks <N>Job trigger is automatic on push; if no new runs appear within 60s, the workflow may filter by file paths — confirm with
gh workflow listand look forpull_requesttriggers withoutpathsfilters.
Why Variant B preserves review context: PR #8932's CodeRabbit thread, any user comments, the headRefName in any external dashboards (Hermes agento cron, babysit jobs pointing at the PR), and any in-flight /er evidence reviews all reference the PR NUMBER, not the branch SHA. Force-pushing onto the same branch keeps all of those valid. Variant A abandons them — anyone watching the old PR has to update their bookmarks.
When Variant B is wrong even with clean commits: If the PR has been forked into downstream tooling (e.g. another team has shipped a patch derived from the old head), Variant A's "close + replace" gives them a heads-up. Variant B silently changes the head. Default to Variant A when in doubt — preserving the PR number is a benefit, not a guarantee.
Variant D — lost-PR replay (original commit was never merged, and mainline already refactored the same file)
A separate replay variant seen on issue #9057 / PR #9058 (jleechanorg/worldarchitect.ai, 2026-08-18). The failure mode is invisible to standard PR-cleanup signals because there is no open PR to inspect:
- The original fix lived ONLY on a branch that never reached
main(fix/narrative-response-schema-required-9021, head098ca8316d). No PR was ever opened againstmain. Verified withgit merge-base --is-ancestor 098ca8316d origin/main→ exit 1. - Meanwhile, an unrelated PR (here #9045) landed on
origin/mainand refactored lines 396-510 of the SAME file the original fix touched. The unrelated PR removed a ~100-line block that contained a fallback example the original fix was relying on. So the original fix is no longer a "self-contained" replay — its work overlaps with a refactor that already shipped.
This is not Variant A (no polluted branch to discard) and not Variant B (no open PR head to preserve). It is its own variant because:
gh pr view <N>returns 404 for the original commit's branch — there's nothing to close.- The "conflict" is not a git merge conflict at all — git will auto-merge cleanly because the unrelated refactor touched lines far away from the schema section at the top. The semantic conflict (intent drift between the two changes) is what matters.
Detection recipe (run BEFORE cherry-picking any fix onto origin/main that was authored before a recent mainline refactor):
# 1. Confirm the original commit is NOT an ancestor of origin/main
git merge-base --is-ancestor <orig-sha> origin/main && echo "MERGED" || echo "LOST"
# LOST = candidate for Variant D
# 2. Find files touched by the original commit and check their recent history on main
git show <orig-sha> --stat --name-only
for f in $(git show <orig-sha> --name-only --format=""); do
echo "=== $f ==="
git log --oneline origin/main -5 -- "$f"
done
# If the file has had a refactor commit on main since <orig-sha>, this is Variant D
# 3. Check whether the PR refactor and the lost fix overlap on intent
gh pr view <refactor-PR> --json files,title,body --jq '.title, (.files | map(.path))'
Variant D sequence (cherry-pick + extend the surviving refactor):
- Branch fresh from current
origin/main(same as Variant A Step 2). - Cherry-pick the original commit with
-x: usually lands cleanly because the schema section was inserted at the TOP of the file (lines 1-75), and the mainline refactor targeted lines 396-510 — far enough apart that git's three-way merge resolves without manual intervention. Confirm withgit show HEAD --statthat all 3 of the original commit's files are present (the test file, the lint script, and the prompt file). - Verify tests + lint green on the cherry-pick alone. This catches the case where the cherry-pick's intent depended on prose that the mainline refactor deleted.
- Extend the surviving refactor's stub with the missing intent. In #9057 / #9058, the mainline refactor's
**⚠️ DICE RESOLUTION & UI DISPLAY:**stub (3 lines) explicitly told the LLM "you do not need to populateaction_resolution.mechanics.rolls" — which is what the regression symptom was. The fix added ONE new sentence to that stub re-emphasizing thataction_resolutionitself MUST still be emitted. The line count: 1 addition. The semantic restore:action_resolutionre-anchored at the point where the LLM reads the "code_execution handles it" message. - Squash cherry-pick + extension into ONE commit before pushing — see the "Squash recipe" section below for why two separate commits here is wrong.
- Push + open PR as in Variant A Step 7.
Squash recipe (cherry-pick + follow-up edit → single coherent commit):
The naive sequence is git cherry-pick <sha> then git commit -am "extend stub" — which leaves two separate commits where the second one (the extension) has 1 insertion and the first (the cherry-pick) has 70. From the PR's perspective, the second commit looks like drive-by noise. Squash into one commit that describes the outcome:
# After cherry-pick AND follow-up edit are both uncommitted:
git add -A && git commit -m "<single accurate message>"
# OR, if you already committed them separately and the branch is local-only:
git reset --mixed HEAD~N # N = number of fix commits to collapse
git add -A && git commit -m "<single accurate message>"
Pitfall — soft-reset only replays the LAST commit's staging: git reset --soft HEAD~1 followed by git commit produces a new commit whose diff equals ONLY the previous commit's diff (the latest staging), not the cumulative diff vs origin/main. If you ran two git reset --soft HEAD~1 operations in a row thinking you were accumulating staging, you actually replayed the latest commit each time. Use --mixed (or just leave files uncommitted), git add -A, then commit once.
Pitfall — don't open TWO PRs for the cherry-pick and the extension. Some agents will push the cherry-pick branch as PR #1, see CI go red, then push the extension as a follow-up commit on a new branch as PR #2. This produces two review threads, two CI runs, and the same regression audit twice. Squash first, then push once.
Anti-pattern — "the cherry-pick is the fix; the extension is optional." If the regression was caused by the mainline refactor's stub misleading the LLM, then the extension is NOT optional. Without it, the LLM still gets the "you do not need to populate action_resolution.mechanics.rolls" message with no counter-anchor, and emits the same broken response. The extension is the actual fix to the regression; the cherry-pick is just the precondition. Name the commit accordingly.
Variant C — dead-diff replay (PR branched before its target classes existed)
A new failure mode seen on PR #8996 (closed draft, jleechanorg/worldarchitect.ai, 2026-08-17): the PR's git diff origin/main..HEAD was non-empty, reviewable in isolation, and CodeRabbit-skipped only because the PR was a draft — but every selector it added (.composer-stack, .composer-send, .composer-row, .composer-choices) was added by an earlier refactor (PR #8952) that landed on origin/main AFTER the fix branch was cut. So the diff was dead CSS targeting DOM hooks that didn't exist yet. The deploy preview would have served the rules, but no element matched, so the visual bug the rules tried to fix stayed unchanged. The worker reported "fixed" without verifying against the served page.
This is not a Variant A replay (commits are clean — no merge commits, no drift) and not a Variant B replay (the PR has nothing to preserve — the diff is functionally inert). It is its own variant because the failure mode is invisible to all the standard PR-cleanup checks:
git log --first-parent origin/main..origin/<head>shows clean commits advancing the PR's stated scope — passes Step 0.git diff origin/main..HEAD --statshows the intended files only — passes Step 0.gh pr view --json reviewDecisionis empty (no review yet, because draft) — passes Step 2.- CodeRabbit skipped review because of
isDraft: true— review passes vacuously.
The only check that catches it: do the new selectors match any element in the CURRENT served HTML (or current origin/main's index.html / served bundle). If not, the entire diff is dead.
Detection recipe (run BEFORE opening or approving any UI/CSS PR):
# 1. Get the PR's added selectors (rough heuristic: lines that look like CSS selectors)
gh pr diff <N> --repo <owner>/<repo> \
| grep -E '^\+\s*\.[a-z][a-z0-9_-]+[\s,{]' \
| sed -E 's/^\+\s*\.([a-z][a-z0-9_-]+).*/\1/' \
| sort -u > /tmp/pr-new-selectors.txt
# 2. Grep current origin/main's served HTML/JS for each selector
git show origin/main:<entry-html-or-js-path> > /tmp/served.html
while read sel; do
if ! grep -qE "\\.${sel}[^a-z0-9_-]" /tmp/served.html; then
echo "DEAD SELECTOR: .${sel} — not in origin/main's served HTML"
fi
done < /tmp/pr-new-selectors.txt
A non-zero exit on the per-selector grep means the selector has no matching element. Three or more dead selectors in one PR = almost certainly branched before a refactor. The fix is Variant C — close the dead PR, branch fresh from current origin/main, and re-add the same rules onto the markup that now exists.
Variant C sequence (close + new-branch + re-apply):
-
Confirm the diagnosis is real, not just an incomplete index scan:
# Pull the CURRENT served HTML from the deploy preview (not local main — # local main may be stale if the operator hasn't `git fetch`ed recently) curl -s https://<deploy-preview>.run.app/ | grep -c "composer-stack" # If 0, the deploy env is ALSO stale — wait for the prior refactor's # deploy to roll out, then re-check. -
Close the dead PR with a one-line explanation that names the missing class:
gh pr close <N> --repo <owner>/<repo> --delete-branch \ --comment "Closing: this PR branched from stale origin/main before \ PR #<refactor> (<refactor title>) merged. The diff targets \ .<class-1>, .<class-2>, .<class-3> — none of which exist in the \ current origin/main markup. Reopening on a fresh branch." -
Branch fresh from current
origin/main(the standard replay Step 2):git fetch origin main git worktree add -b fix/<slug>-on-refactored-origin origin/main cd <new-worktree-dir> -
Re-apply the same fix, this time onto markup that contains the target classes. The diff will be smaller — usually just the new
@mediablock — because the markup refactor (composer-*) and the CSS scaffolding are already onorigin/main. -
Verify before pushing: re-run the selector-grep against the LOCAL
index.htmlto confirm.composer-stacketc. now exist:grep -c 'composer-stack\|composer-send\|composer-row\|composer-choices' \ mvp_site/frontend_v1/index.html # Expect: > 0 -
Open the new PR with a body that explicitly cites the closure reason of the dead PR — reviewers need to know this was attempted before:
## Why this is a new PR Draft <https://github.com/.../pull/<N>|PR #<N>> added the same CSS rules but branched from stale `origin/main` before <https://github.com/.../pull/<refactor>|PR #<refactor>> (the <refactor title>) merged. So every selector in that PR (`.composer-stack`, `.composer-send`, `.composer-row`, `.composer-choices`) had zero matching elements in the deployed HTML. PR #<N> has been closed. This PR is rebased on current `origin/main` (<sha>) where the markup includes the composer refactor, so the rules now match real elements. Originally <dead-pr-head-sha>: <dead pr title>.
Anti-patterns specific to Variant C:
- "Just un-draft the old PR and merge it" — the diff has zero visible effect because the selectors are dead. Reviewers can't catch this from the diff alone.
- "Trust the deploy preview URL after the PR is open" — the deploy preview rebuilds from the PR's branch, but the HTML served is still the one bundled with the deploy image, not the PR's. If the PR's branch and the deploy image's bundled HTML disagree (because the deploy image was built from a different SHA), the deploy preview URL may serve PR's CSS rules but the markup from an older image — or vice versa. Always check
view-sourceon the deploy preview URL and grep for the new selectors there, not in your local worktree. - "Add the markup classes to the PR too" — if the dead PR also added markup for the same feature, expanding the PR to include both markup and CSS is fine. But that's a different PR (you're re-landing the missing refactor). For the case in PR #8996, the refactor (#8952) was ALREADY merged — the dead PR should have been based on it.
Lesson source: Slack C0BDEAJH8PK/p1786958669.427289 (mobile composer bug, 2026-08-17). Worker opened draft PR #8996 with 245 lines of dead CSS, reported "Fixed" in Slack without deploying or visually verifying, was caught when I diff'd the local-vs-deployed planning-blocks.css and found 168-line local vs 375-line deployed — and composer-* was missing entirely from local index.html. Reopened as PR #8997 on fresh origin/main (HEAD 6327098042), 51-line mobile-only @media block, single file, all selectors now match real markup.
Variant E — closed-not-merged replay (PR closed without merge; replay onto fresh origin/main)
A new failure mode seen on PRs #9166 / #9167 (jleechanorg/worldarchitect.ai, closed 2026-08-22): the prior PRs were closed by an operator (or self-closed by stale pr_ready_checklist.sh runs) BEFORE merging, usually because of isDraft toggling + failing Evidence Gate / Green Gate / merge conflicts. The head branch still exists locally in _wt/feat-<slug>/ worktrees; the code is correct; the issue is purely the GitHub PR object's lifecycle state.
Why gh pr reopen is wrong here:
- Evidence Gate
FAILUREreferences a gist SHA bound to the closed PR's head commit. Reopening does NOT re-evaluate Evidence Gate against the same dead SHA — it stays FAIL. - Green Gate may also be stale (decision rendered against the closed PR's diff).
- Even if all 8 readiness gates pass after reopen, the diff stat is inflated against the closed PR's merge-base — reviewers see a noisy diff with hundreds of unrelated files from agento drift that accumulated since the PR was originally cut.
- The operator's prior "close" is a signal. Reopening without explicit operator permission is the
## COMMIT: never-push-onto-someone-elses-pr-headanalog for closed PRs.
Why this is a Variant and not "just open a new PR":
- The prior worktree (
_wt/feat-<slug>/) still has the working code with the right logic. You don't have to rediscover what the right changes were — they're on disk. - The merged main has moved past the prior PR's base SHA. Cherry-picking the prior commits onto current
origin/mainmay conflict (especially if the file has been refactored since). Easier to re-apply the focused diff directly. - The same
[skill-name]:provenance tag belongs in the new commit message so the SOUL.md audit log can trace "this was a closed-not-merged replay of N".
Detection recipe (run BEFORE deciding reopen vs fresh-branch):
# 1. Confirm state
gh pr view <N> --json state,mergedAt,closedAt,headRefName,headRefOid
# state="CLOSED", mergedAt=null, closedAt=<recent> → Variant E
# 2. Confirm the head branch still exists locally with code on disk
git branch --list 'feat/<slug>' 'fix/<slug>' '<branch-name>'
git worktree list | grep -F '<branch-name>'
# 3. Confirm the prior diff was clean (not 1000+ files of drift)
git -C <path-to-worktree> diff --shortstat origin/main..HEAD
# 4. Confirm no force-push will salvage the original PR — check what Evidence Gate references
gh pr view <N> --json statusCheckRollup --jq '.[] | select(.name | test("Evidence Gate|Green Gate")) | {name, conclusion, detailsUrl}'
If (1) is CLOSED+mergedAt=null, (2) shows the worktree + branch intact, (3) shows a focused diff (≤10 files / ≤500 lines), and (4) shows Evidence Gate FAILURE referencing a gist SHA — use Variant E, not reopen.
Variant E sequence (re-apply focused diff onto fresh origin/main):
-
Create a fresh worktree from current
origin/main(NOT from the closed PR's branch):cd /path/to/<repo> git fetch origin main git worktree add -b <scope>/<slug>-v2 origin/main # append "-v2" to disambiguate from the closed branch cd <new-worktree-dir> git rev-parse HEAD # MUST equal origin/main SHA git status --short # MUST be clean -
Read the prior worktree's diff against ITS origin/main, not yours. The closed PR's branch was cut from a stale
origin/mainSHA. The diff you want is the diff against THAT old base, filtered for focused in-scope files only:# In the prior worktree (closed PR's branch) cd <path-to-prior-worktree> PRIOR_BASE=$(git merge-base HEAD origin/main) # last common ancestor git diff --stat $PRIOR_BASE..HEAD # Filter to focused files only — drop any .beads/, .claude/, scripts/, drift files -
Re-apply the focused changes to the new worktree. Do NOT cherry-pick the prior commits — they were authored against a stale base. Copy the hunks by hand from the prior diff, OR use
git show <sha> -- <file> | git applyfor individual files. Prefer the by-hand approach: cherry-picking pollutes the new branch with merge commits against mainline drift. -
Run the same targeted tests locally that the closed PR's PR body claimed pass. They MUST pass on the fresh branch, otherwise the diff had a stale-base dependency that needs human review.
-
Commit with provenance tag pointing at the closed PR:
git add -A git -c user.name="<cli>/<model>" commit -m "<cli>/<model-id>: <scope>(<area>): <accurate short description> Replaces closed PR #<N>. The closed PR's Evidence Gate referenced a gist SHA bound to the now-dead head; reopening does not re-evaluate. Re-applied the focused diff against current origin/main @ <sha>. Originally <closed-pr-head-sha>: <original first line>." -
Push the fresh branch and open the PR:
git push origin HEAD:refs/heads/<scope>/<slug>-v2 gh pr create --base main --head <scope>/<slug>-v2 \ --title "<original PR title> (v2)" \ --body "<see template below>"PR body template:
## Why this is a new PR (closed-not-merged replay) Closed <https://github.com/.../pull/<N>|PR #<N>> had Evidence Gate / Green Gate FAILURE tied to a gist SHA from its head commit. Reopening cannot reset the stale gate references. Re-applied the focused diff against current `origin/main` (<sha>) on branch `<scope>/<slug>-v2`. Originally <closed-pr-head-sha>: <closed PR title>. ## Diff vs current origin/main <N> files, +<a>-<d> lines (same scope as the closed PR's last code shape): - <file-1>: <one-line change summary> - <file-2>: <one-line change summary> ## Verification - <test-file-1>: N/N pass - <test-file-2>: N/N pass -
Comment on the closed PR (do NOT reopen):
gh pr comment <N> --body "Closed in favor of fresh-branch replay PR #<NEW-N>. \ The original PR's Evidence Gate referenced a gist SHA from its dead head; \ reopening cannot re-evaluate. PR #<NEW-N> contains the same scope re-applied \ against current origin/main @ <sha>." -
Verify the fresh PR:
bash ~/.smartclaw/scripts/pr_ready_checklist.sh <NEW-N> <OWNER/REPO> # ALL gates must PASS before any "ready" claim (per `pr-ready-checklist` skill)
Anti-patterns specific to Variant E:
- "Just
gh pr reopenand wait for CI" — Evidence Gate FAILURE persists because the gist SHA is dead; reopening does not regenerate the gist. - "Force-push onto the closed PR's head branch and
gh pr reopen" — combines two SOUL violations (never-push-onto-someone-elses-pr-head+ reopening a closed PR without operator signal). - "Cherry-pick the prior commits" — they were authored against a stale base; cherry-pick may conflict or, worse, succeed but inject merge commits against mainline drift that pollute the diff.
- "Open a brand-new PR with a different approach" — if the prior code was correct (just on a dead branch), reuse it; the operator doesn't need a third attempt at the same feature.
- "Skip the 8-gate readiness check because the original PR 'passed' it" — the original PR's readiness state is irrelevant; the new PR has its own SHA, its own Evidence Gate gist, its own CodeRabbit review, its own merge-base against current origin/main. Re-run the script.
Lesson source: PRs #9166 (AGY Low thinking opt-in) and #9167 (Gemini direct-API Low thinking) closed 2026-08-22 02:52:33Z and 02:37:44Z respectively. Both had isDraft: true + Evidence Gate FAILURE referencing gist SHAs from their respective heads. Reopening was tempting (the work was correct) but the gate references were structurally dead. Replay: opened fresh branches feat/agy-low-thinking-v2 (HEAD 1bbce598ba) and feat/gemini-thinking-low-v2 (HEAD f30f269695) off origin/main @ 528ddd5147. Diff: 2 files / +57/-7 and 2 files / +452/-5 respectively. Unit tests 94/94 (agy) and 180/181 (gemini_provider_* suite) green. New PRs #9242 and #9243 opened with the same gate compliance — operator can now merge them as soon as CI drains.
Variant F — silent-stale-base rebasable PR (PR is OPEN + CI green, fix is NOT on main)
A new failure mode seen on PR #9874 (fix/world-logic-reducer-mask-9869, jleechanorg/worldarchitect.ai, 2026-09-13 → 2026-09-16). The PR was authored against merge-base 816abce06f4; main has moved 16+ commits forward to 41720ad55a7. The smoking-gun is silent:
gh pr view <N> --json mergeablereturned"MERGEABLE"— no literal conflict, only drift.- All 8 CI checks (
Green Gate,Design Doc Grep Gates,Deploy PR Preview,WorldArchitect Tests (Directory-Based),Presubmit Checks,MVP Shards,limit-pr-runs) wereSUCCESS. CI runs against the PR's own branch tip, NOT against current main. A stale-base PR can pass every check without ever touching current main's code. - Dev Cloud Run SHA matched current main (
41720ad55a7), so dev never had the fix. - User assumed "PR is OPEN with green CI → fix is in flight" — but the fix was only on the branch.
The user reported the bug class on 2026-09-13 (issue #9869), the PR was filed 4 hours later (f25cc041338), all CI green by 20:28Z. The user then re-reported the same bug class on 2026-09-16 (issue #9905, on a 2nd campaign) — 3 days later — because dev still didn't have the fix. The PR had been silently-stale the whole time.
Why this is a NEW variant (not covered by A/B/C/D/E):
- A — polluted with merge commits/drift: NO. PR #9874's single commit was clean (
fix(world_logic): ..., scope was exactly the smoking-gun code). - B — same-PR rebasable, clean commits, same author: YES structurally but the lesson is different — the failure was that nobody checked ancestry before treating the green PR as "ready."
- C — dead-diff (selectors don't exist): NO. The diff targets real code that does exist.
- D — lost-PR (never merged, file refactored): NO. The PR is open and mergeable.
- E — closed-not-merged: NO. PR is OPEN.
The distinguishing feature is: the agent/operator trusted mergeable=MERGEABLE + all-CI-green as "ready to merge" without ever verifying that the branch is an ancestor of main. The CI gate does NOT include "is this branch reachable from main?"
Detection recipe — run BEFORE declaring any "PR is ready / fix is in flight" verdict:
# The single check that catches silent-stale-base:
ANCESTOR=$(git merge-base --is-ancestor origin/<head-branch> origin/main && echo YES || echo NO)
echo "PR #<N> head branch ancestor of origin/main? $ANCESTOR"
# If NO, the fix is on a branch only — main has moved on.
# Cross-check against the deployed environment:
DEV_SHA=$(gcloud run revisions list \
--service=mvp-site-app-dev \
--region=us-central1 \
--project=worldarchitecture-ai \
--limit=1 --format='value(metadata.labels.commit-sha)' 2>/dev/null)
echo "Dev SHA: $DEV_SHA"
echo "Main HEAD: $(git rev-parse --short origin/main)"
# If dev SHA == main SHA AND main SHA != PR head SHA, dev never had the fix.
Three signals that COMBINED indicate silent-stale-base:
mergeable=MERGEABLE(so the operator thinks it's ready)- All CI green (so the operator trusts the merge)
git merge-base --is-ancestor origin/<head> origin/mainreturns NO (the catch)
Any ONE of (1) or (2) is necessary but not sufficient for "fix is in flight." (3) is the only one that confirms mainline containment.
Variant F sequence — same as Variant B but with the explicit stale-base caveat:
- Confirm the PR's commits are clean (no merge commits, all in-scope). If polluted → pivot to Variant A. The silent-stale-base fix for #9874 was a single clean commit (
f25cc041338) targeting 6 lines inworld_logic.py+ a 149-line contract test — the cleanest possible case. - Create worktree from
origin/main(NOT from the PR's branch) — pull the PR's branch last sogit log origin/main..origin/<head>shows the diff:git fetch origin main <head-branch> git worktree add -b fix/rebase-<N> origin/main cd fix/rebase-<N> git log --oneline origin/main..origin/<head-branch> # Confirm: every commit advances PR scope, zero merge commits. - Run
git rebase origin/mainON THE PR's branch inside the worktree (the cleanest path is to checkout the PR's branch then rebase, NOT to cherry-pick — preserves commit shape + SHA provenance):git fetch origin <head-branch> git checkout -B <head-branch> origin/<head-branch> git rebase origin/main - Verify the diff against current
origin/mainis unchanged (git diff origin/main..HEAD --statshould match the pre-rebase line count). A different line count = main drifted through the same files the PR touched = conflicts to resolve manually. - Run the PR's contract tests from the rebased branch. They MUST pass on current main (not just the PR's stale base).
- Force-push onto the SAME head branch with
--force-with-lease:git push --force-with-lease origin HEAD:refs/heads/<head-branch> - Verify the new SHA matches what GH reports on the PR:
gh pr view <N> --json headRefOid - Verify CI re-runs (
gh pr checks <N>shows new run IDs from the post-rebase SHA — old run IDs are stale). - Comment on the PR with a one-line stale-base-rebase note (so reviewers understand why the SHA moved but the diff didn't):
gh pr comment <N> --body "Stale-base rebase onto current origin/main @ <new-sha>. Original head <old-sha> branched from <old-base-sha>; main has moved 16+ commits since. Diff vs main unchanged at <N> files / +<a>-<d>. CI re-running."
Anti-patterns specific to Variant F:
- "CI is green, the PR is ready." CI passing ≠ fix in main. CI runs against the PR's branch tip; if main moved on, the CI result is from the wrong baseline.
- "
mergeable=MERGEABLEmeans safe to merge." Mergeable = no git-level conflict. It does NOT equal "the diff is what we think it is against current main." A stale-base PR can be MERGEABLE with a 30-day-old mainline drift between its base and current main. - "Just merge it and see what breaks." This is the exact anti-pattern that wastes cycles — silent-stale-base PRs can merge cleanly into a now-drifted main and silently roll back fixes that landed between PR-base and merge-time.
- "Open a duplicate PR." Don't. Use Variant F's same-branch force-push — the PR keeps its number, review thread, CodeRabbit history, and any babysit/cron that targets it.
- "Close the stale-base PR without rebasing." Closing leaves the original bug unfixed. Rebase first, then verify CI, THEN ask for merge approval.
Lesson source: PR #9874 (fix/world-logic-reducer-mask-9869), jleechanorg/worldarchitect.ai, 2026-09-13 → 2026-09-16. Authored at merge-base 816abce06f4 against an old base SHA; main moved to 41720ad55a7 via 16+ commits including PR #9384 (combat NPC statuses) and the state-primary-matrix-20260914 merge. PR #9874's CI was green at 20:28Z on Sep 13; user re-reported the bug class on Sep 16 as issue #9905 (campaign FOpeODbNVKzcYB22JBkg). Root cause: dev Cloud Run SHA = 41720ad55a7 (current main) ≠ f25cc041338 (PR head). User frustration quote: "Thought we already had a PR if not make one also look the session header state still wrong why can't god mode see it." Detection: git merge-base --is-ancestor origin/fix/world-logic-reducer-mask-9869 origin/main returned NO. Fix: rebased onto current main HEAD 41720ad55a7 → new head 66c72ab367f, force-pushed, CI re-queued. All 3 contract tests green on the rebased branch.
Discipline update — added to AGENTS.md / SOUL.md workflow: for any "PR is ready" verdict on a jleechanorg/* repo, the agent MUST verify git merge-base --is-ancestor origin/<head> origin/main returns YES before claiming the fix is in flight. Without this gate, the user wastes 3+ days debugging the "same" bug that the PR was supposed to fix. The check is sub-second and catches a class of silent failures that the standard mergeable + ci-green heuristics both miss.
The full sequence (execute in order, no pauses)
Step 0 — Diagnose the drift
Before any work, confirm the diagnosis with three commands:
# 1. Confirm the PR is in concept-good shape (reviewer approved, no real blockers)
gh pr view <N> --repo <owner>/<repo> --json reviewDecision,mergeStateStatus,statusCheckRollup,additions,changedFiles,commits
# 2. Show the first-parent-only commit list — this is the operator's "story" of the PR
git fetch origin <head-branch>
git log --first-parent origin/main..origin/<head-branch> --oneline
# 3. Diff vs origin/main to see the actual file scope
git diff origin/main..origin/<head-branch> --stat
If the first-parent log shows commits like Merge remote-tracking branch 'origin/main' into ..., [fixpr codex-automation-commit], fix(beads), fix(ci):, or chore: mixed in with the real feature commits, the branch is polluted and you must replay.
Step 1 — Identify the IN-SCOPE commits
Inspect each commit:
git log origin/main..origin/<head-branch> --oneline --no-merges
For each commit, ask: "Does this commit advance the PR's stated scope?" Use the PR's title/body as the source of truth.
Common OUT-OF-SCOPE commit patterns to drop:
Merge remote-tracking branch 'origin/main' into ...— merge commits that pumped in mainline drift[fixpr codex-automation-commit] ...— agento-driven mechanical fixes from prior spawnsfix(beads): remove duplicate bead .../.beads/issues.jsonl— beads bookkeepingfix(ci): .../ci(workflow): ...— CI plumbing unrelated to the PRchore: .../style: ...— formatting churn- Anything touching
.claude/hooks/*,.claude/commands/*,.claude/metadata-updater.sh,.claude/activity-updater.sh,.claude/settings.json— agento tooling drift fix(rewards): .../fix(level-up): ...etc. when the PR is about a different feature
CRITICAL coupling pitfall (the lesson from PR #7842 → #8937, 2026-08-15): A "fix" commit may call a helper that was added by a separate "refactor" commit. If the refactor is out-of-scope, you cannot keep the fix — the helper won't exist on origin/main. Check coupling BEFORE cherry-picking:
# For each candidate fix commit, check what symbols it CALLS
git show <fix-sha> -- <file> | grep -E '^\+\s*[a-zA-Z_]+\(' | grep -v 'def ' | grep -v 'class '
# For each out-of-scope refactor commit, list the symbols it DEFINES
git log --all --oneline -S '<helper_name>' -- <file>
If symbol foo is defined in commit A (out-of-scope refactor) and called in commit B (in-scope fix), you have three options:
-
Include both A and B — extends PR scope, but the code is consistent
-
Drop both A and B AND any tests that asserted on A's behavior — strict-scoped, requires test surgery
-
(NEVER) Include only B —
NameError: name 'foo' is not definedat import time, every test that exercises the helper-call path will failReal failure mode (PR #7842): cherry-picked
4f82af582dwhich called_apply_terminal_cleanup_to_patch. The helper was defined indd3b5f801e(out-of-scope refactor).test_projector_for_complete_session_emits_no_offerfailed withNameError: _apply_terminal_cleanup_to_patch is not defined. Caught only because the test was run locally.
Step 2 — Create a fresh worktree from origin/main
NEVER rebase. NEVER amend the polluted branch. Always new worktree:
cd /path/to/<repo>
git fetch origin main
git worktree add -b fix/<N>-<slug>-clean-replay origin/main
cd <new-worktree-dir>
git rev-parse HEAD # MUST equal origin/main SHA
git status --short # MUST be clean
The worktree at ${HOME}/repos/<repo>/origin/main is the canonical path pattern from this skill (mirrors the git worktree add … origin/main recipe).
Step 3 — Cherry-pick the in-scope commits IN ORDER with -x
for sha in <commit-A> <commit-B> <commit-C> <commit-D>; do
git cherry-pick -x "$sha" || break
done
The -x flag preserves the original commit SHA in the message body so provenance is auditable.
If a cherry-pick conflicts:
- Read the conflict. The 54-days-of-mainline-drift case is the most common conflict source.
- Resolve preserving the cherry-pick's INTENT (the structural change), not the original line-by-line text.
git checkout --ours <file>for files that don't need the cherry-pick's version (e.g..beads/issues.jsonlif the cherry-pick only removes a duplicate that's already gone on main).git add <resolved-files>thengit -c core.editor=true cherry-pick --continue.
If a helper-call conflict surfaces (the same helper was added by a now-dropped commit):
- Option 1 (include the refactor commit):
git cherry-pick -x <refactor-sha>on its own, resolve any conflicts, thengit cherry-pick --continue <fix-sha>. - Option 2 (drop the fix and its dependent tests):
git cherry-pick --abort, thengit show <fix-sha> -- <file>to manually extract ONLY the in-scope pieces (e.g. test additions that don't depend on the helper).
Step 4 — Validate locally
Run the targeted tests that exercise the cherry-picked code:
./run_tests.sh <test-file-1> <test-file-2>
If a test fails with NameError: name 'X' is not defined, you have a coupling pitfall (Step 1). Catch it HERE before pushing, not after.
Step 5 — Amend the cherry-pick messages to be accurate
A cherry-pick of 4f82af582d "[fixpr codex-automation-commit] fix PR #7842" may only contain 2 of the 5 files from the original commit. The commit message lies. Fix it:
git -c core.editor=true commit --amend -m "test(<scope>): <accurate short description> (clean replay from <orig-sha>)
Cherry-picked <in-scope-pieces> from <orig-sha>. Dropped the <out-of-scope-pieces>
because <reason: separate refactor / out of scope / drift content>.
Originally <orig-sha>: <original first line>"
This keeps the -x provenance trail in the body while making the actual content of the commit audit-honest.
Step 6 — Push the fresh branch
git push -u origin fix/<N>-<slug>-clean-replay
Step 7 — Open the new PR
# REST is more reliable than GraphQL here when rate limits are tight
gh api -X POST repos/<owner>/<repo>/pulls \
-H "Accept: application/vnd.github+json" \
-f title="[agento] <original PR title> — clean replay" \
-f head="fix/<N>-<slug>-clean-replay" \
-f base="main" \
-f body="<see template below>"
PR body template (clear, reproducible, admits the cause):
## Clean replay of PR #<N>
PR #<N> had <N> commits polluted with <duration> of unrelated agento drift (`<examples>`). This is a CLEAN replay from `origin/main` with only the <N> in-scope commits:
- <sha> <original subject>
- <sha> <original subject>
- ...
Per the SOUL.md `pr-clean-branch-from-main-no-history-bloat` and `never-push-onto-someone-elses-pr-head` commits — the original PR #<N> head is shared with multiple agents and cannot be cleanly updated.
**Diff vs origin/main:** <N> files, +<a>-<d> lines
- <file-1>
- <file-2>
**Tested locally:** <test-name-1>, <test-name-2>, <test-name-3> all pass.
**Out of scope (NOT included from the original PR):**
- <out-of-scope-commit-sha> <reason>
- <out-of-scope-commit-sha> <reason>
Closes #<N>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Step 8 — Comment on the old PR + close it
gh api -X POST repos/<owner>/<repo>/issues/<N>/comments \
-f body="Closing in favor of clean replay PR #<NEW-N> — original PR #<N> head was polluted with <duration> of unrelated agento drift (<N> non-<scope> commits: <examples>). PR #<NEW-N> contains only the <N> in-scope commits (+<a>-<d> across <N> files), tested locally, <reviewer> approval ported over."
gh api -X PATCH repos/<owner>/<repo>/pulls/<N> \
-f state="closed"
Do NOT use Closes #N in the new PR body if the old PR is still open — gh api -X PATCH ... state="closed" will move the auto-closing reference to the closed PR. Close the old PR AFTER the new PR is open, then the chain resolves cleanly.
Step 9 — Verify and report
gh api repos/<owner>/<repo>/pulls/<NEW-N> --jq '{number,state,title,head:.head.ref,base:.base.ref,mergeable,additions,changed_files,url}'
gh api repos/<owner>/<repo>/pulls/<N> --jq '{number,state,closed_at,merged_at}'
Heads-up on the PR diff: GitHub shows additions / changed_files based on the 3-dot diff (merge-base to HEAD). When origin/main has moved past your branch point, the diff will inflate to include unrelated drift between the branch point and current main. This is harmless — the actual PR diff is what you pushed. It will compress to the real numbers once merged.
Anti-patterns (DO NOT do these)
- "Just rebase the polluted branch" — a polluted branch has 24+ commits of mixed provenance. Rebase rewrites them but they're still there. The audit story is broken. Plus, the head is shared with other agents, so a force-push onto
feat/...lands between two people's intermediate states. - "Land a follow-up commit on the polluted branch that cherry-picks the in-scope commits" — same audit-story problem. The PR diff still contains the drift.
- "Force-push onto
feat/<original-branch>to fix the history" — this is the SOUL.mdnever-push-onto-someone-elses-pr-headviolation. The branch is shared. Even if you "own" it, the worktree state is contested. - "Spawn another AO worker to drive to green" — this is what failed 6 times to produce the escalation. The worker keeps re-rebasing the polluted branch and failing. Replay is the fix, not another worker.
- "Disable the agento backfill cron for this PR" — the cron is doing the right thing (alerting on a stuck PR). The PR is stuck because the underlying history is unrecoverable. Replay, then close the old PR so the cron stops targeting it.
- "Include the helper refactor commit because the test fails without it" — only if the refactor is genuinely in scope. If the refactor is a separate feature (like
dd3b5f801e refactor(level-up): extract _apply_terminal_cleanup_to_patch helperin PR #7842 → #8937), the correct answer is option 2: drop the fix and its dependent tests, NOT extend the PR scope.
Worked example — PR #7842 → PR #8937 (2026-08-15)
Symptom: Agent-O escalated "Backfill respawn cap reached for PR #7842: 6 prior workers archived." PR #7842 was 54 days old, CONFLICTING, 28 commits, +1001/-40 across 12 files. CodeRabbit APPROVED. All chatgpt-codex-connector items were stale "backfill meta" discussion, not blockers.
Diagnosis: git log --first-parent origin/main..origin/feat/rag-class-fuzzy-match showed the 28 commits were [feature, lint-fix, codereabbit-fix, fix, merge, merge, ..., fix, merge, ...]. Only 4 commits were in-scope class-lookup work. The rest was drift from 54 days of agento churn.
In-scope commits identified:
e4914e7439feat(class-lookup): semantic fallback for reskinned class corpus (Phase C)a7e8c9fa57fix(lint): resolve Ruff PLR0911, PT018, F401 in class_lookup and its test0a719b6f86fix(class-lookup): address CodeRabbit review comments4f82af582d[fixpr codex-automation-commit] fix PR #7842
Coupling pitfall caught: 4f82af582d CALLED _apply_terminal_cleanup_to_patch, which was DEFINED in dd3b5f801e (out-of-scope refactor). Two options:
- Include
dd3b5f801etoo → clean but extends scope to 5 commits - Drop the level_up_session.py parts of
4f82af582dAND its dependent test → strict scope, 4 commits, 3 files
Chose option 2 (strict scope). The dependent test was test_project_legacy_clears_flags_for_sealed_v2_session — removed from the cherry-pick. The level_up_session.py and test_level_up_session.py changes were reverted to HEAD before continuing the cherry-pick.
Result:
- New PR #8937: 4 commits, 3 files, +378/-3 (vs original 12 files / +1001/-40)
- Old PR #7842: closed with comment pointing to #8937
- Local tests:
test_class_lookup,test_class_lookup_semantic,test_level_up_sessionall pass - PR #8937 starts with
[agento]so the backfill cron will pick it up cleanly
Pre-flight gate — run BEFORE starting the replay
The fastest way to lose 30+ minutes is to start a claudem dispatch on a split the operator has already landed, or on a PR the operator has already closed. The 4-check preflight is the upstream gate for every replay path in this skill:
pr_preflight() {
local pr="$1" target_branch="$2"
echo "=== Preflight for PR #$pr on branch $target_branch ==="
# Check 1: is the PR still alive?
local state
state=$(gh pr view "$pr" --json state,mergedAt --jq '"\(.state) merged=\(.mergedAt)"' 2>/dev/null)
echo "1. PR state: $state"
[[ "$state" == OPEN* ]] || { echo " STOP — PR is not OPEN. Closed-not-merged or already merged."; return 1; }
# Check 2: are YOU the active gh account, and does the PR author match?
local active author
active=$(gh auth status --json activeAccount --jq '.activeAccount.login' 2>/dev/null)
author=$(gh pr view "$pr" --json author --jq '.author.login' 2>/dev/null)
echo "2. Active gh account: $active | PR author: $author"
[[ "$active" == "$author" ]] || { echo " STOP — different author. Per never-push-onto-someone-elses-pr-head."; return 1; }
# Check 3: is the operator's split/successor work already done?
# (catches the most common time-waster: doing a split the operator already finished)
local successors
successors=$(gh pr list --state all --limit 30 --json number,title 2>/dev/null \
| python3 -c "
import json, sys
n = int('$pr')
for p in json.load(sys.stdin):
t = p['title'].lower()
if f'split from #{n}' in t or f'supersedes #{n}' in t or f'replaces #{n}' in t:
print(f\"#{p['number']} — {p['title']}\")")
if [[ -n "$successors" ]]; then
echo "3. Found successors:"
echo "$successors" | sed 's/^/ /'
echo " ACTION: read each successor; if CI-green, link from your reply and stop."
fi
# Check 4: is your target branch name FREE on origin?
local collision
collision=$(git ls-remote origin "refs/heads/$target_branch" 2>/dev/null | awk '{print $1}')
if [[ -n "$collision" ]]; then
echo "4. STOP — branch $target_branch already exists at $collision. Pick -v2 suffix."
return 1
fi
echo "All preflight checks PASSED."
return 0
}
Real failure mode this catches (2026-08-22, PR #9132 → #9272 + #9273): the agent started a claudem -p "split #9132" --max-turns 80 dispatch that would have created -v2 branches duplicating the operator's already-merged split. The preflight caught it AFTER the worker was spawned but BEFORE any push landed, so the dispatch was killed and the orphan worktree was cleaned up with no remote side effects. The earlier session had also force-pushed an a3c302e8d2 rebase onto an orphan fix/pr9132-merge-conflicts branch — this preflight would have flagged the closed PR at Check 1 and the wrong target branch at Check 4 before any push.
Use pr_preflight <N> <your-target-branch> at the start of any work on a non-trivial PR. The four checks are sequential dependencies — fail fast on the first one, don't bother with the rest. The same function is the upstream gate for closed-pr-redrive and any rebase/replay work in drive-pr-to-green.
Pitfalls
Pitfall 1 — git diff origin/main..HEAD shows 200 files because origin/main moved. The 3-dot diff shows ALL the changes between the merge-base and HEAD. When your branch is 54 days old and origin/main has moved past your branch point, the diff inflates to include drift. Use git diff --stat $(git merge-base origin/main HEAD)..HEAD instead, or compare against the branch point SHA (the HEAD~N of your replay branch), not against current origin/main.
Decision rule — when is plain git rebase origin/main correct, vs. full cleanup-replay? The skill's main flow says "NEVER rebase. Always new worktree." But that rule targets actually-polluted branches (mix of in-scope commits + drift commits from other agents / agento churn). A clean branch whose only problem is origin/main moving past its base is a different situation.
Before reaching for full cleanup-replay, run the deleted-files pre-flight:
git diff --shortstat origin/main..HEAD # the inflated diff
git diff --name-only <worktree-head>..HEAD # your commits' actual files
git diff --name-only <worktree-head>..HEAD | while read f; do
[ ! -f "$f" ] && echo "DELETED in main: $f"
done
If the deleted-files pre-flight prints NOTHING, your branch is clean (only origin/main drift inflated the diff) and the right fix is git rebase origin/main on the existing branch, NOT cleanup-replay. Push with --force-with-lease (NOT --force — protects against concurrent pushes to the same ref). Then re-run your tests on the rebased branch. Lesson source: PR #9089 (jleechanorg/worldarchitect.ai, 2026-08-19) — 2 commits, 7 files / +122/-95, no shared head, no merge commits, no agento drift. Plain rebase restored scope; full cleanup-replay would have created an unnecessary new branch + new PR number + orphan review thread.
If the pre-flight prints even ONE deleted file, the branch has crossed into actual pollution territory — pivot to full cleanup-replay (Variant A, B, C, or D per the table at the top).
Pitfall 2 — .beads/issues.jsonl is append-only and gigantic. Any commit that touched .beads/issues.jsonl will create a 4000+ line diff unless the cherry-pick's change is already on origin/main. Use git checkout HEAD -- .beads/issues.jsonl to drop the cherry-pick's beads change — beads is canonical-flushed on main, not on feature branches.
Pitfall 3 — Cherry-pick conflict markers are noise. Conflict markers (<<<<<<<, =======, >>>>>>>) in committed files corrupt git history. Use git grep -nE '^<<<<<<<|^=======$|^>>>>>>>' <file> after each conflict resolution to verify no markers leaked. (The grep matches ======= as a line of only equals signs — the section-divider marker, not table-row separators.)
Pitfall 4 — The [agento] prefix. New PRs created this way MUST start with [agento] so the backfill cron (ai.agento.backfill) detects them as AO-managed and auto-spawns session workers. Without the prefix, the cron ignores the PR and the next backfill escalation will repeat the same cycle.
Pitfall 5 — gh pr create GraphQL bucket gets rate-limited. When the GraphQL bucket is exhausted, REST gh api -X POST repos/.../pulls still works. Record the rate-limit hit, switch to REST, and report the new PR's URL via gh api ... --jq. The fallback chain is documented in gh-rate-limit-resilience skill.
Pitfall 6 — Closing the old PR too early. If you close PR #N before opening the new PR, the Closes #N reference in the new PR's body attaches to the now-closed issue and the issue gets reopened. Sequence: open new PR FIRST → comment on old PR → close old PR.
Pitfall 7 — Forgetting git -c core.editor=true on cherry-pick --continue and commit --amend. Without it, the editor hangs waiting for input on the "Press return to start merge message" prompt. The -c core.editor=true config makes the commit succeed without interaction.
Pitfall 8 — Fresh replay worktree has no venv/ (lesson from PR #8932, 2026-08-15). The worktree created at Step 2 / Variant B Step 1 is bare — it shares git internals with the main checkout but NOT the venv/ directory. Running ./vpython -m pytest ... from the worktree fails with Error: Virtual environment activate script not found at <worktree>/venv/bin/activate. Fix: invoke pytest via the main checkout's venv at absolute path, with PYTHONPATH=. set to the worktree:
cd ${HOME}/projects/wt-pr-<N>-clean-replay
PYTHONPATH=. ${HOME}/projects/<repo>/venv/bin/python -m pytest \
<test-file-1> <test-file-2> -x --tb=short
The alternative (./vpython -m pytest ... from the worktree) requires first creating the worktree's venv (python3 -m venv <worktree>/venv && ./venv/bin/pip install -r requirements.txt) — slow and not worth it for a 374-test regression run on the replay's targeted files. The same pattern applies to any git worktree add workflow (drive-pr-to-green, agento workers, etc.) — capture as a class-level pitfall, not just this skill.
Pitfall 9 — git ls-remote origin 'refs/pull/<N>/head' != refs/heads/<branch-name> before the replay. GH's refs/pull/<N>/head is the immutable ref that always points at the PR's current head. After Variant B's force-push, both refs
*Truncated - read the full file at https://github.com/jleechanorg/jleechanbrain/blob/c33d1788ed3130e20ea624a05143b3fc6db9eed6/skills/pr-cleanup-replay/SKILL.md.