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.
Files1
View on GitHubSKILL.mdskills/first-party/pvmalove/to-pull-requests/SKILL.md
GitHubTo 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.
Resolve the ticket and its exact PR target branch from the ticket's
## Integration Branchsection, its parent epic, orbase_branchfor an epic-less ticket. If this cannot be resolved, stop and ask the developer.Verify that the current branch matches
branch_pattern, is neitherbase_branchnorintegration/*, 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/implementor/fast-implement, whichever route the ticket is on. Fetch its upstream and requiregit rev-parse HEADto equal the upstream branch SHA; if it differs, fast-forward the clean local branch before continuing.For a valid
backend-orchestrationopt-in, resolve the full current SHA withgit rev-parse HEADafter that synchronization and validate it before any PR action. Every command below ispython .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 reportsunavailable, the integration ref could not be read: stop and report it to the developer as an operational stop, never as a code failure. Ifrefreshesis not empty, the branch was already refreshed: validateoriginal_candidate_shabelow 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 labeledpipeline::fast. Forpipeline::fasttickets, 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 itsstep:refresh: the integration ref moved. Runintegration 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 suregit rev-parse HEADequals the newcandidate_shaand repeatintegration next. Onstate: conflictrunintegration resolve <ticket-branch>, which only creates the resolver batch, and hand itsbatch_idto the coordinator session (batch approve, thendispatch 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-failureorhuman-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/implementroute (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 runsintegration prepare --ticket <ticket> --branch <branch> --batch <new batch>, so repeatintegration 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-decisionmeans 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_sourceisoriginal-qa(the original pair),verification-pending-after-pr(a refreshed pair that no CI or local QA covers yet), orci/local-qa(a refreshed pair that already has that verified evidence). Do not rerun/qa-gatewhen this validation succeeds; instead, from the PR branch checkout verified in step 2, runbash "$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 ispipeline::fast, run/qa-gateif this repository provides it and stop on failure.
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 indocs/tasks/. A developer may manually runpr-composerin the coding application; otherwise fill in the template directly. Resolve the repository default branch — GitHub:gh repo view --json defaultBranchRef --jq .defaultBranchRef.name; GitLab: thedefault_branchfield ofGITLAB_HOST=<host> glab api projects/<project-id>. UseCloses #<ID>only when the step 1 target branch is that default branch, otherwiseRelated to #<ID>. GitLab closes an issue byCloses #<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.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_shaandtarget_shafromintegration next, the PR target branch andqa_source. The confirmation covers only that pair. Stop for their answer.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: ifcandidate_sha,target_shaorstep(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 orCo-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 carriestask-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 withgh issue comment <ID> --body-file <path>on GitHub orGITLAB_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 fallbackintegration 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). Anunavailableorexhaustedresult 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 throughintegration nextbelow; run no local QA. Thelocal_qa_required: truethat collect-ci returns next to it is inherited semantics for every non-accepted outcome, androutetakes precedence.- Then run
integration next <ticket-branch> --pull-request <n>and act onstepas 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), orhandoff.
- GitHub:
Return the PR/MR link in the primary session (on GitLab, also as
!<iid>). For an opt-in project show thehandoffofintegration next --pull-request <n>before asking the developer to merge: the verifiedcandidate_shaandtarget_sha, theqa_source(original-qa,ciorlocal-qa) with itsreference, 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 repeatintegration 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(stateisMERGED,baseRefNameis the step 1 target). GitLab:glab mr view <iid> -R <project-url> -F json(stateismerged,target_branchis 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(stateisCLOSED). GitLab:glab issue view <ID> -R <project-url> -F json(stateisclosed). GitLab closes it asynchronously, so if it still readsopened, 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.
- Check the merge. GitHub:
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-progresslabel as is: the closed state is its terminaldone. Then find every open ticket this one blocked and move each one whose blockers are now all closed:- GitHub: list open
status::blockedissues (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 anyBlocked by:line in its body. If both show no open blocker, rungh issue edit <n> --remove-label status::blocked --add-label status::ready. - GitLab: list open
status::blockedissues (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 withlink_typeis_blocked_byandstateopenedis an open blocker; GitLab Free has no such links. Also read every#<M>in its description'sBlocked bylines (glab issue view <n> -R <project-url> -F json): that blocker is open whileglab issue view <M> -R <project-url> -F jsonreportsstateopened. If both show no open blocker, runglab issue update <n> -R <project-url> --unlabel status::blocked --label status::ready. - Local tracker (
.scratch/<feature>/issues/*.mdordocs/tasks/*.md): check**Blocked by:** #<ID>and set**Workflow:** status::readyonce every listed blocker is resolved. - Report which tickets moved to
status::readyand which stay blocked, and by what.
- GitHub: list open
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 -dandgit merge-base --is-ancestorfail although the work is merged. GitHub:gh pr view <n> --json state,headRefOid(stateisMERGEDandheadRefOidequalsgit rev-parse <branch-name>). GitLab:glab mr view <iid> -R <project-url> -F json(stateismergedandshaequalsgit 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 hookblock-dangerous-git.shblocksgit branch -Dfor 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-ancestorcheck from the local-tracker paragraph succeeds, delete the branch withgit branch -d <branch-name>.
- 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