close

Use to close a feature, finish a branch, or cut the release commit once every phase is done.

SKILL.md

GitHub

Close

Overview

Closes one feature on its branch: the project's full gate, the version bump the feature declared, a summary of everything the branch carries, and the removal of the local working notes. It refuses while a phase is not done or the tree is not clean. What comes after the branch is the user's and is never done here.

Preflight: when this text opens on a Preflight: passed line, the check already ran before the text was loaded: skip it. Otherwise run bash "${CLAUDE_PLUGIN_ROOT}/scripts/preflight.sh" as the first action. A non-zero exit stops the skill: print its output verbatim and do nothing else.

Announce at start: "I am using implement:close to finish this branch." Speak to the user in the user's language; write every artifact in English.


The Iron Rule

NEVER delete a branch
NEVER close while a phase is not done, or while the tree is dirty
NEVER touch a version file when the bump type is none

This skill's own steps end at the branch. A push and a draft pull request need no consent; taking it out of draft and the merge follow the method's ready gate and consent rules. Once the pull request is open at its final head, run /implement:review: the review is made by another session, never by this one.


Preconditions

implement:close [--docs <path>]. Without it the path is derived from the branch name (feat/<codename> or fix/<codename>). When --docs <path> was given, export DOCS_OVERRIDE=<path> before EVERY block below, not only the first: each block is its own shell and inherits nothing from the one before it.

When the mcp__implement__state tool is listed, one call replaces the derivation: get, with docs set to the --docs path when one was given, answers the branch, the codename, the artifacts path and mode, the ignore-guard verdict, the head, whether the tree is clean, and the parsed state file with its next phase. A verdict other than ok is the stop it names; artifacts path given, ignore guard skipped is not a stop. The bump, the base and every row are in the state it returns. Without the tool, the blocks below are the path, and they compute the same values.

