prkit
Turn the commits on the current branch into a clean GitHub pull request: a title in the repo's commit style, a body that explains
what changed and why, and a test plan, all inferred from the real diff, not guessed. Creation goes through the
CLI, reusing the repo's PR template when one exists.
When this fires
The user wants to open a pull request: "open a PR", "create a pull request", "raise a PR", "submit this for review", "gh pr create". If they only want the PR title and body
drafted (not opened), do everything except the final
and print the result instead.
Procedure
1. Preflight
Confirm the tooling and branch are ready before writing anything:
sh
gh --version # gh installed?
gh auth status # authenticated?
git branch --show-current
- If is missing or unauthenticated, say so and point to / . Don't try to work around it.
- If
git branch --show-current
is empty, stop: detached HEAD needs a branch before a PR can be opened. Offer to create or switch to one.
- If the current branch is the default branch, stop: a PR needs a feature branch. Offer to create one () before continuing, and get the name from gitkit, which owns branch naming, rather than inventing a shape here. Work that traces to an issue gets ; anything else keeps whatever name the repo's convention or the human supplies.
2. Gather context
Get the base branch from gitkit, then read what the branch actually changes; that diff is the raw material for the title and body. Fetch first so every ref below is the real remote state, not a stale local copy:
sh
git fetch origin # refresh remote-tracking refs before anything else
git log origin/<base>..HEAD --format='%s%n%b' # commits in this PR, with their bodies
git diff origin/<base>...HEAD --stat # files touched
Read the full diff (git diff origin/<base>...HEAD
) only when the commits don't already explain the change. On a branch built through this workflow they usually do, because commitkit wrote each message from the change itself, so the log is a summary of exactly the material a PR body needs, and re-deriving it from the raw diff produces a worse description at many times the cost. Reach for the full diff when the commit messages are thin or generic (a branch of
and
commits, or work that came from outside this workflow), when the stat shows files no commit message accounts for, or when you need a specific detail for the test plan. Skip lockfiles, build output, and vendored directories either way.
gitkit owns base-ref resolution. Ask it for the base rather than re-deriving the ladder here; repos whose default is
or
are real, and getting this wrong silently produces an empty or enormous diff. Without gitkit,
gh repo view --json defaultBranchRef -q .defaultBranchRef.name
is the authoritative fallback, and ask rather than guess when it can't answer. Re-check the current branch against the base gitkit returns and stop if they match.
Diff against
(the just-fetched remote tip), not a local
that may be behind. Otherwise the title, body, and file list are computed against commits that are no longer the merge target.
Use the commits, branch name (e.g.
), and diff to determine the scope, the type of change, and any issue reference (
,
). If a linked issue clearly matters and you can't find it, ask rather than invent one.
3. Sync with the base branch
Before pushing, make sure the branch is up to date with the base tip you just fetched. A PR opened from a stale branch either merges outdated code or lands with GitHub's "This branch has conflicts" banner:
sh
git rev-list --left-right --count origin/<base>...HEAD # "<behind>\t<ahead>"; left > 0 means behind
- Behind by zero: nothing to do, so go to Push the branch.
- Behind: the branch needs brought in. gitkit owns the sync rule, and it resolves to rebase (), giving the PR a clean diff. Whether that needs an OK first turns on one thing gitkit states in full: an unpublished branch rebases straight through, because nothing outside this machine points at the commits being rewritten; a branch already pushed previews the rebase and its together and waits. At PR-open time the branch is usually the former, which is why this step normally runs without a prompt.
- Rebase conflicts: if the rebase stops on a conflict, stop and surface it. List the conflicted files (
git diff --name-only --diff-filter=U
) and resolve them (or hand them back to the user), then complete the rebase (). Do not push, and do not open the PR, until the working tree is clean and the sync is finished. If the user declines the sync, say the PR may show conflicts and proceed only if they confirm.
Don't re-read the diff after a clean rebase. A rebase replays your commits onto a new base; it doesn't change what they say or do, so the title and body you derived above still describe the branch correctly. The one exception is a rebase you resolved
conflicts in, because there you made real edits during the replay, and the resolved result is genuinely different from what you read. Re-read just the files you touched resolving them (
git diff origin/<base>...HEAD -- <paths>
), not the whole branch.
4. Push the branch
First, commit any handed-in path. When a caller hands prkit a file that must travel with the branch, most often a QA plan at
docs/qa/qa-<slug>-YYYY-MM-DD.md
, and that file is still uncommitted, commit it here rather than leaving it behind or spawning something else to do it. prkit is already the step that touches git, and it was given the path, so there is nothing to rediscover:
sh
git add <handed-in path> && git commit -m "docs(qa): add manual QA plan for <feature>"
Only a path the caller
named. This is not a licence to sweep the working tree: uncommitted work nobody mentioned is still covered by the rule in
Notes, so point it out and offer, don't commit it silently.
The remote branch must exist before a PR can point at it:
If the branch was rebased (
Sync with the base branch) and the remote rejects a normal push, use
git push --force-with-lease
(never bare
). Don't ask again here: a rejected push means the branch was already published, and gitkit's rule covers the rebase and its lease push under a
single confirmation taken during the sync. If that OK wasn't given, because the branch looked unpublished and the rejection is the first sign it wasn't, stop and ask then.
5. Write the title and body
- Title: one line, imperative, in the repo's commit style (match , often Conventional Commits like
feat(auth): add SSO login
). No trailing period.
- Body: if
.github/pull_request_template.md
(or ) exists, read it and fill it in exactly, matching its sections and checkboxes. Otherwise use: a one-paragraph Summary of what changed and why, a Changes bullet list, and a Test plan (how it was verified, or checkboxes for what to run). Reference the issue in the body () when there is one.
6. Embed proof artifacts (if present)
This step is optional and runs only when a verifykit proof bundle exists. verifykit leaves a dated bundle at
docs/verify/verify-<slug>-YYYY-MM-DD/
(slug = the linked issue number, else the feature slug) with a ready-to-embed
. If more than one matches, use the newest creation date; if multiple bundles share that date, ask which run to use. Splice the selected proof into the body under a
Proof section. The images are already published to a hidden
ref with SHA-pinned raw URLs that render inline, so there's no upload work here; just embed the fragment as-is. If no bundle exists, skip this entirely and open the PR exactly as before. If a bundle exists but its
points at local paths (verifykit couldn't publish, e.g. on a private repo), don't embed dead links: add a short note listing the local artifact paths for manual attachment instead.
7. Create or update the PR
First check for an existing PR on this branch so you update instead of duplicating:
sh
gh pr view --json url,state 2>/dev/null
- If the command returns a PR with equal to : update it with
gh pr edit --title "…" --body-file <file>
rather than opening a second.
- If no PR exists, or the returned PR is merged/closed: write the body to a temp file and create a new one. Passing multi-line markdown with checkboxes through is flaky; is reliable.
sh
gh pr create --base <base> --title "…" --body-file <bodyfile>
# add --draft when the user wants a draft, or the work is incomplete
Use a path in the system temp dir for the body file and remove it afterward.
8. Advance the linked issue
Opening the PR is the moment the linked issue moves from being worked to awaiting review, so flip its lifecycle label
→
(the same transition issuekit's
mode performs when a PR opens). Do this only when the PR references an issue, meaning the
/
found in
Gather context; skip this step entirely if there is none.
- Prefer issuekit when it's installed. Invoke it to reconcile the label so the tracker logic lives in one place. Otherwise fall back to the equivalent call yourself:
sh
gh issue edit <n> --remove-label in-progress --add-label in-review
- Run it without asking. This is prkit's one exemption from the preview rule, and it belongs to this step rather than to whoever called it. Opening the pull request is the instruction to move the issue to review, so a confirmation asks a question the invocation already answered, and it costs the one thing this step protects: an issue that still advertises itself as being worked while its PR sits open for review. Report the flip in the hand-off rather than proposing it first.
- The exemption covers two starting states and no others. An issue carrying gets the flip above. An issue carrying gets added, with no removal, and you say what you found. That is the state issuekit produces, so it is the other one a PR can legitimately arrive from.
- Any other lifecycle state is drift, not a transition. For , , , , already , or no lifecycle label at all, stop and change nothing. Report the state you found. Ask when a human is present; escalate when the run is unattended. A label nobody checked is worse than a label nobody set.
- Everything else in prkit still previews. The exemption is this one label move. Creating the PR, committing a handed-in path, and force-pushing a sync are unchanged.
- If the label is missing from the repo, point the user at repokit or give
gh label create in-review --color 5319E7 --description "a PR is open, awaiting review or merge"
, and don't mutate around the gap. The exemption skips the prompt, never the provisioning check.
9. Hand off
Write this section in the procedural register: one instruction per sentence, active voice, present tense, no metaphor.
What changed. Report the PR created or updated (title and number), whether a sync rebase ran, whether a handed-in path was committed, whether a proof section was embedded, and whether the linked issue was flipped to
.
Where it landed. Give the PR URL and the branch it points at. Mention that CI will run if configured.
Next. The PR now waits on review, so the move is on the reviewer's side:
mergekit when it's installed pulls it down into a worktree for local review and QA; otherwise review it on GitHub. First offer, don't auto-run, the small follow-ups when they apply:
gh pr edit --add-reviewer <user>
,
, or
for a draft. prkit's job ends here.
Notes
- Never merge, close, or force-push without an explicit ask. Creating or editing a PR is fine; is not, unless requested.
- Uncommitted changes are not in a PR. If shows staged or unstaged work the user seems to want included, point it out and offer to commit first, rather than silently leaving it behind or committing it without asking.
- If the branch is not ahead of the base (no commits), stop and say there's nothing to open a PR for.
- Proof embedding is optional and self-contained. prkit only reads verifykit's and embeds it; it never runs the publish itself (that's verifykit's job, with its own bundled script). No verifykit bundle → no Proof section, and prkit works exactly as it always has.
- Advancing the linked issue is optional and exempt from the preview rule. The flip only happens when the PR references an issue, and prefers issuekit when installed, falling back to a plain . It runs unprompted from or and refuses every other state, because those two are the only ones a PR legitimately arrives from. The exemption is the step's, not the caller's: a human at the keyboard and an unattended orchestrator get exactly the same behavior, and prkit never widens it. No linked issue → prkit opens the PR exactly as before.
- No shell or available (e.g. a browser-based agent)? Then you can't push or call . Instead read the diff the user provides and print the finished PR title and body as codeblocks for them to paste into the GitHub "New pull request" form.