commit
Create a semantic commit message — works in SDD pipeline, vibe coding, and git-only workflows
SKILL.mdskills/commit/SKILL.md
GitHubGit Commit
Load: view ${CLAUDE_PLUGIN_ROOT}/skills/commit/SKILL.md
What It Does
| Step | Action |
|---|---|
| 1 | Reads current branch, git status, and staged diff |
| 2 | If nothing staged → offers to stage modified/new files from git status (see Auto-Stage Offer) |
| 3 | Test-coverage gate (STRICT) — blocks if a changed production class has no matching test (see below) |
| 4 | Proposes a semantic commit message and waits for confirmation — does not auto-commit (see Format below) |
| 5 | Makes the commit on approval |
| 6 | Gets the short commit hash via git rev-parse --short HEAD |
| 7 | Offers to push the branch (see Push Offer) |
Step 0 — Whose pipeline is this? (run before anything else)
/sdd:commit works in three modes — pipeline, standalone, and someone else's
pipeline is still on disk. Resolve which one before staging or committing:
BRANCH="$(git rev-parse --abbrev-ref HEAD)"
STATUS="$(python3 ${CLAUDE_PLUGIN_ROOT}/lib/gates.py pipeline-branch-status "$BRANCH")"
STATUS is one JSON object: has_any_pipeline, owns_pipeline, slug,
recorded_branch, recorded_base_branch, is_base_branch.
owns_pipelineistrue— pipeline mode.SLUGisslug; proceed normally.has_any_pipelineisfalse— standalone / vibe-coding mode.SLUGis empty; proceed normally, silently. This is a first-class flow, not a problem.has_any_pipelineistrue,owns_pipelineisfalse,is_base_branchisfalse— a different branch's pipeline is sitting on disk. Warn, then continue in standalone mode (SLUGempty). Do not adopt it, and do not block the commit — the work in front of you is legitimate, it just is not that pipeline:⚠ .specwork/ belongs to '<recorded_branch>' (slug '<slug>'), not '<BRANCH>'. It's gitignored, so it didn't move when you switched branches. Committing anyway, treated as standalone: no spec, no plan, and the coverage gate uses repo-level waivers only — '<slug>'s waivers do not apply here. To clean this up: • /sdd:pause — stash '<slug>' to resume it later (its MR is still open) • /sdd:close — clear it (its MR merged, or the work was abandoned) • /sdd:start — begin a pipeline for '<BRANCH>' insteadPrint it once per
/sdd:commitrun, before the commit-message proposal, so the developer sees it while there is still a decision to make.is_base_branchistrue— you are on the pipeline's own base branch (post-merge cleanup). Treat as standalone, no warning:/sdd:closeis meant to be run from here.
Then rehydrate auto mode from this branch's state only:
if [ "${SDD_NON_INTERACTIVE:-0}" != "1" ] && [ -n "$SLUG" ]; then
SDD_MODE="$(python3 ${CLAUDE_PLUGIN_ROOT}/lib/gates.py non-interactive "$SLUG" 2>/dev/null || echo 0)"
[ "$SDD_MODE" = "1" ] && export SDD_NON_INTERACTIVE=1
fi
non_interactive is a property of one pipeline. Inheriting it from another
branch's leftover state would drop this commit's confirmation prompt — auto-committing
on a branch that never opted into /sdd:auto.
Test-Coverage Gate (STRICT)
Run after staging, before proposing the commit. It is the deterministic
counterpart to /sdd:implement's prose "Test Coverage" step: every changed
production class must have a matching test, or the commit is blocked.
python3 ${CLAUDE_PLUGIN_ROOT}/lib/coverage.py check $SLUG
$SLUG comes from Step 0
and is left unquoted so an empty value passes no argument at all — the standalone
path, which reads the repo-level .sdd-coverage-waivers.json. Only a pipeline that
records this branch contributes its _test/<slug>-coverage-waivers.json; another
branch's leftover waivers must never silently excuse classes here.
- Exit 0 → continue to the commit-message proposal.
- Exit 1 → do not commit. Print the offender list verbatim from the command's output and stop.
What it checks (stack-aware, via detect-stack):
| Stack | Production file | Accepted test |
|---|---|---|
| Java | src/main/.../FooService.java |
FooService{Test,Tests,IT,ITCase}.java anywhere under src/test |
| Frontend | Foo.tsx |
Foo.{test,spec}.* (sibling) or __tests__/Foo.{test,spec}.* |
It is intentionally conservative — pure data/wiring types are not flagged:
Java *Dto/*Request/*Response/*Entity/*Config/*Exception, package-info, and
config//dto//entity//model/ packages; frontend *.d.ts, barrels
(index.ts), *.styles/*.types/*.config/*.stories. Unknown/node stacks pass.
Escape hatch (the only one — "add a test or justify"): a class with no testable surface is waived by listing it with a written reason in a JSON file:
- In a pipeline:
.specwork/_test/<slug>-coverage-waivers.json - Standalone (vibe coding, no
.specwork/):.sdd-coverage-waivers.jsonat the repo root
{ "src/main/java/com/acme/SecurityConfig.java": "pure Spring wiring, no testable logic" }
This gate applies in every mode (interactive, non-interactive, and standalone). There is no blanket skip — the waiver requires naming the file and the reason.
Commit Format
[TICKET-ID] <type>: <concise description>
<optional body explaining why, not what>
Ticket extraction and subject formatting are done in code — decide the <type> and
<description> (judgment), then assemble the subject with:
python3 ${CLAUDE_PLUGIN_ROOT}/lib/titles.py commit-subject "$(git rev-parse --abbrev-ref HEAD)" "<type>" "<description>"
# → [PROJ-123] <type>: <description> (or [NO-TICKET] when the branch has none)
Commit types
| Type | When to use |
|---|---|
feat |
New feature or behavior |
fix |
Bug fix |
refactor |
Code change with no behavior change |
docs |
Documentation only |
test |
Test code only |
perf |
Performance improvement |
Commit Discipline
One logical change per commit.
Good:
[PROJ-15535] feat: add Flyway migration V23 for consent_flag column
[PROJ-15535] feat: add ConsentService with saveConsent logic
[PROJ-15535] feat: add ConsentController POST /api/v1/consent
[PROJ-15535] test: add unit and integration tests for consent flow
Bad:
[PROJ-15535] feat: consent stuff
Auto-Stage Offer
Triggered when git diff --staged is empty. Reads the working tree state via git status --porcelain and lists every modified or new file:
Nothing staged. Files with unstaged changes:
M src/main/java/com/example/leads/service/ConsentService.java
A src/main/java/com/example/leads/dto/ConsentRequest.java
A src/test/java/com/example/leads/service/ConsentServiceTest.java
Stage these files? (yes / no / edit)
- yes: run
git add <file> ...for each listed file, then continue to the commit message proposal - no: skip staging, fall through to Reconcile Mode
- edit: show the list, let the user remove lines, re-display, ask again
If git status --porcelain returns no modified or new files, skip the prompt and fall through to Reconcile Mode.
Push Offer
If nothing is staged and nothing to auto-stage, abort with:
Nothing to commit.
Stage your changes first, or run /sdd:implement to continue.
Otherwise, after committing, ask:
Push branch to origin now? (yes / no)
- yes: run
git push -u origin HEAD- If the push is rejected (divergent history), surface the error and stop — do NOT force-push
- Print the remote URL on success
- no: skip silently
This step is skipped entirely during Reconcile Mode (nothing to push — no new commit was made).
Inside an active SDD pipeline (
.specwork/present) thePreToolUse(git push)quality-gate hook blocks a push whose HEAD has not passedcommands/check.sh./sdd:commitdoes not runcheck.sh, so this push offer can be gated. If it is blocked, push via/sdd:mr(it validates, then pushes) or runbash commands/check.shfollowed bypython3 ${CLAUDE_PLUGIN_ROOT}/lib/validation.py recordfirst. Outside a pipeline (no.specwork/) the hook no-ops and the push proceeds normally.
Related Skills
state— reads git state and spec to show pipeline progressmr— reads commit history to generate the MR description