PR Cleanup Replay — surgical replay of a polluted PR
When an agent discovers (or is told) that a PR is bloated — i.e. its branch carries
commits unrelated to its stated scope, or its diff is >2x the load-bearing fix —
follow this recipe to produce a clean minimal-diff replay.
Phase -2 — patch-as-port: external patch targets a different module split
The most common cause of "git apply --check says the patch doesn't apply" is that the patch was generated against a different repo (or an older commit of the same repo) whose file layout doesn't match the target. The git apply --check recipe in the GUIDE — sed -e 's|snap_factory/engine.py|dark_factory/engine.py|g' | git apply --check — only works when the upstream package is renamed verbatim. When the upstream has been refactored (e.g. snap_factory/engine.py 1932-line class → runner/handler_codergen.py::_codergen + runner/handler_verdict.py), a sed pass alone is insufficient — the symbols land in different modules.
cd <target-repo>
git apply --check /path/to/external.patch
Why this discipline matters (verified 2026-07-20): A 33 KB infra03q-inpipeline-receipt.patch from snap_factory HEAD targeted snap_factory/engine.py with functions _run_llm, _finalize_review_status, _reproduction_receipt_gaps, _is_review_node. None of those symbols exist in the upstream jleechanorg/dark-factory repo (which has a runner/engine.py re-export shim, runner/handler_codergen.py::_codergen for the LLM dispatch, and a totally different module split after PR #77). The agent's default — git apply --stat (which only counts hunks without verifying they apply) — would have said "OK" while the actual git apply --check failed on both the missing file and a context drift. The fix is NOT a sed pass; it's a per-symbol port with an explicit destination table. See references/patch-as-port-symbol-mapping-2026-07-20.md for the full table from this incident (5 net-new symbols, 1 net-new test file, 1 net-new doc, 1 fixture fix; landing targets: runner/handler_verdict.py, runner/handler_codergen.py::_codergen::_finalize, tests/test_review_reproduction_receipt.py, tests/test_engine.py::TestLLMNodeDispatch, docs/ungameable-cold-gate.md).
Three operational rules:
- Always
git apply --check from inside the target repo (not from $HOME or another parent where missing-file errors are silently suppressed as warnings and a green stat-check hides a red apply-check).
- Build the symbol-mapping table before writing any code. Each
def <name>(...) line in the patch → row in the table with (a) target file:line, (b) what the new function does, (c) what existing upstream function it parallels (or "net-new — no upstream equivalent").
- Surface the symbol-mapping table to the user before opening the PR. The user explicitly said "if you understand the goals" when approving this kind of port — and they can't verify the port without seeing the table.
Phase -0.5 — Writable-remote base selection (don't branch from a remote you can't push to)
The most common cause of "the PR is 600k lines / 4000 files" pollution in fork setups is branching from the upstream remote and pushing to the fork remote. The two remotes point at the same repo name but different refs with different histories:
origin = NousResearch/<repo>.git (or other upstream) — read-only from your gh auth user; you have no push permission.
fork (or github) = <your-org>/<repo>.git — writable fork, often 6,000+ commits ahead of origin/main because the fork has accumulated local-only work.
upstream (sometimes) = a separate writable remote the maintainer controls.
When you git worktree add -b fix/x origin/main and git push <writable-remote> HEAD:refs/heads/fix/x, the PR is created against the writable remote's main (not origin/main). The diff is therefore writable-remote/main vs your-branch — which on a 6,000-commit-ahead fork shows the entire fork divergence as "polluted PR diff". Result: a PR with +600k/-90k/4,000 files claiming to "fix Python 3.14".
Mandatory pre-flight recipe (run BEFORE git worktree add):
git -C <repo> remote -v
for r in origin fork; do
echo "=== $r ==="
gh api "repos/$(git remote get-url $r | sed -E 's#.*github.com[:/](.*?)/(.+)\.git#\1/\2#')" --jq '.permissions.push // .permissions.admin'
done
git fetch origin fork --quiet 2>/dev/null
DIVERGENCE=$(git -C <repo> rev-list --left-right --count origin/main...fork/main)
echo "origin/main vs fork/main: ${DIVERGENCE}"
git -C <repo> worktree add -b fix/<topic> /tmp/<repo>-wt <WRITABLE-FORK>/main
The patch-porting adjustment (when the upstream fix lives on origin/main but you must push to fork):
Upstream's tools/daemon_pool.py may not exist on fork/main (the fork refactored to tools/async_delegation.py). You cannot git apply the upstream patch verbatim — the file paths diverge. Two recipes:
git -C <repo> fetch origin <upstream-branch>
git -C <fork-wt> cherry-pick <upstream-sha>
git show origin/main:tools/daemon_pool.py > /tmp/upstream-pool.py
git show fork/main:tools/async_delegation.py | grep -n '_adjust_thread_count\|_DaemonThreadPoolExecutor'
Verified case 2026-07-24, jleechanorg/hermes-agent Python 3.14 _initializer fix:
- Branched from
origin/main (NousResearch upstream, read-only). Pushed to fork (jleechanorg, writable).
- Resulting PR #3: +600,146 / -93,707 / 3,930 files — the entire fork divergence appeared as "unrelated diff".
- Closed PR #3.
- Rebased on
fork/main, ported the patch from tools/daemon_pool.py (upstream) to tools/async_delegation.py::_DaemonThreadPoolExecutor (fork inlined the class).
- New PR #4: +25/-6 / 1 file (
be0a896ba on fix/daemon-pool-py314). Clean.
Diagnostic symptom: the PR shows >1000-line additions in dozens of unrelated files (configs, workflows, AGENTS.md, .dockerignore, etc.). That is not pollution from agent drift — it is wrong-base-branch pollution from a remote-ownership mistake. The recovery is rebase onto the writable remote's main, NOT Phase 1/3.5 cleanup-replay.
Cross-link: SOUL.md ## COMMIT: pr-clean-branch-from-main-no-history-bloat covers the principle; this Phase -0.5 is the operational recipe for the fork-vs-upstream variant.
Phase -1 — Prevention (don't push onto someone else's PR head in the first place)
The most common cause of a "polluted PR" is pushing new commits onto an existing
PR's head branch that the agent does not own. Before git push origin HEAD:refs/heads/<branch>:
gh pr view <N> --json headRefName,author | jq '.author.login + " owns " + .headRefName'
git -C <repo> diff --shortstat origin/main..HEAD
2026-07-14 incident class — happens twice in one day. Same root cause, two
repos, two open PRs:
- $GITHUB_REPOSITORY PR #8401 (head
feat/fix-xp-overflow-no-level-up-7931-full-brief-at-tmp-wa-task-i,
+670k/-24k baseline) → polluted with 22 commits → closed in favor of PR #8403.
- jleechanorg/claude-commands PR #321 (head
fix/real-claude-team-tmux,
+670k/-24k / 3001 files) → polluted with 2 commits (this skill's origin
incident) → reverted head back to 286311a97 via git push --force-with-lease,
clean replay at PR #329.
Both incidents have the same anti-pattern: the agent saw an open PR whose topic
overlapped the task, picked that PR's head branch as the push target, and never
asked "does this branch belong to me?". User feedback both times was the same
flavor of "this isn't a clean PR from origin/main, why do you keep screwing
this up?" — confirming the preventive gate belongs here, not buried in a
SOUL.md ## COMMIT: that future agents may not surface.
Phase 0 — Confirm the diagnosis
Run these checks BEFORE doing anything:
git diff --shortstat origin/main...HEAD
git log --oneline origin/main..HEAD
git diff origin/main...HEAD -- <load_bearing_path> | head -50
If any of these fails (diff too big, unrelated commits, change does not match the
stated bug), the PR is polluted. Proceed to Phase 1.
Phase 1 — Identify the load-bearing diff
Two strategies:
Strategy A — Cherry-pick individual commits. Use when the PR's history contains
3-5 distinct commits whose messages match the PR scope, plus noise (merge commits,
beads drift, CI fixes).
git log --oneline origin/main..HEAD --no-merges --grep='^fix' --grep='^feat' --grep='^chore(level-up)'
git worktree add -b fix/<topic>-replay /tmp/wt-<topic> origin/main
cd /tmp/wt-<topic>
git cherry-pick -x <sha1> <sha2> <sha3>
Strategy B — Extract the file diff directly. Use when the PR's history is
entirely noise (only merge commits + beads drift) but the file-level diff IS the fix.
git diff --name-only origin/main...HEAD
cd /tmp/wt-<topic>
git show origin/<polluted-branch> -- <file1> <file2> > /tmp/load_bearing.patch
git apply /tmp/load_bearing.patch
Strategy B Variant — Surgical 2-hunk patch apply when main has overlapping changes.
The naive git checkout origin/<branch> -- <file> is unsafe when origin/main
has accumulated edits in the same files since the PR was created — the
whole-file checkout silently reverts main's improvements (verified on PR #8139
replay → PR #8561, 2026-07-24: app.js checkout reverted 554 lines of
main-side route-handling / draft-persistence code that landed after 8139 was
first opened).
Use this recipe when git diff origin/main...HEAD -- <file> shows main-side
edits in the file:
git diff origin/main...HEAD -- <file> > /tmp/file.patch
wc -l /tmp/file.patch
cd /tmp/wt-<topic>
git apply --check /tmp/file.patch
diff <(git show origin/main:<file>) <(git show origin/<polluted-branch>:<file>) > /tmp/full-file-diff.txt
patch -p1 --dry-run < /tmp/load_bearing-only.patch
patch -p1 < /tmp/load_bearing-only.patch
git diff origin/main -- <file>
How to identify load-bearing hunks when the file is large:
- Search for the function/method name that defines the feature (
grep -n 'applyMobileScrollLock\|setupScrollIndicator\|teardownScrollIndicator' <file>
in the PR's version).
- The hunks that introduce/call these functions are the load-bearing ones.
- Hunks that REMOVE main-side code (e.g. deleting a route-handling block
added to main after 8139) are the dangerous ones — exclude them.
Symptom that flags this variant: after git checkout origin/<branch> -- <file>,
git diff origin/main -- <file> shows >100 lines of deletions that don't look like
the feature (often lastRouteFullPath, pendingStream, draftDebounceTimeout,
or other main-side state that the PR's snapshot predates). Revert the checkout
and use the surgical patch approach instead.
Phase 2 — Run tests in the fresh worktree
pytest <test_path> -v
Phase 3 — Commit + push + open new PR + close old PR
cd /tmp/wt-<topic>
git add -A
git commit -m '[<scope>] <one-line summary> (#<orig_issue>)' \
-m '<multi-line description matching the PR body scope>'
git push -u origin fix/<topic>-replay
~/.hermes/scripts/gh-safe-publish pr create --base main --head fix/<topic>-replay \
--title '<same title as the old PR>' \
--body '<updated body, link the old PR + issue, explain the cleanup>'
gh pr close <old_pr_number> --comment "Closing in favor of clean replay #<new_pr_number>. \
Original PR inadvertently pulled <N> unrelated commits (merge chains, beads drift, \
CI fixes). The clean replay contains only the load-bearing fix in <M> files."
Phase 3.5 — In-place force-push replay (same PR number, NOT close-and-reopen)
When the polluted branch was created by an AO worker you can correct (the agent is still alive or the branch is recoverable from origin/<branch>) AND the polluted PR is already open with the correct title/base/issue link, the right move is to force-push the clean replay onto the same branch so the existing PR's headRefOid updates in place. Do NOT close the old PR and open a new one.
Why in-place beats close-and-reopen:
- Preserves the PR number (issue auto-close link stays intact:
Closes #<N>).
- Preserves any reviewer comments, CodeRabbit review state, and cursor[bot] bugbot history.
- Preserves the headRefName — anyone watching the branch via
gh pr list --head <branch> keeps seeing it.
- Avoids the duplicate-PR risk (
always-pr-never-local-edit v1.1.0 / v1.2.0 PR-topology pre-flight).
Recipe:
BAD_SHA=$(git -C <repo> rev-parse origin/<branch>)
git -C <repo> show --name-only "$BAD_SHA"
git -C <repo> worktree add -B <branch> /tmp/wt-<topic>-replay origin/main
cd /tmp/wt-<topic>-replay
git -C <repo> show "$BAD_SHA" -- <file1> <file2> ... | \
git apply --include='<file1>' --include='<file2>' ...
git -C <repo>/.worktrees/wt-<topic>-replay diff --name-only origin/main..HEAD
cd /tmp/wt-<topic>-replay
git add <intended-files-only>
git commit -m "<scope>: <one-line summary>" \
-m "Clean replay from origin/main." \
-m "<multi-line description matching the polluted PR's body scope>"
git push --force-with-lease origin <branch>
git -C <repo> rev-parse origin/<branch>
gh pr view <N> --json headRefOid,files --jq '{headRefOid, files:[.files[].path]}'
cd $HOME/projects/<main>
git status --short
Why --force-with-lease and not --force: force-with-lease refuses to overwrite if the remote moved between your fetch and your push. This protects against a sibling worker (AO, babysit cron, drive loop) pushing their own resolution in the meantime — same race-with-AO-worker guard as Phase 5.5.
When to escalate to close-and-reopen instead: if gh pr view <N> --json files after force-push shows ANY drift file persisting (rare — would mean your git apply --include whitelist didn't actually filter), close-and-reopen is the safer escape hatch. But verify the apply first; close-and-reopen is the LAST resort, not the default.
Verified case 2026-07-23, $GITHUB_REPOSITORY PR #8541 (feat/read-tmp-god-mode-generic-repro-20260723-ao-ta[REDACTED_OPENAI_KEY]): AO worker wa-3389 (MiniMax-M3 mid-tier) shipped a polluted commit 0c5a2b6a64 containing 9 files / +706 / −424 with 5 drift files ($PROJECT_ROOT/bq_logging.py +21, $PROJECT_ROOT/world_logic.py +92, $PROJECT_ROOT/tests/test_godmode_directive_lifecycle_events.py −332, roadmap/README.md −1, roadmap/activity/2026-07-23.md −33) alongside the 4 intended files. Force-pushed replay commit 0b0bc4ac73521c14b402d0c5dd1211730479a469 contained only the 4 intended files (+651 / −0). Same PR #8541, same branch, same issue-link, no close-and-reopen. PR diff after replay:
$PROJECT_ROOT/agent_prompts.py
$PROJECT_ROOT/prompts/god_mode_instruction.md
$PROJECT_ROOT/tests/test_god_mode_formula_registry_contract.py
testing_mcp/test_god_mode_avatar_partition_contract_real_api.py
The 5 drift files disappeared from gh pr view 8541 --json files immediately after the force-push. No new PR number needed; issue #8538's Closes link in the body stayed intact.
Phase 5.5 — Resolve merge conflicts against fast-moving main (kept-history variant)
Distinct from cleanup-replay: the PR's history is intentional (5+ prior merge commits, each absorbing main at its current state), and you want to preserve that history while absorbing the next round of main. This is the "merge-main into feature branch" recipe used when the PR is already 80+ commits behind main and you cannot replay.
When to use this phase (not Phase 1–4 cleanup, not Phase 5 supersede):
- The PR has 3+ commits of substance that should NOT be replayed
- The PR's prior
Merge remote-tracking branch 'origin/main' into <branch> commits are intentional (CI-retrigger commits, follow-up fixes, evidence-refresh commits)
- The PR is currently
mergeable: CONFLICTING, mergeStateStatus: DIRTY against current origin/main
- Merging main into the feature branch produces 1-5 conflicts in different files
- You CAN push to the PR's branch (you are the PR author)
Recipe:
gh pr view <N> --repo <OWNER>/<REPO> --json author --jq .author.login
git worktree add -B <branch>-merge /tmp/<repo>-merge origin/main
cd /tmp/<repo>-merge
git merge --no-ff origin/<branch> --no-edit
git diff --name-only --diff-filter=U
git add <resolved-files>
git commit -m "Merge origin/main into <branch> (resolve PR #<N> conflict)
Resolved in <files>:
- <file1>: <one-line description of what was kept/added>
- <file2>: <one-line description>
Refs: PR #<N> / mergeStateStatus: DIRTY → CLEAN"
git push origin HEAD:refs/heads/<branch> --force-with-lease
gh pr view <N> --json mergeable,mergeStateStatus,headRefOid
Three reusable conflict-resolution patterns:
Pattern A — Info-comment collision in workflow files (.github/workflows/*.yml). When both sides added informational comments about historical PR activity, keep the most recent main-side comment and append a new comment about this merge's specific delta. Bump any line-count / version ratchet values in the workflow YAML to absorb the merge's net file growth.
Pattern B — Prompt contract hash conflict in $PROJECT_ROOT/schemas/prompt_tool_contracts.json. Both sides modified the same .md prompt, so each side's version field is the sha256[:12] of their respective prompt bytes. The auto-merged .md file has a third, NEW sha256 (the combination). Resolution: take the auto-merged file's actual sha256.
NEW_SHA=$(sha256sum $PROJECT_ROOT/prompts/<name>.md | awk '{print $1}')
NEW_VERSION="${NEW_SHA:0:12}"
Pattern C — Helper-function vs inline structural merge in $PROJECT_ROOT/llm_service.py (or any module where main uses inline and the PR refactored to helpers). Keep the PR's helper-based structure (the helpers exist in the PR's tree above the conflict). Add any main-side behavioral changes (e.g. a core_memories strip that landed on main after the PR was created) to the PR's helper function. This combines both sides' value.
Anti-patterns (verified wasted 30+ minutes):
- Picking one side verbatim and losing the other side's behavior. The PR's tree and main's tree each have unique value; structural-vs-inline conflicts require preserving both.
- Re-running
/es capture to "fix" an Evidence Gate freshness failure caused by the merge. The freshness check is SHA-based, not behavioral. New merged HEAD = new required capture. The fix is structural (re-capture, label (historical) in PR body, or accept).
Mandatory post-push verify loop (merge-conflict treadmill, verified PR #8292): When the PR is 80+ commits behind origin/main, every successful merge origin/main resolution re-dirties within minutes — origin/main keeps moving while CI runs. After pushing your merge commit, do NOT assume victory:
for i in 1 2 3 4 5 6 7 8 9 10; do
STATE=$(gh pr view <N> --json mergeStateStatus --jq .mergeStateStatus)
echo "tick $i: mergeStateStatus=$STATE"
[ "$STATE" = "CLEAN" ] && { echo "DONE"; break; }
[ "$STATE" = "DIRTY" ] && { echo "RE-DIRTIED — re-merge against new main"; break; }
sleep 30
done
If mergeStateStatus flips back to DIRTY during your CI wait, re-merge against the new main HEAD (Phase 5.5 round N+1). The treadmill continues until either (a) your PR merges, (b) main freezes for >1 hour, or (c) you stop and surface the structural problem to the user.
Race-with-AO-worker guard (verified PR #8292): While your CI runs, an automated AO worker / babysit cron / drive loop using the SAME credentials may push their own conflict-resolution commit. ALWAYS check the remote tip before pushing:
LOCAL_HEAD=$(git -C <worktree> rev-parse HEAD)
REMOTE_HEAD=$(git ls-remote origin <branch> | awk '{print $1}')
if [ "$LOCAL_HEAD" = "$REMOTE_HEAD" ]; then
echo "REMOTE ALREADY HAS YOUR WORK — skip the push"
exit 0
fi
if git merge-base --is-ancestor "$REMOTE_HEAD" "$LOCAL_HEAD"; then
echo "Safe to push (your commit is ahead of remote)"
git push origin HEAD:refs/heads/<branch> --force-with-lease
else
echo "DIVERGENCE — remote is NOT your ancestor"
echo "Common case: AO worker pushed an equivalent resolution. Inspect:"
git diff "$LOCAL_HEAD" origin/<branch> --stat
echo "If equivalent or better: let it stand, do NOT push your own"
fi
Verified PR #8292 (2026-07-23): PR feat/provenance-narrow had 132 commits (5 prior merge commits). During my 8-minute CI wait, an AO worker using jleechan2015 credentials pushed dc5fb6381652 with the identical conflict resolution strategy (helper-function refactor + core_memories strip + auto-merged hash). I discovered the race by git ls-remote origin feat/provenance-narrow BEFORE pushing my own. Letting AO's commit stand preserved the durable state and avoided a non-fast-forward force-with-lease.
See also:
references/rebase-fork-divergent-patch-anchor-2026-07-21.md in drive-pr-to-green for the rebase variant of this technique
references/merge-conflict-treadmill-2026-07-23.md in drive-pr-to-green for the post-push verify loop and the Evidence Gate freshness vs post-merge-main scope detail
- SOUL.md
push-pr-donot-stop-halfway — durable state on the remote PR branch IS the deliverable
- SOUL.md
never-push-onto-someone-elses-pr-head — verify PR author before any push
god-mode-generic-mechanic-handoff §"Branching & clean-replay contract" — this skill is the umbrella Phase 3.5 in-place force-push recipe applied during the PR #8541 cycle (2026-07-23). The god-mode skill points here for the agent-drift recovery pattern.
Phase 6 — Verify
gh pr view <new_pr_number> --json additions,deletions,changedFiles
gh pr view <old_pr_number> --json state
git branch --list 'fix/<topic>*' | head -5
Pitfalls
-
Dev-server port-race diagnostic (added 2026-07-24, PR #8561): When the
evidence-capture flow boots a local Flask/dev server on the canonical port
(8081 for your-project.com), check FIRST whether another worktree's stale
server is already bound. The trap is silent: the port returns HTTP 200
serving a different branch's JS, the capture script screenshots the wrong
DOM, and the BEFORE/AFTER is comparing against pre-feature code without the
reviewer ever knowing. The capture script ran fine, the screenshots were
generated, the GIF was rendered — every layer passed — but the
applyMobileScrollLock + setupScrollIndicator calls were never invoked
because the served JS was 5 lines (404 page) instead of 2700 lines.
lsof -nP -iTCP:8081 -sTCP:LISTEN
curl -s -m 5 "http://localhost:8081/frontend_v1/js/campaign-wizard.js" \
| grep -cE "isMobileViewport|applyMobileScrollLock|wizard-scroll-indicator|setupScrollIndicator"
curl -s -m 5 "http://localhost:8081/frontend_v1/js/campaign-wizard.js" | wc -l
Force-kill the stale listener if found: kill <pid> && sleep 1 && lsof -nP -iTCP:8081 -sTCP:LISTEN to confirm the port is free. Then start your
own server with nohup python3.11 -m mvp_site.main serve > /tmp/server.log 2>&1 < /dev/null &.
Also verify the static path: the page HTML's <script src=> tags
point to /frontend_v1/js/... (NOT /static/js/...). A naive curl to
/static/js/campaign-wizard.js returns 404 even when the server is
serving the right code on the right path.
-
Agent-drift detection (added 2026-07-23, PR #8541): when an AO worker commits
the PR, run BEFORE merging. If the diff
includes files that DON'T match the PR title (e.g. ,
, deleted unrelated tests, , ),
the worker bundled its own file edits into the commit. Do NOT merge. Apply Phase 3.5
in-place force-push replay. Pre-push gate recipe:
Phase 5 — Supersede + Stack (the V1+V2 pattern, distinct from cleanup-replay)
This phase covers a different workflow than pollution cleanup: closing a PR
because the work was logically superseded by a sibling PR on the same topic, then
opening a new PR that stacks cleanly on the prior layer's branch base so the
diff is minimal and reviewable.
When to use this phase (not Phase 1–4):
- PR X carries Layer 1 (e.g. V1 spec, system-agnostic reference doc).
- PR Y (your PR) carries Layer 2 (e.g. V2 overlay on top of V1).
- PR X and PR Y both modify the same files, so PR Y's diff vs
main shows both
layers — reviewable but not minimal.
- The user wants a single self-contained PR with both layers stacked cleanly.
Recipe:
TOKEN=$(gh auth token)
curl -fsS -X POST -H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
-d '{"body": "Closing as superseded. Layer 2 will land as an incremental
improvement on top of <PR X URL>. New PR opens with both layers stacked
cleanly on <PR X branch>."}' \
"https://api.github.com/repos/<owner>/<repo>/issues/<Y>/comments"
curl -fsS -X PATCH -H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
-d '{"state": "closed"}' \
"https://api.github.com/repos/<owner>/<repo>/pulls/<Y>"
cd <repo>
git fetch origin 'refs/heads/<X-branch>:refs/remotes/origin/<X-branch>'
git worktree add -b <Y-plus-branch> ~/.worktrees/<X-Y-stacked> origin/<X-branch>
cp ~/.worktrees/<Y-worktree>/<file1> ~/.worktrees/<X-Y-stacked>/<file1>
cp ~/.worktrees/<Y-worktree>/<file2> ~/.worktrees/<X-Y-stacked>/<file2>
cd ~/.worktrees/<X-Y-stacked>
git add -A
git commit -m "<Y-commit-message>"
git push -u origin <Y-plus-branch>
curl -fsS -X POST -H \
-H \
-d \
Why base=main (NOT base=): PR reviewers need to see the combined
V1+V2 diff against the canonical target (main). Branching the PR from
would show ONLY the V2 delta — but the user wants one self-contained
PR for review. The trade-off: the diff is bigger, but the reviewer can verify
the full stack in one read.
Why the explicit refs/heads/<X>:refs/remotes/origin/<X> refspec: When
the prior PR's branch is local-only or just-pushed, git fetch origin may NOT
auto-create the remote-tracking ref. Without it, git worktree add -b X origin/X
fails with "fatal: not a valid object name: 'origin/X'". The explicit refspec
guarantees the ref is materialized locally.
Verified 2026-07-21, $GITHUB_REPOSITORY: PR #8487
(feat/nocturne-v2-spec, +503/-0) closed as superseded → PR #8488 opened
(feat/god-mechanics-v2, +974/-0, base=main) stacking V1 PR #8484 + V2
overlay cleanly. The combined PR contains both god_mechanics_general.md (V1)
nocturne-v2-god-mechanics-design.md (V2) + Section 9 in
campaign_module_god_of_murder.md — fully self-contained for review.
Pre-push hook blind spots — TWO distinct bugs, both silent
A polluted PR is sometimes caused not by your merge strategy but by your pre-push
hook MISSING a class of leak. Both bugs produce silent failures that let forbidden
content reach origin:
-
Gitleaks scans the wrong range on new branches — see
references/gitleaks-pre-push-hook-bypass.md. Hook falls back to
rev-list --max-parents=0 HEAD when remote_sha is all-zeroes, scanning the
entire repo history. The 4259 leaks observed on 2026-07-14 (claude-commands
PR #329) were all from pre-existing commits, not the new one. Symptom: push
blocked, but gitleaks git --log-opts 'origin/main..HEAD' from inside the
worktree is clean. Fix: git -c core.hooksPath= push for that one push, OR
patch the hook to use git merge-base origin/HEAD local_sha.
-
Gitleaks doesn't scan path prefixes at all — see
backup-folder-leak-purge skill. Hook scans for secret patterns
(apiKey=, token:, etc.) but never checks path prefixes like backup/,
snapshot/, secrets/. A backup/ folder containing your entire ~/ passes
gitleaks cleanly because no secret regex matches. Verified 2026-07-15 against
jleechanorg/claude-commands: 491 MiB / 6,820 files pushed to public repo before
anyone noticed. Fix: add a path-prefix check to your pre-push hook that
runs BEFORE the secret scan — they belong together, not as alternatives.
-
Pre-push hook stdin field-order trap — see
backup-folder-leak-purge/references/git-hooks-pre-push-stdin-format.md.
Per git/githooks.adoc: stdin is <local-ref> SP <local-sha> SP <remote-ref> SP <remote-sha>.
Wrong order (read -r local_sha local_ref remote_sha _) makes the hook
silently pass — the comparison [[ $local_ref == refs/heads/main ]] evaluates
a SHA against a ref string and never matches. Run the 3-test harness in
backup-folder-leak-purge/scripts/verify-hook-blocks-backup-push.sh on every
new pre-push hook BEFORE declaring it done.
Worked example — 2026-07-14 Visenya V8 stuck-lu
Polluted PR: #8401
— 22 commits, 31 files, +1413/-629. Branched from origin/feat/fix-xp-overflow-no-level-up-7931-full-brief-at-tmp-wa-task-i
which itself had been rebased 8+ times.
Diagnosis: git log --oneline origin/main..origin/fix/visenya-v8-stuck-lu-8400
showed 19 of 22 commits were noise (Merge remote-tracking branch,
fix(beads): resync issues.jsonl, [fixpr jleechan2015-automation-commit],
Merge branch 'main' into feat/...). The load-bearing commits were only 3:
21ccc605e2 fix(level-up): preserve rewards_pending.level_up_available in streaming path
8bd3b9894d fix(level-up): gate streaming path threshold preservation on not level_up_complete
93cc946509 fix(level-up): drop unpaired streaming level-up rewards
Clean replay PR: #8403
— 1 commit, 2 files, +597/-20. Cherry-picked the 3 load-bearing commits, resolved
the conflicts in $PROJECT_ROOT/world_logic.py (different from PR #7952's branch's
state), added a 4th surgical change for the level_up_complete guard, ported
test_xp_overflow_level_up_ceremony.py, added TestCustomLevelCapXpOverflow
with Visenya V8 fixtures.
Result: PR #8401 closed; PR #8403 ready for CI.
Decision tree — Strategy A vs B
Use this to decide which strategy to apply before opening a worktree:
Polluted PR has:
├── 3-5 distinct commits whose messages match the PR scope (plus noise)
│ └── Strategy A (cherry-pick)
├── Entirely noise (only merge commits + beads drift) but file-level diff IS the fix
│ └── Strategy B (extract file diff)
└── Cherry-pick conflicts are structural (whole-file mismatches)
└── PIVOT to Strategy B (this is the 2026-07-14 Visenya V8 case)
Why the pivot works: Strategy A fails when the target branch was rebased and the
load-bearing commit was already partially applied (the diff context is stale). Strategy B
extracts the file content directly, which is independent of commit ancestry. Both produce
the same end-state; the difference is whether commit messages are preserved (A) or not (B).
Trigger phrases
- "clean up this PR"
- "replay this PR"
- "minimal diff replay"
- "fix PR scope"
- "branch is bloated"
- "this PR has too many commits"
- "rewrite this PR cleanly"
- "polluted PR"
- "PR has unrelated history"
- "branch from origin main"
- "pre-push hook not blocking"
- "gitleaks didn't catch this"
- "backup folder in public repo"
- "home dir leaked to github"
Phase 3.7 — Value retune of an open PR (NOT pollution, NOT supersede)
Distinct from cleanup-replay (pollution) and Phase 5 (supersede+stack): the user
explicitly wants a single value in an existing PR changed (constant flap,
threshold flip, percentage change, etc.) WITHOUT polluting the original PR's
branch with unrelated drift that has accumulated since it was opened.
Symptoms that indicate this pattern:
- The user names a PR by number and gives a one-line override of one of its
constants: "cap to 350k instead of 450k", "raise threshold from 5 to 7",
"change TTL from 30d to 60d".
- The PR's head branch is not yours (different author, or owned by an automation
cron, or was rebased) —
never-push-onto-someone-elses-pr-head blocks
Phase 3.5 in-place force-push.
- The PR's head branch has unrelated drift (worktree-script cleanup, CI workflow
edits, .claude/settings.json) since its original creation — Phase 3.5 in-place
replay would commit the drift alongside the retune.
- The retune is small enough (1 constant flap + 1 test class) that opening a new
PR is clearer than replaying onto the old one.
Recipe (verified 2026-07-23, $GITHUB_REPOSITORY PR #8537 450K → 350K
retune, branch fix/8537-cap-350k):
gh api /repos/<owner>/<repo>/pulls/<N> | python3 -m json.tool | head -200
git -C <repo> fetch origin
git -C <repo> worktree add -b fix/<PR>-<new-value> /tmp/wt-<PR>-<new-value> origin/main
cd /tmp/wt-<PR>-<new-value>
git -C /tmp/wt-<PR>-<new-value> diff --shortstat origin/main
git -C /tmp/wt-<PR>-<new-value> log --oneline origin/main..HEAD
git -C /tmp/wt-<PR>-<new-value> add <surgical files>
git -C /tmp/wt-<PR>-<new-value> commit -m "fix(<scope>): <retune description> (clean replay of PR #<N>)"
git -C /tmp/wt-<PR>-<new-value> push -u origin fix/<PR>-<new-value>
Why push without auto-opening the PR: A "value retune" is exactly the kind
of fork that the user wants to inspect before it becomes a PR. The existing
PR is the canonical artifact until the user says otherwise; pushing a new
branch is cheap, opening a new PR creates reviewer noise and (per SOUL.md
scope-pivot-to-ao) may cross the write/PR threshold the user did not
authorize. Push the branch, surface it, wait for the user.
Difference from Phase 3.5 in-place force-push: Phase 3.5 force-pushes
the clean replay onto the SAME PR's branch (same PR number preserved).
Phase 3.7 opens a NEW branch because (a) the existing branch is not yours
to push onto, OR (b) the existing branch has unrelated drift you'd carry
into the replay, OR (c) the user has not yet authorized a new PR.
Difference from Phase 5 supersede+stack: Phase 5 closes PR Y as
superseded by PR X (both layers stack cleanly in one self-contained PR).
Phase 3.7 does NOT close the original PR — the user may still want the
original 450K version, or may merge the 350K replay only after a comparison
review.
Verified 2026-07-23, PR #8537 (450K → 350K retune). User's mid-pipeline
"Hold on this I'll decide later" landed AFTER I'd already pushed the
clean-replay branch fix/8537-cap-350k (1 commit, +103/-1 across
$PROJECT_ROOT/llm_service.py constant + $PROJECT_ROOT/tests/test_context_budgeting.py
3-test regression suite). No PR was opened. The branch sat pushed waiting
for the user's verdict on whether 350K is the right cap, or whether to
revert to 450K, or to keep both as parameterizable. Without the
"pause-before-open" rule, I would have auto-opened PR #8548 (or similar)
and forced the user to choose between closing it or merging it
out-of-band — a clear SOUL.md no-confirmation-gate and scope-pivot-to-ao
boundary violation.
Phase 3.8 — Stale-target replay check (origin/main advanced during the worker run)
Distinct from Phase 3.7 (value retune) and Phase 5 (supersede+stack): the replay target value
the user originally chose may have become stale because origin/main advanced between
the dispatch and the worker's push. The replay branch is then technically
clean-from-origin/main-at-dispatch-time, but the target value is already BELOW (or above) the
current main value — the replay is a no-op or a regression.
Symptoms that indicate this pattern:
- The replay branch is clean (2 files, < 500 lines, single commit), all CI green,
mergeable=CONFLICTING (mergeStateStatus: dirty).
- The PR's diff is
300_000 → 350_000 in $PROJECT_ROOT/llm_service.py, but origin/main HEAD already has 400_000 in that exact line (because a sibling PR merged between dispatch and now).
- The
git log origin/main -- <target-file> shows commits between the dispatch base and current HEAD that touched the same constant.
- The worker's branch base SHA is older than the most recent main commit (verify with
git fetch origin then git log --oneline origin/main -5 and compare to the branch's first-parent merge-base).
Why this matters: The replay is technically clean from origin/main at the worker's
branch base, but the value retune made no semantic progress relative to current main.
Merging it would revert a sibling PR's mainline change. The clean-from-main audit
(git diff --shortstat origin/main..HEAD) doesn't catch this because the branch's base
IS origin/main — the diff is just the worker's change. The stale-target check is a
semantic check on the diff's expected post-merge state, not a structural diff audit.
Recipe (verified 2026-07-24, PR #8537 350K replay vs main #8555 400K):
cd /tmp/<replay-wt>
REPLAY_VALUE=$(grep -E 'DEFAULT_COMPACTION_TOKEN_LIMIT\s*=\s*[0-9_]+' $PROJECT_ROOT/llm_service.py)
echo "replay: $REPLAY_VALUE"
git fetch origin main
git show origin/main:$PROJECT_ROOT/llm_service.py | grep -E 'DEFAULT_COMPACTION_TOKEN_LIMIT\s*=\s*[0-9_]+'
git diff origin/main HEAD -- $PROJECT_ROOT/tests/<test_file>.py | grep -E "test.*[0-9]{3,}_000|cap_to_[0-9]+"
If the replay target is stale:
-
Surface in the dispatcher's status reply — the user originally chose the replay target
to override the original PR's value. If the original PR's value is no longer on main
(it was reverted to a different value by a sibling PR), the user's override premise has
changed. Ask the user: "Replay target (350K) is now BELOW current main (400K — PR #8555
merged between dispatch and now). Did you still want to override to 350K, or do you want
to retarget the replay to 400K (current main, no-op change)?"
-
Do NOT auto-rebase + retarget — silently changing the replay target is a SOUL.md
no-confirmation-gate violation. The user explicitly chose the target value; the
fact that main moved is a SIGNAL to ask, not a license to change it.
-
Do NOT auto-close the replay PR — even if the replay is now a regression, the
user may still want to merge it (e.g., to undo the sibling PR's change). Close only
on explicit user direction.
Three durable fixes the dispatcher should consider for FUTURE replays:
-
Pre-dispatch target-vs-main check — at dispatch time, before spawning the worker,
run git show origin/main:<file> | grep <target-value-or-pattern> and confirm the
replay target is still meaningfully different from current main. If main already has
the replay target, abort the dispatch and tell the user "replay target already on main,
no work needed."
-
Mid-run main-advance check — for long-running workers (>30 min), add a Phase 0.5
pre-flight at the worker's commit-and-push time: git fetch origin main && git diff --shortstat origin/main..HEAD AND git log origin/main -- <target-file> to confirm
the target value hasn't been touched by a sibling PR. If it has, post a status update
in the originating thread and ask the user before pushing.
-
Sibling-PR-time-window-narrowed query — if the user knows the dispatch's time
window, narrow the sibling-PR check to gh pr list --state merged --merged <dispatch-time>..NOW --search "<constant-name>". This avoids the false-positive of
"PR #8555 merged 400K last week" when the dispatch was yesterday.
Verified case 2026-07-24, $GITHUB_REPOSITORY PR #8537 350K replay:
- User asked for 350K cap override of PR #8537's 450K value (2026-07-23, thread
C0AH3RY3DK6/1784866738.297949, "I said a million times to get this to 350k max tokens
get it done").
- PR #8555 ("reduce DEFAULT_COMPACTION_TOKEN_LIMIT 450k → 400k") merged to origin/main
at
c5b759d974 between the user's message and the dispatch spawn.
- AO worker
worldarchitect-121 spawned at 2026-07-24T05:52Z, branched from origin/main
at 5d14bb1013 (PR #8537 merge SHA), created PR #8556 with 350K retune. Branch is
clean (2 files, +37/-120), all 22 CI checks PASS, CodeRabbit APPROVED, Bugbot NEUTRAL.
- However,
mergeable=CONFLICTING because origin/main advanced to c5b759d974 after
the worker branched. Same file ($PROJECT_ROOT/llm_service.py) and same test file
($PROJECT_ROOT/tests/test_context_budgeting.py) modified by PR #8555.
- Replay target value (350K) is now BELOW current main (400K). The replay is a
technical cleanup but a semantic regression if merged.
- The user's override premise (350K vs 450K) is no longer the right comparison; the
right question is now "350K vs 400K" and the user has not been asked.
Why this is a §"Stale-target" pitfall and not a Phase 3.5 (pollution) case:
Phase 3.5 is for when the PR carries unrelated diff (drift files, merge commits,
beads.jsonl). The replay PR is clean — the only issue is the target value is wrong
relative to current main. The fix is NOT a force-push replay; the fix is a value
re-decision by the user, followed by a rebase + retune or a close-and-reopen.
Why this is a §"Stale-target" pitfall and not a Phase 5 (supersede+stack) case:
Phase 5 is for when the user's PR is logically superseded by a sibling PR (e.g., the
sibling PR is the canonical artifact). In this case, the user's replay PR is a
rejection of the original PR's value, not a sibling of any subsequent PR. The
sibling PR (#8555) is itself a downgrade of #8537, and the user's 350K is a further
downgrade. The user may want to (a) merge 350K anyway and downgrade #8555's 400K, or
(b) abandon 350K and accept 400K, or (c) re-raise to 500K. The three options
fundamentally diverge on user intent — auto-deciding is wrong.
Cross-links:
references/stale-replay-target-detection-2026-07-24.md — the full evidence file:
three sibling PRs (#8537 450K, #8555 400K, #8556 350K-replay), the dispatch time
window, the mergeable_state: dirty even-when-CI-green signal, and the three-option
decision matrix. (Authored after skill update.)
babysit-ao-pr-loop §"Phase 1 — Observe" — the babysit should treat mergeable_state: dirty + check-rollup.status: success as a stale-target signal, not a CI failure.
Post the three-option question to the originating thread; do NOT auto-nudge the worker.
dispatch-task §"Pre-compute target-vs-main check" — the durable fix at dispatch time.
Changelog
- 1.7.0 (2026-07-24): Add Phase -0.5 — Writable-remote base selection. The fork-vs-upstream
mistake: branching from
origin/main (read-only upstream) and pushing to fork
(writable fork) creates a 600k-line "polluted" PR because the fork is 6,000+ commits
ahead. Recipe: gh api repos/<path>/permissions.push per remote, git rev-list --left-right --count origin/main...fork/main to measure divergence, base on the
WRITABLE remote's main. When upstream's file structure diverged from the fork's
(e.g. tools/daemon_pool.py upstream → inlined tools/async_delegation.py
in fork), port the patch conceptually to the fork's symbol location via
git grep -n 'class.*DaemonThreadPoolExecutor\|def _adjust_thread_count' then
patch / write_file instead of git apply. Verified PR #3 → PR #4 cleanup
on jleechanorg/hermes-agent (+600,146/-93,707/3,930 files polluted → +25/-6/1
file clean). Diagnostic symptom: PR shows >1000-line additions in dozens of
unrelated files (configs, workflows, AGENTS.md) — that is wrong-base-branch
pollution, NOT agent drift; recovery is rebase-on-fork-main, NOT Phase 1/3.5
cleanup-replay. Reference: references/fork-vs-upstream-writable-base-2026-07-24.md.
- 1.6.0 (2026-07-24): Add Phase 1 Strategy B Variant — Surgical 2-hunk
patch apply
when origin/main has accumulated overlapping edits in the load-bearing files. The
naive git checkout origin/<branch> -- <file> silently reverts main-side improvements
(verified PR #8139 → PR #8561, 2026-07-24: app.js checkout reverted 554 lines of
main-side route-handling/draft-persistence code that landed after 8139 was first opened).
Recipe: compute git diff origin/main...HEAD -- <file>, identify hunks that match the
load-bearing function calls (e.g. applyMobileScrollLock, setupScrollIndicator),
exclude hunks that revert main-side state, write a custom patch file, apply with
patch -p1. Also add Pitfalls: dev-server port-race diagnostic — verify served JS
contains expected functions (e.g. grep -cE 'applyMobileScrollLock|setupScrollIndicator')
and reasonable line count (>2000 for your-project.com's wizard module), NOT just
HTTP 200. The trap is silent: a stale server on port 8081 from another worktree serves
5-line 404 pages that pass the capture script's every layer, generating screenshots
against the wrong code. New reference references/strategy-b-surgical-patch-port-race-2026-07-24.md
captures the full transcript.
Related
- SOUL.md
## COMMIT: pr-clean-branch-from-main-no-history-bloat (the trigger-based
- SOUL.md
## COMMIT: never-push-onto-someone-elses-pr-head (Phase -1 prevention)
- SOUL.md
## COMMIT: push-pr-donot-stop-halfway (durable state on the remote PR branch)
dispatch-task Phase 0.5 PR-topology pre-flight — should grow a sibling
"writable-remote check" gate: dispatchers should verify gh api repos/<path>/permissions.push per remote AND git rev-list --left-right --count origin/main...fork/main before git worktree add -b fix/x. The
fork-vs-upstream pollution case (PR #3 → PR #4) shows the gap.
github-pr-workflow — base-branch / fork-remotes interaction (the upstream
skill on PR lifecycle; this skill operationalizes the "wrong base remote" recovery)
apply-supplied-patch-and-open-pr — for the case where the patch is
generated against one remote and must be applied to another (Phase -0.5's
"port upstream patch to fork" recipe overlaps with this skill's path-rewrite
recipe)
harness-postmortem Phase 1.5c (parent meta-skill — classifies this as
rule that fires this skill)
.cursor/rules/pr-branch-from-main.mdc (the project-level rule this skill
operationalizes)
harness-postmortem Phase 1.5c (parent meta-skill — classifies this as
ta[REDACTED_OPENAI_KEY] + wrong-tool-discussed, MAST FC1+FC3,
ETCLOVG Tool+Verification)
harness-postmortem/references/polluted-pr-cleanup-replay.md (the detection
recipe + pre-push audit checklist)
backup-folder-leak-purge (sister skill — the path-prefix leak variant that
caused 2026-07-15's 491 MiB public-repo incident; this skill covers the
gitleaks-side, that one covers the path-prefix side)
tests/test_pr_clean_branch_contract.py (5 contract tests verifying the rule