Skip to content
Back to skills

To Pull Requests

ASecurity

Prepare and open a pull request for an already pushed issue branch. Use after `/implement` or `/fast-implement` offers it, or when the developer explicitly asks to open a PR.

  • 3 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 19, 2026
developmentpythongoshellbashcode-reviewgitapibackend

Works with

  • terminal
  • cli
  • api

Security analysis

A100/100

Scanned October 19, 2026

npx -y skills add PVMalove/claude-agent-harness --skill to-pull-requests --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of To Pull Requests?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for To Pull Requests
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/pvmalove-to-pull-requests/badge)](https://www.skillsdirectory.com/skills/pvmalove-to-pull-requests)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: to-pull-requests
description: Prepare and open a pull request for an already pushed issue branch. Use after `/implement` or `/fast-implement` offers it, or when the developer explicitly asks to open a PR.
disable-model-invocation: true
---

# To Pull Requests

Create the pull request for the named ticket. This is a manual command: never run it merely because `/implement` completed.

On GitLab, name the project explicitly: every `glab` command takes `-R <project-url>`, and every `glab api` call is written `GITLAB_HOST=<host> glab api projects/<project-id>/...`. `<host>`, `<project-url>` and `<project-id>` are defined in `docs/agents/issue-tracker.md` → GitLab → Conventions. A GitLab merge request is referenced as `!<iid>` and an issue as `#<iid>`; on GitHub both are `#<n>`.

On the local markdown tracker (`.scratch/<feature>/issues/NN-*.md`) there is no PR or MR (`docs/agents/git-workflow.md` §2, step 6), so steps 3–7 do not apply. Take the target branch from the ticket's `**Integration branch:**` line, or `base_branch` for an epic-less ticket, and run the branch checks of step 2; skip its upstream checks when the repository has no remote. Ask the developer to merge the issue branch into that target themselves and to confirm the merge; never merge it yourself. After the confirmation, require `git merge-base --is-ancestor HEAD <target-ref>` to succeed, where `<target-ref>` is `origin/<target-branch>` after a fetch, or the local `<target-branch>` when the repository has no remote. Then set the ticket file's `**Workflow:**` line to `done` (when it carries `**Task report:** required`, fold the completion report into that same update) and continue with steps 8 and 9.

1. Resolve the ticket and its exact PR target branch from the ticket's `## Integration Branch` section, its parent epic, or `base_branch` for an epic-less ticket. If this cannot be resolved, stop and ask the developer.
2. Verify that the current branch matches `branch_pattern`, is neither `base_branch` nor `integration/*`, has no uncommitted changes, and has no commits absent from its upstream branch. If the branch has not been pushed, stop and ask the developer to return to `/implement` or `/fast-implement`, whichever route the ticket is on. Fetch its upstream and require `git rev-parse HEAD` to equal the upstream branch SHA; if it differs, fast-forward the clean local branch before continuing.
3. For a valid `backend-orchestration` opt-in, resolve the full current SHA with `git rev-parse HEAD` after that synchronization and validate it before any PR action. Every command below is `python .harness/orchestration/coordinator.py --repo . <command>`, and `<ticket-branch>` stands for `--ticket "#<ID>" --branch "<current-issue-branch>"`.
   First observe the integration record, read-only: `integration status <ticket-branch>`. A refusal saying that no integration record exists does not block the PR: skip the integration steps in this skill and continue with the evidence check and the old flow. If it reports `unavailable`, the integration ref could not be read: stop and report it to the developer as an operational stop, never as a code failure. If `refreshes` is not empty, the branch was already refreshed: validate `original_candidate_sha` below instead of the current SHA, because the old accepted QA only permits entering PR preparation and is never QA of the refreshed candidate.
   Then validate the accepted QA evidence of that SHA:
   `python .harness/orchestration/coordinator.py --repo . qa evidence --ticket "#<ID>" --branch "<current-issue-branch>" --candidate-commit "<full-SHA>"`.
   The CLI selects the unique batch with accepted QA evidence for that SHA, so abandoned
   historical batches with the same ticket and branch do not matter. If more than one batch accepted that exact
   SHA, stop and repeat the command with its explicit `--batch <batch-id>`. Stop if the command rejects
   missing, unaccepted, or SHA-mismatched evidence, unless the ticket is explicitly labeled `pipeline::fast`. For `pipeline::fast` tickets, bypass the coordinator evidence check.
   Then ask for the next step, read-only: `integration next <ticket-branch>`. It never writes Git, the ledger, a dispatch or a PR; act on its `step`:
   - `refresh`: the integration ref moved. Run `integration refresh <ticket-branch>`; a clean rebase updates only your own issue branch (it does not need re-review and spends no resolver cycle), so make sure `git rev-parse HEAD` equals the new `candidate_sha` and repeat `integration next`. On `state: conflict` run `integration resolve <ticket-branch>`, which only creates the resolver batch, and hand its `batch_id` to the coordinator session (`batch approve`, then `dispatch create --role conflict-resolver`). On any other refusal (dirty worktree, changed remote) stop and hand the data to the developer through the rebase route of ADR 0012; do not create a dispatch or rerun QA yourself.
   - `resolver-open`: an open resolver batch owns the branch. Stop and wait for the coordinator session; no cycle is spent by waiting.
   - `route-failure` or `human-decision`: a check of the current pair failed. Stop and hand the data to the coordinator session. The same resolver takes a failure of a refreshed pair inside its budget (`integration resolve`); a failure of the original pair is the task's own defect and goes to a regular developer with review and QA: the coordinator session plans a new batch of the same ticket and issue branch through the ordinary `/implement` route (`batch create --ticket <ticket> --branch <branch> --worktree <worktree> --integration-ref <integration ref>`, then architect, developer, code-review, QA and publish; the completed source batch is terminal, is never decided again and does not block the new one), and after its accepted publish runs `integration prepare --ticket <ticket> --branch <branch> --batch <new batch>`, so repeat `integration next --record <new record> --pull-request <n>`. Once the follow-up record exists, one ticket and branch have two integration records and `<ticket-branch>` is refused with "several integration records match": `--record <new record>` replaces `<ticket-branch>` in the integration commands of steps 3, 6, 6a and 7. `human-decision` means the two resolver cycles or the same-target fixes are spent: only the developer's decision extends them (`integration resolver-event --kind human-decision --extends-budget`), and an answer never resets the budget.
   - `confirm-pr`: continue. `qa_source` is `original-qa` (the original pair), `verification-pending-after-pr` (a refreshed pair that no CI or local QA covers yet), or `ci` / `local-qa` (a refreshed pair that already has that verified evidence).
   Do not rerun `/qa-gate` when this validation succeeds; instead, from the PR branch checkout verified in step 2, run `bash "$CLAUDE_PROJECT_DIR/.claude/hooks/record-qa-gate-pass.sh"` so the PR hook accepts that SHA. For a refreshed SHA this marker only permits opening the PR; it is not QA of the new candidate, which step 6a verifies after the PR exists. If the project is not a valid opt-in, or if the ticket is `pipeline::fast`, run `/qa-gate` if this repository provides it and
   stop on failure.
4. Prepare the PR body using `docs/agents/git-workflow.md` §3. Store it only at `.harness/.sandboxes/pr_body/pr-body-<issue>-<slug>.md`, never in `docs/tasks/`. A developer may manually run `pr-composer` in the coding application; otherwise fill in the template directly. Resolve the repository default branch — GitHub: `gh repo view --json defaultBranchRef --jq .defaultBranchRef.name`; GitLab: the `default_branch` field of `GITLAB_HOST=<host> glab api projects/<project-id>`. Use `Closes #<ID>` only when the step 1 target branch is that default branch, otherwise `Related to #<ID>`. GitLab closes an issue by `Closes #<ID>` only when the MR lands in the default branch, and a project can turn that auto-close off, so step 7 always checks the ticket state.
5. Ask the developer for explicit, separate confirmation that the branch is ready to become a PR; this question is never merged with another. For an opt-in project state in it the exact `candidate_sha` and `target_sha` from `integration next`, the PR target branch and `qa_source`. The confirmation covers only that pair. Stop for their answer.
6. After approval, open the PR/MR from the branch checkout verified in step 2. For an opt-in project run `integration next <ticket-branch>` once more immediately before: if `candidate_sha`, `target_sha` or `step` (`confirm-pr`) changed, the confirmation is void; repeat the steps above and ask again. The PR title and body, like every commit, carry project metadata only: no automated-agent attribution, model names or `Co-Authored-By`. `<target-branch>` is the step 1 target; without it, both CLIs would target the default branch. `<title>` is the ticket title in single quotes, with each apostrophe written as `'\''` in a POSIX shell or as `''` in PowerShell. Run one publication command per shell call:
    - **GitHub:** `gh pr create --base <target-branch> --title '<title>' --body-file <path>`.
    - **GitLab:** `glab mr create -R <project-url> --target-branch <target-branch> --title '<title>' --description-file <path> --yes`. The MR is `!<iid>`, where `<iid>` is the last segment of the `.../-/merge_requests/<iid>` URL the command prints.

    Delete the `.harness/.sandboxes/pr_body/` body file only after that command succeeds; on failure, retain it for retry. If the ticket carries `task-report::required`, publish its completion report on the ticket, unless the developer asked to skip it: write the report to `.harness/.sandboxes/pr_body/issue-comment-<issue>-<slug>.md`, then post it with `gh issue comment <ID> --body-file <path>` on GitHub or `GITLAB_HOST=<host> glab api projects/<project-id>/issues/<ID>/notes -F body=@<path>` on GitLab.
   **6a. Verify the opened or updated PR (opt-in project).** Run these after opening the PR and after any later update of its branch (a refresh or resolver push):
   - `integration collect-ci <ticket-branch> --pull-request <n>` asks the tracker for combined-result CI. If it is accepted, the CI replaces a repeat of full local QA (`qa_replacement.applies`) and nothing else runs.
   - `next.action: wait` (a check is still running): repeat the command after a pause of about a minute, at most ten times, then ask the developer whether to keep waiting or to use local QA. Waiting spends no resolver cycle.
   - `next.action: local-qa` (CI is missing or unusable): run the existing fallback `integration local-qa --record <integration_record_id> --ci-condition <next.ci_condition> --reason "<reason from collect-ci>"` (the full local QA of the exact pair through the shared QA lane). An `unavailable` or `exhausted` result is an operational stop reported as such, never a code failure.
   - `next.action: route` (CI of exactly this pair failed and was recorded): follow the route action through `integration next` below; run no local QA. The `local_qa_required: true` that collect-ci returns next to it is inherited semantics for every non-accepted outcome, and `route` takes precedence.
   - Then run `integration next <ticket-branch> --pull-request <n>` and act on `step` as in step 3: `verify` (repeat this step), `route-failure`/`human-decision` (stop and hand over; operational failures never appear here), `refresh` (the target moved: repeat refresh, update the PR and this step), or `handoff`.
7. Return the PR/MR link in the primary session (on GitLab, also as `!<iid>`). For an opt-in project show the `handoff` of `integration next --pull-request <n>` before asking the developer to merge: the verified `candidate_sha` and `target_sha`, the `qa_source` (`original-qa`, `ci` or `local-qa`) with its `reference`, and whether the branch was refreshed. State that this verification belongs to exactly that pair and does not promise that nothing changes before the merge. Do not enable server branch protection or a merge queue. Ask whether the developer wants to review it, and repeat `integration next --pull-request <n>` right before the merge: a newly detected target means refresh and a new verification, never a reuse of the old one. Never merge it. Act on the ticket only after the developer confirms the merge:
    - **Check the merge.** GitHub: `gh pr view <n> --json state,baseRefName` (`state` is `MERGED`, `baseRefName` is the step 1 target). GitLab: `glab mr view <iid> -R <project-url> -F json` (`state` is `merged`, `target_branch` is the step 1 target). If it is not merged into that target, leave the ticket open and tell the developer.
    - **`Related to #<ID>`:** close the ticket explicitly. GitHub: `gh issue close <ID> --reason completed`. GitLab: `glab issue close <ID> -R <project-url>`; it takes no comment, so post any closing comment first through the notes API, as in step 6.
    - **`Closes #<ID>`:** verify that the tracker closed the ticket. GitHub: `gh issue view <ID> --json state` (`state` is `CLOSED`). GitLab: `glab issue view <ID> -R <project-url> -F json` (`state` is `closed`). GitLab closes it asynchronously, so if it still reads `opened`, read it once more; if it is still open, close it with the explicit command above and tell the developer why the tracker did not.
8. **Unblock dependencies after every close, in the same session.** Only a confirmed merge and normal close of a predecessor releases a dependency; a push, publish, accepted QA or an open PR never does. Leave the closed ticket's own `status::in-progress` label as is: the closed state is its terminal `done`. Then find every open ticket this one blocked and move each one whose blockers are now all closed:
    - **GitHub:** list open `status::blocked` issues (`gh issue list --state open --label status::blocked --json number`). For each, read its native blockers (`gh api repos/<owner>/<repo>/issues/<n>/dependencies/blocked_by --jq '[.[] | select(.state=="open")] | length'`) and any `Blocked by:` line in its body. If both show no open blocker, run `gh issue edit <n> --remove-label status::blocked --add-label status::ready`.
    - **GitLab:** list open `status::blocked` issues (`glab issue list -R <project-url> --label status::blocked --output json`). For each, read its native blocking links (`GITLAB_HOST=<host> glab api projects/<project-id>/issues/<n>/links`), where an entry with `link_type` `is_blocked_by` and `state` `opened` is an open blocker; GitLab Free has no such links. Also read every `#<M>` in its description's `Blocked by` lines (`glab issue view <n> -R <project-url> -F json`): that blocker is open while `glab issue view <M> -R <project-url> -F json` reports `state` `opened`. If both show no open blocker, run `glab issue update <n> -R <project-url> --unlabel status::blocked --label status::ready`.
    - **Local tracker** (`.scratch/<feature>/issues/*.md` or `docs/tasks/*.md`): check `**Blocked by:** #<ID>` and set `**Workflow:** status::ready` once every listed blocker is resolved.
    - Report which tickets moved to `status::ready` and which stay blocked, and by what.
9. **Cleanup:** After confirming the PR is merged and the ticket is closed, clean up the local environment to prevent garbage accumulation. Switch out of the worktree if necessary and remove it using `git worktree remove <path>`. Then handle the local branch:
    - **With a PR:** prove the merge from the PR, not from Git history. A squash or rebase merge leaves the branch commits outside the target history, so `git branch -d` and `git merge-base --is-ancestor` fail although the work is merged. GitHub: `gh pr view <n> --json state,headRefOid` (`state` is `MERGED` and `headRefOid` equals `git rev-parse <branch-name>`). GitLab: `glab mr view <iid> -R <project-url> -F json` (`state` is `merged` and `sha` equals `git rev-parse <branch-name>`).
    - **The check passes:** ask the developer whether to delete the local branch, and give them `git branch -D <branch-name>` to run themselves. The hook `block-dangerous-git.sh` blocks `git branch -D` for an agent unconditionally and cannot see an answer in the chat, so never run it and never work around the block.
    - **The PR is not merged or the SHAs differ:** keep the branch and tell the developer.
    - **Local markdown tracker (no PR):** after the `git merge-base --is-ancestor` check from the local-tracker paragraph succeeds, delete the branch with `git branch -d <branch-name>`.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.