Use when committing or pushing code, opening or merging a PR, resolving REVIEW_REQUIRED, or landing changes in jinwon-int repos. PR-first — exact-head green checks, cross-account review (jinon86/seoseo-ai via relay) after fresh user approval, squash merge, cleanup. Not for Wiki edits (wiki-record).
Resources
6Install
npx skillscat add jinwon-int/ccc-node/gh-pr-flow Install via the SkillsCat registry.
gh-pr-flow — PR-first GitHub flow
Use this for code changes that land in GitHub. Operational repos live underjinwon-int where possible. Never push directly to main; use a branch and PR.
For Wiki content use wiki-record instead.
The flow enforces: no direct main pushes, exact-head and green-check
validation, an independent cross-account review in either direction (protected
PRs; see the table below) after fresh explicit user approval, secret-safe
credential use, squash merge, and verified cleanup.
Identities and review directions
A node's local gh holds jinon86 (typically the only local account).seoseo-ai review authority lives on the relay node as an isolated,
root-owned profile config, and the relay node separately holds an authorizedjinon86 session. Check with gh auth status. Whichever account authored
the PR, the OTHER account reviews it — both directions are supported
symmetrically:
| PR author | Reviewer / merger | Mechanism |
|---|---|---|
jinon86 |
seoseo-ai reviews; jinon86 merges |
approve-via-relay.sh --review-profile seoseo-ai (relay-held profile, exact-head) |
seoseo-ai |
jinon86 reviews and, if needed, merges |
approve-via-relay.sh --review-profile jinon86 (exact-head) / merge-via-relay.sh (relay-held session over SSH) |
Every cross-account review or merge is a privileged credential use and needs
fresh explicit user approval in the current conversation, in both directions.
The sole exception is the owner-switched standing approval for mechanical
fleet-skills promote PRs at the end of this skill (#2239 L3).
GitHub transport policy
- Use local
gitand the authenticatedghCLI for every GitHub read and write. - Do not use GitHub App, connector, MCP, or plugin tools unless the user
explicitly requests that transport in the current task. - If
ghfails, report the error; do not automatically retry through a GitHub
connector. - Run
gh auth statusbefore the first authenticated GitHub operation when the
current session has not already verified it.
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.
Procedure
Sync and branch:
git switch main git pull --ff-only git switch -c <type>/<slug>Stage only the intended files and commit with the required trailer:
git add <file>... git commit -F - <<'EOF' <type>(<scope>): <imperative summary> <optional body — what and why> Co-Authored-By: <the model running this session> <noreply@anthropic.com> EOFPush and open a PR against the repository's default branch. It is
mainin
mostjinwon-intrepos butmasterinseoyoon-family-wiki— the review
and merge helpers verify the base againstgh api repos/<owner>/<repo> --jq .default_branch, so a PR opened against the wrong branch is refused:git push -u origin <branch> base="$(gh api repos/<owner>/<repo> --jq .default_branch)" gh pr create --repo <owner/repo> --base "$base" --head <branch> \ --title "<type>(<scope>): <summary>" --body "<what / why / evidence>"Inspect identity, review, exact head, checks, and merge state:
gh pr view <n> --repo <owner/repo> \ --json author,reviewRequests,reviewDecision,headRefOid,isDraft,mergeable,mergeStateStatus,statusCheckRollupResolve
REVIEW_REQUIREDwith an independent reviewer. Never use--admin
merely to bypass branch protection.Old approvals, memory, environment state, or approval for another PR do
not count. If approval is absent or ambiguous, stop and ask.Pushing to someone else's PR can delete the only remaining approver.
jinwon-int/ccc-nodemainsetsrequire_last_push_approval: true, so
the account that pushed last cannot approve that PR. Push a review fix to
ajinon86-authored PR asseoseo-aiand both paths close:seoseo-ai
is now the last pusher,jinon86is the author. Nothing can approve it,
andenforce_admins: truemeans no bypass. Read the rule and count who is
left before pushing:gh api repos/<owner>/<repo>/branches/main/protection \ --jq '.required_pull_request_reviews.require_last_push_approval'Prefer a review comment, or carry the fix in your own PR. A one-line push
is still fine when the PR is already blocked for another reason (failing
required checks, draft) — there is no approval to lose yet.Force-pushing the offending commit away does not undo it. GitHub keys
this rule to the pusher, not the commits: rewinding the branch to the
author's own SHA still records you as the last pusher and the enqueue is
refused with the same message. Verified 2026-09-19 on #1802.
The ways out are (a) the original author pushes anything, even an empty
commit, or (b) reopen the work as a PR you author, so the other account
can review it. Keep the original commits (and their author metadata)
when you do (b), and say in both PRs why the number changed.One narrow exception, and it is not this one. Some repos deny
infrastructure paths (scripts/,.github/) at a required check on every
branch and deliberately leaveenforce_adminsfalse, documenting the admin
merge as the intended way in. That is a sanctioned hatch, not a bypass —
seeinfra-gate-admin-merge-discipline, which also covers the trap that
such gates exit before running their own tests. It applies only when the
gate's own source sanctions it; absent that sentence, this rule stands.After approval, set the approval flag only on the one approved command.
Helper scripts live next to this SKILL.md. Resolve the directory once —
the installed copy first, the template checkout as fallback (e.g. when an
installed copy is stale, as happened on one node on 2026-08-07):GH_PR_FLOW_DIR="${CCC_CLAUDE_DIR:-$HOME/.claude}/skills/gh-pr-flow" if [ ! -d "$GH_PR_FLOW_DIR" ]; then for _cand in /opt/ccc-node "$HOME/ccc-node" /root/ccc-node; do [ -d "$_cand/claude/skills/gh-pr-flow" ] || continue GH_PR_FLOW_DIR="$_cand/claude/skills/gh-pr-flow"; break done fi(The fallback tries the fleet's checkout locations in order —
/opt/ccc-node,$HOME/ccc-node,/root/ccc-node— instead of hardcoding one; nodes install
the repo in different places, which previously forked this snippet per node.)Direction A —
jinon86-authored PR,seoseo-aireviews (relay-held
profile):Both directions use the same exact-head profile helper. It ships with the
managed Codex skills thatsetup.shinstalls on every node. Resolve it
first, with template-checkout fallback:RELAY_FLOW_DIR="${CODEX_HOME:-$HOME/.codex}/skills/gh-pr-flow/scripts" if [ ! -d "$RELAY_FLOW_DIR" ]; then for _cand in /opt/ccc-node "$HOME/ccc-node" /root/ccc-node; do [ -d "$_cand/codex/skills/gh-pr-flow/scripts" ] || continue RELAY_FLOW_DIR="$_cand/codex/skills/gh-pr-flow/scripts"; break done fiBoth relay helpers (
approve-via-relay.sh,merge-via-relay.sh) default
to--ssh-target relay. A node without arelayssh alias must pass the
credential-holding node explicitly (or exportCCC_RELAY_SSH_TARGET) —
observed 2026-09-05 on a node whose ssh config names the relay node by its
own hostname alias, notrelay:head_sha="$(gh pr view <n> --repo <owner/repo> --json headRefOid --jq .headRefOid)" CCC_EXPLICIT_USER_APPROVAL=1 \ bash "$RELAY_FLOW_DIR/approve-via-relay.sh" \ --review-profile seoseo-ai --repo <owner/repo> --pr <n> \ --expected-head "$head_sha" --operator-approvedThe helper runs
ghon the relay node with the isolatedseoseo-aiprofile
config — credentials never leave the relay node and are never printed,
copied, or switched.
It fail-closes unless the remote actor isseoseo-ai, the open default-
branch PR is authored byjinon86,seoseo-aiis the requested reviewer
(request it withgh pr edit --add-reviewer seoseo-aiwhen missing), the
head matches--expected-headexactly, the PR is mergeable, and every
reported check on that head is successful.--operator-approvedrecords
the fresh user authorization for this exact PR and head. After approval,jinon86(the local account) merges normally in step 6.Direction B —
seoseo-ai-authored PR,jinon86reviews (via the relay
node):Same helper, opposite profile:
head_sha="$(gh pr view <n> --repo <owner/repo> --json headRefOid --jq .headRefOid)" CCC_EXPLICIT_USER_APPROVAL=1 \ bash "$RELAY_FLOW_DIR/approve-via-relay.sh" \ --review-profile jinon86 --repo <owner/repo> --pr <n> \ --expected-head "$head_sha" --operator-approvedIt runs
ghon the relay node with the root-ownedjinon86config and fail-closes
unless the remote actor isjinon86, the open default-branch PR is authored
byseoseo-aiwithjinon86requested, the head matches--expected-head
exactly, the PR is mergeable, and every reported check on that head is
successful. The approval is commit-bound and the credential never leaves
the relay node. 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
readsnullon repos whose branch protection requires zero approving
reviews (#1714), so only an explicitCHANGES_REQUESTEDfails verification.With required review and checks green, squash-merge normally (on
jinwon-int/ccc-nodeandjinwon-int/fleet-skills,mainruns a merge
queue — see the merge-queue subsection at the end of this step; enqueue
instead of merging):gh pr merge <n> --repo <owner/repo> --squash --delete-branchIf the local identity lacks repository merge permission, use the exact-head
exact-head relay merge fallback below. Do not weaken branch protection.mergeable: UNKNOWNis "not computed yet", not "blocked". GitHub builds
a throwaway test merge commit in a background job; until it finishes the
GraphQL enum readsUNKNOWN("The mergeability of the pull request is still
being calculated") and RESTmergeablereadsnull, which the REST docs
tell you to resolve by resubmitting the request. Closed and merged PRs also
reportUNKNOWN, so it is never by itself evidence of a problem. Re-read
until it settles instead of reaching for a workaround:for i in $(seq 1 30); do read -r m s <<<"$(gh pr view <n> --repo <owner/repo> \ --json mergeable,mergeStateStatus --jq '"\(.mergeable) \(.mergeStateStatus)"')" [ "$m" = UNKNOWN ] || { echo "mergeable=$m mergeStateStatus=$s"; break; } sleep 10 doneThen act on
mergeStateStatus, not on the merge command's failure text:CLEANmerge;UNSTABLE/BLOCKEDmeans checks or required reviews are the
real gate — fix those;DIRTYis a genuine conflict (seegithub-merge-state-conflict-diagnosis);BEHINDmeans the base moved.Two failures here look like merge problems but are repository settings, and
neither justifies--admin, weaker protection, or a credential escalation:gh pr view --jsonexposes neither capability flag — it only carriesautoMergeRequest. Read them over GraphQL before attempting either command:gh api graphql -f query=' query($o:String!,$r:String!,$n:Int!){repository(owner:$o,name:$r){pullRequest(number:$n){ viewerCanEnableAutoMerge viewerCanUpdateBranch}}}' \ -F o=<owner> -F r=<repo> -F n=<n> --jq .data.repository.pullRequestgh pr merge --autofails when the repo hasallow_auto_merge: false
(viewerCanEnableAutoMerge: false; alsogh api repos/<owner>/<repo> --jq .allow_auto_merge). The observed message is roughlyAuto merge is not allowed for this repository— match it loosely, since GitHub does not
document the string and a configured merge queue can produce it too.
Enabling the setting is a repository-settings change and a separate
approval; without it, watch and merge in the foreground instead — withci-watch.sh(below), not a hand-rolled loop.- Watching checks without the bridge wait: when
gh-ci-wait
registration fails (route-unavailable), runbash "$HOME/.claude/skills/gh-pr-flow/ci-watch.sh" --repo <owner/repo> --pr <n> --head <sha>
in the background and branch on its exit code (0green ·10failed,
names the checks ·11merged ·12closed ·13head moved ·20
timeout). It derives the verdict inside jq; two ad-hoc loops on
2026-10-02 greppedgh --jqoutput for a key order gh never produces
and silently missed "green" twice (#2113, #2120). Green also requires
every required context of the base branch (branch protection plus
rulesets) to be present and successful: right after a push orupdate-branchonly CodeQL had registered, "nothing pending" held, and
an older version reported green on a head whose required check later
failed (#2200). When the required set cannot be read it falls back to
green on--settle(default 2) consecutive polls;--required-context
pins the set explicitly.approve-via-relay.shrefuses a head with a
required check not yet reported, for the same reason. gh pr update-branchonly does something when the head is genuinely
behind. Gate it onviewerCanUpdateBranch, which GitHub documents asfalsewhen the head is already up to date. The REST endpoint answers202 Acceptedwith"Updating pull request branch."— an async
acknowledgement, not proof a commit was created — so confirm by comparingheadRefOidbefore and after rather than trusting the success line.
Several PRs under strict up-to-date protection. Once one merges, every
sibling flips toBEHIND. Refresh each over REST — olderghbuilds have
nogh pr update-branchsubcommand, the endpoint works on any build:gh api -X PUT repos/<owner>/<repo>/pulls/<n>/update-branchThen wait for CI on the new head and re-run the relay approval: approvals
are commit-bound to--expected-head, so the pre-refresh approval no
longer counts.
Merge queues: jinwon-int/ccc-node main (since 2026-09-13),jinwon-int/fleet-skills main (since 2026-09-17), and jinwon-int/pirimain (observed 2026-09-21). Each has a merge_queue ruleset rule, so plaingh pr merge is refused ("the merge strategy for main is set by the merge
queue") — including by the relay merge helper. Do not treat this list as
closed: it is a per-repo ruleset that anyone can enable, and piri was found
only when gh pr merge --squash --delete-branch failed mid-batch. Confirm
before merging rather than after:
gh api repos/<owner>/<repo>/rulesets --jq '.[].id' | while read -r id; do
gh api repos/<owner>/<repo>/rulesets/"$id" --jq '[.name,([.rules[].type]|join(","))]|join(" | ")'
doneLand by enqueueing instead; exact-head and independent-review rules
are unchanged, and the queue re-runs every required check on a speculativegh-readonly-queue/main/... group ref before squash-landing:
pr_id="$(gh pr view <n> --repo <owner/repo> --json id --jq .id)"
gh api graphql \
-f query='mutation($id:ID!){enqueuePullRequest(input:{pullRequestId:$id}){clientMutationId}}' \
-f id="$pr_id"
# then watch BOTH the PR state and the queue entry — never "poll until MERGED":
bash "$HOME/.claude/skills/gh-pr-flow/merge-queue-watch.sh" --repo <owner/repo> --pr <n>Watch for eviction, not just for MERGED. When the speculative group run
fails, GitHub drops the queue entry silently: the PR stays OPEN/CLEAN
with its approval intact and nothing on the PR changes, so a watch that only
polls state waits forever (2026-10-02, ccc-node#2113 — evicted by a flaky
shard test, noticed by the operator, not the watch). merge-queue-watch.sh
polls the PR and the repo's mergeQueue entries together and exits on the
first terminal event: 0 merged, 10 evicted (it names the failedgh-readonly-queue/<branch>/pr-<n>-… run and its failing jobs), 11 closed
unmerged, 12 never enqueued, 20 timeout. The queue deletes the entry the
moment it squash-lands, so an empty entry is re-confirmed against the PR state
(--settle-tries, default 3 × 5 s) before it becomes 10/12 — a PR that
merged between the two reads reports merged (#2208, pr-shepherd#154). Run it in the background and
act on the exit code; on 10, classify the failing job first — an unrelated
flaky test means re-enqueue the same approved head (and record the flake on
its issue); a real failure means push the fix, get fresh exact-head approval
for the new head, then re-enqueue, as with any other push.
Enqueue needs the head up to date and its checks green; a stale BEHIND head
goes through the update-branch loop above first.
A queue merge does not clean up the head branch, and --delete-branch
cannot be used to make it. gh pr merge -d is refused outright on a queued
repo, so cleanup falls entirely to the repo's own delete_branch_on_merge
setting — which is off on ccc-node:
ccc-node delete_branch_on_merge=false piri true fleet-skills trueSo every queue merge on ccc-node leaves its head branch behind (the repo
carries 95 remote heads as of 2026-09-21), while the same flow on piri
self-cleans and looks like the queue handled it. Do not generalise from one
repo. A direct gh pr merge --squash --delete-branch deletes regardless of
the setting, which is why this only bites on queued repos. Verify and delete
explicitly, per step 7:
git ls-remote --heads https://github.com/<owner>/<repo> <branch> | grep -q . \
&& gh api -X DELETE repos/<owner>/<repo>/git/refs/heads/<branch>Do not assume a repo without a queue is simply slower to merge. main on
both repos also requires strict (up-to-date) status checks, and that pairing
without a queue is what makes a batch land serially: the first merge puts
every sibling PR BEHIND, and each one then needs update-branch plus a
full CI round trip. Promotion batches #232-#235 cost four such cycles by hand
on 2026-09-16 before the fleet-skills queue existed.
Before enabling a queue on another repo, check the workflow triggers. The
queue reports against the merge_group event, so a required check whose
workflow only lists pull_request (and push) never reports inside a group
and every enqueued PR waits forever. fleet-skills needed
#236 to addmerge_group: to validate.yml — which owns the required skills andmerge-boundary contexts — before the queue could be turned on. A check that
is not in the required set (fleet-skills receipts) does not gate the queue
and needs no trigger change.
Enabling is a ruleset operation, not branch protection: GraphQLupdateBranchProtectionRule rejects requiresMergeQueue
(argumentNotAccepted). Add a ruleset carrying only the merge_queue rule
and leave classic protection (required checks, approval count,enforce_admins) untouched — rollback is deleting that one ruleset.
Verify and clean up:
gh pr view <n> --repo <owner/repo> --json state,mergedAt,mergeCommit git switch main git pull --ff-only git ls-remote --exit-code --heads origin <branch>A nonzero
ls-remoteresult means the remote branch is absent, as expected.git branch -d <branch>may reject a squash-merged branch because squash
does not preserve ancestry. Usegit branch -D <branch>only after verifying
the PR is merged,maincontains the change, and the remote branch is gone.
If the PR links an issue with a closing keyword, verify that issue is closed.
Relay-held jinon86 exact-head merge fallback
Use this only when all of the following are true:
- The operator explicitly approved merging this specific repository and PR in
the current task. Approval does not carry to another PR or a changed head. - The local identity cannot merge it, while the relay node already has the
authorizedjinon86GitHub session. - The PR is non-draft, targets
main, isMERGEABLE/CLEAN, has all required
reviews, and its exact head has passing GitHub checks or documented equivalent
validation.
Capture the exact head locally, then call the fail-closed helper:
head_sha="$(gh pr view <n> --repo <owner/repo> --json headRefOid --jq .headRefOid)"
bash "$GH_PR_FLOW_DIR/merge-via-relay.sh" \
--repo <owner/repo> --pr <n> --expected-head "$head_sha" \
--operator-approvedThe helper verifies actor jinon86, re-reads the PR on the relay node, requires the
same head SHA and clean merge state, rejects pending or failed checks, and uses
GitHub's merge API with the SHA precondition. It never uses an admin bypass. A
PR with no reported checks is allowed only when exact-head equivalent evidence
is already recorded and the operator is told that GitHub reported no checks.
Delete the contributor branch using the identity that owns it. If cleanup
permission is unavailable, report it rather than moving a credential.
Relay-held jinon86 owner-repo hygiene
The merge helper above cannot change repository settings, and running an ad-hocgh api as jinon86 on the relay node is a credential use outside any
fail-closed helper. For the one sanctioned settings change — Dependabot
vulnerability alerts plus a minimal default-branch ruleset
(default-branch-no-delete-no-force-push: blocks deletion and force-push, no
review or PR requirement) — use repo-hygiene-via-relay.sh. The target
repositories come from a node-local allowlist (one OWNER/REPO per line,
default ~/.config/ccc/repo-hygiene-allowlist, override withCCC_REPO_HYGIENE_ALLOWLIST or --allowlist); repository names are fleet
data, so they never live in this canon file.
# dry run (no mutation) — still a relay credential use, so still approved
bash "$GH_PR_FLOW_DIR/repo-hygiene-via-relay.sh" --operator-approved
# apply after the operator approves the dry-run result
bash "$GH_PR_FLOW_DIR/repo-hygiene-via-relay.sh" --operator-approved --apply--repo OWNER/REPO (repeatable) narrows the run; a name outside the allowlist
is refused. The helper verifies actor jinon86 and per-repo admin, never
overwrites a same-name ruleset whose rules differ (reported asconflict-mismatch), re-reads every change it makes, and exits non-zero unless
every selected repository ends compliant. Widening the ruleset is a code change
through this skill's PR flow; editing the allowlist is an operator decision
recorded with the run's approval.
Promotion branches (#2292). A branch that nodes deploy from — for example astable ref that a scheduled self-update follows while the default branch is
auto-merged — must not be movable by a plain push. Opt a repository in withprotect-branch=NAME tokens on its allowlist line
(OWNER/REPO protect-branch=NAME [protect-branch=NAME2]). For each name the
helper ensures a protected-branch-NAME ruleset on refs/heads/NAME:
deletion and force-push blocked, plus a pull request with one approval from
someone other than the last pusher (stale approvals dismissed, merge commits
only, no bypass actors). Promotion then becomes a PR into that branch,
reviewed cross-account like any other. The helper refuses master/main (any
case) locally and the repository's actual default branch remotely, because a
PR requirement there would break node automation; an unknown default branch
fails closed the same way. An existing ruleset of that name is accepted only
if it is active, targets branches, includes exactly refs/heads/NAME with no
exclude, has an empty bypass list (and current_user_can_bypass never), and
carries exactly those three rules with those pull-request parameters; anything
weaker is a conflict-mismatch and is left untouched. Each repository row
reports protected_branches as NAME:<state> entries. The helper needs bash
4+ locally and validates names in the C locale; repository and branch lists
travel to the relay as shell-inert OWNER/REPO:b1,b2 arguments, because ssh
re-parses the remote command line with the relay's login shell.
Environment deployment approval
A workflow job gated by a GitHub Actions environment with required reviewers
(for example a release environment whose reviewer is jinon86) waits inpending_deployments until that reviewer approves. Approving it is a
release-gate action: it needs fresh explicit user approval for that exact
repository, run, environment, and head, every time — an earlier release
approval or a PR merge approval does not carry over. Never approve it with an
ad-hoc gh api call on the relay node; use the fail-closed helper, which ships
with the managed Codex skills next to approve-via-relay.sh (resolveRELAY_FLOW_DIR as in step 5):
CCC_EXPLICIT_USER_APPROVAL=1 \
bash "$RELAY_FLOW_DIR/approve-deployment-via-relay.sh" \
--repo jinwon-int/<repo> --run-id <run-id> --environment release \
--expected-head <full-40-char-sha> --operator-approved
# optional: --expected-workflow .github/workflows/release.yml (default)
# --expected-branch main (default) --expected-event workflow_dispatch
# (default) --ssh-target <relay-alias> --dry-runThe helper runs gh on the relay node with the root-owned jinon86 config and
fail-closes unless the actor is jinon86, the run belongs to the repository
(not a fork), its head SHA, branch, event, and workflow path match exactly, and
exactly one environment — the named one — is pending withcurrent_user_can_approve: true. It records the approval with a comment naming
the operator approval and the source node, then verifies it from the run's
review history. It is idempotent: when the environment is no longer pending (or
the run is no longer waiting) and jinon86 already approved it, it reportsalready_approved without posting again; a run that is no longer waiting and
was never approved exits 66 with its state and submits nothing. Approving the
deployment authorizes only that job to proceed — not a re-run, another
environment, or a follow-up release.
Security and merge rules
- Never push directly to
main; always use a branch and PR. - The PR author cannot approve their own PR. Keep author and reviewer identities
independent. - Under
require_last_push_approval, whoever pushed last cannot approve either.
Author and last pusher are two separate disqualifications, and on a two-account
repo pushing to the other account's PR can leave nobody eligible. See step 5. - Only merge with green required checks and a mergeable state. Report failed or
pending checks instead of forcing the merge. - Never read, print, copy, persist, export, or re-login with the relay's gh
token. Runghon the relay node so the credential stays there. Do not
enable shell trace, switch the persistent local account, or put credentials
in arguments, commits, PR text, logs, or memory. - The relay-held
seoseo-aiprofile credential follows the same discipline:
use it only through the exact-headapprove-via-relay.shprofile helper,
which runsghon the relay node with the isolated profile config for a
single approved invocation. Never print it, never copy it off the relay
node, nevergh auth switchto it, and never reuse one approval for a second invocation
or a different head. - An approval flag records a fresh user decision; it does not grant standing
authority (the only standing approval is the #2239 L3 owner switch below).
Set it for one approved invocation only. Review approval and merge
approval are separate privileged writes unless the user's current instruction
explicitly authorizes both for that exact PR. - A merge instruction authorizes the merge, not token disclosure, credential
transfer, release/publish, deploy, restart, migration, or another mutation.
Standing approval for mechanical fleet-skills promote PRs (#2239 L3)
Same policy as the codex copy (codex/skills/gh-pr-flow/SKILL.md, same
heading); keep both in step. It is the single exception to the
fresh-approval rules above and exists only while the owner has switched it on:
- 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.