erplibre/conf/template_claude_commands_commit.md
Mathieu Benoit 76c548a781 [UPD] commit rule: English subject first, French title under --- FR ---
git log --oneline shows only the subject, and it read in whichever language
the author was thinking in. The subject and the body under it are now in
English, then --- FR --- opens the French section, which starts with the
subject translated under the same tag. The hook refuses a --- EN --- marker
and a French section without that title, and checks the title like a
subject; it does not count against the body budget. Whether the subject is
really English is not checked. /commit and /git_prepare_merge follow.
Checked: 55 hook tests, 7 of them new; i18n tests pass.

--- FR ---

[UPD] règle de commit : sujet anglais d'abord, titre FR sous --- FR ---

git log --oneline ne montre que le sujet, et il se lisait dans la langue où
l'auteur pensait. Le sujet et le corps qui le suit sont désormais en anglais,
puis --- FR --- ouvre la section française, qui commence par le sujet traduit
sous le même tag. Le hook refuse un marqueur --- EN --- et une section
française sans ce titre, et juge ce titre comme un sujet ; il ne compte pas
dans le budget du corps. Que le sujet soit vraiment en anglais ne se vérifie
pas. /commit et /git_prepare_merge suivent.
Vérifié : 55 tests du hook, dont 7 nouveaux ; tests i18n au vert.

Assisted-by: Claude Opus 5.5
2026-09-25 01:31:27 -04:00

12 KiB

name description disable-model-invocation allowed-tools
commit ERPLibre commit: OCA tag, bilingual body, Assisted-by trailer per AI_POLICY.md. true
Bash(git add:*)
Bash(git status:*)
Bash(git commit:*)
Bash(git diff:*)
Bash(git log:*)
Bash(python3:*)

Context

  • Git status: !git status
  • Full diff: !git diff HEAD
  • Current branch: !git branch --show-current
  • Last 5 commits (for style reference): !git log --oneline -5

Task

Write a commit that satisfies AI_POLICY.md — the OCA generative AI policy ERPLibre adopts — and the conventions below.

Resolve the model — {MODEL}

Run this first. It reads the model from the CURRENT session transcript, which is the only source that stays right when the model is switched mid-session with /model or a CLI flag:

python3 -c "
import glob, json, os, sys
sid = os.environ.get('CLAUDE_CODE_SESSION_ID', '')
hits = glob.glob(os.path.expanduser('~/.claude/projects/*/%s.jsonl' % sid)) if sid else []
mid = ''
for path in hits[:1]:
    with open(path) as fh:
        for line in fh:
            try:
                m = json.loads(line).get('message', {}).get('model', '')
            except Exception:
                continue
            if m and not m.startswith('<'):
                mid = m
if not mid:
    sys.exit('UNKNOWN')
mid = mid.removeprefix('claude-')
parts = [p for p in mid.split('-') if not (len(p) == 8 and p.isdigit())]
print('Claude %s %s' % (parts[0].capitalize(), '.'.join(parts[1:])))
"

It prints the trailer value: Claude Opus 5, Claude Sonnet 4.6, Claude Haiku 4.5. There is deliberately no fallback name — on UNKNOWN, use the model you know you are running as, and never a value read from a settings file: ~/.claude/settings.json usually has no model key at all, so it would quietly declare the default instead of the truth.

Tags

Tag Usage
[UPD] Update existing code, data or configuration
[FIX] Bug fix
[ADD] New module, file or capability
[IMP] Improvement to something that already works
[REF] Refactoring, no observable behaviour change
[REM] Remove code or module
[MOV] Move or rename
[I18N] Translations

The first five cover every one of the last 400 commits. Reach for [REM], [MOV] or [I18N] only when one of them genuinely fits better.

Format

[TAG] scope: short description in imperative mood, in English

Explain WHY the change was made — the diff already shows what. Name the
failure mode, and what was measured rather than assumed, in words a
stranger to this site can read: the software, the version, the class of
situation — never a customer, a real database, a machine or an address.
Wrap at 80 characters.

--- FR ---

[TAG] portée : le sujet, traduit en français

The same body, translated.

Assisted-by: {MODEL}

The subject line is in ENGLISH, and the English body follows it: together they are the English section. --- FR --- then opens the French section, which starts with the subject translated, under the same tag, and goes on with the translated body.

The subject line

The subject is read a hundred times for every time the body is read: in git log --oneline, in a blame, in a release note, in a bisect. It has one job — say what the code is about.

The test. Read the subject alone, with no diff and no body. Can you say which part of the system it concerns, and what is now different about it? If not, it is not finished.

Name the thing, then what changed about it. The symptom, the quoted error and the metaphor are EVIDENCE, and evidence belongs in the body. A subject built on them reads well and tells the next reader nothing:

Instead of Write
[FIX] cleanup: the children leave with their bounce [FIX] cleanup: ssh entries that bounce through a deleted VM
[FIX] proxmox: "storage is missing" was the symptom, not the cause [FIX] proxmox: report pmxcfs down, not "no storage"
[FIX] migration: a faulty module no longer takes the whole batch [FIX] migration: isolate a module's failure during uninstall

The scope is not the subject. proxmox says WHERE; the words after the colon must say WHAT. A subject that works with its scope removed is usually the right one.

Summarise the whole commit, not its largest piece. When the work has two faces — a guard moved and the check that proves it, a screen and the service under it — the subject covers both or the commit should have been two. If the only honest subject needs an and joining two unrelated things, split it.

It must be complete in 72 characters. A subject cut mid-phrase by --oneline has failed at the one place it is read most. Write it to fit rather than trimming it afterwards: drop the adjectives, keep the nouns.

