PR
Commit, push, and create a pull request in one automated step. Never prompt the user for input -- make opinionated decisions at every step.
Workflow
1. Gather Context
First, detect the repository's base branch:
gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name'
If gh is not available or the command fails, fall back to:
git remote show origin | grep 'HEAD branch' | sed 's/.*: //'
Use the detected value as <default-branch>.
Detect the PR base branch
The current branch may have been created from a non-default branch (e.g., for stacked PRs). Check the reflog for the branch creation point:
git reflog show $(git branch --show-current) --format='%gs' | tail -1
This produces output like branch: Created from develop, branch: Created from origin/develop, or branch: Created from refs/heads/feature/parent. If the last entry matches branch: Created from <name>, extract <name> and normalize it by stripping any refs/heads/, refs/remotes/origin/, or leading origin/ prefix.
If a source branch name was extracted, verify it is a valid base for the PR. Run these checks in order, stopping at the first failure:
Not the current branch: The source branch must differ from the current branch.
Exists on the remote: Note that
git ls-remotealways exits 0 regardless of whether the branch exists, so check for non-empty output:git ls-remote --heads origin <source-branch> | grep -q .Not already merged into the default branch: The source branch may have been merged into the default branch since this branch was created (e.g., a parent feature branch that has since landed). Check whether the source branch is an ancestor of the default branch:
git fetch origin <default-branch> <source-branch> --quiet git merge-base --is-ancestor origin/<source-branch> origin/<default-branch>If the exit code is 0, the source branch has been fully merged into the default branch. Skip it and use
<default-branch>as<base-branch>instead.
If all three checks pass, use the source branch as <base-branch>. If any check fails, use <default-branch> as <base-branch>.
Then run these commands in parallel to understand the current state:
# Current branch and changed files
git status
# Staged changes
git diff --cached
# Unstaged changes
git diff
# Recent commit messages for style reference
git log --oneline -10
# Full diff of this branch against the base branch
git diff <base-branch>...HEAD
# Commit history of this branch since diverging from the base branch
git log --oneline <base-branch>..HEAD
# Check remote tracking status
git rev-parse --abbrev-ref --symbolic-full-name @{u} 2> /dev/null || echo "no upstream"
2. Detect Connected Issues
Search for GitHub issues that this branch addresses. Combine results from the strategies below, deduplicate by issue number, and record the final list for use in the commit message (step 4) and PR body (step 7).
Strategy 1 -- Issue numbers in the branch name
Extract the current branch name. Look for issue numbers in patterns like:
TYPE/N-description(e.g.,fix/42-login-bug→ #42)TYPE/description-N(e.g.,feature/login-bug-42→ #42)TYPE/issue-NorTYPE/issue-N-description(e.g.,fix/issue-42→ #42)N-description(e.g.,42-add-login→ #42)
For each candidate number, verify it refers to an existing issue:
gh issue view NUMBER --json number,title,state --jq '.number' 2> /dev/null
Only include it if the command succeeds (the issue exists).
Strategy 2 -- Issue references in commit messages
Scan the git log <base-branch>..HEAD output (already gathered in step 1) for #N references. Collect all unique issue numbers. For each, verify it refers to an actual issue:
gh issue view NUMBER --json number,title,state --jq '.number' 2> /dev/null
Strategy 3 -- GitHub issue search by branch slug
Only run this strategy if strategies 1 and 2 found zero issues.
Extract the slug portion of the branch name (everything after the first /). Convert hyphens to spaces to form search keywords. Search for matching open issues:
gh issue list --search "KEYWORDS" --state open --json number,title --limit 5
Evaluate the results:
- If exactly one issue is returned, include it.
- If multiple issues are returned, compare each issue title against the branch slug. Include an issue only if its title, when slugified (lowercased, spaces and special characters replaced with hyphens), is a near-exact match with the branch slug. If no single issue clearly matches, include none.
- If zero issues are returned, skip.
Combine results
Merge issue numbers from all three strategies into a single deduplicated list. Preserve the order: branch-name issues first, then commit-message issues, then search-matched issues. Note the branch type prefix (fix/* vs other) for choosing the closing keyword later.
3. Validate Preconditions
Stop and report an error if any of these are true:
- The current branch is the base branch. Do not create a PR from the base branch to itself.
- There are no changes to commit and no commits ahead of the base branch. There is nothing to open a PR for.
4. Commit Changes (if needed)
If there are no staged changes, unstaged changes, or untracked files, skip this step.
Never stage files that likely contain secrets (.env, credentials.json, *.pem, *.key, etc.). If such files are detected, warn the user and exclude them.
Handle plan files
Before identifying logical chunks, check for plan files among the uncommitted changes. Plan files live under docs/plans/ and its subdirectories (todo/, done/). If any Markdown files in these directories are among the staged, unstaged, or untracked files, apply Plan-Aware Commits rules to them.
Plan files always form their own logical chunk, committed separately from code changes. Process the plan file chunk first, then proceed with the remaining changes.
Identify logical chunks
Review all uncommitted changes (excluding any plan files already handled above) and group them into the smallest logical chunks. Each chunk should be a self-contained, coherent change that makes sense on its own:
- Examine the diff: Look at all changed and untracked files and understand what each change accomplishes.
- Group by purpose: Changes that serve the same purpose belong together. A new function and its tests are one chunk, but an unrelated formatting fix is a separate chunk.
- Check for independence: If a change can be committed on its own without leaving the codebase in a broken or inconsistent state, it is a candidate for its own chunk.
- Respect dependencies: If change B depends on change A, commit A first.
If all changes form a single logical chunk, create one commit. If they form multiple chunks, create a commit for each, processing them sequentially.
Create each commit
For each chunk:
- Stage only the files belonging to the current chunk using
git addwith specific file paths. - Analyze the diff to generate a commit message:
- Examine
git log --oneline -10output to match the repository's commit message style. - Determine the commit type (
feat,fix,docs,refactor,test,chore,style) based on the changes. - Write a concise description (under 72 characters) focused on why the change was made.
- Reference connected issues detected in step 2. For
fix/*branches, usefixes #N; for other branch types, usecloses #N. If no connected issues were detected, omit issue references from the commit message. Only reference issues in the commit that most directly addresses them.
- Examine
- Create the commit using GPG signing and a HEREDOC:
git commit -S -m "$(
cat << 'EOF'
type: description here
EOF
)"
CRITICAL: Never use git commit --amend. Always create a new commit. If a pre-commit hook fails, fix the issue, re-stage, and create a new commit.
5. Lint and Fix
Run the lint-and-fix skill to catch lint and formatting errors before pushing. This prevents CI failures from code that does not pass project linters.
Invoke the
lint-and-fixskill using the Skill tool with--no-push:lint-and-fix --no-push Parent continuation: - Caller: pr - Resume target: Step 6, push branch, then Step 7, create the pull request. - On lint success: Continue immediately to Step 6 without asking the user for confirmation. - On lint failure or skipped required lint work: Stop before push and PR creation, then report the unresolved lint state.This runs all detected project linters and formatters, auto-fixes what it can, manually resolves remaining issues, and commits the fixes without pushing.
If no linters are detected: Proceed to step 6. The absence of linters is not an error.
If all linters pass (with or without auto-fixes) and no issues remain unresolved or skipped: Proceed to step 6. Any fix commits created by
lint-and-fixwill be included in the push.If any linting issues remain unresolved, any required lint work is skipped, or a required linter cannot run: Stop and report the unresolved lint state. Do not push or create the PR. The user must resolve the remaining issues before retrying.
6. Push to Remote
Push the branch to the remote:
git push
If the branch has no upstream, use:
git push -u origin HEAD
If the push is rejected because the remote has diverged, report the error and stop. Never force push.
7. Create the Pull Request
Analyze all commits on the branch (from git log <base-branch>..HEAD and git diff <base-branch>...HEAD) to generate the PR title and body.
Detect the title convention
The title rules below are this skill's default, not an override. They yield to a PR title convention that the project enforces in CI, or that the project or the user documents in an agent config. Check these signals in order and stop at the first match:
CI lints the PR title. Use Glob to find
.github/workflows/*.ymland.github/workflows/*.yaml, then read each one. Look for a workflow that feedsgithub.event.pull_request.titleinto a linter, either piped intocommitlint:- name: Lint PR title env: PR_TITLE: ${{ github.event.pull_request.title }} run: printf '%s\n' "$PR_TITLE" | pnpm exec commitlintor through an action such as
amannn/action-semantic-pull-request.A commitlint config on its own (
.commitlintrc*,commitlint.config.*, or acommitlintkey inpackage.json) is not sufficient. It usually lints commit messages rather than the PR title. The reference togithub.event.pull_request.titleis what makes the title itself constrained.When such a workflow exists, take the title rules from whichever linter it runs, not from the defaults below:
- commitlint: read the config it resolves (
.commitlintrc*,commitlint.config.*, or thecommitlintkey inpackage.json) and follow itstype-enum,scope-enum,subject-case, andheader-max-length. Where the config only extends a preset, the preset supplies those values. - An action such as
amannn/action-semantic-pull-request: the rules live in the workflow step's ownwith:block (types,scopes,requireScope,subjectPattern), not in a commitlint config. Read them there. - Anything else: read whatever config the step points at, and fall back to the defaults below only for values it does not set.
- commitlint: read the config it resolves (
Project agent config states a PR title format. Read whichever of these exist:
CLAUDE.mdandAGENTS.mdin the repository root, andcopilot-instructions.mdunder.github/. Any of them may be absent, which is normal and not an error, andCLAUDE.mdis often a symlink toAGENTS.md, so read the target rather than reporting a duplicate. Also honor any user-level instructions already present in context. If any of them specify a PR title format, follow it.Merged PR titles are consistent. As a fallback:
gh pr list --state merged --limit 20 --json title --jq '.[].title'If most of the returned titles match
^[a-z]+(\([^)]+\))?!?:\s, the project uses conventional-commit PR titles. Match that style.
If no signal matches, no convention is enforced, so use the defaults below.
If signal 1 matched, record the workflow's display name, meaning its top-level name: value rather than its filename. Step 8 matches that string against the workflow field of gh pr checks, which reports display names. A workflow file with no top-level name: is reported by its path instead, so record the path in that case.
Title
When a convention was detected, follow it and skip the defaults below. Derive the conventional-commit type from the commits on the branch using the same type selection as step 4, use the project's scope vocabulary if it defines one, and respect its configured subject case and length. Take the length from the project's own configuration rather than assuming this skill's 70-character default: under @commitlint/config-conventional, header-max-length is 100. That preset also sets type-case to lower-case and sets subject-case to reject sentence-case, start-case, pascal-case, and upper-case subjects, so the shape is type(scope): subject with the subject left uncapitalized.
Otherwise, use this skill's defaults:
- Under 70 characters.
- Summarize the overall change, not individual commits.
- Use sentence case (capitalize the first word only).
- Do not include a conventional-commit type prefix in the PR title.
Body
Use the following format:
## Summary
- Bullet point describing key change 1
- Bullet point describing key change 2
- Bullet point describing key change 3
## Test plan
- [ ] TODO: describe how to verify this change
## Closes
Closes #N
Keep the summary to 1-4 bullet points. Focus on what changed and why.
If connected issues were detected in step 2, add a ## Closes section after ## Test plan. Use one line per issue with the appropriate keyword:
- For issues detected from a
fix/*branch:Fixes #N - For all other issues:
Closes #N
If no connected issues were detected, omit the ## Closes section entirely.
Create the PR
First, generate a unique temporary file path using mktemp -u:
mktemp -u /tmp/gh-pr-body-XXXXXX
# Returns a unique path that does NOT exist on disk, e.g.: /tmp/gh-pr-body-x4y5z6
The -u flag is required. Plain mktemp creates an empty file at the path it prints, and the Write tool refuses to overwrite a file it has not Read first, so the write fails with File has not been read yet. With -u the path is unique but unoccupied, so Write creates it fresh.
Then use the Write tool to write the full PR body (Summary, Test plan, and Closes sections) to the exact path returned by mktemp -u. In the examples below, TMPFILE is a placeholder for that path.
Then create the PR with --body-file:
gh pr create --title "the pr title" --body-file TMPFILE
Pass --base <base-branch> if <base-branch> differs from <default-branch>. Do not pass --draft. Do not add labels or reviewers.
Never batch the Write call and gh pr create into one message. Issue them as two separate, sequential tool calls, and wait for the Write to return before invoking gh. gh reads the body file at invocation time, so a parallel batch can start gh pr create before the file exists and open the PR with an empty body. The command still succeeds and still prints a URL, so the failure is silent. This is a deliberate exception to the general preference for parallel tool calls: that preference covers calls with no dependencies between them, and these two are dependent, because gh pr create consumes the file Write produces.
Verify the PR body
Run this step only if gh pr create succeeded and printed a PR URL. If it failed, no PR exists and there is no URL to pass, so skip both verification and recovery, go straight to cleanup, and handle the failure per Error Handling. Never substitute a placeholder or a URL left over from an earlier run.
gh pr create prints the PR URL on success, but a successful exit says nothing about whether the body landed. Before cleaning up, confirm the stored body is non-empty. Pass the URL that gh pr create just returned, shown below as <pr-url>; it is the identifier this step is guaranteed to have. (gh pr view also accepts a bare PR number, or no argument at all, in which case it targets the current branch's PR.)
gh pr view <pr-url> --json body --jq '.body | length'
If the length is 0, the body file was empty or missing when gh read it. Recover by re-writing TMPFILE with the Write tool and then, as a separate call:
gh pr edit <pr-url> --body-file TMPFILE
Re-run the length check to confirm the recovery worked.
Clean up the tmpfile
Always remove the tmpfile after the PR creation attempt, regardless of whether it succeeded or failed. When creation succeeded, run the cleanup only after the verification above, since recovery needs the file to still exist. When creation failed, verification is skipped, so clean up immediately. Issue the cleanup as a separate Bash tool call, not chained onto gh pr create:
rm -f TMPFILE
Each Bash tool call runs unconditionally and the prior call's exit code is preserved by the harness, so a separate call cleans up after both successful and failed PR creations without any shell-level wrapping. Never combine the two with ; followed by an exit-code preservation idiom such as gh pr create ...; status=$?; rm -f TMPFILE; exit $status. In zsh (the macOS default shell), status is a read-only built-in alias for $?, so the assignment fails with read-only variable: status and falsely reports a successful PR creation as failed. See the use-git skill's tmpfile pattern reference for the full rationale.
8. Verify the Title Check
Skip this step entirely unless step 7 found a workflow that lints the PR title. When it did, confirm the title passed:
gh pr checks --json name,state,link,description,workflow 2> /dev/null || true
gh pr checks exits non-zero when checks are failing or still pending, so a non-zero exit is not an error here. Checks also frequently have not registered yet immediately after gh pr create.
This command returns every check on the PR, so narrow the result to the title lint before judging it. The workflow field holds the workflow's display name (its top-level name:, for example CI), and name holds the individual check or job name (for example Lint and validate). Match workflow against the display name recorded in step 7. When a workflow contributes several checks, use name to pick the title-lint job among them. Only that check's state matters here; a red check belonging to any other workflow is out of scope for this step and belongs in the step 9 report as an ordinary CI failure.
Title check passed: continue to step 9.
Title check failed: report that check's
name, itsdescription(the short summary the check itself supplies;gh pr checksexposes no fuller reason, so link out rather than inventing one), and itslink. Then give the user the exact remediation command with a corrected title:gh pr edit --title "<corrected title>"Do not run
gh pr editautomatically.Checks pending, or the title check is not among those returned: report that the title check has not reported yet and continue.
This step is best-effort and must never block the workflow.
9. Report Results
After the PR is created, report:
- The PR URL (returned by
gh pr create). - The PR title, and whether a project title convention was detected (naming which signal matched) or the skill's defaults were used.
- The commit hash(es) included.
- A brief summary of what was committed and pushed.
- Connected issues (if any) and the closing keywords used.
Plan-Aware Commits
When plan files are detected among uncommitted changes, apply the rules in this section during step 4.
Detecting Plan Files
Plan files live under docs/plans/ and its subdirectories (todo/, done/). Look for Markdown files in these directories among the changed or untracked files.
Plan Name Cleanup
Well-named plans follow the pattern YYYY-MM-DD-meaningful-description.md. Auto-generated names are nonsensical word combinations with no datestamp (e.g., wandering-copper-lantern.md, quizzical-amber-turnstile.md).
Every time a plan file is part of a commit, check its filename. If the name lacks a datestamp prefix or uses a nonsensical auto-generated name:
- Read the plan file to understand its content.
- Choose a meaningful slug derived from the plan's title or purpose (e.g.,
consolidate-ci-workflows,add-user-authentication). - Rename the file to
YYYY-MM-DD-<slug>.md, using the current date for new plans or the date from the plan's title/content if one is stated. - Stage both the deletion of the old path and the addition of the new path.
If the filename already has a datestamp prefix and a meaningful description, leave it as-is.
Moving Completed Plans
Creating a PR typically means the work described in a plan is complete. When plan files are detected among the changes:
- Check whether the plan's work is complete. Indicators: all the code changes on the branch correspond to the plan, or the user has already stated that the work is done.
- If the work is complete, apply Plan Name Cleanup first (if needed), so any rename happens before the move.
- Move the (possibly renamed) plan file to
docs/plans/done/(creating the directory if it does not exist). If both a rename and a move apply, perform a singlegit mvfrom the original path directly to the final destination (e.g.,docs/plans/done/YYYY-MM-DD-slug.md). - If the plan is currently in
docs/plans/todo/, the move goes fromtodo/todone/. - If the plan is in the
docs/plans/root, the move goes from there todone/. - Stage the rename (if any) and the file move together as part of the plan commit.
Do not move a plan to done/ if the work is only partially complete. Plans for work still in progress should stay in docs/plans/todo/ or the docs/plans/ root.
Plan Commit Message
When committing plan files, use a message like docs: add plan for <meaningful-description> for new plans, or docs: move plan to done for <meaningful-description> when moving a completed plan.
Error Handling
- On the base branch: Report that PRs cannot be created from the base branch. Suggest creating a feature branch first.
- Nothing to commit and no commits ahead: Report there is nothing to create a PR for.
- Pre-commit hook failure: Fix the issue, re-stage, and create a new commit (never amend).
- Lint issues unresolved: If the
lint-and-fixskill reports unresolved issues, skipped items, or a required linter that cannot run, stop before pushing. Report the remaining lint errors and suggest the user fix them manually before retrying/pr. - Push rejected: Report the error. Suggest
git pull --rebaseif the remote has diverged. Never force push. - PR already exists: If
gh pr createfails because a PR already exists for this branch, rungh pr view --webto open the existing PR and report it to the user. - PR title lint check fails: Report the failing check and a corrected title, following step 8. Never delete and recreate the PR to fix the title;
gh pr edit --titleis the remedy, and the user runs it. - No gh CLI: Report that the
ghCLI is required and link to https://cli.github.com/. - Secret files detected: Warn the user and exclude them from staging. Continue with the remaining files.
- Issue detection fails: If
gh issue vieworgh issue listcommands fail (network error, auth issue), skip issue detection silently and proceed without the## Closessection. Issue detection is best-effort and must never block PR creation. - Detected issue is already closed: Still include it in the
## Closessection. GitHub handles this gracefully (the keyword is a no-op for already-closed issues, and it still creates a visible cross-reference).