# Commit rules

Apply to every commit produced by any skill that touches source code — most commonly `/code-implement`, `/code-quality`, `/code-review-fix`, and `/code-autopilot` delegations.

## Message format

```
${COMMIT_PREFIX}[(${SCOPE})]: <short english subject, imperative mood>

<optional body — wrap ~72 chars, explain why, not what>

Co-Authored-By: Claude <noreply@anthropic.com>
```

`[(${SCOPE})]` means the parenthesized scope is **optional** — see [Scope](#scope) below.

- Subject in **English**, **imperative mood** (`add X`, not `added X` / `adds X`), **≤ 72 characters** including the prefix and scope.
- Body is optional. When present, wrap at ~72 chars and explain the *why* and any non-obvious tradeoff — not what the diff already shows.
- **Never reference the ticket currently being implemented** anywhere in the message — neither in the subject, the body, nor any Git trailer (e.g. `Closes:`, `Refs:`, `Fixes:`). The link is carried by the **changelog entry** and by the **MR description** (which itself ends with `Closes #<IID>`); duplicating `#944` / `!799` / `&7` on every commit creates noise and stale references when a commit is cherry-picked or rebased.
- Referencing **another** ticket (not the one being implemented) is allowed when there is a real semantic link — a related bug, a follow-up to a past issue, a known regression. Write the full `#IID` / `!IID` / `&IID` in the body and explain the link in one sentence.

### Scope

The scope follows the [Conventional Commits](https://www.conventionalcommits.org/) convention. Its job is to make the **perimeter** of the change obvious from the subject line alone. Pick it in this order:

- ✅ **Application module** (preferred) — the name of a module / sub-package / CLI subcommand taken directly from the codebase (directory, file, or command name): `fix(install-claude-perms): …`, `feat(slack-send-message): …`, `refactor(detect-context): …`, `tech(env-resolve): …`.
- ✅ **Functional perimeter** (fallback) — a feature, domain, or user-facing flow when no single module captures the change: `feat(driver-app): …`, `fix(trip-list): …`, `tech(gtfs-import): …`.
- ❌ **Never** a ticket reference (`#944`, `!799`, `&7`), branch name, or author — even when referencing another ticket in the body is allowed, it never goes into the scope.
- ❌ Avoid generic noise like `(misc)`, `(stuff)`, `(cleanup)`.

#### Choosing a scope

- Prefer the **narrowest** name that still describes the change accurately — leaf module over parent package.
- **Omit the scope entirely** (`fix: <subject>`) if the change spans too many areas to name one, or if no narrow scope fits. A scopeless commit is better than a generic one.
- If you feel tempted to coin a wide scope, the commit is probably too broad — **splitting** it into atomic commits is usually the right fix.

## Determining `COMMIT_PREFIX`

Valid prefixes: `fix`, `feat`, `tech`, `refactor`. Pick using the first matching rule:

1. **Explicit override** — when the user (or a parent skill that has already resolved the type) passes a prefix as an argument, use it as-is.
2. **Source issue `type::*` label** — map via [issue-type-mapping.md](issue-type-mapping.md) (`type::bug` → `fix`, `type::feature` → `feat`, `type::technical` → `tech`, `type::refactoring` → `refactor`).
3. **Current branch prefix** — a branch named `fix/...` gives `fix`, `feat/...` gives `feat`, `tech/...` gives `tech`, `refactor/...` gives `refactor`.
4. **Fallback** — `fix`.

## MR title (used as the squash-merge commit message)

Every MR is merged with **squash on**, and GitLab uses the **MR title** as the squash commit message. That commit message is validated by **commitlint** (Conventional Commits) on the protected branch. **A non-conforming MR title therefore blocks the merge.**

So the MR title MUST be a valid Conventional Commits header:

```
<type>[(<scope>)]: <short english subject, imperative mood>
```

- **type** — one of the commitlint type-enum: `build`, `chore`, `ci`, `docs`, `feat`, `fix`, `perf`, `refactor`, `revert`, `style`, `test`, `tech`, `release`. **Never `bump`** (removed). For a code MR opened by `/code-implement`, this is the resolved `COMMIT_PREFIX` (`fix`/`feat`/`tech`/`refactor`); broader types (`chore`, `ci`, `docs`, …) are fine for non-feature MRs (tooling, CI, docs).
- **scope** — optional, same rules as the [Scope](#scope) section above.
- **subject** — English, imperative mood, no trailing period, no ticket reference (`#IID` / `!IID`); the issue link lives in the MR description (`Closes #<IID>`).
- **Never** prefix with `Draft:` / `WIP:` — that breaks the convention and the squash message.

Any skill that opens an MR (directly or by delegating to `/glab-mr`) must follow this. Examples: `feat(ndp): add trip export endpoint`, `fix: prevent null pointer in trip sync`, `chore(iam): offboard jdupont`, `tech(env-resolve): chain AWS Secrets fallback`.

## Atomicity

- One logical change per commit. Split unrelated changes into separate commits.
- Never bundle a refactor with a feature, nor tests for one concern with code for another.
- When the implementation notes file (`docs/ai-notes/<...>.md`) ships alongside a substantial change, do **not** touch the subject line; instead add a body line `AI Notes have been updated.` at the end of the commit message (as the last line, before any `Co-Authored-By:` trailer). Example body:
  ```
  feat(driver-app): add idle screen

  <optional body paragraphs>

  AI Notes have been updated.
  ```

## Pre-commit checks

Before each commit:
- Tests for the changed code pass.
- Linters pass (`ruff`, `black`, `mypy`, `eslint`, plus any project-specific tool from `CLAUDE.md`).
- The pre-commit hook (if installed) passes — **never** use `--no-verify`.

If a pre-commit hook fails, fix the underlying issue and create a **new** commit. Never `--amend` after a hook failure — the previous commit may carry unrelated work that would be silently rewritten.

## Never commit

- Generated files (build artifacts, lockfile churn unrelated to the change, etc.).
- Secrets, tokens, credentials.
- `.env*` files.
- `.gitlab-ci.yml` — unless the user explicitly asked for a CI change. If it shows up incidentally in the diff, unstage it and ask the user before including it.
