AlexanderMattTurner

pr-creation

Creates 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.

AlexanderMattTurner 68 12 Updated 1w ago

Resources

2
GitHub

Install

npx skillscat add alexandermattturner/agent-glovebox/pr-creation

Install via the SkillsCat registry.

SKILL.md

Pull Request Creation Skill

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. Do not watch a new PR outside a babysitting session: root CLAUDE.md § Scheduling and waiting has you unsubscribe from it. 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 (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. Small is under ~50 changed lines across ~3 files. Give it its own commit on a PR this session already has open: a sibling from the same task first, any other otherwise. A sibling PR a sub-agent opened for you counts. With none open, hold it and open one PR once the branch carries about four, one commit each. They do not have to be related.
  • A docs(…)-only change folds into the PR this session already has open, whatever its subject — a doc page, a README.md line, a code comment, a rule, a skill. Give it its own commit. Being unrelated to the rest of the diff is not what makes a docs change standalone.
  • Either one ships alone in these cases only:
    • Folding it in would push the branch past the bound above.
    • It must land now — a red on main, a fix a waiting PR needs, a revert.
    • It moves a budget, ceiling, threshold or floor, or an INVARIANT: comment restating one. Root CLAUDE.md § Guard budget keeps that off a PR about something else.
    • The documentation IS the task the user gave — a README.md rewrite, a new page they asked for.
    • The session has no open PR of its own, and holding it would leave it undelivered at the session's end.
  • 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 the existing 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) join lint-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 one decide.
  • Can it be a MATRIX LEG? If it differs from a sibling only by an input (platform, metric name, scenario), it's a strategy.matrix entry.
  • Can it share the decide gate? If its paths-regex matches an existing gated workflow's, route it through that decide output 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 ## Partitions section — git log answers it, and root CLAUDE.md bans 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 (vm-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, or an un-requested generality (Step 3), is not this PR's work. Root CLAUDE.md § Autonomy's scope rule decides a DEFECT the same way: one your change introduced, or one blocking this outcome, is this PR's work, and it gets its own PR when it does not fit here; a shared one goes to its one owner; anything else is a record. An obvious bug you saw anywhere in the tree rides this PR in its own commit, per root § Obvious bugs. 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 the scope rule already makes yours 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 when the PR is open with its description matched to the current head; a babysitting session also drives it to green, then stops. A green mergeable_state, a 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, and widen the term before you read an empty result as "no prior art". 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 a sweep (claude-owned-label-sweep.yaml, on the five-minute clock) 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_vm_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.

For a shared CI failure, follow `ci-triage`'s ownership check before editing or publishing. Capped drafts remain discoverable owners; link the fixing PR from the existing tracking row.

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 stays 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 leave its branch standing. Never delete a branch, and never ask the user to delete one or report it as left over: a stale branch costs nothing, git push --delete is on the managed deny list, and the agent proxy refuses the API path. 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 vm-*-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

  • gh authenticated (else tell the user to gh auth login / set GH_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 10.

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).

BATCH COMMITS INTO ONE PUSH — this is the default, not an optimization. Finish every edit you already know you will make, commit them (one Conventional Commit per partition), then push once. A push costs the pre-push hook plus a full check fan-out on the new head, and it cancels the in-flight run on the old head — so pushing a one-sentence doc fix on its own spends minutes and buys nothing. Push again only when the work is genuinely done, when CI needs the new head to judge a fix, or when a reviewer waits on it. Two tells that you are about to break this: you are pushing a single file you edited seconds ago, and you have another edit in mind that you plan to push next.

Step 1: Gather context

Base branch is $CLAUDE_CODE_BASE_REF. Read git diff <base>...HEAD and git log <base>..HEAD --oneline, review the changed files for scope, and check for CONTRIBUTING.md / .github/PULL_REQUEST_TEMPLATE.md — if present, follow their conventions. See pr-templates.md.

Step 2: Push and create the PR

