Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .claude/commands/review.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
# SPDX-FileCopyrightText: Fondation RERO+
# SPDX-License-Identifier: AGPL-3.0-or-later
description: Code review, with the findings also rendered as readable prose
argument-hint: [low|medium|high|max] [PR number|branch|path] [--fix|--comment]
---

Run the `code-review` skill, passing it verbatim: $ARGUMENTS

The effort level, the scope and the verification passes stay the skill's
business. Only the reporting changes, at the end: report the verified findings
twice over.

First the `ReportFindings` call the skill asks for, unchanged — the host UI
reads it. Then, overriding the skill's rule against printing the findings as
text, write them out in the reply as well:

- Under headings by severity, worst first, numbered continuously across the
headings.
- One entry per finding: the claim in bold, then a link to the exact spot —
`[SKILL.md:37](.claude/skills/commit-message/SKILL.md#L37)`, relative to the
repository root so the editor opens it.
- Below it, one or two sentences on the mechanism: what input reaches the code
and what it does wrong. Never a paraphrase of the code the reader can see.
- A closing `→` line with the fix, whenever it fits in a sentence.
- Last, a short synthesis: which findings share one cause, and what to repair
first.
- In the language the user is writing in.

When nothing survived verification, say so in one sentence and skip the
headings altogether.
184 changes: 184 additions & 0 deletions .claude/skills/commit-message/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
---
# SPDX-FileCopyrightText: Fondation RERO+
# SPDX-License-Identifier: AGPL-3.0-or-later
name: commit-message
description: Write Conventional Commits messages for the current changes, splitting them into several commits when they cover several concerns, and propose each message for review before anything is committed. Use whenever the user asks for a commit message, asks to commit the current changes, or invokes /commit-message.
Comment thread
PascalRepond marked this conversation as resolved.
---

# Commit message

Draft the message, then let the user have the last word: the commit is theirs to
approve. Staging is yours to arrange, so is proposing more than one commit — but
nothing is committed before they have read the message and said to go ahead.

## Workflow

### 1. Survey the changes

```bash
git status --short
```

Then read the actual diffs, `git diff --cached` and `git diff`. Untracked files
count too: list them with `git status --short --untracked-files=all`, then read
each one. No diff carries their content, and a message cannot cover a file that
was never read.

Whatever is **already staged is a signal**: the user chose that scope. Write for
it, and ask before widening it. When nothing is staged, the grouping is yours to
decide.

### 2. Decide the split

One commit per concern. A fix and the refactor that made room for it are two
commits; a change and its tests are one.

For a single commit, go on. For several, **propose the plan before touching the
index** — a numbered list, each entry a subject line plus the files it takes —
and let the user confirm or rearrange. Then run the rest of this workflow once
per commit, in the listed order, stopping if one is aborted.

Staging is file-level: `git add -- <paths>`. The interactive flags (`git add -p`,
`-i`) are unavailable here, so when one file has to be split across two commits,
say so and let the user split it themselves.

Before each commit of a plan, make the index hold exactly the files of that
commit — the commit command carries no pathspec and takes the index whole:

```bash
git add -- <paths of this commit>
git reset -q -- <paths of the later commits>
```

`git reset` on paths leaves the working tree untouched, and unlike
`git restore --staged` it also works before the very first commit of a
repository. A file whose `git status --short` line carries a non-space in both
columns short of a conflict (`MM`, `AM`, `MT`, `RM`, `AD`, and the rest) is only
partly staged, though, and unstaging it drops that split — ask first.

### 3. Gather the why

The diff shows *how*; the message must say *why*. Use the conversation, the
linked issue, `git log` on the touched files, and the branch name. If a reason
is missing, ask rather than guess — and never invent an issue number.

### 4. Draft it

Follow *Format*, *Content* and *Trailer* below. `git log -20` is the reference
for tone, not for format: much of the history predates the rules below.

### 5. Write it to the git dir

A quoted heredoc, so nothing in the message expands:

```bash
cat > "$(git rev-parse --git-path CLAUDE_COMMIT_MSG)" <<'EOF'
<the message>
EOF
```

### 6. Verify the line limits

This prints every offending line; silence is a pass. Rewrite until it is
silent — never truncate mid-sentence.

```bash
awk 'NR==1 ? length>50 : (length>72 && !/^[A-Za-z-]+-by: /) {printf "L%d (%d): %s\n", NR, length, $0}' \
"$(git rev-parse --git-path CLAUDE_COMMIT_MSG)"
```

### 7. Propose it

Print the message in the reply, whole, in a fenced block, followed by the path
of the draft file. Then stop there: never commit in the turn that proposes.

The user changes it either way — by replying with what to fix, or by editing the
draft file themselves. When they reply with corrections, rewrite the file, check
it again with step 6, and propose the new version the same way.

### 8. Commit

