to-pull-requests

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

SKILL.md

GitHub

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