# Each bash block is its own shell; derive rather than inherit.
BRANCH=$(git branch --show-current) || exit 1
CODENAME=${BRANCH#*/}
DOCS=${DOCS_OVERRIDE:-docs/features/$CODENAME}
[ -f "$DOCS/IMPLEMENTATION.md" ] || { echo "no IMPLEMENTATION.md under $DOCS: pass --docs <path>"; exit 1; }
[ -z "$(git status --porcelain)" ] || { echo "the tree is not clean"; exit 1; }
grep -qE '^\| *[0-9]+ *\|[^|]*\| *(todo|doing) *\|' "$DOCS/IMPLEMENTATION.md" && { echo "a phase is not done"; exit 1; }
BUMP=$(grep -E '^\*\*Bump\*\*:' "$DOCS/IMPLEMENTATION.md" | sed 's/^\*\*Bump\*\*: *//')
MODE=$(python3 -c 'import json,os; print(json.load(open(".implement.json")).get("artifacts","local") if os.path.exists(".implement.json") else "local")')
if [ -n "${DOCS_OVERRIDE:-}" ]; then
  echo "artifacts path given, ignore guard skipped"
elif [ "$MODE" = committed ]; then
  if git check-ignore -q docs/features/probe || git check-ignore -q ROADMAP.md; then
    echo "docs/features and ROADMAP.md must not be ignored in committed mode"; exit 1
  fi
elif ! git check-ignore -q docs/features/probe || ! git check-ignore -q ROADMAP.md; then
  echo "docs/features and ROADMAP.md must be ignored in local mode: add them to the excludes, or set artifacts to committed in .implement.json"; exit 1
fi
echo "$CODENAME ready to close, bump $BUMP, artifacts $MODE"

Both artifact paths are tested one by one, and the test inverts in committed mode rather than being skipped there: this skill deletes under one of them and trims the other, and it does that only where the repository agrees they are working notes. An explicit --docs <path> skips the test altogether, the caller having taken the responsibility for the directory it named. Step 4 derives MODE again, for the same reason the path is re-exported: it runs in a shell of its own. The guard reads docs/features/probe, not docs/features: before the directory exists, check-ignore on the bare path reads not ignored, while the inner path is matched by every form the pattern takes.


Step 1 — The full gate

The whole project, not the files of one phase. The commands are the ones the repository names, in this order of discovery: the check and test commands of its CLAUDE.md, else those of its CONTRIBUTING.md, else its test suite as its own documentation describes it. Run each one synchronously, in the call that waits for it, and state it with its exit code.

A non-zero code stops here: name the command, its exit code and the first lines of its output, and repair nothing. A feature is not closed over a red gate, and a gate whose exit code was not read was not run.


Step 2 — The version

Only when the bump type read above is not none. Detect the version file in this order, and take the first one that exists:

VERSION
pyproject.toml
package.json
composer.json
.claude-plugin/plugin.json

The order is the contract: a repository holding several of them is bumped in the first one, always the same one. Read the current version, then apply the bump — major raises the first number and resets the two others to zero, minor raises the second and resets the third, patch raises the third. Write it back in place: python3 for a JSON field or a TOML field, so the rest of the file is untouched; printf for a plain VERSION file.

Run the full gate of Step 1 again, every command with its exit code. A non-zero code stops here with the status question and one line: version written in <file>, gate red: <the first failing line> — a repository that mirrors its version elsewhere (a marketplace entry, a suite literal) says so through its own gate, and aligning those places is the repository's decision, not this skill's guess.

Then one commit, with no trailer, carrying the version file alone — and, in committed mode, the feature's artifacts with it. Its subject takes the shape of the repository's last release commit:

# Each bash block is its own shell; derive rather than inherit.
git log --format=%s -E --grep='^([a-z]+(\([^)]*\))?: )?[Rr]elease' -3

chore: release pr-review 0.3.3 where that log reads chore: release pr-review 0.3.2, and chore: release <version> where it holds no release subject at all.

A bump type other than none with none of the five files present stops the skill: say which five were looked for and wait. Guessing a version file is how a version lands in a file nothing reads.


Step 3 — The summary

# Each bash block is its own shell; derive rather than inherit.
BRANCH=$(git branch --show-current) || exit 1
CODENAME=${BRANCH#*/}
DOCS=${DOCS_OVERRIDE:-docs/features/$CODENAME}
BASE=$(grep -E '^\*\*Base\*\*:' "$DOCS/IMPLEMENTATION.md" | sed 's/^\*\*Base\*\*: *//')
[ -n "$BASE" ] || { echo "no Base line in $DOCS/IMPLEMENTATION.md"; exit 1; }
git log --format='%h %s' "$BASE"..HEAD
git diff --stat "$BASE"..HEAD

The summary states, in the user's language: the branch, every commit since the base, the version if one was written, and each gate command with its exit code. It ends on one sentence, said once: this skill's steps end at the branch: a push and a draft pull request need no consent, and ready and merge follow the method's gate.


Step 4 — The local files

In local mode, and only there, the feature's own working notes go: that directory and no other, plus the feature's line in the roadmap when it has one.

# Each bash block is its own shell; derive rather than inherit.
BRANCH=$(git branch --show-current) || exit 1
CODENAME=${BRANCH#*/}
DOCS=${DOCS_OVERRIDE:-docs/features/$CODENAME}
MODE=$(python3 -c 'import json,os; print(json.load(open(".implement.json")).get("artifacts","local") if os.path.exists(".implement.json") else "local")')
[ "$MODE" = committed ] && { echo "committed mode: nothing is deleted"; exit 0; }
[ -d "$DOCS" ] && rm -r "$DOCS"
rmdir docs/features docs 2>/dev/null || true
[ -f ROADMAP.md ] && { TRIMMED=$(mktemp); grep -v "^- $CODENAME " ROADMAP.md > "$TRIMMED"; mv "$TRIMMED" ROADMAP.md; }
[ -s ROADMAP.md ] || rm -f ROADMAP.md

The directory is removed only when it is there: a second run of this skill, or a close over a directory a caller had already taken away, is not an error. rmdir takes each parent only when the removal emptied it, and says nothing when it did not: a repository holding another feature, or holding documentation of its own under docs, keeps both directories. The roadmap goes the same way when the trim emptied it, since a file holding nothing is not a roadmap.

In committed mode nothing is deleted: the design, the plan and the state are part of the repository and stay in it.


Stops

Situation The one line
No IMPLEMENTATION.md under the derived path no IMPLEMENTATION.md under <docs>: pass --docs <path>
The tree is dirty the tree is not clean
A row is still todo or doing a phase is not done
docs/features or ROADMAP.md is not ignored in local mode docs/features and ROADMAP.md must be ignored in local mode: add them to the excludes, or set artifacts to committed in .implement.json
Either of them is ignored in committed mode docs/features and ROADMAP.md must not be ignored in committed mode
The state file carries no Base line no Base line in <docs>/IMPLEMENTATION.md
A gate command came back non-zero <command> exit <code> and the first lines of its output
A bump is asked and no version file exists the five names looked for, in order, then wait
The gate is red on the run that follows the version write version written in <file>, gate red: <the first failing line> — status question

Each stop says its line once and waits. No answer to a stop turns it into an outward action that no instruction calls for.


Report

branch:    <type>/<codename>, base <sha>
commits:   <short sha> <subject>   (every commit since the base)
files:     the version file if one was written
gate:      <command> exit <code>   (one line per command: both runs when a
           version was written — the full gate of Step 1, then the same gate
           again — otherwise the one gate of Step 1)
checks:    every phase row done, tree clean before and after
deviations: what departs from the plan or the bump type the feature declared
open questions: what the branch leaves open
status:    done|blocked|question — <one line>

Quick Reference

1. Preflight, then derive BRANCH, CODENAME, DOCS, BUMP, MODE, and guard both
   artifact paths (--docs exports DOCS_OVERRIDE and skips the guard)
2. Refuse on a dirty tree, a missing state file, or a row not done
3. Full-project gate, every command with its exit code. Red stops, nothing repaired
4. Bump none: skip. Otherwise the first of VERSION, pyproject.toml, package.json,
   composer.json, .claude-plugin/plugin.json, then the same gate again, then one
   commit in the shape the repository's last release subject gives
5. Summary: branch, commits since the base, version, gate codes, the closing sentence
6. local mode: delete the feature directory, the parents it empties and its roadmap
   line. committed: keep all
7. The steps end at the branch; ready and merge belong to the gate