--- name: merge-update-pr description: "Rebase PR branch on main: merge --no-ff, resolve conflicts intelligently, lint-after-rebase gate, push only if merge commit created. Triggers: update pr, merge main into, /update-pr." --- # merge-update-pr ## Host execution **Claude Code:** retain the agent-dispatch and concurrency behavior defined below. **Codex:** use only the inline behavior stated here. On Codex this sub-skill runs inline under `agile-11-merge-train` with `concurrency=0`; never spawn or assume a named agent. Perform its full gate and return its normal receipt to the caller. ## Purpose Bring a PR branch up to date with main and push. Resolve conflicts by understanding both sides — never blindly take one. **Input:** PR number or branch name from args; ask if neither is given. ## Steps 1. `git fetch origin main` — **refresh the base without checking it out** (step 3 merges `origin/main`). A `git checkout main` here mutates the shared checkout under whatever sibling work is in flight, and fails outright when a worktree already holds the base. 2. Get on the branch: with a PR number prefer `gh pr checkout ` (handles fetch + tracking); with a branch name, `git fetch origin && git checkout && git pull origin `. - **A worktree may already hold this branch.** `agile-10-implement` leaves one per unmerged ticket, so in a drain the branch you are about to check out is often checked out elsewhere and the command fails with `fatal: '' is already used by worktree at `. Do not work around that with `--force` or a detached HEAD: `git worktree list` names the path, so **work there instead, by absolute path** (`cd `, `git -C `) and run the remaining steps in it. Rebasing the branch where it actually lives also means a later rework by the build agent sees your merge commit instead of diverging from it. 3. `git merge --no-ff origin/main -m "chore: merge main into "` — the `-m` is consumed only if a merge commit is actually created. **Detect the outcome from stdout, not the exit code** (an auto-resolved merge also exits 0): `Already up to date.` vs `Merge made by the 'ort' strategy.` vs conflict markers. 4. **On conflicts:** `git diff --name-only --diff-filter=U`, then for each file read it, understand both sides, and write the semantically correct merge. **Never `git checkout --ours` / `--theirs` without reading both sides** — taking one side wholesale to make the conflict go away silently drops the other side's change. - Documentation tables (coverage tables, run-command sections) and append-only structures (dict/list literals, include lists, beat schedules, route tables): **keep every entry from both sides**, chronological order — earlier ticket first, matching merge order on `main`. - Genuinely ambiguous conflict → explain both sides and ask before resolving. - Then `git add ` and **`GIT_EDITOR=true git merge --continue`** — `git merge --continue` does not accept `--no-edit` and will hang on the editor in a non-interactive environment without it. 5. **Lint-after-rebase gate — mandatory whenever a merge commit was created**, including when conflicts auto-resolved cleanly: a textual merge leaves artifacts linters reject (a formatter collapsing a multi-line ternary, a duplicated import block). Run the linters/formatters covering the touched paths; the commands live in the consumer repo's `CLAUDE.md` / `AGENTS.md`. - **A hand-resolved conflict needs the tests too, not just the linters.** Lint cannot see a resolution that dropped one side's contribution from an expression — the result is clean and silently wrong. Run the touched tiers' tests on the MERGED tree before pushing. Where both sides edited one expression in OPPOSITE directions (one adding a term, one removing another), the correct result matches neither side's version: it carries both contributions. - **Run any lint the PR itself introduces, against the rebased tree.** Find it with `git diff main HEAD -- scripts/lint_*.py .github/workflows/*.yml`. Without this, a PR introducing a new lint passes its own pre-push CI on the original tree and then fails the first commit to `main` after merge — the rebase pulls in sibling-PR files the original sweep never touched. - If a formatter modified files, `git add` them and `GIT_EDITOR=true git commit --amend --no-edit` (the merge commit is unpushed, so amending is safe), then re-run to confirm clean. 6. `git push origin ` — **skipped entirely on a no-op merge.** ## No-op merge (branch already on top of main) When step 3 prints `Already up to date.`: - **Do not push** — there is nothing new, and a push may falsely trigger CI re-runs. - **Do not force an empty merge commit to "trigger CI".** The branch tree already matches what will land, so the existing run on branch HEAD is the canonical result. If the caller needs fresh CI on the exact tree, check `git merge-base --is-ancestor origin/main `: exit 0 → branch HEAD contains all of main, the existing run covers the landable tree, proceed; exit 1 → contradicts the no-op signal and indicates a state-tracking bug, so investigate before proceeding. - **Report the existing run's conclusion** (`gh pr view --json statusCheckRollup,mergeStateStatus`) so the caller acts without another `gh run view`. A no-op rebase does **not** imply CI is green — the existing run may be `FAILURE`/`UNSTABLE`, and routing that into the fix loop instead of merging is the caller's job (`agile-11-merge-train` 3a). ## Report exactly one outcome These status lines are what `agile-11-merge-train` 3a routes on, so write them as full sentences in normal English: - **Pushed merge commit** — conflicts resolved (if any), new HEAD sha, fresh CI run expected. - **No-op (already up to date)** — no commit, no push; name the existing CI run id, its conclusion, and its sha. - **Conflict still open** — the unresolved files; halt, the caller must intervene.