name: rk:ef-ship
description: "Commit + push the current Everfit feature branch and open a PR targeting develop in one shot. Parses the branch (dev_./-) to build the commit subject <type>(<feature>): <CARD-ID> <slug>, asks for the feature scope, pushes with upstream tracking, opens the PR, then auto-chains rk:ef-pr-description to fill the body. Triggers on: 'ship', 'commit and push', 'create PR', 'tạo PR', 'commit + PR', 'push and PR'."
argument-hint: "[--feature=] [--draft] [--no-desc] [--assignee=] [--no-assign] [--slack] [--slack-channel=] [--slack-group=] [--slack-mentors=<n1,n2,n3>] [--slack-set-default-mentors=<n1,n2,n3>] [--slack-clear-default-mentors] [--yes] [--dry-run]"
metadata:
author: rock288
version: "1.0.0"
<type>(<feature>): <CARD-ID> <slug>, asks for the feature scope, pushes with upstream tracking, opens the PR, then auto-chains rk:ef-pr-description to fill the body. Triggers on: 'ship', 'commit and push', 'create PR', 'tạo PR', 'commit + PR', 'push and PR'."
argument-hint: "[--feature=Ship: commit + push + open PR (Everfit flow)
Finalize a feature branch in one command: stage all working-tree changes, commit with the team's commit subject format, push to origin (sets upstream on first push), open a PR targeting develop, then chain [[rk:ef-pr-description]] to fill the PR body.
👋 New to this skill? Read references/quickstart.md for the team-oriented walkthrough (prerequisites, first-time setup, common scenarios, troubleshooting). The rest of this file is the spec the skill executes — denser, less narrative.
Commit subject format
<type>(<feature>): <CARD-ID> <short-slug>
Examples:
feat(auth): UP-70961 auth-refreshfix(webhook): UP-72003 token-refreshrefactor(workout): UP-70814 swap-video
All fields except <feature> come from the current branch name (produced by [[rk:ef-branch-name]]). <feature> is user input.
Inputs
| Arg | Required | Default | Notes |
|---|---|---|---|
--feature=<scope> |
no | asked | One-word feature/module name (kebab-case). Inserted as <feature> in commit subject. |
--draft |
no | off | Create the PR as draft. |
--no-desc |
no | off | Skip the auto-chain to rk:ef-pr-description. PR body left empty. |
--assignee=<user> |
no | @me |
GitHub username to assign on the PR. Default assigns the current authenticated user. Pass a username to assign someone else. |
--no-assign |
no | off | Skip assignment entirely. Wins over --assignee. |
--slack |
no | off | After PR is created, post <group> <mentor1> [<mentor2> <mentor3>]\n<PR-URL> (URL on its own line so Slack unfurls it into a PR preview card) to a Slack channel. Default is OFF to avoid accidental notifications. Auto-enabled if any of --slack-mentors/--slack-channel/--slack-group is passed — those flags imply the user wants to post. |
--slack-channel=<name> |
no | C05F65TBB9P (#backend-review-code) |
Channel name or ID. Everfit default is the #backend-review-code channel (C05F65TBB9P) — hard-coded so no first-run lookup is needed. Passing this implies --slack. |
--slack-group=<group> |
no | from memory (default backend) |
Single group mention in Slack syntax (e.g. <!subteam^S0123ABC>). For Everfit, this is always the @backend user group. Passing this implies --slack. |
--slack-mentors=<list> |
no | default_mentors from memory, else asked |
Comma-separated Slack display names (the name shown in chat, e.g. Long (BE)). Use quotes if any name contains spaces or commas: --slack-mentors="Long (BE),Duy Le (BE)". 1–3 names accepted. Skill resolves each display name to <@U…> via slack_search_users + exact display-name match, and caches the lookup in mentor_roster. Resolution order: this flag → default_mentors in memory → interactive prompt. Passing this implies --slack — no need for the explicit --slack flag. |
--slack-set-default-mentors=<list> |
no | — | Save the comma-separated display names as default_mentors in memory/ef_ship_slack.md. Subsequent ships will use them automatically when --slack-mentors isn't passed. Resolves each name to <@U…> immediately and stores the resolved entries so no Slack search is needed on later ships. Setup-only flag; can be combined with a normal ship. |
--slack-clear-default-mentors |
no | off | Remove default_mentors from memory. Future ships fall back to interactive prompt. Setup-only flag. |
--yes |
no | off | Skip the final confirmation gate before commit + push. |
--dry-run |
no | off | Print the planned commands without executing. |
Workflow
1. Pre-flight checks
git rev-parse --is-inside-work-tree— abort if not a git repo.git rev-parse --abbrev-ref HEAD— capture current branch.- Refuse on protected branches:
develop,main,master,staging, branches matchingrelease/*. Abort with a clear error pointing tork:ef-branch-name. git status --porcelain— if clean AND there are no unpushed local commits (git log @{u}.. 2>/dev/nullis empty), abort with "nothing to ship".gh auth status— verify GitHub CLI is logged in. If not, surfacegh auth login.
2. Parse current branch
Expect format dev_<sprint>.<type>/<CARD-ID>-<slug>. Regex:
^dev_(?<sprint>[^.]+)\.(?<type>feat|fix|refactor|perf|chore|docs|test)/(?<card>[A-Z]+-\d+)(-(?<slug>[a-z0-9-]+))?$
If branch doesn't match, fall back to AskUserQuestion:
<type>(feat / fix / refactor / perf / chore / docs / test)<CARD-ID>(e.g.UP-70961)<slug>(optional, kebab-case)
3. Resolve <feature>
Order of precedence:
--feature=<scope>flag.- Last-used feature for this branch from memory (
memory/last_feature_<branch>.mdif present) — suggest, don't auto-apply. - Ask the user via
AskUserQuestion: "Feature/scope for this commit (e.g.auth,webhook,tracking)?"
After resolving, save it to memory keyed by branch so re-ships in the same branch prefill.
Normalize: lowercase, kebab-case, strip non-[a-z0-9-].
4. Build commit subject
Subject (target ≤ 72 chars):
<type>(<feature>): <CARD-ID> <short-slug>
If <slug> is missing from the branch, drop it:
<type>(<feature>): <CARD-ID>
No commit body — context belongs in the PR description.
5. Confirmation gate
Unless --yes is set, print the plan and ask via AskUserQuestion to proceed:
About to:
1. git add -A (N files changed, see below)
2. git commit -m "<subject>"
3. git push -u origin <branch>
4. gh pr create --base develop --title "<subject>" [--draft] [--assignee @me]
5. invoke rk:ef-pr-description on the new PR
6. [--slack only] post "<group> <mentors...>\n<PR-URL>" to #<channel> (URL on own line → Slack unfurls)
Files staged:
M src/auth/refresh.ts
A src/auth/refresh.test.ts
...
Proceed?
Default action: proceed. Options: proceed / abort.
With --dry-run, print the plan and stop here.
6. Stage + commit
git add -A
git commit -m "<subject>"
If pre-commit hooks fail: surface the error and abort. Don't --no-verify unless the user explicitly passes the flag.
Verify with git log -1 --pretty=%s — must equal <subject>.
7. Push with upstream
git push -u origin <branch>
First push sets tracking to origin/<branch> (this completes the pairing with the --no-track step in [[rk:ef-branch-name]]). On subsequent ships, -u is harmless.
If push is rejected (non-fast-forward): surface the error, never auto-force-push. Suggest the user run git pull --rebase origin <branch> and re-ship.
8. Create PR
Check if PR already exists for this branch:
gh pr view --json number,url,title,isDraft 2>/dev/null
- Exists → skip creation. Capture
<pr-url>. PrintExisting PR: <url>. - Not exists → create:
Defaultgh pr create \ --base develop \ --title "<commit-subject>" \ --body "" \ [--draft] \ [--assignee <user>]<user>is@me(the authenticatedghuser). Pass--no-assignto omit the flag entirely. Capture the returned PR URL.
If PR already existed and was found in step 8, still apply the assignee:
gh pr edit <pr-number> --add-assignee <user>
(Skip when --no-assign.)
9. Chain rk:ef-pr-description (default ON)
Unless --no-desc is set, invoke [[rk:ef-pr-description]] with:
- the Jira card ID parsed in step 2
- the PR URL captured in step 8
The downstream skill handles fetching Jira context, generating the body, and updating the PR description. If the PR already has a non-empty body, the downstream skill is responsible for the update-vs-replace decision — this skill does not gate it.
10. Slack notification (opt-in via --slack or any --slack-* flag)
Runs when --slack is passed OR any of --slack-channel / --slack-group / --slack-mentors is passed (the presence of any Slack-related flag implies the user wants to post). Context: Everfit Slack workspace; channel is #backend-review-code (ID C05F65TBB9P); group is always @backend; mentors vary 1–3 people per ship.
MCP backend: by default the skill uses the bundled
mcp__claude_ai_Slack__slack_send_message. URLs in bot messages do not auto-unfurl (Slack API default forchat.postMessagefrom bot/app isunfurl_links: false) — the skill prints the URL prominently in the terminal output so the user can paste it manually for a preview card if needed.Optional upgrade — link unfurl support: if you want URLs to render as preview cards automatically, install korotovsky/slack-mcp-server and register it as
slack-mcpin~/.claude.jsonwithSLACK_MCP_ADD_MESSAGE_UNFURLING=github.com. The skill will prefermcp__slack-mcp__conversations_add_messagewhen available. See references/slack-mcp-setup.md for token extraction + install steps.
Channel is hard-coded to
C05F65TBB9P(#backend-review-code). No memory lookup, no search — just use this ID directly.--slack-channel=<other>overrides for a single run.Load other config from
memory/ef_ship_slack.md(per-repo). Expected fields:group— single mention string for the backend user group, e.g.<!subteam^S0123ABC|@backend>(saved on first run)default_mentors— optional ordered list of pre-resolved mentor entries[{ display_name, id, email }, …]. Used when--slack-mentorsis not passed. Max 3 entries.mentor_roster— map ofdisplay_name → { id: <@U…>, email: "…" }. Cache for display-name→ID lookups so the skill doesn't search Slack on every ship. Auto-populated as new names get resolved.
Flags
--slack-group/--slack-mentorsoverride individual fields for this run only (don't overwrite memory'sgroup/default_mentors; do updatementor_rosterwith any new resolutions).Setup flags
--slack-set-default-mentors/--slack-clear-default-mentorsmutatedefault_mentorsdirectly:--slack-set-default-mentors=<names>→ resolve each name via the normal resolution path, then write the resolved entries intodefault_mentors(replaces any existing list). Print confirmation:Default mentors set to: <name1>, <name2>.--slack-clear-default-mentors→ delete thedefault_mentorsfield from memory. PrintDefault mentors cleared.- These flags can be combined with a normal ship — the update happens before the message is posted, so the new defaults take effect on this same run if
--slack-mentorsis not passed.
First-run setup for the backend group (only if missing from memory):
- Call
mcp__claude_ai_Slack__slack_search_userswith querybackendto surface the user group's ID; ask user to confirm. - Save as
<!subteam^S…|@backend>syntax (rendered mention shows@backend) tomemory/ef_ship_slack.md.
Important: store mention strings verbatim — Slack only notifies on proper
<@U…>/<!subteam^S…>syntax. Plain@nametext won't ping anyone.- Call
Resolve mentors (every ship) — 1–3 names.
Input — resolution order:
--slack-mentors=<list>flag → parse comma-separated names directly (honor quotes for names with spaces/commas).default_mentorsin memory → use the pre-resolved entries directly. No Slack search needed (already resolved at set-time). Skip the interactive prompt.- Interactive prompt via
AskUserQuestion:- Show quick-pick options from
mentor_roster(multi-select, max 3) — labels showdisplay_name (email). - Allow free-text input for new display names (one or more, comma-separated).
- Show quick-pick options from
Validate count: 1 ≤ names ≤ 3. Reject 0 and >3.
Resolve each display name →
<@U…>:- Cache hit: display name is in
mentor_roster(case-insensitive match on key) → use cachedid. No Slack call. - Cache miss: call
mcp__claude_ai_Slack__slack_search_userswith the display name as the query.- Filter results to those whose
display_namematches the input exactly (case-insensitive). This is the key disambiguation step — search may return multiple loose matches (e.g. "Long Nguyen Hoang" returns "Hoang Long (Design)" + "Long (BE)"); only the one where Slack'sdisplay_namefield equals the input is the intended mentor. - If exactly 1 candidate after filter → use it. Cache
display_name → { id, email }inmentor_roster. - If multiple candidates remain (rare — Slack allows duplicate display names) → ask user via
AskUserQuestionto pick from {display_name + email + role}. Cache the choice. - If 0 candidates after filter → fall back to looser match: pick a candidate whose
real_nameorname(username) matches case-insensitively. Still 0 → abort the Slack step withUser '<name>' not found in workspace(don't fail the whole ship — PR has already landed).
- Filter results to those whose
Output: list of
<@U…>mentions in the same order as input names.Build message (2 lines, URL alone on line 2):
<group> <mentor1> [<mentor2> <mentor3>] <PR-URL>- Mentions on line 1 (space-separated).
- PR URL alone on line 2 — bare URL (no markdown
[text](url)wrapping). The bare URL is what Slack uses forlink_shared→ GitHub Slack app renders the preview card.
Post — pick the available MCP backend:
- Preferred (if
mcp__slack-mcp__conversations_add_messageexists): call it withchannel_id,payload= 2-line message,content_type=text/markdown. URL unfurls viaSLACK_MCP_ADD_MESSAGE_UNFURLING=github.com. - Fallback (default): call
mcp__claude_ai_Slack__slack_send_messagewithchannel_id+message= 2-line text. URL won't unfurl (bundled MCP can't passunfurl_links: true). Print the URL again in the terminal output so the user can paste manually if they want a preview card.
- Preferred (if
Verify: capture the returned timestamp (
ts) and channel ID. If the send fails (channel not found, no permission, restricted bySLACK_MCP_ADD_MESSAGE_TOOLwhitelist, expired token), surface the error but don't fail the whole ship — the commit/push/PR already landed; Slack is a side-effect notification.
11. Output
Branch: dev_s9_26.feat/UP-70961-auth
Commit: feat(auth): UP-70961 auth-refresh
(a1b2c3d)
Pushed: origin/dev_s9_26.feat/UP-70961-auth (upstream set)
PR: https://github.com/<org>/<repo>/pull/<n> [READY|DRAFT]
Assignee: @me (tuannguyen-everfit)
Body: filled by rk:ef-pr-description ✓
Slack: posted to #backend-review-code (ts 1716800000.001234) — group @backend + 2 mentors
(bundled MCP — URL won't auto-unfurl; paste manually for preview card)
https://github.com/<org>/<repo>/pull/<n>
With --dry-run, the block shows each step as (planned).
Style rules
- Subject is one line, ≤ 72 chars, no trailing period.
<feature>is lowercase kebab-case, 1–2 words max.<CARD-ID>is uppercase —UP-70961, notup-70961.<slug>is lowercase kebab-case — same casing as the branch.
Edge cases
- No working-tree changes but local commits unpushed: skip steps 6 (stage + commit) and go straight to push + PR creation. Surface the existing local commits in the confirmation gate so user knows what's about to ship.
- PR already exists: skip
gh pr create. Still chainrk:ef-pr-descriptionunless--no-desc— let it decide whether to update. - Repo has no
developbranch on remote: abort with a clear error. Don't silently re-targetmain. (Usegh pr create --base <other>manually if needed.) <feature>collides with<type>word (e.g.feat(feat): ...): warn and re-prompt for a clearer scope.- Commit subject exceeds 72 chars: warn but proceed. Don't truncate silently — the user can shorten
<feature>or<slug>and re-ship.
Safety rules
- Never
--forcepush (no--force-with-leaseeither unless user explicitly asks). - Never
--no-verifyunless user passes the flag. - Never commit on
develop/main/master/staging/release/*— refuse upfront. - Never
--amend, never rebase. One ship = one new commit on top. - Never delete or rename branches.
When NOT to use this skill
- Hotfix branches on
main(different convention). - Release branches (
release/v*). - Multi-commit work that should be split by file/scope — use [[rk:git]] for smart-split commits instead.
- Personal sandbox branches without a Jira card.
Related
- [[rk:ef-branch-name]] — creates the branch this skill operates on.
- [[rk:ef-pr-description]] — chained automatically after PR creation to fill the body.
- [[rk:ef-pr-comment]] — post inline review comments on an existing PR.
- [[rk:git]] — alternative for smart-split commits when one-commit-per-ship isn't enough.