When the work genuinely will not fit in a sentence, do not write an amputated one — write keywords that summarise. A comma-separated list of the nouns that matter says more in the space than half a sentence does:

[FIX] proxmox: pmxcfs down, pvesm silent, diagnosis at the source
[ADD] migration: site copies, doubled indexes, lost settings

That form is a fallback, not a default. Prefer the sentence when it fits.

The guard rail. script/git/hooks/commit-msg refuses a subject with no tag, one over 72 characters, and one opening on a quotation. It reads the body too: over ten lines for one language, an IP address, an e-mail, and a /home/<account>/ path. And the order of the languages: a --- EN --- marker, or a French section that does not open on the translated subject under the same tag. Whether the subject is really in English stays yours to check. Install the hook with git config core.hooksPath script/git/hooks; git commit --no-verify passes a legitimate exception. It checks only what is mechanical — whether the subject says what the code is about, and whether the body tells the story instead of the mechanism, stay judgements, and the tests above are how you make them.

Keep it short

The body answers one question: why was this necessary. Stop once it is answered — the reader owes you nothing beyond that.

Eight lines per language. Ten is the ceiling. Past that, the reasoning belongs in tasks/, which is not versioned, and the commit points at it — never in a code comment: a comment says how the code WORKS, not what happened the week it was written. The budget is per language: bilingual doubles everything, so it buys terseness, it does not excuse length.

Cut, in this order:

  • Anything the diff already says. adds function X is visible; X because the DHCP lease can be stale is not.
  • Headings and bullet lists. If the change really needs sections, it needs several commits.
  • Every clause that would not change what a reader does: no this commit, no I decided to, no summary of the summary, no restating the subject.

Keep, always: the failure mode the change removes, the figure that bounds it, and one line naming what you verified and how. A single Checked: 4 jobs, 1.63 s at parallelism 1 vs 0.58 s at 4 is worth three paragraphs of prose.

Two tests decide what survives, and they apply to every sentence of the body.

Tense and subject. Each sentence says what the code now does or refuses, in the present. A sentence whose subject is an incident, a session, a machine, a date or a person is cut — including your own reasoning: no my conclusion was wrong, no three faults found by running it. That belongs in tasks/.

Nothing identifying. No customer or third-party organisation, no real database name, no VM or host name, no IP address, no e-mail, no path carrying a user name, no label or figure taken from a customer's data. Generalise to the CLASS of situation instead of censoring: on a production database, on a development VM, on a host that demands interactive sudo — the class is what serves the reader; the name never was. A figure that is a durable limit or threshold stays (65 536 inotify watches); a reading taken during one incident goes.

Bilingual body

Every AI-assisted commit carries its subject and its body twice, English first: the subject and the body under it are in English, then --- FR ---, then the subject translated into French under the same tag, then the body translated. The order never varies — --- EN --- is refused — so a reader of git log --oneline always gets English, and a French reader finds the French title where the French section starts.

The translated subject obeys the same rules as the subject: 72 characters, no opening quotation. It does not count against the body's line budget.

Translate, do not re-summarise: a reader of either language must get the same reasoning, the same measured figures and the same caveats.

The Assisted-by trailer

AI_POLICY.md makes this binary — there was AI involvement or there was not, with no threshold to judge. Anything from a single suggestion to fully autonomous coding means the trailer, and it says nothing about the quality of the work.

  • One Assisted-by: line per model. A session that switched models declares each of them, one line each.
  • NEVER name an AI in Co-authored-by:: authorship of a work by a machine is legally undefined. That field is for other HUMANS who worked on the change. You are already the author, so never co-author yourself.
  • No blank line between trailers.

Rules

  • Subject: imperative mood, 72 characters maximum, aim for 50.
  • scope is the Odoo technical module (sale_order, account, stock) or the area of the repository (script todo, qemu ssh, migration).
  • The commit stands on its own: state what was verified, and how. If a claim was not checked, say so rather than implying it was. Standing on its own means it needs no OTHER COMMIT to be understood — not that it carries the whole investigation. The line budget above still binds.
  • If you cannot explain and defend every line, do not commit it.

Size and pace

A patch under 30 lines in a single file is the reference point. Past ~500 lines the policy asks for prior agreement with a maintainer — say so instead of committing quietly. When several unrelated modules are touched, propose splitting into separate commits before writing anything.

Execute

The timezone comes from the system, as it should: nothing is forced here, so each contributor's commits carry their own zone. If yours land at +0000, the machine itself is on UTC — common on a server or a VM — and the fix belongs there, sudo timedatectl set-timezone <Area/City>, because it affects every commit and not just this one.

The identity is passed explicitly with -c, which sets the author AND the committer. --author alone sets only the author, and a checkout with no configured user.email then fails on the committer.

Use a heredoc rather than -m: a body with quotes, backticks or accented characters survives it unharmed.

Name the files. Never git add -A. It stages everything untracked, and this repository keeps two directories untracked ON PURPOSE: private/, the only place allowed to hold customer data, and tasks/, where the convention sends the investigation precisely because it is not versioned. A sweep commits both. It also swallows whatever else is in flight in the checkout — another tool's output, a half-finished edit — under a subject that does not cover it.

git status --porcelain lists what changed; stage the paths that belong to the subject you just wrote, and no others. When one file carries two subjects, git add -p stages the hunks that belong to this commit.

git status --porcelain
git add script/module/thing.py test/test_thing.py
git -c user.name="Your Name" -c user.email="your@email.com" commit -F - <<'MSG'
[TAG] scope: description in English

Explain WHY here.

--- FR ---

[TAG] portée : la description en français

The same body, translated.

Assisted-by: {MODEL}
MSG