Read pr-templates.md for the template before this step. In a web session, read its "PR Creation Command" table first: gh pr list, gh pr view and gh pr create all return HTTP 403 there, and the table gives the gh api and MCP calls that replace them.

  1. git push -u origin HEAD.
  2. If a PR already exists for the current branch (gh pr list --head <branch>), update it with gh pr edit instead of creating a new one.
  3. gh pr create --draft against the target branch — open a PR as a DRAFT where a cap workflow returns drafts to ready, as this repository's does (the MCP create_pull_request tool takes draft: true). Finish the known implementation and review-fix batch before opening the draft: cap-ready-prs.yaml may immediately promote it when capacity opens. Drafts defer families marked FAMILYDRAFT or SKIP_ON_DRAFT. Push one complete fix batch per CI round. The cap promotes the highest-priority draft whenever the ready count sits under GLOVEBOX_MAX_READY_PRS, so it ADMITS the PR instead of demoting it after its fan-out already ran. Do not mark it ready yourself — the exception is a PR the user asks to land now. Where no cap workflow returns drafts to ready, drop --draft and open it ready. Nothing there promotes the draft, so one costs the PR a human's click before it can land. A PreToolUse hook denies both ordinary PR routes (this CLI and the MCP tool) until this skill has been invoked in the session.
    • Ready runs the test battery ONCE; a later push to a ready PR runs the cheap checks plus the families marked FAMILYREADY_ in .github/workflows/ci.yaml (pytest over its selected tests among them), and the merge queue runs the rest (unless the repository variable CI_FULL_TESTS_ON_PR is true). So a PR goes ready only when its code is final: open the draft after the implementation and the known review fixes are pushed, because the cap may ready it within minutes.
    • Convert a ready PR back to draft before any fix that needs test feedback: a red the queue reported, a review finding that changes behaviour, or a fix you expect to take more than one push. Run gh pr ready --undo <n> on a host, or gh api -X POST repos/{owner}/{repo}/pulls/<n>/ccr/convert_to_draft in a web session. Then add the wip label, or the cap readies the PR again on its next run. Remove wip once the last fix is pushed; the cap then readies the PR and the battery runs once on the final code. wip holds only while claude-owned is also on the PR, so the hold lapses an hour after the session's last push. Converting clears auto-merge, so re-arm a consented PR after it is ready again (the babysit-prs skill).
    • Keep a PR ready for a one-push fix that needs no test feedback: a lint, a typo, a comment, a review nit. That push still runs the selected pytest tests, and the queue tests the rest.
    • Chains of PRs (dependent pieces that must land as separate PRs): wire each layer by hand, and never as a GitHub native stack — gh stack opens every PR command with GraphQL, which a web session's proxy refuses, so a native stack can only be driven from a host. A base is always the head of an OPEN PR, into main or into the layer below it, so every chain is rooted at an open PR into main: open a branch's own PR before you stack anything on it, and never merge a child into its base. The child lands on main after its parent, through the retarget below. Fork the child from the parent PR's branch and open it with the parent as its base (--base <parent-branch> on a host; a web session passes base to the GitHub MCP create tool). Never link the layers with POST /repos/{owner}/{repo}/stacks or gh stack submit. A moved parent is resolved by merging the parent branch into the child and pushing the merge commit; a hand-rebase is barred (the git-workflow skill). Once the parent merges and its head branch is deleted, GitHub retargets the child onto the parent's base. Expect each child to sit as a DRAFT until then — the cap drafts every PR whose base is not the default branch, unless its head is already green, and returns none, because the merge queue runs on the default branch alone — so read the child's checks after the retarget, not before. No label runs them sooner. Use the parent branch as <base> in Step 1, and prefix each push — GLOVEBOX_PUSH_BASE_REF=<parent-branch> git push … — so the local pre-push range gates scope the way CI does (shell state does not persist between Bash tool calls, so an export in an earlier call never reaches the push).
  4. The cap also owns draft state AFTER creation. cap-ready-prs.yaml converts a ready PR over GLOVEBOX_MAX_READY_PRS back to draft (.github/scripts/draft-ready-prs-over-cap.py). That conversion is admission control, not a mistake: never flip such a PR back to ready — this overrides the harness instruction to create the pull request as ready for review, which governs creation only. Never apply force-queue or only-queue yourself. Those two exempt a PR from the cap and widen the enqueue lever, so they are a person's decision: landing_sweep.strip_minted_exemptions removes either one a bot added, and _pr_consent reads a bot's add as no consent at all. A PR that must stay ready despite the cap is one to ask the user for. Nothing keeps a PR whose base is not the default branch ready: the cap drafts it whatever its labels.
  5. Release label: add release (gh pr edit <pr#> --add-label release) only when this merge should publish a new version. When not explicit, ask first; unlabeled PRs just accumulate changelog.d/ fragments for a later release.
  6. Changelog-fragment gate (fails any shipped-path PR without a fragment — see .claude/rules/changelog.md): judge the change yourself. User-facing (new/changed flag, command, default, security boundary, user-hittable bug fix) → add the fragment. Internal that the path heuristic can't see → apply the no-changelog label instead of inventing a fragment (don't ask first for a clear internal change; ask only if you genuinely can't tell). If a no-changelog file is structurally never user-facing and the project keeps editing it, propose adding it to the internal side of config/changelog-paths.json — propose, don't silently add.

Step 3: Scope & necessity check

Is all this code necessary to fulfill the request? For each file, function, abstraction, flag, test, and dependency, ask whether deleting it would still fully meet the user's actual ask. Delete anything serving only an un-requested goal — speculative generality, single-caller abstractions, "just in case" code paths, gold-plating. Scope creep is a defect, not thoroughness. Surface anything you're unsure is safe to drop rather than keeping it silently.

Step 4: Iterative compress-critique-fix loop

CI is running (Step 2); use the time to improve the code. This is the self-critique pass applied to the full diff. Read .claude/skills/pr-creation/critique-prompt.md once before the first pass.

Each pass: launch a general-purpose critique sub-agent with the full diff (git diff $CLAUDE_CODE_BASE_REF...HEAD), the commit messages (git log $CLAUDE_CODE_BASE_REF..HEAD), the current PR body, git log -n 30 --no-merges --oneline -- <changed paths>, and the open PRs that touch the same files, plus the critique prompt. These are inputs, not context: the prompt's closing question is asked of the claims and the touched subsystem's corrective history as much as the code. Assess each issue and take the easy wins — compress (dead code, unused imports, WHAT-comments, compat shims, premature abstractions), readability (names, un-nest, guard clauses), reuse (extract duplication; search for existing utilities first), parametrize/fixture tests, correctness (bugs, edge cases, security, swallowed errors). Commit fixes (Conventional Commits), then start a fresh pass (prior output is stale).

This is an adversarial review, so model: "opus" on the Agent call follows CLAUDE.md → Delegation's tiering. When the session model that wrote the diff was not Opus, add one line to the prompt: a weaker or cheaper model wrote the diff, so scrutinize it harder than a routine pass.

Every pass ends with the mandatory question — "are you making any bullshit choices?" It is the last item in the critique prompt, left undefined there on purpose. Answer it in the report and fix what it surfaces before the next pass — never answer "none" without saying what you checked. A residue that survives in the current diff goes in ## Review focus or ## Decisions made.

Stop at a fixed point (a pass with nothing actionable). The cap is at most three passes (.claude/rules/development-workflow.md § Self-critique); if real issues remain at the cap, say so in the report and proceed — don't ask. Skip for trivial changes (typos, one-line config, pure docs) — say so explicitly.

Step 5: Stress-test infrastructure changes

If the diff touches infra (sandbox config, container orchestration, firewall rules, CI workflows, entrypoint hardening, domain allowlist) and doesn't already include comprehensive tests for it, run /stress-test on those components.

Step 6: Verify critical-codepath coverage

Line coverage ≠ a test that fails when the path breaks. For each new/changed branch apply the litmus: invert this branch — does a test go red? Watch the paths happy-path tests miss: error/fail-loud branches (assert exit code and message), auto-detect/fallback arms (test each arm over stubbed inputs — an override flag doesn't cover the auto-detected arm), cleanup/teardown/idempotency (drive the real function, not a stubbed no-op), and each enumerated member. See the writing-tests skill for non-vacuity and member-by-member coverage.

For every bug this PR fixes, ask what test — knowing nothing of today's symptom — would catch this class in advance, and run it in the configuration where the bug manifests. State the property of any correct run that the bug violated and assert THAT, not the one input that first tripped it — see the writing-tests skill's "Would a generalizable invariant have caught it in advance?" for the recurring shapes and how each catches a silent degradation. This matters most for reach/wiring bugs (right logic, wrong place): the logic is identical before and after the fix, so a logic-in-isolation test passes both ways — only asserting the user-observed outcome in the broken configuration catches it.

Add a focused failing-when-broken test for any path currently exercised only incidentally. If a path is only reachable live (a real container launch), say so and name the unit-level proxy. A hard-to-test branch usually signals a refactor (extract it into a sliceable function) rather than a skip. Don't analytically predict a coverage gate's verdict when you can't run the tool locally (kcov, mutation runners) — write the obvious per-branch tests, push, and let the gate name the uncovered lines.

Every stub in the diff owes the body one line: the command you ran to reach the real dependency, what that command did, and the capability this session lacks. A stub encodes a belief about the dependency, so the body is where you show the belief was tested rather than assumed — an unrun "it cannot work here" is the commonest way a wrong belief ships green. Quote the attempt and its observed output. "apt-get install docker-sbx → Unable to locate package, which a preceding apt-get update then fixed" is a finding; "sbx is unavailable in web sessions" is a guess that survived because nobody typed the command. No probe yet means run one before writing the line.

TURN THE STUB REAL IN THIS PR when a workflow can reach the dependency — dispatch it and read the verdict before you call the PR done. "Unverified until some suite reports" names no event and no owner, so nothing ever closes it: the PR merges, the sentence is unsearchable, and the belief the stub encodes stays untested for as long as the code lives. Find the workflow that exercises the real dependency (grep .github/workflows/ for the script or check id), dispatch it against this branch or main, quote the run id and its conclusion, and fix what the red surfaces. Only when NO runner can provide the capability does the line stand — and then it names the missing capability, not the missing time, plus the suite that would cover it if the capability existed.

Step 7: Run validation

Run the project's test/lint/typecheck commands (see pr-templates.md) and fix failures; loop back to Step 4 if new defects surface. Read scripts/background-coverage.sh's verdict too, with uv run python scripts/background-verdict.py --name background-coverage: a changed file it names under 100% loops back to Step 6 for its test, and an answer that is not about HEAD is no answer.

Also run the repo's tree-wide invariant checks, not just tests for files you touched. Drift guards, SSOT-consistency checks, "every file classified" gates, and version-pin tests assert global tree properties, so file-targeted selection structurally can't surface them — and they can fail from a merged base-branch bump, not your diff. Run pre-commit run --all-files yourself, plus the fast consistency gates (discover by name, e.g. pytest -k 'consistent or drift or pinned or ssot or sync' -q). Before git restore-ing a "pre-existing/unrelated" working-tree change, confirm it isn't a required SSOT sync. When a drift gate fires, the root-cause fix is usually to convert the guard into an SSOT (make the lagging file a generated artifact) — surface that to the maintainer even if you also hand-apply the quick fix.

Every minutes-long validation command runs BACKGROUNDED — pre-commit run --all-files and the consistency gates alike. Read its task result before the claim that depends on it; an unread background run reads as verification in the transcript while verifying nothing. A local full-suite run is the one thing this does NOT cover: it is discouraged outright and never waited on, so no claim in the body should depend on one.

A diff that touches CI machinery — a workflow, a landing or queue script, the scheduler — owes its PR body the TLA+ spec sentence `.claude/rules/tla-plus.md` asks for, before Step 8 writes that body.

Step 8: Update PR title and description

If you committed anything after Step 2, re-read the diff and log and rewrite the title + body to describe the current totality of changes, not the original scope. Push new commits first. Skip if no post-Step-2 commits. Re-run the prior-art search over MERGED PRs touching your files before this rewrite — one that landed while yours was in review can already own your change, and the pre-branch search cannot see it. In a web session gh pr edit answers HTTP 403 — take the gh api -X PATCH form from pr-templates.md's table BEFORE you compose, not after the write is refused.

A rewrite keeps every statement a reviewer named as a merge condition. Before you replace a body, list the old body's sentences a review asked for — a risk the deployment owner must decide on, a trust note, a prerequisite, a failed run — and carry each one that is still true into the new body.

Compose the body from the diff and the log — NEVER read a body back with pull_request_read or issue_read and edit a slice of it. This is the SSOT for the read-back hazard; pr-templates.md points here. Both readers return the body with HTML comments and collapsed elements removed, and they escape &, " and '. The loss is silent and writes no recovery sidecar, so a body written back from that read loses every comment and fold the author wrote.

To edit a body rather than recompose it, read it losslessly: gh api "repos/<owner>/<repo>/pulls/<pr#>" --jq '.body'. gh api is REST, so it answers in a web session too where the raw API is reachable at all — try it before concluding otherwise. The net-line breakdown is CI's own comment on the PR, so no body edit can drop it.

Write the body under CLAUDE.md → Writing, and count only ## What & why against its word budget before you submit. pr-templates.md "Body Guidelines" adds the per-section budgets that bound everything below it instead. The three that decide whether a human engages: open with the fault and the change in plain words, with no identifier in the prose and no list of parts, files or functions, then ONE fenced example; DELETE every command, log excerpt and forensic detail rather than folding it away; and write a ## Review focus of at most two sentences, naming the file to read first, the cross-file invariant one screen cannot show, and the part you are least sure of.

## Proposed guards carries every guard the session's plan or self-critique named for a class this PR's diff leaves reachable, in the five-line shape pr-templates.md gives. The plan file is scratch, so a guard written only there is gone when the session ends; the body is the one place a later session or a human reads it from.

Step 9: Claim check — an independent agent confirms the account

Dispatch ONE read-only agent to confirm this PR's own claims against the tree, then fix what it confirms. Step 4 critiques the diff you wrote; this reads the ACCOUNT you wrote of it — the lead, each part line, the commit messages, the changelog fragments — and asks whether the code licenses each sentence. An overstatement ships as a fact, and the reader has no way to tell one from a measurement.

Give it Read/Grep/Glob only, so it needs no worktree, and model: "opus". The prompt carries the body and every commit message on the branch quoted in full, one line per claim with the file:line that claim rests on, and this instruction: answer CONFIRMED or REFUTED per claim, quote the code that decides it, and write the true statement in at most two sentences wherever it refutes.

Then fix the CLASS, not the sentence:

  • A claim the code does not license is corrected in EVERY artifact carrying it — body, changelog fragment, code comment — in one commit. A commit message cannot be amended, so name the correction in the body instead.
  • A caller set narrower than the claim ("a launch does this", where only a check calls it) usually means the changelog fragment is not owed at all. Read config/changelog-paths.json again before rewording one.
  • A defect it finds in the TREE rather than in your account takes root CLAUDE.md § Autonomy's scope rule: in code this diff already touches, fix it here in its own commit; elsewhere it is arm 2's or arm 3's, and neither opens a search for more sites.

One agent per PR, not per push. Re-run it only when a later push changes what the body claims.

Step 10: Wait for CI, and read the reviewer's findings too (babysitting sessions only)

Any other session skips this step: it reports the PR at Step 11 and follows it up only when the user asks.

uv run .github/scripts/pr-status.py <pr#> gives one reading of both axes below, and it is the only route that works on both surfaces — gh pr checks is GraphQL and 403s in a web session. On any failure, investigate, fix, push, update the description (Step 8), and wait again.

A CI-failure webhook for a SHA that is NOT the current PR head is supersession noise — end the turn silently; do not investigate or report it. Before acting on any CI-failure event, compare its HeadSHA to the PR's current head (gh api repos/<owner>/<repo>/pulls/<pr#> --jq .head.sha, or MCP pull_request_read get → head.sha; gh pr view --json is GraphQL and 403s in a web session). The classic generator: pushing a changelog fragment with a placeholder id makes the pre-commit autofix git mv it to <PR-number>.<category>.md and force-push the amend, cancelling your original SHA's in-flight run — the decide/reporter jobs then show cancelled, collapsing a required check to a red failure on the now-stale SHA. Avoid generating this noise by naming changelog fragments with the actual PR number up front. Same rule for the ongoing subscribe_pr_activity watch: a superseded SHA gets a silent turn, never a reply.

Green checks are only ONE of the two axes that gate a merge — never conclude "not red / nothing to change" from the check list alone. mergeable_state is the aggregate SSOT (gh api repos/<owner>/<repo>/pulls/<pr#> --jq .mergeable_state, or the MCP pull_request_read get field); a blocked state can mean a pending/failing required check OR an unresolved reviewer finding (or, on a stacked-PR child, just an open parent — bottom-up landing order; judge a child by its own check runs and threads), so inspect both:

  • Checks axis — pr-status.py / MCP get_check_runs. One of them is Review findings resolved (the review-findings gate): red until the automated reviewer's first review lands, red while any unresolved reviewer thread carries a 🔴/🟡 finding, and red while an unresolved Codex thread carries a P0 or P1 badge (config/review-severities.json § external_gating lists which logins and priorities). A Codex P2 and below is advisory, like a 🔵 nit. It reports as a commit STATUS, so get_check_runs does not list it; pr-status.py shows both kinds.
  • Findings axis — the unresolved reviewer threads (MCP pull_request_read get_review_comments, or the GraphQL reviewThreads read), Codex's P0/P1 among them. Neither reviewer posts CHANGES_REQUESTED; their whole hold is those threads. Land the fix, then resolve each addressed thread yourself (see "Resolve each review thread once you have addressed it" above) — there is no automated resolver to do it for you. Resolving a thread fires no workflow event, so the gate status stays stale until your next push or reply, or until required-check-liveness.py re-runs the gate on its next tick. Touch no label to hurry it. A human reviewer's CHANGES_REQUESTED still blocks the ordinary GitHub way and needs that reviewer's re-review or a dismissal.

Give Codex at most TWO review cycles on one PR. A cycle is one push for its P0/P1 findings plus the review of the new head. After the second, stop pushing for Codex on that PR: reply to and resolve each thread you addressed, then put what is still flagged to the user in one line with your own reading of it. Its findings reshape rather than converge — the writable-path screen it asked for in the interpreter recorder became the safe-launch.sh loader screen it then asked to make recursive over ancestors — and each round costs a full check battery. The bound stops the pushing, never the answering: a gating thread still needs its reply and its resolve.

Proceed only when all checks are green AND no gating reviewer thread is unresolved.

Step 11: Report result

Give the PR URL. A babysitting session also confirms all checks passed. Do not merge — leave the merge to the user unless they directly told you to merge this PR.

Step 12: Iteration retrospective

Skip the ## Lessons Learned section entirely when the repo this PR MERGES INTO is named claude-automation-template, whatever its owner. vars.TEMPLATE_SYNC_ORG redirects a fork to its own template, so the owner is not fixed and the name is. phone-home runs in the base repo, so your checkout does not decide: from a fork, read the PR's base repo; otherwise git remote get-url origin answers it. Never take a repo's own CLAUDE.md prose as evidence: a repo made from the template often still carries the template's opening line. In the template a lesson propagates nothing, because the change is already there. Everywhere else phone-home is the only channel, and it runs one way into the template, so omitting the section drops the lesson for good.

Skip if trivial. Otherwise state each insight as one concrete line: what could have run in parallel; where a targeted check would have beaten a full local sweep; whether the critique loop or local validation duplicated something CI caught anyway.

Error handling

  • PR already exists (HTTP 422): find it with the REST form in pr-templates.md's table, then gh pr edit.
  • Push fails: check branch permissions and remote config.
  • No changes to PR: confirm the work is actually committed.
  • Critique/test failures are fixed, never skipped (Steps 4, 7).