PR Analysis Skill
Analyze all open PRs targeting a base branch, determine a safe merge order, assess
complexity, and produce machine-readable output for the merge-prs recipe.
When to Use
- At the start of a
merge-prsrun - User wants to understand which PRs can be merged safely and in what order
- User says "analyze PRs", "order PRs", or "assess PR complexity"
Critical Constraints
NEVER:
- Merge, close, or modify any PR
- Modify any source code files
- Create files outside
{{AUTOSKILLIT_TEMP}}/merge-prs/directory - Run subagents in the background (
run_in_background: trueis prohibited)
ALWAYS:
- Use subagents to fetch PR data in parallel
- Use
model: "sonnet"when spawning all subagents via the Task tool - Abort clearly if
ghCLI is not authenticated - Include every open PR targeting base_branch in the output — no PR is silently dropped (blocked PRs appear in ci_blocked_prs / review_blocked_prs arrays, not in the ordered prs list)
- Default to parallel batch processing: When multiple PRs are present, ALWAYS process all analysis tasks (diff fetching, overlap computation, complexity tagging) using parallel subagent batches. Processing PRs one-at-a-time without an explicit user instruction to do so is the wrong default. Up to 8 PRs should be processed in a single parallel batch; launch additional batches for larger sets.
Arguments
{base_branch} [merge_queue_data_path=<path>]
base_branch— the base branch to list PRs against (e.g.,main)merge_queue_data_path(optional) — absolute path to a JSON file containing pre-fetched merge queue data (produced byfetch_merge_queue_datarun_python step). When provided and present, read from file instead of calling GitHub GraphQL API inline.
Workflow
Step 0: Authenticate and List PRs
Run:
gh pr list --base {base_branch} --state open --json number,title,headRefName,author,body,additions,deletions,changedFiles --limit 100
If zero PRs are returned: write a summary to terminal and exit cleanly with an empty
pr_order_{ts}.json (zero PRs, no integration branch needed).
If gh returns an auth error: abort with a clear message.
Step 0.5: Detect GitHub Merge Queue
Before fetching diffs, load pre-fetched merge queue data to determine whether a merge queue
is active on {base_branch} with MERGEABLE entries.
Read pre-fetched merge queue data from disk when available:
if [ -n "${merge_queue_data_path:-}" ]; then if [ -f "$merge_queue_data_path" ]; then QUEUE_ENTRIES="$(cat "$merge_queue_data_path")" else echo "WARNING: merge_queue_data_path='$merge_queue_data_path' provided but file not found — possible misconfiguration. Falling back to no-queue mode." QUEUE_ENTRIES="[]" fi else QUEUE_ENTRIES="[]" # no path provided (standalone invocation) → QUEUE_MODE=false fiQUEUE_ENTRIESis a JSON array of{position, state, pr_number, pr_title}objects, pre-fetched by thefetch_merge_queue_datarun_python step in the recipe. Whenmerge_queue_data_pathis absent (standalone invocation),QUEUE_ENTRIESdefaults to[], which setsQUEUE_MODE = false.Determine mode:
- If the resulting entry list contains at least one entry with
state == "MERGEABLE": setQUEUE_MODE = trueand store the full sorted entry list asQUEUE_ENTRIES. - Otherwise (empty list or no MERGEABLE entries):
set
QUEUE_MODE = falseand proceed with the existing analysis path.
Log which mode was selected and the entry count to the terminal so pipeline runs are observable.
- If the resulting entry list contains at least one entry with
State variables set by this step (referenced in Steps 1–4):
| Variable | Type | Description |
|---|---|---|
QUEUE_MODE |
boolean | true when the merge queue has ≥1 MERGEABLE entry; false otherwise |
QUEUE_ENTRIES |
list[dict] | Sorted queue entries {position, state, pr_number, pr_title} when QUEUE_MODE = true; empty list when false |
Step 1: Fetch PR Data
If
QUEUE_MODE = false: ALWAYS launch subagents in parallel — never process PRs sequentially. Launch one Explore subagent per PR (up to 8 simultaneously; batch in groups of 8 if more):Each subagent fetches:
gh pr diff {number}— full unified diffgh pr view {number} --json files— structured file list; extract path strings viagh pr view {number} --json files -q '[.files[].path]'gh pr view {number} --json body -q .body— PR body to extract## Requirementssection if present
Each subagent returns:
pr_number: inttitle: strbranch: str (headRefName)files_changed: list of file paths (strings extracted from.files[].path)additions: intdeletions: inttest_files_changed: list of test file paths (files matchingtest_*.py,*_test.py,*.test.*,tests/**)requirements_section: str — the## Requirementssection extracted from the PR body, or""if not present
If
QUEUE_MODE = true: for each PR number inQUEUE_ENTRIES, fetch only the metadata needed for the manifest (no diffs, no body extraction):gh pr view {number} --json headRefName,files,additions,deletions,changedFilesExtract file paths from the
filesarray:gh pr view {number} --json files -q '[.files[].path]'. Collectfiles_changed(list of file path strings),test_files_changed(subset matchingtest_*.py,*_test.py,*.test.*,tests/**),additions,deletions, andbranch(headRefName). Run these fetches in parallel (up to 8 simultaneously). The PR list for subsequent steps is exactlyQUEUE_ENTRIES— do not use thegh pr listoutput.
Step 1.5: Filter PRs by CI and Review Status
If
QUEUE_MODE = false: After fetching all PR diffs, filter the candidate list before building the overlap matrix. PRs that fail either gate are reported in the manifest but excluded from merge ordering.Use a single GraphQL alias query to fetch CI status and reviews for all PRs at once (1 API call regardless of PR count, instead of 2N sequential REST calls). Chunk into batches of 20 PRs to stay within GraphQL complexity limits.
ELIGIBLE_PRS=() CI_BLOCKED_PRS=() # [{number, title, reason}] REVIEW_BLOCKED_PRS=() # [{number, title, reason}] # Build GraphQL alias query for all PRs in batches of 20 ALL_PR_NUMS=($(echo "$ALL_PRS" | jq -r '.[].number')) BATCH_SIZE=20 for batch_start in $(seq 0 $BATCH_SIZE $((${#ALL_PR_NUMS[@]} - 1))); do BATCH_NUMS=("${ALL_PR_NUMS[@]:$batch_start:$BATCH_SIZE}") QUERY="query { " for i in $(seq 0 $((${#BATCH_NUMS[@]} - 1))); do NUM="${BATCH_NUMS[$i]}" QUERY="${QUERY} pr${i}: repository(owner: \"${OWNER}\", name: \"${REPO}\") { pullRequest(number: ${NUM}) { number reviews(last: 20) { nodes { state } } commits(last: 1) { nodes { commit { statusCheckRollup { state contexts(last: 100) { nodes { ... on CheckRun { name status conclusion } } } } } } } } }" done QUERY="${QUERY} }" BATCH_RESULT=$(gh api graphql -f query="${QUERY}") # Parse per-PR results from BATCH_RESULT for i in $(seq 0 $((${#BATCH_NUMS[@]} - 1))); do NUM="${BATCH_NUMS[$i]}" PR_TITLE=$(echo "$ALL_PRS" | jq -r --argjson n "$NUM" '.[] | select(.number == $n) | .title') PR_DATA=$(echo "$BATCH_RESULT" | jq --arg key "pr${i}" '.data[$key].pullRequest') # --- CI Gate --- FAILING=$(echo "$PR_DATA" | jq ' [(.commits.nodes[0].commit.statusCheckRollup.contexts.nodes // [])[] | select(.conclusion != null and .conclusion != "success" and .conclusion != "skipped" and .conclusion != "neutral")] | length') IN_PROGRESS=$(echo "$PR_DATA" | jq ' [(.commits.nodes[0].commit.statusCheckRollup.contexts.nodes // [])[] | select(.conclusion == null)] | length') if [ "${FAILING:-0}" -gt 0 ] || [ "${IN_PROGRESS:-0}" -gt 0 ]; then REASON="CI failing: ${FAILING} failed, ${IN_PROGRESS} in-progress" CI_BLOCKED_PRS+=("{\"number\":${NUM},\"title\":\"${PR_TITLE}\",\"reason\":\"${REASON}\"}") continue fi # --- Review Gate --- CHANGES_REQUESTED=$(echo "$PR_DATA" | jq ' [.reviews.nodes | group_by(.author.login)[] | last | select(.state == "CHANGES_REQUESTED")] | length') if [ "${CHANGES_REQUESTED:-0}" -gt 0 ]; then REASON="${CHANGES_REQUESTED} unresolved CHANGES_REQUESTED review(s)" REVIEW_BLOCKED_PRS+=("{\"number\":${NUM},\"title\":\"${PR_TITLE}\",\"reason\":\"${REASON}\"}") continue fi mapfile -t _pr_entry < <(echo "$ALL_PRS" | jq -c --argjson n "$NUM" '.[] | select(.number == $n)') ELIGIBLE_PRS+=("${_pr_entry[@]}") done doneAll subsequent steps (overlap matrix, topo sort, PR ordering) operate on
ELIGIBLE_PRSonly.CI_BLOCKED_PRSandREVIEW_BLOCKED_PRSare written to the manifest in Step 5.If
QUEUE_MODE = true: skip this step entirely. TheMERGEABLEstate returned by the merge queue API already signifies that the PR passed CI checks and has no blocking reviews. Initialize both arrays explicitly so Step 5 can reference them unconditionally:CI_BLOCKED_PRS=() REVIEW_BLOCKED_PRS=()
Step 2: Build File Overlap Matrix
If
QUEUE_MODE = false: For each pair of PRs, compute:shared_files: files modified by both PRsshared_test_files: test files modified by both PRs
A PR pair is conflicting if
shared_filesis non-empty.If
QUEUE_MODE = true: skip this step entirely. The merge queue has already proven compatibility. Setoverlap_with_pr_numbers = []for every PR.
Step 3: Determine Merge Order
If
QUEUE_MODE = false: Order PRs to minimize cascading conflict risk:- PRs with no overlapping files with any other PR → place first (order by additions ASC)
- PRs with overlap → order so the PR that others depend on (touches foundational files) comes first; use topological sort on the overlap graph
- Large PRs (additions > 200) → place after small PRs that touch the same files, unless they have no overlap
Produce a final ordered list. Document the rationale for each ordering decision.
If
QUEUE_MODE = true: the merge order isQUEUE_ENTRIESsorted bypositionascending (ascending is already guaranteed byparse_merge_queue_response).
Step 4: Tag Complexity
If
QUEUE_MODE = false: For each PR in the ordered list, assign a complexity tag:simple— all of the following are true:- No shared files with any PR ahead of it in the merge order
- Total additions < 100
- No shared test files with PRs ahead of it
- No substantial logic changes in files also touched by earlier PRs (based on diff inspection)
needs_check— any of the following:- Shares files with one or more PRs ahead of it in merge order
- Additions ≥ 100 and touches files also present in earlier PRs
- Modifies shared test files
- The diff suggests it depends on function signatures or class structures that earlier PRs may change
If
QUEUE_MODE = true: tag every PR whose queue entry hasstate == "MERGEABLE"assimple. Any entry with a different state (e.g.,AWAITING_CHECKS) retains theneeds_checktag (defensive — such entries should not appear since Step 0.5 only sets QUEUE_MODE when MERGEABLE entries exist, but the tag is applied correctly for robustness).
Step 5: Write Outputs
Compute a timestamp: YYYY-MM-DD_HHMMSS.
Compute integration branch name: pr-batch/pr-merge-{YYYYMMDD-HHMMSS}.
Ensure {{AUTOSKILLIT_TEMP}}/merge-prs/ exists.
5a. Machine-readable order file: {{AUTOSKILLIT_TEMP}}/merge-prs/pr_order_{ts}.json
{
"batch_branch": "pr-batch/pr-merge-YYYYMMDD-HHMMSS",
"base_branch": "{base_branch}",
"generated_at": "{ISO timestamp}",
"pr_count": 5,
"prs": [
{
"number": 42,
"title": "Add user authentication",
"branch": "feature/auth",
"complexity": "simple",
"files_changed": ["src/auth.py", "tests/test_auth.py"],
"test_files_changed": ["tests/test_auth.py"],
"additions": 87,
"deletions": 12,
"overlap_with_pr_numbers": []
},
{
"number": 47,
"title": "Refactor database layer",
"branch": "feature/db-refactor",
"complexity": "needs_check",
"files_changed": ["src/db.py", "src/auth.py", "tests/test_db.py"],
"test_files_changed": ["tests/test_db.py"],
"additions": 165,
"deletions": 45,
"overlap_with_pr_numbers": [42]
}
],
"ci_blocked_prs": [
{"number": 99, "title": "Broken CI PR", "reason": "CI failing: 1 failed, 0 in-progress"}
],
"review_blocked_prs": [
{"number": 88, "title": "Needs changes PR", "reason": "2 unresolved CHANGES_REQUESTED review(s)"}
]
}
pr_count reflects the number of eligible PRs (i.e., ${#ELIGIBLE_PRS[@]}).
5b. Human-readable analysis plan: {{AUTOSKILLIT_TEMP}}/merge-prs/pr_analysis_plan_{ts}.md
This file is named *_plan_*.md so audit-impl can discover it as the baseline specification.
# PR Analysis: Integration into {base_branch}
**Date:** {YYYY-MM-DD}
**Base Branch:** {base_branch}
**Integration Branch:** pr-batch/pr-merge-YYYYMMDD-HHMMSS
**PRs Analyzed:** {count}
## Merge Order
1. PR #{number} — "{title}" (complexity: simple)
2. PR #{number} — "{title}" (complexity: needs_check)
...
## Excluded PRs
### CI-Blocked ({ci_blocked_count})
| PR | Title | Reason |
|----|-------|--------|
| #{number} | {title} | CI failing: 1 failed, 0 in-progress |
### Review-Blocked ({review_blocked_count})
| PR | Title | Reason |
|----|-------|--------|
| #{number} | {title} | 2 unresolved CHANGES_REQUESTED review(s) |
## File Overlap Matrix
| PR | Files | Overlaps With |
|----|-------|---------------|
| #{number} | {file list} | None |
| #{number} | {file list} | PR #{number} (src/auth.py) |
## Per-PR Assessment
### PR #{number}: "{title}"
- **Branch:** {branch}
- **Complexity:** simple / needs_check
- **Rationale:** {why this complexity tag was assigned}
- **Key files:** {list}
- **Risk notes:** {any concerns}
{If requirements_section is non-empty, include this block so reviewers can trace intent:}
#### Requirements
{requirements_section from PR body}
{repeat for each PR}
## Integration Strategy
{2–3 sentences describing the overall merge strategy and key risk areas}
- **If `QUEUE_MODE = true`**: PR order sourced from GitHub merge queue (position ordering). File overlap analysis skipped.
Step 6: Verify and Report
Verify:
pr_order_{ts}.jsonis valid JSON and parseable- Every listed PR number appears exactly once
batch_branchfield is set
Report to terminal:
- Order file path
- Analysis file path
- Number of PRs: {simple_count} simple, {needs_check_count} needs_check
- Proposed integration branch name
- Any PRs flagged as high risk
Output Location
{{AUTOSKILLIT_TEMP}}/merge-prs/
├── pr_order_{ts}.json # Machine-readable manifest (captured by recipe)
└── pr_analysis_plan_{ts}.md # Human-readable analysis (discovered by audit-impl)
Output Fields (for recipe capture)
After writing all output files and printing the terminal report, emit the following structured output tokens as the very last lines of your text output:
IMPORTANT: Emit the structured output tokens as literal plain text with no markdown formatting on the token names. Do not wrap token names in
**bold**,*italic*, or any other markdown. The adjudicator performs a regex match on the exact token name — decorators cause match failure.
pr_order_file = {absolute_path_to_pr_order_json}
analysis_file = {absolute_path_to_pr_analysis_plan_md}
batch_branch = {batch_branch_name}
pr_count = {eligible_pr_count}
simple_count = {simple_pr_count}
needs_check_count = {needs_check_pr_count}
ci_blocked_count = {ci_blocked_pr_count}
review_blocked_count = {review_blocked_pr_count}
queue_mode = {queue_mode} # true when merge queue has ≥1 MERGEABLE entry; false otherwise
Related Skills
/autoskillit:merge-pr— Merges individual PRs from this skill's ordered list/autoskillit:make-plan— Called for complex PRs that need conflict resolution plans/autoskillit:audit-impl— Receives{{AUTOSKILLIT_TEMP}}/merge-prs/as plans_input