SKILL.md
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](../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](../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:
``bash gh api "repos/{owner}/{repo}/pulls?state=open&perpage=100" \ --jq '[.[] | select(.head.ref == "feature/<id>")] | .[0] | {number, url: .htmlurl, state, draft, base_ref: .base.ref}' ``
- Verify you're on the
feature/<id>branch — if not, switch to it:
``bash 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): ``bash 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:
``bash 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 #<number> for feature/<id> 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: ``bash # true when the base branch has a mergequeue rule (via ruleset). QUEUED=$(gh api "repos/{owner}/{repo}/rules/branches/<base>" \ --jq 'any(.[]; .type == "mergequeue")' 2>/dev/null || echo false) ``
No queue (QUEUED = false) — direct REST merge (the common path). Use the mergemethod read from .feature-workflow.yml (default merge if unset). For example maxwell sets mergemethod: squash so features land as one clean commit: ```bash # <mergemethod> = the mergemethod from .feature-workflow.yml (squash | merge | rebase; default merge) gh api "repos/{owner}/{repo}/pulls/<pr-number>/merge" \ --method PUT \ --field mergemethod=<mergemethod>
# Delete the remote branch (REST): 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: ```bash 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 || 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): ``bash 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):
``bash python3 ${CLAUDEPLUGINROOT}/skills/shared/lib/rundashboard.py <projectroot> ``
- Clear the statusline:
``bash python3 ${CLAUDEPLUGINROOT}/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>