From mad-skills
Ship changes through the full PR lifecycle. Use after completing feature work to commit, push, create PR, wait for checks, and merge. Handles the entire workflow: syncs with main, creates feature branch if needed, groups commits logically with semantic messages, creates detailed PR, monitors CI, fixes issues, squash merges, and cleans up. Invoke when work is ready to ship.
npx claudepluginhub slamb2k/mad-skills --plugin mad-skillsThis skill is limited to using the following tools:
When this skill is invoked, IMMEDIATELY output the banner below before doing anything else.
Creates isolated Git worktrees for feature branches with prioritized directory selection, gitignore safety checks, auto project setup for Node/Python/Rust/Go, and baseline verification.
Executes implementation plans in current session by dispatching fresh subagents per independent task, with two-stage reviews: spec compliance then code quality.
Dispatches parallel agents to independently tackle 2+ tasks like separate test failures or subsystems without shared state or dependencies.
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. The first line of the art has 4 leading spaces — you MUST preserve them.
{tagline}
⠀ ██╗███████╗██╗ ██╗██╗██████╗
██╔╝██╔════╝██║ ██║██║██╔══██╗
██╔╝ ███████╗███████║██║██████╔╝
██╔╝ ╚════██║██╔══██║██║██╔═══╝
██╔╝ ███████║██║ ██║██║██║
╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝╚═╝
Taglines:
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.
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 mergeDetect 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})
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 |
Platform-conditional rules:
gh: Only required when PLATFORM == github. Skip for AzDO repos.az devops: Only checked when PLATFORM == azdo. Skip for GitHub repos.For each applicable row, in order:
{PLATFORM}✅ commit agent general-purposeWhen PLATFORM == azdo, determine which tooling is available. Set AZDO_MODE
for use in all subsequent stages:
if az devops -h &>/dev/null; then
AZDO_MODE="cli"
else
AZDO_MODE="rest"
fi
cli: Use az repos / az pipelines commands (preferred)rest: Use Azure DevOps REST API via curl. Requires a PAT (personal
access token) in AZURE_DEVOPS_EXT_PAT or AZDO_PAT env var. If no PAT
is found, prompt the user to either install the CLI or set the env var.Report in pre-flight:
az devops cli — version foundaz devops cli — not found → using REST API fallbackaz devops cli — not found, no PAT configured → halt with setup instructionsWhen PLATFORM == azdo, extract organization and project from the remote URL
and validate they are usable. These values are needed by every az repos /
az pipelines command and every REST API call.
# Extract org and project from remote URL patterns:
# https://dev.azure.com/{ORG}/{PROJECT}/_git/{REPO}
# https://{ORG}@dev.azure.com/{ORG}/{PROJECT}/_git/{REPO}
# {ORG}@vs-ssh.visualstudio.com:v3/{ORG}/{PROJECT}/{REPO}
REMOTE_URL=$(git remote get-url origin 2>/dev/null)
if echo "$REMOTE_URL" | grep -q 'dev\.azure\.com'; then
# HTTPS format: https://dev.azure.com/{ORG}/{PROJECT}/_git/{REPO}
# Also handles: https://{ORG}@dev.azure.com/{ORG}/{PROJECT}/_git/{REPO}
AZDO_ORG=$(echo "$REMOTE_URL" | sed -n 's|.*dev\.azure\.com/\([^/]*\)/.*|\1|p')
AZDO_PROJECT=$(echo "$REMOTE_URL" | sed -n 's|.*dev\.azure\.com/[^/]*/\([^/]*\)/.*|\1|p')
AZDO_ORG_URL="https://dev.azure.com/$AZDO_ORG"
elif echo "$REMOTE_URL" | grep -q 'vs-ssh\.visualstudio\.com'; then
# SSH format: {ORG}@vs-ssh.visualstudio.com:v3/{ORG}/{PROJECT}/{REPO}
AZDO_ORG=$(echo "$REMOTE_URL" | sed -n 's|.*vs-ssh\.visualstudio\.com:v3/\([^/]*\)/.*|\1|p')
AZDO_PROJECT=$(echo "$REMOTE_URL" | sed -n 's|.*vs-ssh\.visualstudio\.com:v3/[^/]*/\([^/]*\)/.*|\1|p')
AZDO_ORG_URL="https://dev.azure.com/$AZDO_ORG"
elif echo "$REMOTE_URL" | grep -q 'visualstudio\.com'; then
# Legacy HTTPS format: https://{ORG}.visualstudio.com/{PROJECT}/_git/{REPO}
AZDO_ORG=$(echo "$REMOTE_URL" | sed -n 's|.*//\([^.]*\)\.visualstudio\.com.*|\1|p')
AZDO_PROJECT=$(echo "$REMOTE_URL" | sed -n 's|.*/\([^/]*\)/_git/.*|\1|p')
AZDO_ORG_URL="https://${AZDO_ORG}.visualstudio.com"
fi
# URL-decode for CLI/display; keep URL-safe versions for REST API paths
AZDO_PROJECT_URL_SAFE="$AZDO_PROJECT"
AZDO_ORG=$(python3 -c "import urllib.parse; print(urllib.parse.unquote('$AZDO_ORG'))")
AZDO_PROJECT=$(python3 -c "import urllib.parse; print(urllib.parse.unquote('$AZDO_PROJECT_URL_SAFE'))")
if [ -z "$AZDO_ORG" ] || [ -z "$AZDO_PROJECT" ]; then
echo "❌ Could not extract organization/project from remote URL"
echo " Remote: $REMOTE_URL"
echo ""
echo "Ensure the remote URL follows one of these formats:"
echo " https://dev.azure.com/{ORG}/{PROJECT}/_git/{REPO}"
echo " https://{ORG}.visualstudio.com/{PROJECT}/_git/{REPO}"
echo " {ORG}@vs-ssh.visualstudio.com:v3/{ORG}/{PROJECT}/{REPO}"
# HALT — cannot proceed without org/project context
fi
When AZDO_MODE == cli, also configure the defaults so commands work correctly:
az devops configure --defaults organization="$AZDO_ORG_URL" project="$AZDO_PROJECT"
When AZDO_MODE == rest, store these for API calls:
$AZDO_ORG_URL/$AZDO_PROJECT_URL_SAFE/_apisAuthorization: Basic $(printf ":%s" "$PAT" | base64 | tr -d '\n')Report in pre-flight:
azdo context — org: {AZDO_ORG}, project: {AZDO_PROJECT}azdo context — could not parse from remote URL → halt with instructionsPass {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}.
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).
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_ORG_URL}, {PAT} into the prompt.
Parse SHIP_REPORT. Abort if failed.
Rollback: If push succeeds but PR creation fails, report the error and suggest the manual PR creation command. Do NOT revert the push.
gh pr create --head {branch}az repos pr create --source-branch {branch} --target-branch {DEFAULT_BRANCH} --org {AZDO_ORG_URL} --project {AZDO_PROJECT}{AZDO_ORG_URL}/{AZDO_PROJECT}/_apis/git/repositories/{repo}/pullrequests?api-version=7.0If --pr-only flag: Stop here and report PR URL to user.
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.
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.
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.
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)
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:
/sync or any other skillOnce 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).
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.
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.
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}"
After sync completes, 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
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):
TaskList for any in-progress or pending tasks
in the current sessionclaude-mem plugin is available, search for recent
checkpoints or plans related to this projectSummarize 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.
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}