name: feature-ship description: Complete a feature by writing shipped.md, committing to the feature branch, and merging the PR. Use when external reviews are done and the feature is ready to ship. user-invocable: true
Ship Feature
You are executing the SHIP FEATURE workflow — writing the completion record, committing it to the feature branch, and merging the PR.
Branch Configuration
Before doing anything else, read .feature-workflow.yml in the project root for branch settings. See ../shared/config.md for details.
Multi-repo workspace? If the root has a .feature-workspace.yml, ship from inside the member repo — cd <member> first, so the branch/PR/merge target that member's own remote and config. See ../shared/workspace.md.
| Setting | Default | Used for |
|---|---|---|
branch.prefix |
feature/ |
Branch naming: <prefix><id> |
branch.target |
dev |
Merge target, checkout after merge |
merge_method |
merge |
How Phase 4 merges the PR: squash / merge / rebase. Ignored when the base branch requires a merge queue — the queue's own configured method wins, so keep them aligned. |
Throughout this skill, replace feature/<id> with <prefix><id> and dev with <target> based on the config.
First Step (Do This Now)
Read the file at path: docs/features/DASHBOARD.md
This file shows in-progress features. Look at the "In Progress" section to find features ready to ship.
Feature Target
$ARGUMENTS
If no specific feature ID was provided above, you will help the user select from in-progress items.
Workflow Overview
| Phase | Name | Purpose |
|---|---|---|
| 1 | Pre-flight | Verify feature is in-progress and has a PR |
| 2 | Write shipped.md | Create completion record on the feature branch |
| 3 | Prepare PR | Remove review labels + commit and push shipped.md |
| 4 | Merge PR | Mark PR ready, merge into dev, clean up branches |
| 5 | Update Dashboard | Regenerate dashboard and clear statusline |
Phase 1: Pre-flight
- Read the feature's
idea.mdandplan.mdfor context - Verify feature is in-progress (has plan.md, no shipped.md)
- Check for the PR:
gh api "repos/{owner}/{repo}/pulls?state=open&per_page=100" \ --jq '[.[] | select(.head.ref == "feature/<id>")] | .[0] | {number, url: .html_url, state, draft, base_ref: .base.ref}' - Verify you're on the
feature/<id>branch — if not, switch to it:git checkout feature/<id>
If there's no PR, warn the user — they may want to run /feature-review-impl first, or proceed with a local merge.
Phase 2: Write shipped.md
Write docs/features/<id>/shipped.md with the following format:
---
shipped: YYYY-MM-DD
---
# Shipped: [Feature Name]
## Summary
Brief summary of what was delivered...
## Key Changes
- Change 1
- Change 2
- Change 3
## Files Changed
- `path/to/file1.ts`
- `path/to/file2.ts`
## Testing
How the feature was tested and verified...
## Notes
Any follow-up items, known limitations, or context for future maintainers...
Populate this from the plan.md, commit messages, and git diff.
Phase 3: Prepare PR (labels first, then commit + push)
Order matters here. The review workflow (feature-review.yml)
triggers on pull_request: types: [labeled, synchronize]. If review
labels (plan-review, impl-review) are still on the PR when the
shipped.md push fires a synchronize event, the workflow runs a
pointless extra review on a docs-only commit, spending API quota and
posting irrelevant review comments on a PR that's about to merge.
Remove the labels before pushing shipped.md:
Remove review labels (must be first — prevents the push-triggered re-review):
gh pr edit <pr-number> --remove-label plan-review --remove-label impl-review 2>/dev/null || trueCommit shipped.md to the feature branch and push:
git add docs/features/<id>/shipped.md git commit -m "docs(<id>): mark feature as shipped" git push
After writing shipped.md, regenerate the dashboard by running:
python3 ${CLAUDE_PLUGIN_ROOT}/skills/shared/lib/run_dashboard.py <project_root>
DASHBOARD.md is auto-resolved on merge via CI — no need to commit it from feature branches.
Phase 4: Merge PR
PRs are opened as non-draft (since v9.5.2), so no draft → ready conversion is needed.
Confirm with user: "Merge PR #
for feature/ into dev?" If you encounter a draft PR (legacy / opened externally): convert with
gh pr ready <pr-number>once. This is a GraphQL mutation, used at most once per stuck PR. Don't retry on rate-limit failure — wait for the GraphQL window to reset (gh api rate_limit --jq '.resources.graphql.reset').Detect whether the base branch requires a merge queue, then merge accordingly. A queue-protected branch rejects a direct merge — the only way in is the queue — so the merge path forks on this:
# true when the base branch has a merge_queue rule (via ruleset). QUEUED=$(gh api "repos/{owner}/{repo}/rules/branches/<base>" \ --jq 'any(.[]; .type == "merge_queue")' 2>/dev/null || echo false)No queue (
QUEUED= false) — direct REST merge (the common path). Use themerge_methodread from.feature-workflow.yml(defaultmergeif unset). For example maxwell setsmerge_method: squashso features land as one clean commit:# <merge_method> = the merge_method from .feature-workflow.yml (squash | merge | rebase; default merge) gh api "repos/{owner}/{repo}/pulls/<pr-number>/merge" \ --method PUT \ --field merge_method=<merge_method> # Delete the remote branch (REST): gh api "repos/{owner}/{repo}/git/refs/heads/feature/<id>" --method DELETEWhy REST merge:
gh pr mergeuses GraphQLmergePullRequest. The REST endpointPUT /pulls/{n}/mergeis functionally equivalent, doesn't count against the GraphQL points budget, and isn't subject to the secondary mutation rate limit. The 405 "still a draft" failure mode no longer applies because we never open as draft.Queue required (
QUEUED= true) — enqueue, then wait for the queue to merge. A direct REST merge returns 405 here;gh pr mergeenters the queue natively (no merge-strategy flag — the queue's own configured method governs the merge, so the.feature-workflow.ymlmerge_methodis ignored on this path; keep the two in sync). If required checks haven't passed yet it enables auto-merge instead, so the same command is correct either way:gh pr merge <pr-number> # enqueues (GraphQL, once); queue's merge method applies # The queue re-runs the required checks against the queued branch and merges # asynchronously. Poll until the PR is MERGED before declaring shipped: until [ "$(gh pr view <pr-number> --json state -q .state)" = "MERGED" ]; do sleep 30 done # The queue deletes the head branch on merge; tolerate an already-gone ref. gh api "repos/{owner}/{repo}/git/refs/heads/feature/<id>" --method DELETE 2>/dev/null || trueIf the queue is slow and you don't want to block, you may stop after
gh pr mergeand report the PR as enqueued — it merges when the queue drains. Phase 5 cleanup (dashboard, local branch delete) only applies once it has actually merged.Switch to the base branch, pull, and delete the local feature branch (after the merge has landed — for a queued PR that's after the poll above):
git checkout dev && git pull && git branch -d feature/<id>
Phase 5: Update Dashboard and Cleanup
- Regenerate the dashboard on dev (shipped.md is now merged):
python3 ${CLAUDE_PLUGIN_ROOT}/skills/shared/lib/run_dashboard.py <project_root> - Clear the statusline:
python3 ${CLAUDE_PLUGIN_ROOT}/skills/shared/lib/statusline.py clear - Display completion summary:
## Feature Shipped
**Feature**: [name]
**ID**: <id>
**PR**: <pr-url> (merged)
**Shipped**: YYYY-MM-DD
The feature is now in dev. Dashboard updated.
Error Handling
| Error | Resolution |
|---|---|
| Feature not in-progress | Direct user to correct command or status |
| No PR exists | Suggest /feature-review-impl first, or offer local merge |
| Not on feature branch | Switch to feature/<id> |
| Already completed | Feature has shipped.md — nothing to do |
| Direct merge returns 405 / "merge queue required" | The base branch requires a merge queue — use the queue path in Phase 4 step 3 (gh pr merge <pr>, then poll for MERGED) instead of the REST merge |
Fallback: Manual Ship
If shipped.md wasn't created, you can use the ship script:
python3 ${CLAUDE_PLUGIN_ROOT}/skills/feature-ship/scripts/ship_feature.py <project_root> <feature-id> "Summary message"
After creating shipped.md, regenerate the dashboard:
python3 ${CLAUDE_PLUGIN_ROOT}/skills/shared/lib/run_dashboard.py <project_root>