Ship - Full PR Lifecycle
When this skill is invoked, IMMEDIATELY output the banner below before doing anything else.
Pick ONE tagline at random — vary your choice each time.
CRITICAL: Reproduce the banner EXACTLY character-for-character, including the diagonal / glyph before the name.
{tagline}
/$$ /$$$$$$ /$$ /$$ /$$$$$$ /$$$$$$$
/$$//$$__ $$| $$ | $$|_ $$_/| $$__ $$
/$$/| $$ \__/| $$ | $$ | $$ | $$ \ $$
/$$/ | $$$$$$ | $$$$$$$$ | $$ | $$$$$$$/
/$$/ \____ $$| $$__ $$ | $$ | $$____/
/$$/ /$$ \ $$| $$ | $$ | $$ | $$
/$$/ | $$$$$$/| $$ | $$ /$$$$$$| $$
|__/ \______/ |__/ |__/|______/|__/
Taglines:
- 🚚 Special delivery!!!
- 📦 If it compiles, it ships!
- 🚢 Anchors aweigh!
- 🙏 git push and pray!
- ⚡ Shipping faster than Amazon Prime!
- 🏀 Yeet the code into production!
- 📬 Another one for the merge queue!
- 🟢 LGTM — Let's Get This Merged!
Output Formatting
After the banner, display parsed input:
┌─ Input ────────────────────────────────────────
│ {Field}: {value}
│ Flags: {parsed flags or "none"}
└────────────────────────────────────────────────
Pre-flight results:
── Pre-flight ───────────────────────────────────
✅ {dep} {version or "found"}
⚠️ {dep} not found → {fallback detail}
❌ {dep} missing → stopping
──────────────────────────────────────────────────
Stage/phase headers: ━━ {N} · {Name} ━━━━━━━━━━━━━━━━━━━━━━━━━
Status icons: ✅ done · ❌ failed · ⚠️ degraded · ⏳ working · ⏭️ skipped
Ship changes through the complete PR lifecycle. Deterministic stages (sync, CI
polling, merge) run as bash scripts for speed and reliability. Only stages that
require reasoning (commit/PR authoring, CI fix analysis) use LLM subagents.
Stage prompts are in references/stage-prompts.md.
Flags
Parse optional flags from the request:
--pr-only: Stop after creating the PR--no-squash: Use regular merge instead of squash--keep-branch: Don't delete the source branch after merge--no-superpowers: Force standalone merge even when Superpowers is installed--auto: Full autopilot (pr-first-autonomous-build.md REQ-013, superseding autonomous-execution-mode.md REQ-029) — CI-watch and fix-loop run exactly as interactive/ship(2-attempt cap unchanged), and on green checks it proceeds to merge (Stage 5) and post-merge sync/teardown (Stage 5b) just like interactive/ship, rather than stopping at the PR. The PR description body remains the durable report; seereferences/autonomous-report.md.
Platform Detection
Detect the hosting platform before pre-flight so dependency checks are platform-specific:
REMOTE_URL=$(git remote get-url origin 2>/dev/null)
if echo "$REMOTE_URL" | grep -qiE 'dev\.azure\.com|visualstudio\.com'; then
PLATFORM="azdo"
elif echo "$REMOTE_URL" | grep -qi 'github\.com'; then
PLATFORM="github"
else
PLATFORM="github" # default fallback
fi
Pass {PLATFORM} into all stage prompts. Each stage uses the appropriate
CLI tool: gh for GitHub, az repos/az pipelines for Azure DevOps.
Display the detected platform to the user immediately after detection:
⚙️ Platform: GitHub (github.com)
or:
⚙️ Platform: Azure DevOps ({AZDO_ORG}/{AZDO_PROJECT})
Pre-flight
Before starting, check all dependencies in this table. The table contains all dependencies — some are platform-conditional (see notes after table).
| Dependency | Type | Check | Required | Resolution | Detail |
|---|---|---|---|---|---|
| git | cli | git --version |
yes | stop | Install from https://git-scm.com |
| gh | cli | gh --version |
yes | url | https://cli.github.com |
| az devops | cli | az devops -h 2>/dev/null |
no | fallback | Falls back to REST API with PAT; see AzDO tooling below |
| superpowers | plugin | on-disk glob via scripts/lib/superpowers.js | no | fallback | Replaces final merge with superpowers:finishing-a-development-branch when present; see references/superpowers-deferral.md |
Platform-conditional rules:
gh: Only required whenPLATFORM == github. Skip for AzDO repos.az devops: Only checked whenPLATFORM == azdo. Skip for GitHub repos.
For each applicable row, in order:
- Skip rows that don't apply to the detected
{PLATFORM} - Run the Check command (for cli/npm) or test file existence (for agent/skill)
- If found: continue silently
- If missing: apply Resolution strategy
- stop: notify user with Detail, halt execution
- url: notify user with Detail (install link), halt execution
- install: notify user, run the command in Detail, continue if successful
- ask: notify user, offer to run command in Detail, continue either way (or halt if required)
- fallback: notify user with Detail, continue with degraded behavior
- After all checks: summarize what's available and what's degraded
- Always show the commit/PR agent:
✅ commit agent general-purpose
Azure DevOps Setup (AzDO only)
When PLATFORM == azdo, run the AzDO tooling detection and configuration
validation in references/azdo-setup.md — it sets AZDO_MODE (cli vs REST
fallback), extracts AZDO_ORG/AZDO_PROJECT/AZDO_ORG_URL/AZDO_PROJECT_URL_SAFE
from the remote URL, configures az devops defaults (or the REST base URL + auth
header), and reports the azdo context. GitHub repos skip this section.
Pass {AZDO_MODE}, {AZDO_ORG}, {AZDO_PROJECT}, {AZDO_PROJECT_URL_SAFE},
{AZDO_ORG_URL} into
all stage prompts alongside {PLATFORM}.
Read default_branch and remote from Stage 1's SYNC_REPORT. These are
substituted into all stage prompts as {REMOTE} and {DEFAULT_BRANCH}.
Pre-Ship Location Check
Before syncing, run the shared root-mismatch check from
references/location-check.md ({caller} = "before syncing").
Stage 1: Sync
Run the sync script directly (no LLM needed):
SKILL_ROOT="<resolved plugin root>"
bash "$SKILL_ROOT/skills/sync/scripts/sync.sh" "{REMOTE}" "{DEFAULT_BRANCH}"
Parse SYNC_REPORT from output markers. Extract remote and default_branch.
Abort if exit code is 1 (fatal).
If worktree_mode=true and worktree_removed is an actual path (not none
or a skipped … reason), apply the same worktree-mode session-return
handling as Stage 5b before any further git command — this means the branch
was already merged; report that to the user and stop rather than proceeding
to commit on a now-gone branch.
Stage 1b: Follow-ups Ledger (REQ-010/044)
Capture any follow-up ideas, deferred fixes, or open questions surfaced during
this work into the committed ledger so they survive /clear. Capture is
automatic and deduped — don't ask permission to capture, only decide what to
do with the items.
This runs before Stage 2, and that placement is the point. The ledger is a committed file, so a ledger write needs a commit to carry it. Every stage from here on has one; the post-merge report does not. Capturing here means the entries ride the same commit series as the work, land in the same PR, get reviewed alongside it, and never leave the default branch dirty. Capturing after the merge — where this used to live — writes to a freshly-clean default branch with nothing left to pick it up.
Because no PR exists yet, source is the branch name (/ship <branch>), not
/ship #<pr>. That is provenance, not data: the branch is equally traceable
from the merge commit, and it is knowable at the only moment the write is free.
--auto runs this identically. On a headless run, skip the breach-time
AskUserQuestion prompt below and go straight to the real capture (the silent
safety net, matching /build Stage 10's headless path); the interactive
per-candidate prompt only runs when a live session is watching. Capture itself
always runs, in both modes.
Preview before capturing (REQ-006/008). Call the non-mutating preview with the same items about to be captured, so a would-be cap breach surfaces before anything relocates:
_R="${CLAUDE_PLUGIN_ROOT:-$HOME/.claude/plugins/marketplaces/slamb2k}"
node "$_R/hooks/session-guard.cjs" logbook-capture-preview \
'[{"title":"…","category":"ideas","source":"/ship <branch>"}, …]'
Parse would_relocate from the LOGBOOK_CAPTURE_PREVIEW_BEGIN…END block.
- If
would_relocate: none→ skip straight to the real capture below. - If
would_relocatelists candidates → present each one individually via AskUserQuestion (mirroring Stage 10's "Let me choose per item" pattern) before the real capture runs. Address each candidate by its title text, not the preview list's ordinal — the preview numbers candidates in victim-selection order (lowest priority, then oldest date), which does not matchlogbook-resolve/logbook-dismiss's plain-number selector (hot-file display order); the title is matched by substring regardless of ordering, so it's the only selector guaranteed to hit the right item. Options per candidate:- "Resolve now" →
node "$_R/hooks/session-guard.cjs" logbook-resolve "<title>" - "Dismiss" →
node "$_R/hooks/session-guard.cjs" logbook-dismiss "<title>" - "Leave it" → no action; it relocates to
LOGBOOK-ARCHIVE.mdwhen the real capture runs
- "Resolve now" →
Where the items come from. The same sources the "What's Next?" section reads
after the merge — TaskList, the session so far, and Claude Code's auto-memory
— plus anything this session deferred, skipped, or flagged as a known gap. All
of that is knowable now; only the PR number is not. Anything "What's Next?"
would have listed but that isn't being acted on belongs here.
Check for entries already in the tree first. /build Stage 10 captures to
the same ledger at the end of a build, so LOGBOOK.md is often already dirty
when /ship starts. Run git diff --stat LOGBOOK.md before capturing: an item
/build already recorded is present with different wording, which dedup will
not catch, and capturing it again mints a near-duplicate. Capture only what is
genuinely new to this session.
Then run the real capture:
node "$_R/hooks/session-guard.cjs" logbook-capture \
'[{"title":"…","category":"ideas","source":"/ship <branch>"}, …]'
If a captured item is also turned into a TaskCreate task, re-capture it with a
"link":"task#<id>" so it auto-resolves when the task completes (REQ-012).
logbook-capture's output reports relocated:[…] titles — those items moved to
the archive file (LOGBOOK-ARCHIVE.md), never dismissed or resolved; they're
still open and still addressable via /logbook archive. Report them to the user
(never silent, GUD-002). Do NOT show the full ledger here — that comes with the
final report, after the merge.
If no follow-ups were surfaced and the ledger is empty, do nothing (AC-008) and proceed to Stage 2.
Stage 2: Commit, Push & Create PR
This stage needs to read and understand code to write good commit messages and PR descriptions — it's one of the few stages that requires an LLM.
Launch general-purpose subagent (reads diffs + source files):
Task(
subagent_type: "general-purpose",
description: "Analyze, commit, push, and create PR",
prompt: <read from references/stage-prompts.md#stage-2>
)
Substitute {USER_INTENT}, {FILES_TO_INCLUDE}, {FILES_TO_EXCLUDE},
{REMOTE}, {DEFAULT_BRANCH}, {PLATFORM}, {AZDO_MODE}, {AZDO_ORG},
{AZDO_PROJECT}, {AZDO_PROJECT_URL_SAFE}, {AZDO_ORG_URL}, {PAT},
{SKILL_ROOT} into the prompt.
Parse SHIP_REPORT. Abort if failed.
Rollback: PR creation runs through scripts/create-pr.sh, which is
idempotent — it reuses an already-open PR on the branch (reused=true)
instead of erroring, so that is never a failure. If push succeeds but
create-pr.sh reports status=failed, report the error from its errors
field and suggest the manual PR creation command. Do NOT revert the push.
- GitHub:
gh pr create --head {branch} - Azure DevOps (cli):
az repos pr create --source-branch {branch} --target-branch {DEFAULT_BRANCH} --org {AZDO_ORG_URL} --project {AZDO_PROJECT} - Azure DevOps (rest): Create PR via
{AZDO_ORG_URL}/{AZDO_PROJECT}/_apis/git/repositories/{repo}/pullrequests?api-version=7.0
If --pr-only flag: Stop here and report PR URL to user.
Stage 3 + 4: CI Watch & Fix Loop
Run CI monitoring in the foreground so failures are caught and fixed immediately. This stage loops: watch → detect failure → fix → push → watch again.
Maximum 2 fix attempts. If CI still fails after 2 rounds, report to user and stop.
--auto mode: CI-watch/fix-loop behavior is unchanged from interactive /ship — the 2-attempt cap already matches --auto's bound (REQ-031). No dispatch needed here.
Watch
Run the CI watch script directly (no LLM needed — just polling):
SKILL_ROOT="<resolved plugin root>"
bash "$SKILL_ROOT/skills/ship/scripts/ci-watch.sh" \
"{PLATFORM}" "{PR_NUMBER}" "{BRANCH}" \
--azdo-mode="{AZDO_MODE}" --azdo-org-url="{AZDO_ORG_URL}" \
--azdo-project="{AZDO_PROJECT}" --azdo-project-url-safe="{AZDO_PROJECT_URL_SAFE}"
Briefly inform the user: ⏳ Watching CI for PR #{pr_number}...
Parse CHECKS_REPORT from output markers. Exit code 0=passed, 1=failed, 2=timeout.
Fix (if needed)
If CHECKS_REPORT shows some_failed, immediately launch a fix subagent —
do not wait, do not ask the user:
Task(
subagent_type: "general-purpose",
description: "Fix CI failures",
prompt: <read from references/stage-prompts.md#stage-4>
)
Substitute {PR_NUMBER}, {BRANCH}, {FAILING_CHECKS}, {PLATFORM},
{AZDO_MODE}, {AZDO_ORG}, {AZDO_ORG_URL}, {AZDO_PROJECT}, {PAT} into the prompt.
The fix subagent MUST commit and push before returning. Once it returns, immediately loop back to Watch to re-check CI.
Loop summary
attempt = 0
while attempt < 2:
CHECKS = run_watch()
if CHECKS.status == "all_passed":
break → proceed immediately to Stage 5 (do NOT ask user to confirm merge)
if CHECKS.status == "no_checks":
→ prompt user via AskUserQuestion:
"No CI checks found for PR #{PR_NUMBER} after waiting 30 seconds.
The repository may not have CI configured, or checks may still be registering."
Options:
- "Merge without checks (Recommended)" → break to Stage 5
- "Wait another 30 seconds" → re-run the watch (do not increment attempt)
- "Cancel" → stop /ship and display failure banner
break (if user chose merge or after re-wait resolves)
attempt += 1
run_fix(CHECKS.failing_checks)
→ loop back to watch
if attempt == 2 and still failing:
→ display failure banner and stop (see Failure Handling below)
Failure Handling
When /ship fails at ANY point (CI exhausts fix attempts, merge fails, post-merge verification fails), display the failure banner and STOP:
┌─ Ship · FAILED ──────────────────────────────────
│
│ ❌ PR #{PR_NUMBER} was NOT merged
│
│ Reason: {specific failure reason}
│ Branch: {BRANCH} (still active)
│
│ ⚠️ You are still on branch '{BRANCH}'.
│ Run /sync to return to main before starting new work.
│
└───────────────────────────────────────────────────
Critical rules on failure:
- Do NOT proceed to "What's Next?"
- Do NOT suggest next tasks or follow-up work
- Do NOT invoke
/syncor any other skill - Do NOT use language like "will be auto-merged" or "PR is pending"
- The failure banner is the LAST output — nothing follows it
Stage 5: Merge & Final Sync
--auto proceeds through this stage (pr-first-autonomous-build.md REQ-013,
superseding REQ-029's stop-at-PR): on green checks it merges and runs Stage 5b
teardown exactly as interactive /ship does — it does not stop at the open PR.
The PR description body remains the durable report (references/autonomous-report.md),
now a record of what was merged. The completion-mode opt-in that authorizes an
unattended merge is set earlier, on the spec (completion_mode: auto-ship), not
by this flag.
Once checks pass, immediately proceed to merge — do not ask the user for
confirmation. The user invoked /ship expecting the full lifecycle; stopping
to ask defeats the purpose. Squash merge and delete the source branch are the
defaults (override via --no-squash and --keep-branch flags only).
5a. Merge the PR
Superpowers deferral: When Superpowers is detected and --no-superpowers is not set, after CI is green and auto-fix, announce ⚡ Superpowers detected — deferring final integration to superpowers:finishing-a-development-branch and invoke that skill to present merge/PR/cleanup options instead of calling merge.sh directly (CI-poll + auto-fix and the no-manual-CI-trigger rule still apply); otherwise run merge.sh below.
Run the merge script directly (no LLM needed):
SKILL_ROOT="<resolved plugin root>"
bash "$SKILL_ROOT/skills/ship/scripts/merge.sh" \
"{PLATFORM}" "{PR_NUMBER}" \
{MERGE_FLAGS} {BRANCH_FLAGS} \
--azdo-mode="{AZDO_MODE}" --azdo-org-url="{AZDO_ORG_URL}" \
--azdo-project="{AZDO_PROJECT}" --azdo-project-url-safe="{AZDO_PROJECT_URL_SAFE}"
Parse LAND_REPORT from output markers. Exit code 0=merged, 1=failed.
5a-verify. Verify merge completed
After merge.sh reports success, verify the PR actually merged:
GitHub:
PR_STATE=$(gh pr view "$PR_NUMBER" --json state --jq '.state')
Expected: "MERGED"
AzDO CLI:
PR_STATE=$(az repos pr show --id "$PR_NUMBER" --query status -o tsv)
Expected: "completed"
AzDO REST:
PR_RESP=$(curl -s -H "$AUTH" "${AZDO_ORG_URL}/${AZDO_PROJECT_URL_SAFE}/_apis/git/repositories/${REPO_ID}/pullRequests/${PR_NUMBER}?api-version=7.1")
PR_STATE=$(echo "$PR_RESP" | jq -r '.status')
Expected: "completed"
If the PR is NOT in the expected merged/completed state, treat as a failure — display the failure banner (see Failure Handling) with reason "PR merge was accepted but PR is still in '{PR_STATE}' state. The merge may be queued or deferred." and stop.
5b. Sync local repo
After the merge script succeeds, run the sync script to checkout the default branch, pull the merge commit, and clean up stale branches:
bash "$SKILL_ROOT/skills/sync/scripts/sync.sh" "{REMOTE}" "{DEFAULT_BRANCH}"
Parse the SYNC_REPORT block from the output for worktree_mode and
worktree_removed.
Worktree mode (worktree_mode=true). Skip the verification block below
entirely in ALL worktree_mode=true cases — regardless of whether
worktree_removed is an actual path, none, or a skipped … reason
(dirty, unfinished, etc.) — sync.sh already synced main in the primary
(main_sync) and, by design, never checks out the default branch in a
worktree.
Additionally, if worktree_removed is an actual path (not none or a
skipped … reason), the session's cwd no longer exists — return to
primary_path (from the same report) BEFORE any further git command: use
the native ExitWorktree tool if the session entered this worktree via
EnterWorktree and the tool is available, otherwise run the next Bash
command with cd "<primary_path>" first.
Non-worktree mode (worktree_mode=false). Verify the working tree is on
the default branch:
CURRENT=$(git branch --show-current)
if [ "$CURRENT" != "{DEFAULT_BRANCH}" ]; then
git checkout {DEFAULT_BRANCH}
git pull {REMOTE} {DEFAULT_BRANCH}
fi
What's Next?
Only run this section if /ship succeeded (PR is merged). If any failure occurred, the failure banner was already displayed and nothing should follow it.
After a successful merge, determine what work comes next by checking these sources (in priority order):
- Active tasks — check
TaskListfor any in-progress or pending tasks in the current session - Session context — review the conversation so far for any stated plans, follow-up items, or deferred work the user mentioned
- Memory — check Claude Code's built-in auto-memory
(
~/.claude/projects/<project>/memory/MEMORY.md) for recent checkpoints or plans related to this project
Summarize the result as 1–3 short bullet points for the ⚡ Next section of
the report. If nothing is found, omit the section entirely — do not fabricate
next steps.
Final Report to User
Compile all stage reports into a summary:
┌─ Ship · Report ────────────────────────────────
│
│ ✅ Ship complete
│
│ 🌿 Branch: {branch}
│ 🔗 PR: {pr_url}
│ 🔀 Merged: {merge_commit} ({merge_type})
│ 🌿 Now on: {DEFAULT_BRANCH} (up to date)
│
│ 📝 Commits
│ • {commit message 1}
│ • {commit message 2}
│
│ 📊 {count} files changed ({diff_summary})
│
│ ⚡ Next
│ • {next item 1}
│ • {next item 2}
│
└─────────────────────────────────────────────────
If nothing was found for "What's Next?", omit the ⚡ Next section.
If any stage failed, add:
│ ❌ Failed at: {stage name}
│ {error description}
│ {suggested resolution}
After presenting the report (PR merged, tree clean, not an active cycle), resurface any deferred lifecycle step:
_R="${CLAUDE_PLUGIN_ROOT:-$HOME/.claude/plugins/marketplaces/slamb2k}"
node "$_R/hooks/session-guard.cjs" lifecycle-checkpoint
If the command prints a LIFECYCLE_OFFER_BEGIN…END block, present that offer to
the user via AskUserQuestion as instructed inside the block. If it prints nothing
(or LIFECYCLE_OFFER_NONE), do not mention the lifecycle engine.
Follow-ups Ledger — post-merge net (REQ-010/044)
The bulk of capture already happened at Stage 1b, before the commit, so those entries are in the merge commit. This is only the net for follow-ups that could not have been known then — ones the shipping itself produced: a CI failure and its fix, a merge surprise, something learned from the deploy.
Show the ledger either way, so the report ends with what is outstanding:
_R="${CLAUDE_PLUGIN_ROOT:-$HOME/.claude/plugins/marketplaces/slamb2k}"
node "$_R/hooks/session-guard.cjs" logbook-list
If nothing new arose after Stage 1b — the common case — stop here. If the ledger is empty and nothing was surfaced, show nothing at all (AC-008).
If shipping genuinely produced new follow-ups, capture them with
source: "/ship #<pr>" (the PR number is known now), using the same
preview-then-capture flow as Stage 1b. Then tell the user plainly:
The ledger now has uncommitted changes on
{DEFAULT_BRANCH}— captured after the merge, so no commit was left to carry them.
Offer, do not assume: commit in place on the default branch, or leave them for
the next ship's Stage 1b to fold into that branch. Both are reasonable; a
docs-only PR for one ledger line usually is not. Never leave the default
branch dirty without saying so — a silently modified ledger looks like a
clean tree to the next session, and /sync will stash it.