ship
Skill0 starsChanged 30 days agoMITView on GitHub
Full docs-commit-audit-test-push-deploy pipeline in one command. Brings README, CLAUDE.md, CHANGELOG and help pages back in sync with the change before committing, stages tracked changes, generates a conventional commit message (or uses the provided one), enforces audit and (where the project configures one) the full test suite before push, deploys via project-specific method, and verifies the deploy. Use when ready to ship completed work. Push requires a fresh audit marker and a green suite; bypassing either is only possible as an explicit, logged user decision, never silently.
- Reads credentials
- Commits and pushes
194 other projects use a version of this.
Adopted by
Most recently taken up by
- wboayue/rust-ibapitheir own version, first seen Nov 9, 2026
- haiggoh/run-to-completiontheir own version, first seen Nov 8, 2026
- sanmidable/skillstheir own version, first seen Nov 8, 2026
- Yesterday-AI/ytstacktheir own version, first seen Nov 8, 2026
- gaoyu06/harnesstheir own version, first seen Nov 8, 2026
- PeanutGT/NutsSkillstheir own version, first seen Nov 8, 2026
Used with
Projects with it also use ios-sync, open-gstack-browser and 10 more.
Other files people use alongside this one.
Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
Saqlain-811Skillios-sync
Regenerate the iOS debug bridge against the latest upstream gstack templates. (gstack)
Also in 6 projects that use this
seek888Skillopen-gstack-browser
Launch GStack Browser — AI-controlled Chromium with the sidebar extension baked in. Opens a visible browser window where you can watch every action in real time. The sidebar shows a live activity feed and chat. Anti-bot stealth built in. Use when asked to "open gstack browser", "launch browser", "connect chrome", "open chrome", "real browser", "launch chrome", "side panel", or "control my browser". Voice triggers (speech-to-text aliases): "show me the browser".
Also in 10 projects that use this
ayudaySkillsetup-deploy
Configure deployment settings for /land-and-deploy. Detects your deploy platform (Fly.io, Render, Vercel, Netlify, Heroku, GitHub Actions, custom), production URL, health check endpoints, and deploy status commands. Writes the configuration to CLAUDE.md so all future deploys are automatic. Use when: "setup deploy", "configure deployment", "set up land-and-deploy", "how do I deploy with gstack", "add deploy config".
Also in 19 projects that use this
ShreyRaviSkilllanding-report
Read-only queue dashboard for workspace-aware ship. Shows which VERSION slots are currently claimed by open PRs, which sibling Conductor workspaces have WIP work likely to ship soon, and what slot /ship would pick next. No mutations — just a snapshot. Use when asked to "landing report", "what's in the queue", "show me open PRs", or "which version do I claim next". (gstack)
Also in 9 projects that use this
zebbernSkilldebug-fix
Find and fix a bug or issue — from any source (GitHub issue, error message, user report, or observed behavior)
Also in 4 projects that use this
ramybarsoumSkilldocument-release
Post-ship documentation update. Reads all project docs, cross-references the diff, updates README/ARCHITECTURE/CONTRIBUTING/CLAUDE.md to match what shipped, polishes CHANGELOG voice, cleans up TODOS, and optionally bumps VERSION. Use when asked to "update the docs", "sync documentation", or "post-ship docs". Proactively suggest after a PR is merged or code is shipped.
Also in 23 projects that use this
Other versions194 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 194
- garrytan/gstack★ 141ksame name
- rtk-ai/rtk★ 86ksame name
- simstudioai/sim★ 30ksame name
- millionco/react-doctor★ 15ksame name
- BuilderIO/agent-native★ 7.6ksame name
- nightly-labs/openbot★ 6.3ksame name
- fallow-rs/fallow★ 5.3ksame name
- puncsky/system-design-and-architecture★ 3.6ksame name
- plugin87/ux-ui-agent-skills★ 3.1ksame name
- ShenSeanChen/waku-agent★ 2ksame name
184 more not listed
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository13 other files it gives its agents.
- audit/CLAUDE.mdCLAUDE.md
- CLAUDE.mdCLAUDE.md
- delegate/CLAUDE.mdCLAUDE.md
- plan-it/CLAUDE.mdCLAUDE.md
- screens/CLAUDE.mdCLAUDE.md
- ship/CLAUDE.mdCLAUDE.md
- store-assets/CLAUDE.mdCLAUDE.md
- auditSkill
- delegateSkill
- plan-itSkill
- readinessSkill
- screensSkill
- store-assetsSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
21 rules. 0 are common, 7 are distinctive, and 14 are very specific to this project.
0
Common
Most of the 180 edited versions we read keep these.
7
Distinctive
This author's own choices. Worth a look.
14
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 42
If no tracked changes and no staged files: report and stop.
- Very specific: line 145
If DEPLOY_COMMAND = "fastlane": AskUserQuestion — which lane?
- Distinctive: line 150
If no deploy method found: AskUserQuestion with options:
- Distinctive: line 191
Ask once, before anything is committed, so the user knows up front whether this run takes minutes or includes the audit gate.
- Distinctive: line 240
Never create a doc file that does not exist: No CHANGELOG in the repo means this ship has no changelog entry.
- Distinctive: line 244
Only the claims the diff invalidates: No rewrites, no tidying of neighbouring prose, no reformatting.
- Very specific: line 278
If
$ARGUMENTSis non-empty: use it verbatim as the commit message (skip generation). - Very specific: line 282
If
$ARGUMENTSis empty: read the diff and generate a conventional commit message: - Very specific: line 296
Check the staged diff for sensitive files AND for secret-shaped content.
- Very specific: line 315
If commit fails (hook rejection, empty): run the log call (outcome=commit_failed), report the hook output and stop.
- Very specific: line 319
If Phase 0.5 set SHIP_GATE=skipped: print Audit skipped (quick-fix mode chosen in Phase 0.5).
- Distinctive: line 345
If marker missing or stale: AskUserQuestion:
- Very specific: line 349
Never silently skip the audit.
- Very specific: line 353
Only when test-command: is set in .claude/ship.md.
- Distinctive: line 372
Never silently skip a red suite.
- Very specific: line 389
If DEPLOY_COMMAND is ci: skip this phase (SHIP_DEPLOY=ci, sourced block, orch_state_save SHIP_DEPLOY).
- Very specific: line 457
Only in quick-fix mode, AskUserQuestion (header "Audit") when $SHIP_GATE is skipped (detail: references/audit-collection.md):
- Very specific: line 463
If CI_STATUS is not success for a ci deploy: run the log call (outcome=deploy_failed), report the run URL, stop.
- Very specific: line 465
If deploy failed: run the log call (outcome=deploy_failed), then:
- Very specific: line 476
If health check failed: run the log call (outcome=health_failed), then:
- Very specific: line 488
Run the log call (outcome=shipped).
“Common” is measured: kept by at least 6 in 10 of the 180 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Ship
Docs -> Commit -> Audit -> Tests -> Push -> Deploy -> Verify. In that order. No skipping.
(The test phase runs only when the project sets test-command: in .claude/ship.md. The audit
phase is skipped only when the user picks the quick-fix mode in Phase 0.5.)
Phase 0: Pre-flight
# Must be in a git repo
git rev-parse --show-toplevel 2>/dev/null || { echo "Not a git repo."; exit 1; }
# Check for changes to ship
git status --short
git diff --stat HEAD 2>/dev/nullIf no tracked changes and no staged files: report and stop. Nothing to ship.
Detect deploy method and health check URL (check in priority order):
# 1. Project config wins — read the values if present, all three through the one parser in the lib
for c in "$(dirname "${CLAUDE_SKILL_DIR:-/nonexistent}")/audit/bin/lib-orchestrator.sh" "$HOME/.claude/skills/audit/bin/lib-orchestrator.sh"; do [ -f "$c" ] && { . "$c"; break; }; done # fresh shell per block
orch_state_clear # /ship has no in-progress claim; start this run's cross-block state empty
DEPLOY_COMMAND=$(orch_ship_value deploy-command) || DEPLOY_COMMAND=""
HEALTH_URL=$(orch_ship_value health-check) || HEALTH_URL=""
# 2. Detect deploy method from known files
if [ -z "$DEPLOY_COMMAND" ]; then
[ -f fly.toml ] && DEPLOY_COMMAND="fly deploy"
[ -f vapor.yml ] && DEPLOY_COMMAND="vapor deploy production"
[ -f vapor.yaml ] && DEPLOY_COMMAND="vapor deploy production"
[ -f deploy.sh ] && DEPLOY_COMMAND="bash deploy.sh"
[ -f .vercel/project.json ] && DEPLOY_COMMAND="vercel --prod"
[ -f netlify.toml ] && DEPLOY_COMMAND="netlify deploy --prod"
[ -f railway.toml ] && DEPLOY_COMMAND="railway up"
ls .github/workflows/deploy*.yml 2>/dev/null | head -1 | grep -q . && DEPLOY_COMMAND="ci"
# iOS / BaaS
[ -f fastlane/Fastfile ] && DEPLOY_COMMAND="fastlane" # lane asked below
[ -f firebase.json ] && DEPLOY_COMMAND="firebase deploy"
[ -f .firebaserc ] && DEPLOY_COMMAND="firebase deploy"
[ -f supabase/config.toml ] && DEPLOY_COMMAND="supabase db push && supabase functions deploy"
[ -f amplify.yml ] && DEPLOY_COMMAND="amplify push --yes"
[ -d .xcode/cloud ] && DEPLOY_COMMAND="ci" # Xcode Cloud triggers on push
fi
# Fastlane: ask which lane (beta / release / custom)
if [ "$DEPLOY_COMMAND" = "fastlane" ]; then
LANES=$(grep -E '^\s*lane\s+:' fastlane/Fastfile 2>/dev/null \
| sed 's/.*lane\s*:\([a-z_]*\).*/\1/' | tr '\n' ' ')
# DEPLOY_COMMAND resolved by AskUserQuestion below (see "no method found" block)
# Pass LANES as context to the question
fi
# 3. Auto-detect health check URL from codebase (only if not in .claude/ship.md)
if [ -z "$HEALTH_URL" ]; then
# fly.toml: app name + health_checks path
if [ -f fly.toml ]; then
FLY_APP=$(grep '^app\s*=' fly.toml | head -1 | sed 's/.*=\s*["\x27]\(.*\)["\x27]/\1/')
FLY_HEALTH=$(grep -A2 'health_checks' fly.toml | grep 'path' | head -1 \
| sed 's/.*path\s*=\s*["\x27]\(.*\)["\x27]/\1/')
FLY_HEALTH="${FLY_HEALTH:-/health}"
[ -n "$FLY_APP" ] && HEALTH_URL="https://${FLY_APP}.fly.dev${FLY_HEALTH}"
fi
# vapor.yml: production domain -> Laravel /up route
if [ -z "$HEALTH_URL" ] && [ -f vapor.yml ]; then
VAPOR_DOMAIN=$(awk '/^production:/,/^[a-z]/' vapor.yml | grep 'domain:' | head -1 \
| awk '{print $2}' | tr -d '"')
[ -n "$VAPOR_DOMAIN" ] && HEALTH_URL="https://${VAPOR_DOMAIN}/up"
fi
# netlify.toml: production URL
if [ -z "$HEALTH_URL" ] && [ -f netlify.toml ]; then
NETLIFY_URL=$(grep -A5 '\[context\.production\]' netlify.toml | grep 'url' | head -1 \
| cut -d'"' -f2)
[ -n "$NETLIFY_URL" ] && HEALTH_URL="${NETLIFY_URL}/health"
fi
# .env.example: APP_URL (readable, unlike .env)
if [ -z "$HEALTH_URL" ] && [ -f .env.example ]; then
APP_URL=$(grep '^APP_URL=' .env.example | cut -d= -f2- | tr -d '"' | tr -d "'" | tr -d ' ')
if [ -n "$APP_URL" ] && [ "$APP_URL" != "http://localhost" ] \
&& [ "$APP_URL" != "http://127.0.0.1" ]; then
# Laravel has /up built-in (11+); else try /health
[ -f artisan ] && HEALTH_URL="${APP_URL}/up" || HEALTH_URL="${APP_URL}/health"
fi
fi
# Firebase Hosting: site name from firebase.json -> https://{site}.web.app/
if [ -z "$HEALTH_URL" ] && [ -f firebase.json ]; then
FB_SITE=$(python3 -c "import json,sys; d=json.load(open('firebase.json')); \
h=d.get('hosting',{}); print((h[0] if isinstance(h,list) else h).get('site',''))" \
2>/dev/null)
# Fall back to project ID from .firebaserc
[ -z "$FB_SITE" ] && FB_SITE=$(python3 -c "import json; \
print(json.load(open('.firebaserc'))['projects']['default'])" 2>/dev/null)
[ -n "$FB_SITE" ] && HEALTH_URL="https://${FB_SITE}.web.app/"
fi
# Supabase: project_id from supabase/config.toml -> https://{id}.supabase.co/health
if [ -z "$HEALTH_URL" ] && [ -f supabase/config.toml ]; then
SB_ID=$(grep '^project_id' supabase/config.toml | cut -d'"' -f2)
[ -n "$SB_ID" ] && HEALTH_URL="https://${SB_ID}.supabase.co/health"
fi
# Fastlane / iOS: no HTTP health check applicable
# Xcode Cloud / TestFlight: no HTTP health check applicable
fi
orch_state_save DEPLOY_COMMAND HEALTH_URLLog what was detected:
Deploy: {DEPLOY_COMMAND}
Health: {HEALTH_URL or "not detected"}If DEPLOY_COMMAND = "fastlane": AskUserQuestion — which lane?
- Each detected lane name as an option (from
$LANES) - "Custom lane..." (user types it)
→ Set
DEPLOY_COMMAND="fastlane {lane}"
If no deploy method found: AskUserQuestion with options:
- "CI/CD deploys on push (no extra command)"
- "fly deploy"
- "firebase deploy"
- "fastlane beta"
- "Custom..." (user types the command)
Save to .claude/ship.md if not already present (never overwrite existing config):
for c in "$(dirname "${CLAUDE_SKILL_DIR:-/nonexistent}")/audit/bin/lib-orchestrator.sh" "$HOME/.claude/skills/audit/bin/lib-orchestrator.sh"; do [ -f "$c" ] && { . "$c"; break; }; done # fresh shell per block
orch_state_load
DEPLOY_COMMAND="{the resolved command: the fastlane lane or the chosen option when a question above was asked, else the detected value}"
if [ ! -f .claude/ship.md ]; then
mkdir -p .claude
{
echo "deploy-command: ${DEPLOY_COMMAND}"
[ -n "$HEALTH_URL" ] && echo "health-check: ${HEALTH_URL}"
} > .claude/ship.md
echo "Created .claude/ship.md"
fi
orch_state_save DEPLOY_COMMANDRun log setup (resolve once, used by every phase below — fail open, never blocks ship):
# Shared prologue (audit/bin/lib-orchestrator.sh); finding it is the one loop that stays inline.
for c in "$(dirname "${CLAUDE_SKILL_DIR:-/nonexistent}")/audit/bin/lib-orchestrator.sh" \
"$HOME/.claude/skills/audit/bin/lib-orchestrator.sh"; do
[ -f "$c" ] && { . "$c"; break; }
done
type orch_run_log >/dev/null 2>&1 || echo "lib-orchestrator.sh not found; run log and helpers unavailable (skill continues)"
# Run-ledger start marker — this is the true beginning of the run, before commit/audit/test/push/deploy.
orch_run_log --start --skill ship
UB=$(orch_unaudited_base) && echo "UNAUDITED_COUNT=$(git rev-list --count "$UB"..HEAD 2>/dev/null)" # collected quick-fix commits (references/audit-collection.md)Every later "run the log call" below means, in a block that starts with the lib source line and orch_state_load: orch_run_log --skill ship --outcome {outcome} --gate "${SHIP_GATE:-n/a}" --counts "tests=${SHIP_TESTS:-n/a},deploy=${SHIP_DEPLOY:-n/a},docs=${SHIP_DOCS:-n/a}", substituting that step's outcome. Never in the same Bash call as git push. Every SHIP_*= assignment this file names happens inside a sourced block that ends with orch_state_save <that name>; without that, every block read n/a (fresh shell per block) and the ledger recorded a gate that never fired.
Phase 0.5: Audit Mode
Ask once, before anything is committed, so the user knows up front whether this run takes minutes or includes the audit gate. AskUserQuestion (header "Audit"):
- "Mit Audit (Recommended)" → Phase 2 runs as written (a fresh marker from an earlier
/auditis still accepted there); mentionUNAUDITED_COUNTpending commits when printed above. Nothing to save. - "Schneller Fix, ohne Audit" →
SHIP_GATE=skippedin a sourced block ending withorch_state_save SHIP_GATE. Phase 2 is skipped. The secret scan in Phase 1 and the test gate in Phase 2b still run.
skipped is a deliberate up-front choice and is logged as such; it is not bypassed (which stays
reserved for declining the audit after the gate fired).
Phase 0.8: Docs Sync
A ship that changes behaviour and leaves the docs behind is how a README starts lying. This phase runs BEFORE the commit, so doc updates land in the same commit as the change they describe.
Inventory what changed and which docs this repo actually has:
CHANGED=$( { git diff --name-only HEAD; git diff --cached --name-only; } 2>/dev/null \
| sort -u | grep -v '^$')
DOC_FILES=$(git ls-files 2>/dev/null | grep -Ei \
'(^|/)(README|CHANGELOG|CONTRIBUTING|CLAUDE|AGENTS|INSTALL|UPGRADING)[^/]*\.(md|mdx|rst|txt)$|^(docs?|documentation|help|guides?|handbook)/.*\.(md|mdx|rst|txt)$|(^|/)SKILL\.md$|(^|/)\.env\.example$')
printf 'Changed:\n%s\n\nDocs in repo:\n%s\n' "$CHANGED" "${DOC_FILES:-none}"Mechanical drift check, when the audit helper is reachable. Fails open, never blocks the ship:
for c in "$(dirname "${CLAUDE_SKILL_DIR:-/nonexistent}")/audit/bin/check-docs-claims.sh" \
"$HOME/.claude/skills/audit/bin/check-docs-claims.sh"; do
[ -f "$c" ] && { bash "$c" . 2>/dev/null; break; }
doneNow decide, do not ask. Read the diff, read every doc the diff could touch, update the ones that are actually wrong or incomplete. This is triage: pick, act, and report one line per file with the decision and its reason.
| Doc | Update when the diff ... |
README.md | changes install steps, commands, requirements, supported platforms, the feature list, or any other claim the README states as fact |
CLAUDE.md / AGENTS.md | changes architecture, conventions, invariants, or the command table, or adds a gotcha a future session would otherwise rediscover |
CHANGELOG.md | changes anything user-visible. Add an entry under the existing Unreleased heading, matching the format already in the file |
docs/**, help pages | documents a behaviour, flag, endpoint, or screen the diff changed |
.env.example | adds or renames a config variable |
SKILL.md | changes a skill's tools, phases, or trigger phrases |
Hard rules:
- Never create a doc file that does not exist. No CHANGELOG in the repo means this ship has no changelog entry. Report that as a line, do not invent the file.
- Match the file's existing format, including heading style, tense and language. A German README stays German.
- Only the claims the diff invalidates. No rewrites, no tidying of neighbouring prose, no reformatting. Same surgical rule as any other edit.
- A version bump is intent, not triage. An entry under
Unreleasedneeds no question. Cutting a new version number is the user's call, so AskUserQuestion only when there is noUnreleasedsection to add to.
Fact check (only when this phase updated at least one doc). Dispatch ONE fact-checker agent
(run_in_background: false) with the list of doc files you edited and the instruction to check only
the claims in the hunks you changed (git diff -- <doc files>) against the code. Every WRONG line:
correct the doc before the commit. UNVERIFIABLE lines need no action. No doc edits, no dispatch:
the agent costs a run, and unchanged docs are check-docs-claims.sh's job.
Phase 1 stages these with git add -u, since every file touched here was already tracked. Doc-only
edits do not invalidate a fresh audit marker: the marker is bound to the audited tree, and Phase 2
accepts a delta whose every path is prose by classify-diff.sh's own definition
(orch_marker_matches rc 2); a code path in the delta makes the marker stale.
Set SHIP_DOCS to the number of doc files updated, or none when the diff touched no documented
surface. none is a legitimate outcome for a pure refactor, not a reason to skip the check.
for c in "$(dirname "${CLAUDE_SKILL_DIR:-/nonexistent}")/audit/bin/lib-orchestrator.sh" "$HOME/.claude/skills/audit/bin/lib-orchestrator.sh"; do [ -f "$c" ] && { . "$c"; break; }; done # fresh shell per block
SHIP_DOCS={N|none}
orch_state_save SHIP_DOCSPhase 1: Commit
Show what will be committed:
git diff --stat HEAD 2>/dev/null
git status --shortIf $ARGUMENTS is non-empty: use it verbatim as the commit message (skip generation).
$ARGUMENTS, not a named argument: a commit message is free text, and named or indexed arguments map to shell-quoted positions, so /ship fix: broken login would bind only fix:. $ARGUMENTS expands to the full argument string as typed.
If $ARGUMENTS is empty: read the diff and generate a conventional commit message:
- Format:
{type}({optional-scope}): {what changed in imperative mood} - Types:
feat,fix,refactor,chore,docs,style,test - One line, max 72 chars, no period at end
Show the generated message. AskUserQuestion:
- "Commit with this message" (default)
- "Edit the message" → user provides the correct message in the next turn
Stage all tracked modified files (not untracked):
git add -u # already-staged files stay stagedCheck the staged diff for sensitive files AND for secret-shaped content. The filename grep alone is
not a secret check: a token pasted into config/services.php has an innocent name, and the audit
marker Phase 2 accepts may be up to 1800s old, so a secret added after that audit ran would reach
the remote unscanned (2026-09-16 audit, Critical). pre-checks.sh scans file CONTENT of every
changed and staged file with the same patterns /audit uses and prints file:line: pattern-name
only, never the value:
for c in "$(dirname "${CLAUDE_SKILL_DIR:-/nonexistent}")/audit/bin/lib-orchestrator.sh" "$HOME/.claude/skills/audit/bin/lib-orchestrator.sh"; do [ -f "$c" ] && { . "$c"; break; }; done # fresh shell per block: source the lib again
git diff --cached --name-only | grep -iE '(\.env|secret|credential|\.pem|\.key)' # -i: SECRET.txt and DB_CREDENTIAL.json are the same class
PRECHECK=$(orch_helper pre-checks.sh) || { echo "pre-checks.sh not found: the content secret scan cannot run, refusing to commit"; exit 1; }
bash "$PRECHECK" | grep -E '^SECRET_SCAN_RESULT=|^SECRET 'Filename hit: warn and AskUserQuestion — continue or abort? SECRET_SCAN_RESULT=FINDINGS: stop, do
not commit, name the file:line lines; this one is not a question.
git commit -m "{message}"If commit fails (hook rejection, empty): run the log call (outcome=commit_failed), report the hook output and stop.
Phase 2: Audit Gate
If Phase 0.5 set SHIP_GATE=skipped: print Audit skipped (quick-fix mode chosen in Phase 0.5).
and go straight to Phase 2b. Do not run the block below, it would overwrite the saved value.
Phase 3's git push then triggers the push-guard hook's permission prompt (no fresh marker); that
prompt is expected in this mode.
for c in "$(dirname "${CLAUDE_SKILL_DIR:-/nonexistent}")/audit/bin/lib-orchestrator.sh" "$HOME/.claude/skills/audit/bin/lib-orchestrator.sh"; do [ -f "$c" ] && { . "$c"; break; }; done # fresh shell per block: source the lib again
MARKER="/tmp/claude-audit-passed-$(orch_hash_passed)" # passed-family hash (no trailing newline), lib-orchestrator.sh
if [ -f "$MARKER" ]; then
AGE=$(( $(date +%s) - $(stat -f%m "$MARKER" 2>/dev/null || stat -c%Y "$MARKER" 2>/dev/null) ))
orch_marker_matches; MATCH_RC=$?
if [ "$AGE" -ge 1800 ]; then
echo "STALE: Audit marker is ${AGE}s old (limit: 1800s)."; SHIP_GATE=stale
elif [ "$MATCH_RC" -eq 1 ]; then
echo "STALE: Audit marker certifies a different tree than the one just committed (a code path changed after /audit ran; changed since the audit:)"; orch_marker_delta; SHIP_GATE=stale
elif [ "$MATCH_RC" -eq 2 ]; then
echo "Audit marker fresh (${AGE}s ago); only prose changed since the audited tree (docs sync), accepted:"; orch_marker_delta; SHIP_GATE=passed
else
echo "Audit marker fresh (${AGE}s ago) and bound to this tree. Proceeding to push."; SHIP_GATE=passed
fi
else
echo "MISSING: No audit marker found."; SHIP_GATE=missing
fi
orch_state_save SHIP_GATEIf marker missing or stale: AskUserQuestion:
- "Run /audit now" → invoke the audit skill (
/audit), then re-check marker.SHIP_GATE=passed-after-rerunon success (notpassed: the gate DID fire, the ledger must show that, otherwise the ledger reads as a gate that never blocks even when it blocked-and-reran every time; 17 such runs on 2026-08-28). On failure:SHIP_GATE=blocked, run the log call (outcome=audit_failed), then stop. - "Push without audit (risky)" →
SHIP_GATE=bypassed, log the bypass and continue with a warning in the output
Never silently skip the audit. The bypass must be an explicit user choice. Each of these is set in a sourced block ending with orch_state_save SHIP_GATE (4a).
Phase 2b: Test Gate
Only when test-command: is set in .claude/ship.md. Skip silently otherwise (SHIP_TESTS=not_configured).
Whatever you run, run it through bash "$AUDIT_BIN/test-lock.sh" [--cached] --cmd "$TEST_COMMAND" (zsh does not word-split the variable), also a test command you assemble yourself. Why the lock and the cache: references/test-gate.md.
Re-derived here, not carried over from Phase 0: every block is a fresh shell.
for c in "$(dirname "${CLAUDE_SKILL_DIR:-/nonexistent}")/audit/bin/lib-orchestrator.sh" "$HOME/.claude/skills/audit/bin/lib-orchestrator.sh"; do [ -f "$c" ] && { . "$c"; break; }; done # fresh shell per block
TEST_COMMAND=$(orch_test_command_declared) || { SHIP_TESTS=not_configured; orch_state_save SHIP_TESTS; echo "SHIP_TESTS=not_configured"; exit 0; }
orch_resolve_audit_root || { echo "Abgebrochen: audit-Root nicht gefunden."; SHIP_TESTS=failed; orch_state_save SHIP_TESTS; exit 0; }
# --cached: the declared command is the full suite; a green run on an identical tree (deploy test, or an /audit that fell back to full) within 30 min is reused (TEST_CACHE_HIT)
if bash "$AUDIT_BIN/test-lock.sh" --cached --cmd "$TEST_COMMAND"; then SHIP_TESTS=passed; else SHIP_TESTS=failed; fi
orch_state_save SHIP_TESTS; echo "SHIP_TESTS=$SHIP_TESTS"Green → SHIP_TESTS=passed, continue to push. Red → SHIP_TESTS=failed, run the log call (outcome=test_failed), then STOP. Report the failing tests and do not push. Fixing them is
new work: the user commits the fixes, then re-runs /ship (which re-audits and re-tests).
Why this phase exists: references/test-gate.md.
Never silently skip a red suite. A bypass must be an explicit user choice, logged like the audit bypass.
Phase 3: Push
Quick-fix mode records the unaudited base before pushing, so commits stay findable for a later /audit (references/audit-collection.md):
for c in "$(dirname "${CLAUDE_SKILL_DIR:-/nonexistent}")/audit/bin/lib-orchestrator.sh" "$HOME/.claude/skills/audit/bin/lib-orchestrator.sh"; do [ -f "$c" ] && { . "$c"; break; }; done; orch_state_load; [ "$SHIP_GATE" = "skipped" ] && orch_unaudited_record; git push # fresh shell per block; SHIP_GATEIf push fails:
- "Updates were rejected" (diverged):
git pull --rebase, then retry push - "No upstream branch":
git push -u origin $(git branch --show-current) - Any other error: run the log call (
outcome=push_failed), then report and stop
Phase 4: Deploy
If DEPLOY_COMMAND is ci: skip this phase (SHIP_DEPLOY=ci, sourced block, orch_state_save SHIP_DEPLOY). CI/CD will deploy from the push. Phase 5 waits for that CI run before the health check.
DEPLOY_COMMAND is read from the audited repo's own .claude/ship.md (same trust boundary as
TEST_COMMAND in Phase 2b — see CLAUDE.md Gotchas). Everywhere else in this pipeline, a
repo-supplied step that is in its expected green state runs without asking; deploy is the
exception, because it is the one step that is both irreversible and production-facing, and
because DEPLOY_COMMAND is persisted once (Phase 0) and then reused silently on every later run —
nothing else in the pipeline shows it again before it executes. AskUserQuestion:
- "Deploy" (default) — show the resolved
{DEPLOY_COMMAND}in the question text - "Skip deploy" →
SHIP_DEPLOY=skipped(sourced block,orch_state_save SHIP_DEPLOY), skip Phase 5 (nothing new was deployed, a health check would only re-check the old version) and go straight to Summary Output withDeploy: skipped by user
Otherwise run the detected/configured deploy command:
for c in "$(dirname "${CLAUDE_SKILL_DIR:-/nonexistent}")/audit/bin/lib-orchestrator.sh" "$HOME/.claude/skills/audit/bin/lib-orchestrator.sh"; do [ -f "$c" ] && { . "$c"; break; }; done # fresh shell per block
orch_state_load # DEPLOY_COMMAND as Phase 0 saved it, after the questions
eval "$DEPLOY_COMMAND" # the value the AskUserQuestion above showed; same trust boundary as the test gate's eval
SHIP_DEPLOY=$([ $? -eq 0 ] && echo executed || echo failed); orch_state_save SHIP_DEPLOY; echo "SHIP_DEPLOY=$SHIP_DEPLOY"Stream output. SHIP_DEPLOY=executed if it exits zero. If command exits non-zero: SHIP_DEPLOY=failed, jump to Phase 6.
Phase 5: Verify
for c in "$(dirname "${CLAUDE_SKILL_DIR:-/nonexistent}")/audit/bin/lib-orchestrator.sh" "$HOME/.claude/skills/audit/bin/lib-orchestrator.sh"; do [ -f "$c" ] && { . "$c"; break; }; done # fresh shell per block
orch_state_load # DEPLOY_COMMAND, HEALTH_URL from Phase 0
# CI deploy: the deploy happens in the workflow the push triggered. A health check before that
# run finishes tests the previous version and reports OK for a deploy that has not happened
# (run 11, 2026-09-16). Wait for it; `gh run watch` blocks without sleeping. Call this block with
# the Bash tool's timeout at 600000 and re-run it when the tool times out: it re-finds the run.
CI_STATUS=n/a
if [ "$DEPLOY_COMMAND" = "ci" ]; then
RUN_ID=$(gh run list --branch "$(git branch --show-current)" --limit 1 --json databaseId --jq '.[0].databaseId' 2>/dev/null)
if [ -n "$RUN_ID" ]; then
gh run watch "$RUN_ID" --exit-status --interval 15 >/dev/null 2>&1
CI_STATUS=$(gh run view "$RUN_ID" --json status,conclusion --jq 'if .status=="completed" then .conclusion else .status end' 2>/dev/null || echo unknown)
gh run view "$RUN_ID" --json url --jq .url 2>/dev/null
else
CI_STATUS=no_run_found # the push may not have registered yet: re-run this block once
fi
echo "CI_STATUS=$CI_STATUS"
[ "$CI_STATUS" = "success" ] || { echo "Health: skipped, CI run is $CI_STATUS (nothing new is deployed)"; HEALTH_URL=""; }
fi
# Health check. HEALTH_URL is repo-supplied (.claude/ship.md): only http(s), never a loopback,
# private, link-local or non-canonical numeric host; the parser and the filter live in the lib
# (orch_url_host / orch_host_public, 21-case test in the commit that added them).
case "$HEALTH_URL" in http://*|https://*) ;; "") ;; *) echo "Health: refusing non-http(s) URL from .claude/ship.md"; HEALTH_URL="";; esac
HEALTH_HOST=$(orch_url_host "$HEALTH_URL")
if [ -n "$HEALTH_URL" ] && ! orch_host_public "$HEALTH_HOST"; then echo "Health: refusing loopback/private/link-local/non-canonical host $HEALTH_HOST"; HEALTH_URL=""; fi
if [ -n "$HEALTH_URL" ]; then
HTTP_STATUS=$(curl -so /dev/null -w "%{http_code}" --max-time 15 "$HEALTH_URL" 2>/dev/null)
if [ "$HTTP_STATUS" -ge 200 ] && [ "$HTTP_STATUS" -lt 400 ]; then
echo "Health: OK ($HTTP_STATUS) — $HEALTH_URL"
else
echo "Health: FAILED ($HTTP_STATUS) — $HEALTH_URL"
# Jump to Phase 6 health-check-failed branch
fi
else
echo "Health: not configured (add 'health-check: https://...' to .claude/ship.md)"
fiPhase 5.5: Audit Collection
for c in "$(dirname "${CLAUDE_SKILL_DIR:-/nonexistent}")/audit/bin/lib-orchestrator.sh" "$HOME/.claude/skills/audit/bin/lib-orchestrator.sh"; do [ -f "$c" ] && { . "$c"; break; }; done; orch_state_load; [ "$SHIP_GATE" = "skipped" ] && COLLECTED_BASE=$(orch_unaudited_base) && echo "COLLECTED_COUNT=$(git rev-list --count "$COLLECTED_BASE"..HEAD 2>/dev/null)" # fresh shell per block; SHIP_GATEOnly in quick-fix mode, AskUserQuestion (header "Audit") when $SHIP_GATE is skipped (detail: references/audit-collection.md):
- "Sammeln, später auditieren (Recommended)" → keep the base; Summary reports the collected count.
- "Jetzt nachträglich auditieren" → invoke
/auditwithAUDIT_BASE_REF=$COLLECTED_BASE; its fixes need their own/ship.
Phase 6: Failure Handling
If CI_STATUS is not success for a ci deploy: run the log call (outcome=deploy_failed), report the run URL, stop.
If deploy failed: run the log call (outcome=deploy_failed), then:
Deploy failed.
Command: {DEPLOY_COMMAND}
Exit code: {N}
Output: {last 20 lines}
Next step: reproduce the deploy failure locally and write a regression test before fixing it.If health check failed: run the log call (outcome=health_failed), then:
Deploy succeeded but health check failed.
URL: {HEALTH_URL}
Response: {curl output}
Check: app logs, recent error monitoring, or reproduce the failure locally with a regression test.Summary Output
Run the log call (outcome=shipped).
Shipped.
Docs: {n files updated, listed / "none needed"}
Commit: {short SHA} {message}
Audit: {passed / passed after rerun / skipped (quick fix) / skipped, collected (N commits pending) / audited retroactively / bypassed}
Push: origin/{branch}
Deploy: {command or "CI/CD triggered" or "skipped by user"}
Health: {OK / FAILED / not configured}
CI run: {url or "n/a"}Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

