| name | merge |
| description | Spec-gated merge: validates branch strategy, runs build/test/lint gates, walks acceptance criteria from the spec file, launches a reviewer on the diff, and only merges when all gates pass. Prevents regression with post-merge test runs. Enforces feature → dev → main flow. Use when the user says "/merge", "/merge <branch>", "/merge --to-main", or "/merge --dry-run".
|
| argument-hint | [branch] [--to-main] [--dry-run] [--spec path/to/spec.md] |
| disable-model-invocation | true |
Merge Skill Invoked
User has requested: /merge $ARGUMENTS
Flow control
Continuous execution. Steps 0 through 9 execute as one uninterrupted flow. Do not pause, narrate, or ask for confirmation between steps. The only legitimate stops are:
- A gate failure that blocks the merge
--dry-run mode (report what would happen, don't execute)
Step 0: Parse args
Parse $ARGUMENTS into:
- branch: token that is not a flag. The source branch to merge. If absent, auto-detect from current branch.
- --to-main: merge dev → main instead of feature → dev.
- --dry-run: run all gates, report results, but do not execute the merge.
- --spec : explicit path to the spec file. Overrides auto-discovery.
- --skip-review: skip the reviewer agent (faster, less safe).
- --force: merge even if non-blocking warnings exist (still blocks on failures).
Auto-detect branch: If no branch arg, use current branch:
CURRENT=$(git branch --show-current)
If CURRENT is main or dev, stop: "You're on $CURRENT — switch to a feature branch or specify one: /merge feature/my-branch"
Step 1: Validate branch strategy
1a: Branch naming
Check the source branch name:
- Allowed patterns:
feature/<name>, fix/<name>, <name> (any lowercase-hyphenated name that isn't main or dev)
- Blocked: direct merge from untracked or detached HEAD
1b: Target branch resolution
| Mode | Source | Target | Rule |
|---|
| Default | feature branch | dev | All feature work merges to dev first |
--to-main | dev | main | Only dev merges to main |
Enforcement rules:
1c: Check for uncommitted changes
git status --porcelain
If non-empty on the target branch, stop: "Target branch <target> has uncommitted changes. Commit or stash them first."
1d: Check branch is up to date
git fetch origin <target>
LOCAL=$(git rev-parse <target>)
REMOTE=$(git rev-parse origin/<target> 2>/dev/null)
If $LOCAL != $REMOTE and remote exists, warn: "Local <target> is behind origin/<target>. Pulling latest."
git checkout <target> && git pull --rebase origin <target>
Step 2: Discover spec file
The spec is the source of truth for what this branch should deliver. Find it:
2a: Explicit spec (--spec flag)
If --spec <path> was provided, read that file. If it doesn't exist, stop: "Spec file not found: <path>"
2b: Auto-discover from branch name
Extract the feature name from the branch:
feature/contacts → contacts
fix/login-bug → login-bug
add-user-dashboard → add-user-dashboard
Search for matching spec in priority order:
specs/<feature-name>.md
specs/<feature-name>-product.md
- Any
.md file in specs/ whose ## Objective heading mentions the feature name (fuzzy match — Read each file's first 20 lines)
2c: No spec found
If no spec discovered:
Store spec_path and parsed spec content for later steps.
Step 3: Pre-merge gates
All gates run against the SOURCE branch. Switch to it:
git checkout <source-branch>
3a: Build check
Detect project type and run build:
| File | Type | Build command |
|---|
package.json | Node/TS | npm install && npm run build (or npx tsc --noEmit if no build script) |
pubspec.yaml | Flutter | flutter pub get && flutter build |
Cargo.toml | Rust | cargo build |
go.mod | Go | go build ./... |
pyproject.toml | Python | pip install -e . 2>/dev/null; python -m py_compile <changed .py files> |
- Pass (exit 0):
build_result = "pass". Continue.
- Fail (non-zero):
build_result = "FAIL". Capture last 30 lines of stderr. STOP — do not merge broken code.
- No build system detected:
build_result = "skipped". Continue with warning.
3b: Test suite
Run the project's test suite:
| Type | Test command |
|---|
| Node/TS | npm test (only if test script exists and is not the default error stub) |
| Flutter | flutter test |
| Rust | cargo test |
| Go | go test ./... |
| Python | python -m pytest -x -q --tb=short (if pytest available) |
- Pass:
test_result = "pass (N tests)". Continue.
- Fail:
test_result = "FAIL". STOP — failing tests are a hard gate.
- No test infra:
test_result = "skipped". Continue with warning.
3c: Lint check (advisory)
Detect and run linter if configured:
-
package.json with lint script → npm run lint
-
.eslintrc* or eslint.config.* → npx eslint --max-warnings 0 <changed files>
-
ruff.toml or pyproject.toml with [tool.ruff] → ruff check <changed files>
-
.golangci.yml → golangci-lint run
-
Pass: lint_result = "clean".
-
Fail: lint_result = "N issues". Non-blocking warning — log issues but don't stop.
-
No linter: lint_result = "skipped".
Step 4: Diff analysis
Generate the diff between source and target:
git diff <target>...<source-branch> --stat
git diff <target>...<source-branch>
Store:
files_changed: list of modified files
lines_added, lines_removed: from --stat
diff_content: full diff (for reviewer)
4a: Diff size check
| Size | Threshold | Action |
|---|
| Small | < 200 lines changed | Proceed normally |
| Medium | 200-1000 lines | Note in report |
| Large | > 1000 lines | Warn: "Large diff (N lines). Consider splitting into smaller PRs." Non-blocking. |
Step 5: Acceptance criteria walk (spec-gated)
Skip if spec_found = false or --to-main mode.
Read the spec file and extract the ## Acceptance Criteria section. Parse each criterion.
For each criterion, attempt verification:
| Criterion type | Detection signal | Verification |
|---|
| File exists | "file exists", "creates", file path | Check file exists on source branch |
| API endpoint | URL pattern, HTTP method | Check route exists in code via Grep |
| Code pattern | "uses", "implements", function/class name | Grep for the pattern in changed files |
| Test coverage | "tested", "test exists" | Check test files exist for the feature |
| UI/visual | "displays", "renders", "shows" | Skip — "manual verification needed" |
| Behavioral | Everything else | Skip — "manual verification needed" |
Also check the ## Requirements section. For each requirement, verify at least one changed file addresses it (fuzzy match — the requirement's key nouns appear in the diff).
Track:
verified: criteria checked and passing
failed: criteria checked and failing
manual: criteria requiring human verification
coverage: verified / (verified + failed + manual) as percentage
Gate logic:
- Any
failed criterion → STOP: "Acceptance criteria not met. Fix these before merging."
coverage < 50% and manual > 0 → WARNING: "Most acceptance criteria need manual verification."
- All verified or manual → proceed.
Step 6: Reviewer agent
Skip if --skip-review flag is set.
Launch a foreground reviewer agent (Sonnet) with the diff:
You are reviewing a feature branch diff before merge.
Branch: <source-branch> → <target>
Spec: <spec_path or "none">
Files changed: <files_changed count>
Check for:
1. BLOCKING issues: broken imports, type errors, undefined references, security vulnerabilities (injection, XSS, exposed secrets)
2. REGRESSION risks: deleted tests, removed error handling, weakened validation, changed public API signatures without updating callers
3. SPEC DRIFT: if a spec is provided, check whether the changes implement what the spec describes — flag additions that aren't in the spec (scope creep) or spec requirements with no corresponding code
Classify each finding as BLOCKING (must fix before merge) or WARNING (should fix, non-blocking).
For BLOCKING: explain what breaks and how to fix it.
For REGRESSION risks: explain what behavior changes and who is affected.
For SPEC DRIFT: quote the relevant spec section.
DIFF:
<diff_content>
<if spec exists>
SPEC:
<spec file content>
</if>
Parse results:
- BLOCKING findings: STOP — "Reviewer found blocking issues."
- Warnings: Log to report. Non-blocking.
- Clean:
review_result = "clean".
Step 7: Execute merge
Skip if --dry-run.
Skip if any gate STOPPED the process.
git checkout <target>
git merge --no-ff <source-branch> -m "merge: <source-branch> → <target>"
Conflict handling:
If merge conflicts occur:
- Abort:
git merge --abort
- Report conflicting files
- Stop: "Merge conflicts detected. Resolve manually or rebase your branch."
On success:
MERGE_HASH=$(git rev-parse --short HEAD)
Step 8: Post-merge regression check
Skip if --dry-run or merge didn't execute.
Run the test suite on the TARGET branch after merge to catch regressions:
<test-command>
Step 9: Report
Merge: <source-branch> → <target>
Branch strategy: <valid | BLOCKED: reason>
Spec: <spec_path | not found (warning) | n/a (--to-main)>
Build: <pass | FAIL | skipped>
Tests: <pass (N tests) | FAIL | skipped>
Lint: <clean | N issues (warning) | skipped>
Diff: <N files, +M/-K lines> (<small|medium|large>)
Acceptance: <N/M verified, J manual | skipped (no spec)>
Review: <clean | N warnings | BLOCKING: description | skipped>
Merge: <commit HASH | dry-run | blocked>
Regression: <clean | DETECTED | skipped>
If --dry-run:
Dry run complete. All gates <passed | would block>:
<list any blocking issues>
Run without --dry-run to execute the merge.
If blocked at any gate:
Merge BLOCKED at: <gate name>
<reason and how to fix>
Fix the issue and run /merge again.
Edge cases
- Branch doesn't exist: actionable error with
git branch -a suggestion
- Already merged: detect via
git merge-base --is-ancestor. If source is ancestor of target: "Already merged."
- Empty diff: "No changes between
<source> and <target>. Nothing to merge."
- No git repo: stop with "Not a git repository."
- Detached HEAD: stop with "Detached HEAD — checkout a branch first."
- Multiple specs match: use the first match, note others in the report
- Spec has no acceptance criteria section: warn, skip criteria walk, proceed with other gates
- Dev → main (
--to-main) with failing tests: hard stop, same as feature → dev. Main must always be clean.
- Force flag with blocking issues:
--force only overrides non-blocking warnings. BLOCKING issues (build fail, test fail, reviewer blocking findings, regression) cannot be forced.
Philosophy
This skill enforces one principle: the spec is the contract, and the merge is the enforcement point.
- Before merge: does the code do what the spec says?
- During merge: does the branch strategy prevent chaos?
- After merge: did the merge break anything that was working?
Every gate has a clear fix action. No gate blocks without telling you how to unblock.