| 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.md and plan.md for 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 || true
-
Commit 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:
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 the
merge_method read from .feature-workflow.yml (default merge if unset). For
example maxwell sets merge_method: squash so features land as one clean commit:
gh api "repos/{owner}/{repo}/pulls/<pr-number>/merge" \
--method PUT \
--field merge_method=<merge_method>
gh api "repos/{owner}/{repo}/git/refs/heads/feature/<id>" --method DELETE
Why REST merge: gh pr merge uses GraphQL mergePullRequest. The REST endpoint PUT /pulls/{n}/merge is 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 merge enters the queue natively (no
merge-strategy flag — the queue's own configured method governs the merge, so the
.feature-workflow.yml merge_method is 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>
until [ "$(gh pr view <pr-number> --json state -q .state)" = "MERGED" ]; do
sleep 30
done
gh api "repos/{owner}/{repo}/git/refs/heads/feature/<id>" --method DELETE 2>/dev/null || true
If the queue is slow and you don't want to block, you may stop after gh pr merge
and 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>