Only once the user has agreed to it. They may have edited the draft or the
index in the meantime, so check both before committing: read the file again and
rerun step 6 over it, and confirm `git status --short` still stages exactly the
files of this commit. A staged scope that has drifted stops the commit — report
what changed and ask. A line the user's own edit made too long stays theirs to
keep, so only flag it. Then commit what the file holds:

```bash
git commit --file "$(git rev-parse --git-path CLAUDE_COMMIT_MSG)"
```

A non-zero exit is a genuine failure, not a signal: report what git printed, and
leave the rest of the plan alone until it is sorted out. Otherwise show
`git log -1 --oneline` and carry on with the next commit.

## Format

`<type>(<scope>): <subject>`

- Subject: 50 characters maximum, imperative mood ("add", not "adds"),
lowercase start, no final period.
- Body: wrapped at 72 characters, blank line after the subject.
- Bullets: `*`, never `-`, however many `-` the older history holds.
- Types: `feat`, `fix`, `chore`, `docs`, `refactor`, `test`, `style`, `perf`,
`build`, `ci`, `revert`.
- Scope: the module under `sonar/modules/` that the change touches
(`documents`, `deposits`, `users`, `organisations`, `collections`). Outside
`sonar/modules/`, use the area, as the history already does — `theme`,
`docker`, `data`, `fixtures`, `translations`, `deps`, `dev`. Check with
`git log --format=%s | grep -oP '^\w+\(\K[^)]+' | sort | uniq -c | sort -rn`.
Omit the scope when the change is repository-wide. An issue number is never a
scope.
- `!` before the colon marks a breaking change: `feat(documents)!: ...`.

## Content

Explain *what* and *why*, not *how* — the code says how, so the body is never a
technical list of the changes made. It answers one question, why the change was
necessary, and stops there: a developer should grasp the change and its context
before opening the diff.

Keep it short. Two to six `*` bullets, each carrying a reason rather than a file
name; add an opening paragraph only if the motive does not fit in the bullets.
Cover the motive of the commit, not its incidental fallout — whatever the diff
makes plain on its own earns no bullet.

Too verbose:

> The commit conventions lived in CLAUDE.md, where they were loaded into
> every session whatever the task at hand, and where they could only ever
> be a format spec: they described the shape of a message without saying
> anything about how to arrive at one.
>
> * A skill loads on demand, so the rules reach the context only when a
> commit is actually being written.
> * As a workflow rather than a list of rules.
> * The draft is proposed for review, so the message is always read
> before it is committed and the commit stays the author's own.
> * Ignore the local Claude settings, whose permission allowlist is
> personal to each developer's machine.

Enough, for that very same commit:

> * Create a skill for commit conventions to avoid loading it into
> every session.
> * Specify the conventions to make commit messages more readable and
> standardised.

The opening paragraph only restates the first bullet; the review step and the
`.gitignore` entry are plain from the diff and were never the point of the
commit. Each bullet is a concise description of a broad change and the motive for it.

Close with the issue when there is one, as its own bullet: `* Closes #123.`
Never open a line with `#`, there or anywhere in the body: git takes it for a
comment and drops it without a word.
Flag a manual migration step under a `:warning:` paragraph.

## Trailer

Credit the humans, one per line, in the last paragraph, as
`Co-Authored-by: <name> <email>`. Read the pair from `git config user.name` and
`git config user.email` — never from an example or from memory — and add any
colleague the user names. Do not sign this section as an LLM: no Claude or
Anthropic trailer, whatever the default of the harness is.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,6 @@ celerybeat-schedule*
data/wiki/_index/

.python-version

# Claude Code local settings
/.claude/settings.local.json
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ The `sonar/ext.py` file wires up all signal listeners. There is no per-module in
- Do not use Python type annotations (no `-> str`, `: str`, etc. in signatures).
- Ruff is configured in `pyproject.toml`: `line-length = 120` under `[tool.ruff]`, the enabled rule sets under `[tool.ruff.lint]`, and the pep257 docstring convention under `[tool.ruff.lint.pydocstyle]`.
- Since Python 3.14 (PEP 758), parentheses around multiple exception types are optional when the `except`/`except*` clause has no `as` target: `except AttributeError, UnboundLocalError:` is valid and equivalent to `except (AttributeError, UnboundLocalError):` — not the old Python 2 comma syntax. `ruff format` removes the parentheses in that case; this is expected, not a bug. Parentheses are still required when binding the exception: `except (AttributeError, UnboundLocalError) as error:`.
- Commit messages follow [Conventional Commits](https://www.conventionalcommits.org)
- Commit messages follow Conventional Commits; the `commit-message` skill holds the conventions and the workflow, so invoke it instead of writing one by hand. In every case, whatever the default of the harness, never sign a commit as an LLM: no Claude or Anthropic trailer.

### Translations

Expand Down