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
287 lines
12 KiB
Markdown
287 lines
12 KiB
Markdown
---
|
|
name: commit
|
|
description: "ERPLibre commit: OCA tag, bilingual body, Assisted-by trailer per AI_POLICY.md."
|
|
disable-model-invocation: true
|
|
allowed-tools:
|
|
- 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:
|
|
|
|
```bash
|
|
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.
|
|
|
|
```bash
|
|
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
|
|
```
|