/ship — Ship the current branch end-to-end
Drives commit → push → PR in one go. Calling /ship is the explicit
authorization for all three actions; do NOT add AskUserQuestion gates around
commit, push, or PR creation.
Step 1 — Pre-flight checks
Run in parallel:
git status --short
git branch --show-current
Detect the package manager (bun.lock/bun.lockb → bun, else yarn.lock → yarn,
else package-lock.json → npm) and use it for every step below.
Run format first so the final diff is what actually gets committed. Format
only the files the branch changes, never the whole project (bun run format
rewrites the entire codebase and is slow):
files=$( { git diff --name-only HEAD; git ls-files --others --exclude-standard; } | sort -u | while read -r f; do [ -f "$f" ] && echo "$f"; done )
code=$(echo "$files" | grep -E '\.(ts|js|vue)$')
styles=$(echo "$files" | grep -E '\.scss$')
[ -n "$code" ] && echo "$code" | xargs bunx eslint --fix
[ -n "$styles" ] && echo "$styles" | xargs bunx stylelint --fix
[ -n "$code$styles" ] && printf '%s\n%s\n' "$code" "$styles" | grep . | xargs bunx prettier --write
(xargs rather than an unquoted $code: zsh does not word-split variables.)
(With yarn/npm, swap bunx for yarn/npx.) Fall back to the full format
script only for a very wide change.
Then show the post-format diff stat:
git diff --stat
Then run the typecheck (bun run typecheck / yarn typecheck / npm run typecheck).
Then decide whether to lint. The project-wide lint is left to a local pre-push
hook when one exists — running it here would do the same work twice:
hooks=$(git config core.hooksPath || echo "$(git rev-parse --git-common-dir)/hooks")
[ -x "$hooks/pre-push" ] && echo "pre-push hook present" || echo "no pre-push hook"
- Hook present → skip
lint. - No hook → run the lint script now (
bun run lint/yarn lint/npm run lint): nothing else will lint this change before it reaches the remote.
If any .spec.ts / .spec.js file is in the diff, run the affected tests:
npx vitest run <path/to/spec.ts> [...]
If ANY check fails: report the failure to the user and STOP. Do not proceed.
Step 2 — Draft the commit message
Determine the prefix from the current branch name:
| Branch pattern | Commit prefix |
|---|---|
PROD-XXXX-... |
PROD-XXXX |
<N>-... (issue #) |
#<N> |
| Otherwise | (no prefix) |
Rules for the message body:
- Always in English, 1–2 sentences, focused on the why.
- Conventional style:
- new feature:
feat(scope): add X - enhancement:
update(scope): X - bug fix:
fix(scope): X - chore/refactor:
chore(scope): X/refactor(scope): X
- new feature:
Display the staged files and the drafted message in the chat as you commit, so the user can see what landed without being prompted.
Step 3 — Commit
- Stage relevant files explicitly:
git add <file1> <file2> ...— nevergit add -Aorgit add .. Exclude.env*, credentials, and large binaries. - Run
git commit -m "<message>". - A
PreToolUsehook ongit commit, when the environment has one, checks prettier/eslint/stylelint on the staged files and blocks the commit on failure — it checks, it does not format. - Verify with
git status --shortandgit log -1 --oneline.
Step 4 — Push
- Confirm the branch name and intended remote (typically
origin). - Run
git push(add-u origin <branch>if no upstream is set). A localpre-pushhook, when present (Step 1), runs the fulllinthere; if it blocks the push, report the failure and stop.
Step 5 — PR
After push, check whether a PR exists for this branch:
gh pr view --json url,number,state 2>/dev/null
- No PR: invoke
/prdirectly (no question)./prhandles description, assignee, and Jira transition. - PR exists, open: report the URL, then run Step 6.
- PR exists, closed/merged: report and stop.
Step 6 — Answer and resolve the threads the push addressed
Part of /ship, not a follow-up to suggest. Do it without being asked, in the
same turn as the push.
- Fetch the unresolved threads (
isResolved == false) via GraphQL, keeping each thread'sidand its first comment'sdatabaseId. - For every thread the pushed commits address, reply in-thread with
gh api repos/<owner>/<repo>/pulls/<PR#>/comments -f body="..." -F in_reply_to=<databaseId>— never a top-level comment. Prefix the body with🤖. Use--body-file/"$(cat file)"rather than inline escapes. - Resolve each of those threads:
gh api graphql -f query='mutation($threadId: ID!) { resolveReviewThread(input: {threadId: $threadId}) { thread { isResolved } } }' -f threadId=<id>
The reply carries the reasoning, especially when the fix deviates from what
the reviewer suggested — cite the path:line evidence that justified the
deviation. A commit message is not a reply; the reviewer reads the thread.
Only touch threads this push actually addresses. Leave the rest unresolved and list them in the final report so the user knows what's still open.
Cross-link related front/back PRs
When a single change ships both a front PR (agorize-front) and a linked
back PR (agorize-core) — e.g. a feature plus its translation/API change —
always add each PR's URL to the other's description (a ## Related PRs
section), so reviewers can find the companion. Do this for every multi-repo
ship, in both directions, without being asked. Use gh pr edit <N> --repo <owner/repo> --body-file <file> (never inline --body with escapes).
Hard rules
- No approval gates. Do not call
AskUserQuestionto confirm commit, push, or PR creation. The/shipinvocation already authorizes all three. - Never
--no-verify, never--amend, never--forcewithout explicit user request for that exact operation. - Never stage
.env*, credentials files, or anything that looks like a secret. - Commit messages always in English. Branch prefix conventions
(
PROD-XXXX,#N) are mandatory when the branch matches. - If pre-flight checks fail, do not "fix and retry" silently — surface the failure and let the user decide.
- If the user says only "commit", stop after Step 3. If they say only "push", skip to Step 4 (after confirming there's something to push).