skills/merge-pull-requests/SKILL.md
Triage, review, and merge open pull requests from multiple sources (OpenSpec, Jules, Codex, Dependabot, manual)
npx skillsauth add jankneumann/agentic-coding-tools merge-pull-requestsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Discover, triage, and merge open pull requests from multiple sources. Handles OpenSpec PRs, Jules automation PRs (Sentinel/Bolt/Palette), Codex PRs, Dependabot/Renovate PRs, and manual PRs with staleness detection and review comment analysis.
$ARGUMENTS - Optional flags: --dry-run (report only, no mutations)
Scripts live in <agent-skills-dir>/merge-pull-requests/scripts/. Each agent runtime substitutes <agent-skills-dir> with its config directory:
.claude/skills.codex/skills.gemini/skillsIf scripts are missing, run skills/install.sh to sync them from the canonical skills/ source.
gh CLI authenticated (gh auth status)main branch with clean working directoryBefore any other work, verify exclusive access — this skill merges into main and must not race other agents:
python skills/shared/active_agents.py
0: no active agents → proceed.1: one or more active agents hold worktrees → stop, surface the list to the operator (the script's stdout already prints it), and ask whether to wait or pass --force. Never auto-force.An entry is "active" when it is pinned OR its last_heartbeat is within 1 hour. See skills/shared/active_agents.py and CLAUDE.md "Sync-Point Skills" for the contract; docs/mental-models.md gap G10 for the rationale.
gh auth status
git status
Abort conditions:
gh is not authenticated, stop and ask the user to run gh auth login.git checkout main with a dirty working directory — it could silently carry or lose uncommitted work. Ask the user to commit, stash, or discard changes first.main, check for uncommitted changes before switching.Write access check: Before proceeding, verify the token has write access:
gh api repos/{owner}/{repo} --jq '.permissions.push'
If this returns false, warn the user that merge and close operations will fail and suggest checking token scopes or requesting write access.
git checkout main
git pull origin main
python3 <agent-skills-dir>/merge-pull-requests/scripts/discover_prs.py
This outputs a JSON array of PRs classified by origin:
openspec - Branch matches openspec/* or body contains Implements OpenSpec:sentinel - Jules Sentinel (security fixes)bolt - Jules Bolt (performance fixes)palette - Jules Palette (UX fixes)jules - Jules automation (type not determined)codex - Created by Codexdependabot - Dependabot dependency updatesrenovate - Renovate dependency updatesother - Manual or unrecognizedEach PR also includes:
is_draft - Whether the PR is a draft (cannot be merged)is_stacked - Whether the PR targets a branch other than main/master (part of a PR chain)is_fork - Whether the PR is from a forked repositoryauto_merge_enabled - Whether auto-merge is already configureddep_ecosystem - For Dependabot PRs, the ecosystem (e.g. npm_and_yarn, pip)If no open PRs are found, stop here.
Present the PR list as a summary table:
| # | Title | Origin | Branch | Age | Flags |
|-----|--------------------------------|------------|------------------|--------|--------------------|
| 42 | Fix XSS in login form | sentinel | sentinel/fix-xss | 3 days | |
| 40 | Bump lodash from 4.17.19 | dependabot | dependabot/npm/… | 1 day | auto-merge |
| 39 | Fix typo in README | other | fix-typo | 2 days | fork |
| 38 | feat: Add user export | openspec | openspec/add-… | 5 days | stacked |
| 37 | WIP: Refactor auth module | other | refactor-auth | 7 days | draft |
Draft PRs cannot be merged. Flag them in the summary and skip them during the merge workflow. If the operator wants to process a draft PR, they must first mark it as ready:
gh pr ready <pr_number>
PRs that target a branch other than main/master are part of a PR chain. Warn the operator before taking action on stacked PRs:
PRs from forked repositories have limited permissions:
--delete-branch flag is skipped automatically (no push access to the fork remote)forkPRs with auto-merge already enabled will merge automatically once their conditions are met (CI passes, approvals received). Recommend skipping these during manual triage — they don't need intervention. If the operator wants to override, they can proceed normally.
For each non-draft PR, run staleness detection:
python3 <agent-skills-dir>/merge-pull-requests/scripts/check_staleness.py <pr_number> --origin <origin>
The script fetches the latest remote state (git fetch origin main) before checking. Pay special attention to Jules automation PRs (sentinel, bolt, palette) — the script uses normalized whitespace matching to check whether the code patterns being fixed still exist on main. If not, the PR is marked obsolete.
Staleness levels:
For PRs with failing CI, determine the failure class before choosing a fix strategy:
| Failure Class | Symptoms | Fix Strategy |
|---------------|----------|-------------|
| Transient | Flaky test, network timeout, OOM, runner issue | rerun-checks — same code should pass on retry |
| PR-specific | Lint/test error in files the PR modified | Fix the code, commit, push (triggers fresh CI) |
| Stale merge | Error in files the PR did NOT modify; same error across multiple unrelated PRs | refresh-branch or rebase onto main |
How to detect stale-merge failures: If the same CI check fails identically on 3+ unrelated PRs, and the failing files are not in those PRs' change sets, it is almost certainly a stale merge commit. gh run rerun will NOT fix this — it replays the workflow against the same merge commit snapshot, not a fresh one.
To refresh the merge commit without a local rebase:
python3 <agent-skills-dir>/merge-pull-requests/scripts/merge_pr.py refresh-branch <pr_number>
This calls the GitHub Update Branch API to merge the current base into the PR branch, producing a new merge commit that triggers fresh CI automatically. For PRs that will be squash-merged, the merge commit is discarded at merge time.
The check_staleness.py script includes a ci_merge_base_stale field that compares the PR's merge base with the current base branch HEAD. When ci_merge_base_stale is true and CI is failing, prefer refresh-branch over rerun-checks.
After running staleness checks for all PRs, compare their file lists to identify PR pairs that modify the same files. Warn the operator before the interactive review:
⚠ PRs #42 and #38 both modify src/auth.py — merging one may make the other stale.
⚠ PRs #40, #41, and #43 all touch package.json — consider merge order carefully.
This helps the operator decide merge order proactively rather than discovering conflicts after each merge.
If any PRs are classified as obsolete:
# Show obsolete PRs and ask for confirmation
python3 <agent-skills-dir>/merge-pull-requests/scripts/merge_pr.py batch-close <pr_numbers_comma_sep> \
--reason "Closing as obsolete: the code patterns this PR fixes no longer exist on main. The underlying issue has been addressed by other changes."
Present the list of obsolete PRs and confirm with the operator before closing. Skip this step if no PRs are obsolete.
For remaining PRs (non-obsolete, non-draft), check for unresolved review comments:
python3 <agent-skills-dir>/merge-pull-requests/scripts/analyze_comments.py <pr_number>
This uses the GitHub GraphQL API to get accurate thread resolution status:
is_resolved - Whether the thread has been marked resolvedis_outdated - Whether the comment is on outdated codeFor PRs that lack detailed reviews and are large enough to warrant automated analysis, dispatch multi-vendor reviews using the review infrastructure from parallel-implement-feature.
python3 <agent-skills-dir>/merge-pull-requests/scripts/vendor_review.py <pr_number> \
--origin <origin> --reviews-json <comments_output_path> [--dry-run]
Review is dispatched when ALL conditions are met:
openspec, codex, or other (non-trivial PRs)CHANGES_REQUESTED reviewsReview is skipped when ANY condition is met:
sentinel, bolt, palette, jules, dependabot, or renovate (scoped automation or dependency updates)--dry-run mode is active (reports eligibility only)The script:
Present findings to the operator alongside the existing comment analysis in the interactive review step:
🔍 Vendor Review (2 vendors):
✓ Confirmed (2 vendors agree): 3 findings
- [HIGH/security] Missing input validation on /api/users endpoint
- [MEDIUM/correctness] Off-by-one error in pagination logic
- [LOW/style] Inconsistent naming in helper functions
⚠ Unconfirmed (1 vendor only): 1 finding
- [LOW/performance] Consider caching for repeated lookups
Blocking: 1 (confirmed fix findings)
If vendor review produces blocking findings (confirmed issues with disposition=fix), recommend the operator skip or address the issues before merging.
If vendor CLIs are unavailable or all vendors fail, proceed without vendor review and note the gap.
For OpenSpec PRs (openspec/* branch), check whether Docker-dependent validation has been run. Cloud-created PRs typically pass environment-safe checks (pytest, mypy, ruff, openspec validate) during implementation but lack deployment-based validation.
Triggers when ALL conditions are met:
openspecvalidation-report.md exists at openspec/changes/<change-id>/, OR the existing report is missing deploy/smoke/security/e2e phasesSkip when ANY condition is met:
validation-report.md exists with all phases completed--dry-run mode is activedocker info fails)Action: Delegate to /validate-feature with the Docker-dependent phases only:
/validate-feature <change-id> --phase deploy,smoke,security,e2e
This runs the canonical validation skill targeting only the phases that require local infrastructure. The skill handles worktree isolation, service lifecycle, report generation, and teardown. The resulting validation-report.md is committed to the PR branch so subsequent triage sessions skip this step.
Present findings in the interactive review step alongside vendor review results:
Merge-Time Validation (OpenSpec: <change-id>):
✓ Deploy: Services started (3 containers)
✓ Smoke: 5/5 health checks passed
○ Security: Skipped (Java not available)
○ E2E: Skipped (no tests/e2e/ directory)
Result: PASS (2 passed, 2 skipped)
If any phase fails, flag the PR with a warning but do not hard-block — the operator decides whether to merge, fix, or skip. Critical failures (deploy crash, smoke test failures) should be highlighted prominently.
If openspec/changes/<change-id>/rework-report.json exists, check whether holdout scenario failures block the merge:
REWORK_REPORT="openspec/changes/$CHANGE_ID/rework-report.json"
if [[ -f "$REWORK_REPORT" ]]; then
HAS_BLOCKING=$(python3 -c "
import json
data = json.load(open('$REWORK_REPORT'))
print(data.get('summary', {}).get('has_blocking_holdout', False))
")
if [[ "$HAS_BLOCKING" == "True" ]]; then
echo "WARNING: Holdout scenario failures detected in rework report"
echo "The rework report indicates blocking holdout failures."
echo "Consider running /iterate-on-implementation and /validate-feature before merge."
fi
fi
Holdout gate status is presented as a warning during the interactive review — it does not auto-block. The operator decides whether the holdout failures are acceptable for this merge or need resolution first.
Before the interactive review, sort remaining PRs for optimal merge order:
Within the dependency updates group, consider grouping by ecosystem (e.g. all npm_and_yarn bumps together) — if one fails, it may indicate an ecosystem-wide issue.
This ordering minimizes the chance that merging one PR invalidates another.
Process each remaining PR one at a time in the order determined above. Skip PRs with auto_merge_enabled unless the operator explicitly wants to review them. For each PR, present:
reviewDecision is APPROVED, pending reviewers may indicate a missing required reviewThen offer actions:
When iterating on a complex merge resolution (rebase conflicts, repeated CI fixes, multi-step "address comments" passes), commit at every working slice with a wip: prefix. Squash before final merge.
# After each working slice (tests pass, lint passes, the change makes sense in isolation):
git add -A
git commit -m "wip: <description of the slice that just started working>"
# Before opening / re-opening for review, squash the wip commits into logical units:
git rebase -i <base-branch> # mark wip: commits as `s` (squash) into a parent
# OR if the operator prefers a single squashed commit at merge:
gh pr merge --squash # GitHub squashes them at merge time
Why this matters: A complex merge resolution often takes 5-30 incremental fixes. Without save points, a single mistake can lose all the prior progress. With wip: save points you can git reset --hard <last-wip-sha> to return to the most recent known-good state without re-doing everything. The squash step at the end keeps the public history clean.
The wip: prefix is the agreed signal: any commit starting with wip: is assumed to be squashed before merge and should never appear on main. This skill's pre-merge gate refuses to merge a PR whose head commit message starts with wip: unless --strategy squash is in effect.
Every PR ready-for-review MUST include the following template in its description. Reviewers depend on it; the bot scaffolds it; the skill's gate checks for it.
CHANGES MADE:
- <bullet list of what this PR actually does>
DIDN'T TOUCH:
- <out-of-scope items intentionally not addressed — name them so reviewers don't ask>
CONCERNS:
- <known issues, follow-ups, things reviewers should challenge>
Why all three sections matter:
CHANGES MADE forces the author to enumerate the actual changes (not just link the title) — exposes scope creep early.DIDN'T TOUCH pre-empts the most common reviewer round-trip ("why didn't you also fix X?"). Naming the boundary up front saves a review cycle.CONCERNS is the author's invitation to be challenged. A PR with CONCERNS: none after a non-trivial change is a red flag: real changes always have at least one open question.If a PR is missing this section, this skill flags it as description-incomplete during the interactive review and offers to scaffold the template into the body before merging.
The merge strategy is selected based on PR origin to balance history preservation with cleanliness:
| Origin | Default Strategy | Rationale |
|--------|-----------------|-----------|
| openspec, codex | rebase | Agent PRs with structured commits — preserve granular history for git blame/bisect |
| sentinel, bolt, palette | squash | Jules automation — typically single-purpose fixes |
| dependabot, renovate | squash | Dependency bumps — one logical change |
| other | squash | Manual PRs — unknown commit quality, safe default |
The operator can override any default by passing --strategy <squash|merge|rebase>.
python3 <agent-skills-dir>/merge-pull-requests/scripts/merge_pr.py merge <pr_number> --origin <origin>
Pass --origin using the origin field from discover_prs.py output so the script selects the appropriate strategy automatically. To override:
python3 <agent-skills-dir>/merge-pull-requests/scripts/merge_pr.py merge <pr_number> --origin <origin> --strategy squash
The script validates CI status (distinguishing failed from pending), draft status, merge conflicts, and mergeability before merging. It handles:
--delete-branch--merge-queueCONFLICTING status with specific guidance to rebase or merge the base branchrequired_approving_review_count >= 1). Solo repos and unprotected branches merge without an approval check, the same way gh pr merge would allow. Pass --force-approval to force-bypass even when protection requires approval (admin overrides, or when probing protection failed and the gate fell back to strict mode).After every merge, update local state:
git pull origin main
This ensures subsequent staleness checks and merges operate on the current main.
For OpenSpec PRs: After a successful merge, record the PR number, head branch, and change-id for the final post-merge cleanup approval step. Do not run /cleanup-feature immediately inside the per-PR merge loop.
Example record shape:
{
"pr_number": 42,
"origin": "openspec",
"change_id": "add-user-export",
"branch": "openspec/add-user-export",
"success": true,
"status": "merged"
}
Keep these records scoped to PRs merged during this invocation only. They are the input to Step 11.5.
Important:
rerun-checksreplays the workflow against the same merge commit. It does NOT pick up changes to the base branch. Use this only for transient failures (flaky tests, timeouts, OOM). For failures caused by stale base-branch code, userefresh-branchor rebase instead. See Step 5b for the diagnostic flowchart.
# Transient failure — replay same merge commit:
python3 <agent-skills-dir>/merge-pull-requests/scripts/merge_pr.py rerun-checks <pr_number>
# Stale merge commit — merge current base into PR branch for fresh CI:
python3 <agent-skills-dir>/merge-pull-requests/scripts/merge_pr.py refresh-branch <pr_number>
rerun-checks finds failed workflow runs on the PR's branch and re-runs only the failed jobs. refresh-branch calls the GitHub Update Branch API to merge the base branch into the PR branch, producing a new merge commit. Both trigger CI; after either, offer to Wait for the checks to complete.
After merging a PR, the staleness assessment for remaining PRs may be outdated. Re-run staleness detection for the next PR before presenting it:
python3 <agent-skills-dir>/merge-pull-requests/scripts/check_staleness.py <next_pr_number> --origin <origin>
If a previously fresh PR is now stale (due to overlapping with the just-merged PR), update the assessment before offering actions.
python3 <agent-skills-dir>/merge-pull-requests/scripts/merge_pr.py close <pr_number> --reason "<explanation>"
For PRs with unresolved comments:
git checkout <branch>git checkout mainAfter the PR review loop completes, prepare a single post-merge cleanup prompt for any local OpenSpec PRs merged during this invocation.
Do not run this step in --dry-run mode. In dry-run mode, report which cleanup commands would be offered for approval.
Use the merged PR records collected during Step 11:
python3 <agent-skills-dir>/merge-pull-requests/scripts/post_merge_cleanup.py \
--merged-json <merged_prs_this_pass.json>
The helper is non-mutating. It filters to merged OpenSpec PRs whose openspec/changes/<change-id>/ directory exists locally, checks local worktree registry and branch remnants, and renders an approval prompt like:
Merged local OpenSpec PRs eligible for post-merge cleanup:
| PR | Change ID | Branch | Local remnants | Command |
|----|-----------|--------|----------------|---------|
| #42 | add-user-export | openspec/add-user-export | 2 worktree registry entries, 3 local branches | `/cleanup-feature add-user-export --post-merge --pr 42` |
Ask the operator: Proceed with post-merge cleanup for these changes?
Only run the listed cleanup commands after explicit approval.
If the operator approves, run the listed cleanup commands sequentially:
/cleanup-feature <change-id> --post-merge --pr <pr_number>
The --post-merge cleanup mode must:
If the operator declines, do not clean up local remnants. Record the declined cleanup commands in the summary and merge log.
If a post-merge cleanup command fails, stop the cleanup pass, preserve the error output in the summary, and do not proceed to the next cleanup command until the operator decides how to continue.
After processing all PRs, present a summary:
## PR Triage Summary
- Merged: #42, #38
- Queued (merge queue): #45
- Closed (obsolete): #35, #33
- Skipped: #40
- Skipped (draft): #37
- Skipped (auto-merge): #41
- CI re-run: #39
- Comments addressed: #38
- Post-merge OpenSpec cleanup: #38 add-user-export (approved, completed)
- Post-merge OpenSpec cleanup declined: #44 improve-validation-flow
- Merge-time validation: #38 (deploy: pass, smoke: pass, security: skip, e2e: skip)
Write a merge-log entry to docs/merge-logs/YYYY-MM-DD.md capturing the triage decisions, vendor review findings, and user steering from this session.
Create directory if needed:
mkdir -p docs/merge-logs
touch docs/merge-logs/.gitkeep
Merge-log entry template:
---
## Session: <HH:MM> (<agent-type>)
### PRs Processed
| PR | Origin | Action | Rationale |
|----|--------|--------|-----------|
| #<number> | <origin> | <merged/closed/skipped> | <brief rationale> |
### Vendor Review Findings
- <PR #N>: <N> confirmed findings (<disposition>), <N> unconfirmed (<disposition>)
### User Decisions
- <User steering decisions captured during the session>
### Post-Merge Cleanup
- <OpenSpec cleanup approvals/declines/failures and commands run>
### Observations
- <Cross-PR patterns, recurring issues, notable observations>
Focus on: Cross-PR reasoning (why PRs were processed in this order, how they relate), user steering decisions, vendor review outcomes, and observations about patterns.
Sanitize-then-verify:
python3 "<skill-base-dir>/../session-log/scripts/sanitize_session_log.py" \
"docs/merge-logs/<date>.md" \
"docs/merge-logs/<date>.md"
Read the sanitized output and verify: (1) all sections present, (2) no incorrect [REDACTED:*] markers, (3) markdown intact. If over-redacted, rewrite without secrets, re-sanitize (one attempt max). If sanitization exits non-zero, skip merge log and proceed.
Commit and push:
git add docs/merge-logs/
git commit -m "chore: merge-log <YYYY-MM-DD>"
git push
When invoked with --dry-run, the skill runs all discovery and analysis steps but performs no mutations (no merges, no closes, no comments). Pass --dry-run to each script:
python3 <agent-skills-dir>/merge-pull-requests/scripts/discover_prs.py --dry-run
python3 <agent-skills-dir>/merge-pull-requests/scripts/check_staleness.py <pr> --origin <type> --dry-run
python3 <agent-skills-dir>/merge-pull-requests/scripts/analyze_comments.py <pr> --dry-run
python3 <agent-skills-dir>/merge-pull-requests/scripts/vendor_review.py <pr> --origin <type> --dry-run
python3 <agent-skills-dir>/merge-pull-requests/scripts/post_merge_cleanup.py --merged-json <merged_prs_this_pass.json>
Output a full report:
## Dry-Run Report
| # | Title | Origin | Staleness | Unresolved | CI | Vendor Review | Flags |
|-----|--------------------|------------|-----------|------------|---------|--------------------|--------------------
| 42 | Fix XSS in login | sentinel | obsolete | 0 | pass | skip (origin) | |
| 41 | Bump axios | dependabot | fresh | 0 | pass | skip (origin) | auto-merge |
| 40 | Bump lodash | dependabot | fresh | 0 | pass | skip (origin) | |
| 39 | Fix typo | other | fresh | 0 | fail | skip (small) | fork |
| 38 | feat: Add export | openspec | fresh | 2 | pass | 3 findings (1 fix) | |
| 37 | WIP: Refactor auth | other | — | 1 | pending | skip (draft) | draft |
| 35 | Fix slow query | bolt | stale | 0 | pass | skip (origin) | stacked |
/cleanup-feature --post-merge approvalgh auth loginpermissions.push check — warn before attempting mutationsgit checkout main to prevent losing uncommitted workCONFLICTING status with guidance to rebase or merge base branch--merge-queue when direct merge is rejected--delete-branch (no push access to fork remote)reviewDecision shows APPROVEDgh/git calls have timeouts (30-60s) to prevent hangsgh CLI which handles token refresh; if rate-limited, wait and retry| Rationalization | Why it's wrong |
|---|---|
| "I'll skip the Change Summary — the PR title is self-explanatory" | Titles compress; summaries enumerate. Reviewers waste time inferring scope from diffs when DIDN'T TOUCH would have answered the question in one line. |
| "wip: save points clutter history — I'll just be careful and not break anything" | Careful is a plan that fails the first time. Save points are insurance; squashing at merge erases the clutter. The cost is zero, the upside is recovering hours of lost progress. |
| "All my CONCERNS are minor — I'll write CONCERNS: none" | Non-trivial changes always have at least one open question. none signals either the author hasn't thought hard enough, or is hiding doubts to get the PR through. |
| "Auto-merge is on; I don't need to triage" | Auto-merge merges when checks pass — it doesn't catch obsolete fixes, conflicting PR pairs, or stale base-branch failures. Triage is the layer above auto-merge, not redundant with it. |
| "CI is flaky, just rerun" | rerun-checks replays the SAME merge commit. If the failure is a stale-base issue, rerun is theatre — refresh-branch is the real fix. See Step 5b. |
CHANGES MADE / DIDN'T TOUCH / CONCERNS block.wip: and is being merged with --rebase (the wip commit will land on main).docs/merge-logs/<YYYY-MM-DD>.md) lists every PR processed with its origin, action, and rationale — not just the merged ones.gh pr view <pr> --json body).main has a message starting with wip: after this skill ran (git log main --since=<start-time> --pretty=%s | grep -i ^wip: returns empty).refresh-branch, not repeated rerun-checks (the merge log records which strategy was used and why).development
Open the artifacts relevant to a review (OpenSpec proposal, branch changes, or explicit paths) in VS Code, in a curated read-order, in the right worktree.
tools
Render and seed coordinator-owned task status block in OpenSpec tasks.md
testing
User-invocable skill that omits the tail block
tools
Missing several required keys