ship-loop-pr — push + open PR child loop
Activation
- Standalone:
/loop ship-loop-pr(manual push from CEO) - Orchestrated: invoked by
ship-loop-masterwhen:- All five inner layers
green(build, test, sanity, watch, smoke) git status --shortshows staged or unstaged changes (something to push)pr.state === 'not_opened'
- All five inner layers
Pre-flight (refuse to run if any fail)
git rev-parse --abbrev-ref HEAD— branch name. Ifmainormaster, refuse with## P0 — ship-loop-pr refused: branch is mainand exitgit status --short— if empty, write{"reason":"clean_tree","skipped":true}and return without changing PR stategh auth status— must be authenticated- Read state file — confirm all five inner layers
green; refuse if anyred
Process
Step 1: Stage + commit if needed
If git status --short shows unstaged changes:
- Stage only files relevant to current work (refuse
git add -Ato avoid grabbing accidental scratchpad files; use explicit paths fromgit diff --name-only) - If no commit yet on this branch (HEAD === origin/main): create commit
- Commit message templated from:
- Linear ticket from state (
SYN-XXX) - List of changed file paths (top 5)
- Pre-gate results summary (build/test/sanity/smoke all green)
- Linear ticket from state (
fix(<scope>): <SYN-XXX> — <ticket title>
Files changed:
<top 5 file paths>
Verification:
- npm run type-check → 0 errors
- npm run lint → 0 errors
- npm test → <X> passed / 0 failed
- npm run build → ✓ Compiled successfully
- npm audit --omit=dev → 0 HIGH
Tracked: <SYN-XXX>
Step 2: Push
git push -u origin <branch>
If rejected with non-fast-forward:
git fetch origingit rebase origin/<branch>- If rebase has conflicts: STOP, escalate
## P1 — Rebase conflict on push, do NOT push, do NOT force - If rebase clean: re-attempt push (one retry only)
Step 3: Open PR
gh pr create --repo CleanExpo/Synthex \
--title "<title from commit>" \
--body "$(cat <<'EOF'
## Summary
<bulleted summary from commit message>
## Verification
<gate results from state file>
## Linear
<SYN-XXX>
🤖 Generated by ship-loop-pr
EOF
)"
If gh pr create returns "pull request already exists for branch":
gh pr view --json url,numberto capture existing PR- Update state with the existing URL/number
- Skip create (treat as success)
Step 4: Update state
Atomic update to layers.pr:
{
"state": "opened",
"url": "https://github.com/CleanExpo/Synthex/pull/<num>",
"number": <num>,
"opened_at": "<iso>",
"ci_state": "pending",
"branch": "<branch>"
}
The ci_state will be polled by master loop on subsequent ticks via gh pr checks <num> (master invokes a quick check rather than re-firing this skill — efficient).
Recipe priorities for this loop
- #11 (non-fast-forward) — rebase + retry
- #12 (PR already exists) — capture existing URL, skip create
Hard rules compliance
- No force push — even on rebase conflict, never
git push --force. Conflicts escalate. - No
git add -A— explicit file paths only, prevents accidental commit of scratchpad outputs - No push from main — pre-flight refuses
- Commit messages cite Linear ticket — required by CLAUDE.md hard rule
Verification
- Run on a clean branch with one staged commit; expect successful push + PR open
- Run on clean tree (no changes); expect skip with reason
- Run on
main; expect P0 escalation refusal - Run with sanity layer red in state; expect skip (master shouldn't have invoked, but defense-in-depth)
Out of scope
- PR review request (gh pr edit --add-reviewer) — separate skill if needed
- PR template enforcement — relies on existing
.github/pull_request_template.mdif present - Auto-tagging issues / setting milestones / setting projects on the PR — defer to senior-pm skill or Linear's GitHub integration