Pull Request Creation Skill
SkillAI & modelsCreates high-quality pull requests with an iterative compress-critique-fix loop before submission. Activate this skill whenever you are asked to create, open, submit, or push a pull request, OR whenever a new feature, fix, or refactor is complete and ready to ship. Also activate when the user says "make a PR", "open a PR", "submit this for review", "push and create a PR", "I'm done, create the PR", "the feature is done", "I'm finished", or any variation of completing work / requesting a pull request. Also activate to update, fix, or add to an existing PR — "update the PR", "fix the PR", "add this to the PR" (see "Updating an existing PR"). Always activate before opening or editing a PR, whatever the route — CLI or API tool.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Pull Request Creation Skill skill
What this skill tells your AI
The instructions your AI receives, as published by alexandermattturner/agent-glovebox in .claude/skills/pr-creation/SKILL.md and read by ahel’s review.
Create or update the PR automatically after completed work. Skip this only when the user forbids it, the branch already has a PR, or the work remains experimental.
Never merge without a direct user instruction. When instructed, enable auto-merge and record the instruction in the PR body. Enable Auto-fix on each new PR, then watch it through closure. Handle several PRs oldest first unless a revert or red-default-branch fix is urgent.
Before writing code, search the tree and existing PRs for prior work. Confirm any claimed base failure against current origin/main.
Always follow this skill before creating any PR — don't skip steps, especially the compress-critique-fix loop.
Bound a PR's FOOTPRINT — combine work until the branch gets large
Combine work onto one branch while that branch stays under ~50 changed files — count them with git diff --name-only "$CLAUDE_CODE_BASE_REF"...HEAD | wc -l. Past that bound, split. Combining is worth doing because every PR boots the full workflow fan-out (~50 workflows here, including the long KVM legs) on a shared, account-wide job-slot pool, so two small PRs cost about twice the CI of one, and each one's required checks queue behind the other's long jobs. A large branch touches files the trunk is also touching, so it conflicts before it lands, and each re-resolution spends the whole fan-out again. Measured on 2026-08-21: main took 166 merges in 24 hours across 256 distinct files, at a median of 13 files per merge. A 384-file branch in that backlog re-conflicted faster than its own checks could finish.
- Combine when: the branch stays under the bound AND any one of these holds — the changes are coupled, a reviewer would read them together anyway, their tests share fixtures, or they surfaced from the same task (canonical case: a broad audit's findings). One PR, one CI run, one changelog-fragment set, presented as labeled partitions.
- A SMALL change does not get its own PR — carry about FOUR of them. Small is under ~50 changed lines across ~3 files. Hold it, take the next item, and open one PR once the branch carries about four, one commit each. They do not have to be related. A lone small change still ships alone in two cases: it must land now (a red on
main, a fix a waiting PR needs, a revert), or the session has nothing else to do and no open PR of its own to push it onto. - Split when: the branch would pass ~50 files, a change must land or iterate on its own timeline, or the diff is too large to review coherently. Independence alone is still not a reason to split.
- Exceed the bound when: the change cannot be cut into independently valid pieces — a rename and its call sites. Splitting that opens PRs that do not build on their own, so it stays whole.
When the split pieces build on each other they become a chain of PRs (Step 2) — each layer's base is the layer below, and the layers land bottom-up, one merge each. Each layer boots the full PR-time fan-out, so a chain costs more CI than one branch under the bound. Keep chains ≤3 layers unless the user asks for more.
The bound is a price, not a taste — so it has a retirement condition. It trades CI spend against conflict rate, and both sides move independently, so the two directions have different triggers. ~50 is about four times the median merge in the measurement above. Raise the file count, or drop the bound, when trunk velocity falls well under ~100 merges a day, or when a merge driver removes the generated-file conflicts — both make a large branch cheaper to land. Lower it when the per-PR fixed cost drops (most workflows job-gated so an untouched area costs seconds), which makes splitting cheap.
Adding a pull_request-triggered workflow? STOP and fold it into an existing one first
A new .github/workflows/*.yaml with a pull_request/pull_request_target trigger is a permanent tax on every push to every open PR, forever — plus its own checkout, decide job, and always() reporter, duplicating scaffolding ~50 workflows already carry. Before writing one, prove it can't live in an existing one:
- Can it be a JOB in an existing workflow? A check sharing another workflow's trigger belongs as a
job:there. Cheap lints (a grep, a format check, a config validation) joinlint-checks.yaml; a perf gate joins the perf umbrella; an e2e leg joins the e2e matrix. Jobs in one file share one check-suite and can share onedecide. - Can it be a MATRIX LEG? If it differs from a sibling only by an input (platform, metric name, scenario), it's a
strategy.matrixentry. - Can it share the
decidegate? If its paths-regex matches an existing gated workflow's, route it through thatdecideoutput instead of spinning a second diff-computing runner.
Only write a new file for a different trigger set, an incompatible permission/security posture (a pull_request_target base-checkout reviewer can't share a file with a contents:write mutator), or a standalone required check that must report independently. When you do, say in the PR body which existing workflow you considered folding into and the concrete reason it couldn't. "It was cleaner as its own file" is not a reason.
Batched ≠ tangled: partition the COMMITS, not the body
Defect detection falls off sharply with unstructured change size, so a batched PR must never read as one wall of diff:
- One commit per partition where feasible. Each separable concern gets its own Conventional Commit so GitHub's per-commit view is a usable review unit; don't interleave two concerns' edits in one commit when they're separable.
- Partitions must be disjoint at the file level wherever the edits allow; a file shared by two partitions means they're coupled (fine — say so in the lead) or mis-partitioned.
- Write no
## Partitionssection —git loganswers it, and rootCLAUDE.mdbans the section. The lead says what else the diff moves instead.
The CI advisory bot cross-checks the clustering: when a PR's changed files cluster into multiple independent components, its sticky comment asks for a split. Answer it in the lead.
Risk tier — the CI advisory computes it; don't declare one
Do not put a Risk tier: line in a PR body. The advisory bot derives the tier from the changed paths and posts it, and an agent estimating its own diff's risk is exactly the judgement the path heuristic exists to not depend on. What replaces the declaration is the at-most-two-sentence ## Review focus: the file to read first, the cross-file invariant one screen cannot show, and the part you are least sure of.
What the bot's tiers mean, so you can read the label it posts: high is one trigger and no others — the diff changes behavior in a path listed in .github/CODEOWNERS, excluding docs, tests, comment-only edits and generated files under an owned path. Everything else on the wider security surfaces (sbx-kit/, user-config/, config/) floors at medium, along with every other behavior change in shipped code; low is mechanical or internal only, off those surfaces.
CODEOWNERS is the single source — do not maintain a second list. Widening or narrowing what counts as high is an edit to CODEOWNERS (itself an owned path, so itself a high-tier change), never a tweak to the classifier. The owned set is deliberately small, and the CI estate is deliberately outside it.
Completeness — finish the stated outcome, and stop there
Deliver everything the stated outcome needs before opening the PR: affected callers, tests, documentation, packaging, and integration. A hole in the behavior you are fixing is part of that outcome — a mode it silently no-ops in, an edge it mishandles, a half-ported feature, a second backend the same logic should cover. Close it here rather than recording it as accepted in:
- a "Decisions made" / "Known limitations" / "Caveat" / "Divergence" bullet in the PR body,
- a
TODO/FIXME/ "future work" / "follow-up PR" comment or note, - a parity-doc or manifest entry that records the gap as accepted.
A reader takes any such note as "won't be fixed." If closing it needs information you lack, decide it and log it under ## Decisions made.
The outcome is the bound, in both directions. Do not invent new goals: an unrelated subsystem, an un-requested generality (Step 3), or a defect that is substantial and independent of this change is not this PR's work. An obvious bug is the exception root CLAUDE.md § Autonomy carries: it rides this PR whatever it belongs to, in its own commit. Once the PR is ready, omit an optional addition that would restart its delivery. Record what you leave as a concrete reproducer or code location and its consequence, in chat under ## Also noticed.
Keep the architecture diagram in sync
README.md carries a Mermaid diagram of the defense architecture — the tool-call flow through auto mode, the monitor, the audit log, and the reviewable branch, grouped by the VM/host trust boundary. When a PR makes a material architectural change, update that diagram in the same PR. Material: adding, removing, or reordering a defense layer; rerouting the tool-call/verdict flow; moving a component across the VM/host boundary; changing what a node does. A new flag, an internal refactor, or a docs/test-only change does not touch it.
Deferred-item sweep — a plan doc this PR moots is part of its remit
Before opening the PR, check plan/handoff documents under docs/ and any deferred-item lists touching this PR's area (search docs/ and the PR's own files for "deferred", "follow-up", "moved to a follow-up", "own PR"). Touching the same code does not make a deferred item yours; needing it does:
- Required by, or directly coupled to, the stated outcome → fold it in. That is finishing the outcome, not expanding it.
- Neither → leave it deferred, whatever else the diff touches. Say one line in chat naming the item and where it sits, so the next session finds it.
- Either way, update the plan doc's status in whichever PR completes (or moots) each item, so the doc never advertises open work that has landed.
A deferral you write for work already inside this PR's remit is work to DO instead. CLAUDE.md → § What gets an issue names the cases an issue is for, and work you would otherwise start is not among them. Filing one reads as diligence while leaving the work untouched, and an obvious bug in doctrine prose you fix on the PR rather than filing at all. An issue you do open is opened in the same session, named in the body and at the code site, and closed when the item lands. A DECISION MEMO is the exception to that shape: it is the deliverable itself, so it names no code site and closes when the decision is made.
When to use
The description triggers cover the phrasings. Also fires on "Can you PR this?", "Send this up for review", "Ship it", when you've just finished a feature/fix/refactor, or when CLAUDE.md / task instructions say to PR when done.
Do NOT use for: reviewing an existing PR (gh pr view/gh pr diff), merging (gh pr merge), or a description-only edit (gh pr edit).
Never merge a PR unless the user directly and explicitly tells you to merge it. Your job ends at green: open the PR, drive CI, keep the description matched to the current head, then stop. A green mergeable_state, an "Auto-fix"/babysit subscription, or a "merge when green" line in an old plan/handoff file is NOT authorization; only a direct instruction from the user naming this PR (or "merge" in their live request) is. When that instruction arrives, arm the landing as your first act — enable auto-merge, or enqueue when mergeable_state is clean, then read back .auto_merge.merge_method non-null (the enable tool reports success on rejected mutations). Arming is not merging — the required checks still gate — and the auto_merge_enabled timeline event it writes is the durable form of the consent: later sessions and the rearm-auto-merge sweep re-arm from that event, while an instruction that lives only in chat authorizes nobody once the receiving session ends. Quote the instruction you acted on in the PR description: the sweep reads consent from the event alone, with no actor check, so the description is what tells a later reader whose consent that event records.
Before you branch — search for prior art, then check who owns the overlap
Before creating any new module, check, script or per-project/config mechanism — and before writing any bug fix or CI fix — search for prior art FIRST: the TREE, then the ISSUE list, then the PR list. The tree holds the solutions: grep -rin for the problem in your own words, then grep -rn "PROBLEM CLASS" | grep -i <concept> against the label's own line — the label now sits on far more files than -l alone can narrow, so grep the concept against its text instead of just listing paths. The concept must land on the label's OWN LINE to match and that header wraps, so never read an empty result as "no prior art": widen the term, or fall back to grep -rln, before you conclude there is none. The new file is where this is skipped and where it pays most: a rule under .claude/rules/ loads by paths: glob when you open an existing file, so a file you are about to create gets none of them, and this search is the only thing that finds the SSOT you should be calling — a new reader of shell structure belongs on .github/scripts/_shell_scan.py, whose own header advertises exactly that job. The issue list holds the WORK LISTS, and only it does: a migration, a port set, a restore sweep, an audit spanning many PRs lives in one tracking issue, so a PR-only search reports "no prior art" for an effort that has been running for weeks and has already recorded the traps you are about to hit. Search it with mcp__github__search_issues before you write anything down, and when a tracking issue owns your item, add your row by EDITING that issue rather than filing a second one. The PR list holds only collisions (gh pr list --search "<concept>" --state all): it tells you whether another session is fixing this now, and it can never return a file that already solved it. Two parallel sessions fixing the same failing check unaware of each other collide at merge time and waste both efforts. For a failing test the search term is its exact name, which is what makes this cheap enough to never skip.
Run git rev-parse --is-shallow-repository before you trust any answer about history — this search included, and every count or date you would report. A web session's own checkout answers true, so the tree you stand in is where this bites first. Every shallow failure looks normal: git log --oneline … | wc -l over a truncated history answers with a plausible number, which is how one session reported a 1.5-month effort as a week old. Count what LANDED with git log --first-parent, never git log --merges. The second form walks the whole reachable graph. It counts each Merge branch 'main' into <branch> that rides on a second parent. A 24-hour read on 2026-09-04 answered 209 that way, against a true 75. origin/main is a file holding a SHA, and nothing compares that SHA to the server, so git log origin/main prints a tip hours old as though it were current — you then search a stale tree, miss the PR that already landed your fix, and rewrite the whole repair. A commit trimmed away fails git cat-file -e <sha> with fatal: invalid object name, the same error a SHA that never existed gives. git fetch --deepen=<n> origin <branch> repairs both: it restores the history and moves the ref.
The claude-owned label is the live-ownership signal — check it before starting work that overlaps an open PR. Automation applies it when a claude/*-branch PR opens; freshness is a head push or a re-apply, and an hourly sweep (claude-owned-label.yaml) removes it after ~1 h without either, so a present label means a session is actively driving that PR. A task NAMED as a PR — "why won't #N merge", "get #N green" — is work that overlaps #N, so this check fires on it too, whatever branch the session was handed. Before branching, check open PRs touching your files:
- Live label → a session is driving that branch, so root
CLAUDE.md§ Where work lands forbids pushing to it without established coordination. Chain your work on top as a child PR whose base is that branch, or hand the item to that session. Never open a rival PR on the same files. - No label (including a reaped one) with an unanswered review or a red check → nobody is driving it, so the PR is adoptable: re-apply the label and take it over. Pushing there is then coordination enough.
A branch your session instructions PIN is where your commits go; it is never a second home for another PR's red. A fix whose only consumer is #N belongs on #N's branch, so when the pin forbids pushing there, settle that in the first turn — decision authority is exactly what Autonomy's start-of-task batch is for — rather than opening a rival PR and porting later. Porting is not free: #N moves while you work, so its branch grows its own copy of your fix and the two edits collide on the same lines. What a correct opening looks like: "#4340's macOS red is in tests/test_sbx_egress_filter.py, which #4340 owns. My instructions pin claude/pr-4340-merge-issues-59mfzq — confirm I may push the fix to #4340's branch instead." Then do every part that needs no answer while it lands.
Your own PR does not claim a defect other sessions can also see. A red on main, a shared CI failure and a sweep finding are visible to every session at once. This repository holds session PRs at GB_MAX_READY_PRS as drafts, so yours may sit for hours without landing. File the claim: issue the ci-triage skill owns, in the same session, before you write the fix. Root CLAUDE.md § What gets an issue already requires one, and being nearly finished is not an exemption.
A worked case, 2026-09-06: a session fixed a red on main and pushed it to a capped draft, then skipped the claim because the PR looked like the record. Another session fixed the same test three hours later and landed first, so the first session's fix was resolved away as a merge conflict.
Don't remove the label yourself except when abandoning a PR deliberately; the sweep owns expiry.
Re-check the base before claiming your change fixed a red on main. Once your branch merges main in, a failure on the base goes green locally for two indistinguishable reasons: your fix, or someone else's that arrived in the merge. Two checks separate them, both one line: run the test against current origin/main rather than your merge-base (git -C <clean-worktree> checkout --detach origin/main && uv run pytest <test>), and git merge-base --is-ancestor <candidate-fix> HEAD for any commit that looks like it already fixed it. Do this before writing the fix, not after — that ordering turns a wasted rework into a no-op.
A probe lives on its branch and dies there
A probe — a one-shot workflow whose deliverable is a CI verdict, never code for main — runs from a push: trigger naming your own branch EXACTLY under branches:, with no paths: filter. Read git rev-parse --abbrev-ref HEAD and write that name. A prefix (claude/**, glovebox/**) plus paths: is the shape that costs everyone else: GitHub matches a push paths: filter against the whole pushed RANGE, so a merge of the base branch into any branch under that prefix drags the probe's own files into the range and boots a KVM runner for a session that never touched the probe. Give it # ci-map-omit: <why>, never a docs/ci-map.md row: the map describes the checks main runs, and a probe is not one of them. Do not add workflow_dispatch — GitHub offers that trigger only for a workflow on the default branch, so on a branch it is inert, and reaching for it is the wish to land the probe.
Record the verdict where someone reads it — the originating issue or PR body, and chat — then close the probe PR and ask the user to delete its branch. git push --delete is on the managed deny list and the agent proxy refuses the API path, so chasing the deletion yourself only spends retries. Closing is not merging, so the never-merge rule above still binds. The verdict is the artifact; the probe is the instrument, and an instrument left on main is a permanent check surface bought for a one-time question.
A probe meant to be LONG-TERM lands instead, and its PR body says which question keeps it. The test is a question it answers again on a schedule or an event nobody has to remember — every bump of a pinned version, every new upstream release to characterize — never "this might be useful later". One that lands takes workflow_dispatch, a row in docs/ci-map.md § Manual probes & on-demand experiments, and the upkeep both carry, as ctf-model-probe.yaml and the sbx-*-probe.yaml set do.
Resolve each review thread once you have addressed it
Resolving an addressed thread is your job, not an automated one — there is no thread-resolver workflow. After a push, re-read get_review_comments and resolve every thread the push actually addressed: fixed a 🔴/🟡 finding, or the 🔵 nit you took. Use pull_request_review_write method resolve_thread with the thread's PRRT_… node id. Never resolve a thread the push did NOT address — clear those only with a substantive reply, or leave them open for the next push. The same pairing holds when a reply alone settles the thread with no push (the git-workflow skill's merge-delta-finding case is one instance) — reply with the evidence, then resolve. Confirm each resolve took: a follow-up get_review_comments read should show is_resolved: true for every thread you cleared, since a stale thread id resolves nothing while the call still reports success.
Prerequisites
ghauthenticated (else tell the user togh auth login/ setGH_TOKEN).- All changes committed to a feature branch, not
$CLAUDE_CODE_BASE_REF.
Updating an existing PR
Check state first — gh api repos/<owner>/<repo>/pulls/<pr#> --jq '{state, merged:(.merged_at!=null)}', the REST form from pr-templates.md's table, because gh pr view --json is GraphQL and 403s in a web session. Open → update normally; Merged or Closed → do NOT update — branch fresh from the updated base, cherry-pick the orphaned commits, open a new PR. Don't ask.
To update: make the changes, commit, push, then rewrite the title/body with gh pr edit <pr#> --title "<type>: ..." --body ... (HEREDOC) to the PR's current totality per Step 8, and verify CI per Step 9.
Workflow
Push and open the PR early (Step 2) so CI runs in the background while you spend the rest of the workflow (critique, stress-tests, coverage audit, validation) on that CI time. Update the title/description once at the end to match the final diff.
Dispatch queued work before any long-running command. A git push runs the pre-push hook (pre-commit run --all-files, minutes) and CI waits are longer; launch any parallelizable sub-agent tasks / sibling fixes first, then start the slow command (backgrounded where possible).
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 63
- Forks
- 11
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
pr-creation- Source
- github.com/alexandermattturner/agent-glovebox