Validate, independently review, and normally squash-merge protected GitHub pull requests. Use when a PR must pass exact-head, green-check, author/reviewer separation, or required-review gates; when a jinon86- or seoseo-ai-authored PR needs approval from the other relay-held account; or when landing changes without weakening branch protection.
Resources
2Install
npx skillscat add jinwon-int/ccc-node/codex-skills-gh-pr-flow Install via the SkillsCat registry.
GitHub PR Flow
Use local git and authenticated gh for GitHub reads and writes. Never push
directly to main, approve your own PR, use --admin merely to bypass
protection, or move a credential between nodes.
Issue claim before implementing
Before creating a branch (or dispatching an A2A patch lane) for a GitHub issue,
apply the fleet issue-claim rule (Family Wiki DOC-3508,pages/rules/github-issue-claim). Parallel sessions share one GitHub account,
so the account name is not an identity.
- Read the issue first. An open PR that references it, or an issue-claim or A2A
Startcomment from the last 3 hours, means the issue is already taken: do
not start. Coordinate on that PR or issue, pick other work, or ask the owner. - Otherwise post a claim comment before any code change, with a machine-readable
first line and a one-line human summary:<!-- issue-claim:v1 node=<node> session=<session-or-task-id> state=active -->
followed byclaim: <node> (<session>) — scope: <one line> — PR ETA: <KST>. - Filing an issue is not a claim. Opening the PR supersedes the claim; when
abandoning, poststate=released. A claim with no PR or update for 3 hours
has expired; take it over withstate=takeover prev=<previous session>. - To replace a claimed issue or open PR with a broader one, comment on it
first with the reason. Never silently open and merge a replacement.
Normal flow
Record the PR's full
headRefOid. Require an open, non-draft PR against the
intended base, a mergeable state, and no pending or failed required checks.Confirm the PR author and current actor. Request a different write-capable
reviewer when approval is required. Underrequire_last_push_approval
(jinwon-int/ccc-nodemain) the last pusher is disqualified too, so
pushing a fix to the other account's PR can leave nobody able to approve it
— and force-pushing that commit away does not help, because the rule keys on
the pusher, not the commits. Prefer a review comment, or carry the fix in a
PR you author. Check before pushing:gh api repos/<owner>/<repo>/branches/main/protection \ --jq '.required_pull_request_reviews.require_last_push_approval'After approval, re-read the exact head, review decision, and checks. Squash
merge normally — except onjinwon-int/ccc-nodemain, which runs a merge
queue (see below):gh pr merge NUMBER --repo OWNER/REPO --squash --delete-branchMerge queue on
ccc-nodemain(since 2026-09-13),fleet-skillsmain
(since 2026-09-17), andpirimain(observed 2026-09-21): direct merge
is refused ("the merge strategy for main is set by the merge queue").
Treat the list as open — it is a per-repo ruleset, andpirisurfaced only
when a merge failed mid-batch. Check first:gh api repos/OWNER/REPO/rulesets --jq '.[].id' | while read -r id; do gh api repos/OWNER/REPO/rulesets/"$id" --jq '[.rules[].type]|join(",")' doneEnqueue instead, then poll until
stateisMERGED(the queue evicts on
failing group checks; a new head needs fresh approval before re-enqueueing):pr_id="$(gh pr view NUMBER --repo OWNER/REPO --json id --jq .id)" gh api graphql -f query='mutation($id:ID!){enqueuePullRequest(input:{pullRequestId:$id}){clientMutationId}}' -f id="$pr_id" gh pr view NUMBER --repo OWNER/REPO --json state,mergeStateStatusEnabling a queue on a further repo has a prerequisite: every required
check's workflow must also trigger onmerge_group, or the group never
reports and enqueued PRs wait forever.Verify the merged commit and remote branch deletion before removing a local
squash-merged branch.A queue merge does not clean up the head branch, and
gh pr merge -dis
refused on a queued repo, so cleanup falls entirely to the repo'sdelete_branch_on_merge— off onccc-node, on forpiri/fleet-skills.
The same flow therefore self-cleans on one repo and leaks branches on
another;ccc-nodecarries 95 remote heads as of 2026-09-21. Check and
delete explicitly:git ls-remote --heads https://github.com/OWNER/REPO BRANCH | grep -q . \ && gh api -X DELETE repos/OWNER/REPO/git/refs/heads/BRANCH
Under strict up-to-date protection, merging one PR flips its siblings toBEHIND. Refresh via REST (older gh builds lack gh pr update-branch):gh api -X PUT repos/OWNER/REPO/pulls/NUMBER/update-branch, wait for CI on
the new head, then re-run the relay approval — it is commit-bound to--expected-head.
Relay-held cross-account review
Use the allowlisted review profile matching the PR author. Both directions
require fresh explicit approval for the exact repository, PR, and head (sole
exception: the standing approval for mechanical fleet-skills promote PRs at
the end of this skill):
| PR author | Review profile | Expected reviewer | Remote gh config |
|---|---|---|---|
jinon86 |
seoseo-ai |
seoseo-ai |
isolated root-owned profile config |
seoseo-ai |
jinon86 |
jinon86 |
root default gh config |
relay below is the SSH alias of the credential-holding relay node in your
fleet and the helper default; a node without that alias must pass--ssh-target <relay-host-alias> explicitly or export CCC_RELAY_SSH_TARGET
(observed 2026-09-05 on a node whose ssh config names the relay node by its
own hostname alias):
CCC_EXPLICIT_USER_APPROVAL=1 \
bash "${CODEX_HOME:-$HOME/.codex}/skills/gh-pr-flow/scripts/approve-via-relay.sh" \
--review-profile seoseo-ai \
--repo jinwon-int/REPO --pr NUMBER --expected-head FULL_40_CHAR_SHA \
--ssh-target relay --operator-approved
CCC_EXPLICIT_USER_APPROVAL=1 \
bash "${CODEX_HOME:-$HOME/.codex}/skills/gh-pr-flow/scripts/approve-via-relay.sh" \
--review-profile jinon86 \
--repo jinwon-int/REPO --pr NUMBER --expected-head FULL_40_CHAR_SHA \
--ssh-target relay --operator-approvedThe helper maps each profile to a fixed actor, opposite author, and gh config.
It verifies the root-owned credential boundary, repository write permission,
author separation, requested-reviewer state, exact head, mergeability, and
green checks before submitting a commit-bound approval. The helper is
idempotent per actor and exact head: a re-run reuses an already-recorded
exact-head approving review instead of posting a duplicate, and success is
judged from that recorded review — reviewDecision reads null on repos
whose branch protection requires zero approving reviews (#1714), so only an
explicit CHANGES_REQUESTED fails verification. Compatibility wrappersapprove-via-seoseo-ai.sh and approve-via-jinon86.sh select their named
profiles but cannot override them.
Environment deployment approval
Approving a GitHub Actions environment deployment (a job waiting inpending_deployments on a required reviewer such as jinon86 for a release
environment) is a release-gate action. It needs fresh explicit user approval for
the exact repository, run, environment, and head on every invocation; never do
it with an ad-hoc gh api call on the relay node:
CCC_EXPLICIT_USER_APPROVAL=1 \
bash "${CODEX_HOME:-$HOME/.codex}/skills/gh-pr-flow/scripts/approve-deployment-via-relay.sh" \
--repo jinwon-int/REPO --run-id RUN_ID --environment release \
--expected-head FULL_40_CHAR_SHA --ssh-target relay --operator-approvedThe profile is fixed to jinon86. --expected-workflow (default.github/workflows/release.yml), --expected-branch (default main), and--expected-event (default workflow_dispatch) must match the run exactly;
the run must not come from a fork, and exactly one environment — the named one —
may be pending with current_user_can_approve: true. The approval comment
records the operator approval and source node, and success is verified from the
run's review history. A re-run reports already_approved instead of posting
again; a run that is no longer waiting and was never approved exits 66 with
its state. --dry-run checks every gate without submitting.
Security boundary
- Require fresh explicit approval for every helper invocation and review
profile. Approval does not carry to another repository, PR, changed head, or
opposite credential. The only exception is the standing approval below. - Never read, print, copy, export, re-login, or place the token in arguments.
- Keep shell tracing disabled. Return only body-free gate results.
- Stop on credential owner/mode drift, wrong actor, self-review, head drift,
missing review request, missing checks, or non-green checks. - Review approval and merge are separate writes unless the current user
instruction explicitly authorizes both for that exact PR.
Standing approval for mechanical fleet-skills promote PRs (#2239 L3)
The single exception to per-invocation fresh approval. It exists only while
the owner has switched it on, and it covers only this case:
- Scope:
jinwon-int/fleet-skillsPRs onpromote/auto-*branches opened
by the skill-promotion publisher (ccc-skill-promotion.py, ledger kinda2a-promote-pr), approved by that publisher's own collect sweep with theseoseo-aiprofile (the publisher authors asjinon86). An interactive
session never uses it. - Owner switch: the owner-created
0600flag file
the publisher state-dir flag$CCC_STATE_DIR/skill-promotion.auto-approvereadingtrueon the
publisher (orCCC_SKILL_PROMOTION_AUTO_APPROVE=truein its owner-only
config file).dry-runapproves nothing and only recordswould-approve-promote-pr. Absent/off= no standing approval. - Conditions, all on the exact head: no creation-time identity hits; every
path in the full PR diff underapproved/<audience>/<staged skill>/; every
added line rescanned clean (credentials, home paths, IPs, emails, runtime
coupling, fleet identities) andapproval.jsonin the generator's exact
shape; checks green andMERGEABLE; the helper re-verifies with--expected-head, and the merge uses--match-head-commit(never--admin). - Everything else — another repository, any non-promote PR, a blocked
or changed head, a promote PR a check rejected, a merge outside the queue —
still needs fresh per-PR approval as above. Each decision is recorded in
the publisher ledger (a2a-promote-autoapprove) and the owner is notified
afterwards (PR numbers only). - Revoke: delete the flag file or write
off; the next sweep stops
approving. No restart.