diff --git a/conf/template_claude_commands_git_prepare_merge.md b/conf/template_claude_commands_git_prepare_merge.md
new file mode 100644
index 0000000..be801a8
--- /dev/null
+++ b/conf/template_claude_commands_git_prepare_merge.md
@@ -0,0 +1,170 @@
+---
+name: git_prepare_merge
+description: "ERPLibre merge preparation: changelog entry, then the merge message for the current branch."
+disable-model-invocation: true
+allowed-tools:
+ - Bash(git status:*)
+ - Bash(git branch:*)
+ - Bash(git log:*)
+ - Bash(git diff:*)
+ - Bash(git merge-base:*)
+ - Bash(sed:*)
+ - Bash(make doc_markdown:*)
+ - Bash(python3:*)
+ - Read
+ - Edit
+ - Write
+---
+
+## Context
+
+- Current branch: !`git branch --show-current`
+- Branch commits: !`git log --oneline $(git merge-base HEAD master)..HEAD`
+- Files touched: !`git diff --stat $(git merge-base HEAD master)..HEAD`
+- Working tree: !`git status --porcelain`
+- Changelog head: !`sed -n '24,45p' CHANGELOG.base.md`
+
+## Task
+
+Prepare the merge of the CURRENT branch into its integration branch. Two
+deliverables, in this order: the changelog entry, then the merge message.
+Nothing is merged here — `/git_prepare_merge` prepares, the human merges.
+
+### 0. Read the branch
+
+`master` is production, `develop` is where the work lands. Take the target
+from where the branch forked: `git merge-base HEAD develop` and
+`git merge-base HEAD master`, the closer of the two names the target.
+
+Read the WHOLE branch before writing a word — `git log -p ..HEAD` for
+the commits, `git diff ..HEAD` for the net result. A merge message
+summarises what the branch delivers, which is rarely the concatenation of its
+subjects: commits that undo each other cancel, and a fix to a feature added on
+the same branch is part of the feature, not a separate line.
+
+Stop and say so, rather than inventing, when the branch is empty, when it is
+already merged, or when the working tree carries changes not yet committed —
+uncommitted work is not part of the merge and must not be described as if it
+were.
+
+### 1. The changelog entry
+
+`CHANGELOG.base.md` at the repository root is the SOURCE. `CHANGELOG.md` and
+`CHANGELOG.fr.md` are generated by mmg and every direct edit to them is lost
+at the next `make doc_markdown` — never open them to write.
+
+The entry goes under `## [Unreleased]`, in the section that fits: Added /
+Ajouté, Changed / Modifié, Fixed / Corrigé, Removed / Retiré, Security /
+Sécurité. Create the pair of headings if the section does not exist yet, in
+the file's own order.
+
+The file alternates language blocks with markers. Within one section the
+English bullets sit under `` and the French translation under
+``, in the SAME order: the two lists are read side by side, and a
+bullet added to one language only leaves the other half wrong. Nothing goes
+under `` but the version headings.
+
+What a bullet says: what the software now DOES or REFUSES, in the present, for
+someone who was not on this branch. It is longer than a commit subject and
+shorter than the commit body — the reader is choosing whether to upgrade, not
+reviewing the diff. Keep the failure mode removed, the figure that bounds it,
+the flag or the file a user has to know. Drop the internals nobody outside
+calls.
+
+The two rules of `.claude/rules/04-code-conventions.md` hold here as
+everywhere: nothing identifying — no customer, no real database, no host, no
+address, no account path — and the code as the subject, never the session
+that produced it.
+
+Regenerate afterwards, and stage the three files together, the generated pair
+being what most readers actually open:
+
+```bash
+make doc_markdown
+git status --porcelain CHANGELOG.base.md CHANGELOG.md CHANGELOG.fr.md
+```
+
+### 2. The merge message
+
+Resolve `{MODEL}` exactly as `/commit` does — the trailer is required here
+too, a merge message being as AI-assisted as any other. Run:
+
+```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:])))
+"
+```
+
+The shape, as this repository writes it:
+
+```
+Merge branch ''
+
+[TAG] scope: what the branch delivers, imperative, 72 characters maximum
+
+ commits. Why the branch existed: the failure mode it removes, the
+figure that bounds it, what was verified and how. Wrap at 80 characters.
+
+--- EN ---
+
+The same body, translated.
+
+Assisted-by: {MODEL}
+```
+
+The first line stays `Merge branch ''` — git writes it, tools read it,
+and a merge whose first line says something else no longer looks like a merge
+in `git log --oneline`. The tagged line beneath it is what a reader gets from
+`--oneline` on the second row and from a release note, so it carries the same
+duty as a commit subject: name the part of the system, then what is now
+different about it. The evidence — the symptom, the quoted error, the
+metaphor — belongs in the body.
+
+The body: the same budget as a commit, per language, and here a merge covers
+several commits, so it is a SUMMARY and not a list. Open by stating how many
+commits the branch carries, then say what they add up to. No bullet list, no
+per-commit rundown: `git log ..HEAD` already gives that, and a body
+repeating it teaches nothing.
+
+The bilingual rule holds — body, then the marker naming the language of what
+FOLLOWS, then the translation — as does the ban on naming an AI in
+`Co-authored-by:`.
+
+**The hook does not check this one.** `script/git/hooks/commit-msg` skips any
+message beginning with `Merge `, along with `Revert `, `fixup!` and `squash!`.
+Length, addresses and account paths pass unchallenged here, so the discipline
+is entirely yours.
+
+### 3. Hand it over
+
+Write the message to `tasks/merge_message.txt` — `tasks/` is not versioned,
+which is why the repository sends working material there — and print the two
+commands the human runs, with `--no-ff` so the branch keeps a merge commit and
+its history stays readable:
+
+```bash
+git switch
+git merge --no-ff -F tasks/merge_message.txt
+```
+
+Do not run them. Do not switch branch, do not merge, do not push: the merge is
+the human's decision and the last chance to read the message before it is
+permanent. Report, in a sentence each, the changelog section written to and
+the number of commits summarised.
diff --git a/script/todo/todo.py b/script/todo/todo.py
index 49ff113..a924289 100755
--- a/script/todo/todo.py
+++ b/script/todo/todo.py
@@ -3050,6 +3050,11 @@ class TODO(
print(f"🤖 {t('Deploy Claude Code commands!')}")
choices = [
{"prompt_description": t("Commit - OCA/Odoo commit command")},
+ {
+ "prompt_description": t(
+ "Git prepare merge - Git merge preparation command"
+ )
+ },
{
"prompt_description": t(
"Todo Add Command - Add a command to todo.py menu"
@@ -3071,11 +3076,16 @@ class TODO(
personalize=True,
)
elif status == "2":
+ self._setup_claude_command(
+ "git_prepare_merge",
+ "template_claude_commands_git_prepare_merge.md",
+ )
+ elif status == "3":
self._setup_claude_command(
"todo_add_command",
"template_claude_commands_todo_add_command.md",
)
- elif status == "3":
+ elif status == "4":
self._list_claude_commands()
else:
print(t("Command not found !"))
@@ -3203,6 +3213,9 @@ class TODO(
print(f"{t('Deployed commands'):<22} ~/.claude/commands/")
gabarits = {
"commit": "template_claude_commands_commit.md",
+ "git_prepare_merge": (
+ "template_claude_commands_git_prepare_merge.md"
+ ),
"todo_add_command": "template_claude_commands_todo_add_command.md",
}
for nom, gabarit in sorted(gabarits.items()):
diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py
index ec5e701..318e53e 100644
--- a/script/todo/todo_i18n.py
+++ b/script/todo/todo_i18n.py
@@ -1176,6 +1176,10 @@ TRANSLATIONS = {
"fr": "Commit - Commande de commit OCA/Odoo",
"en": "Commit - OCA/Odoo commit command",
},
+ "Git prepare merge - Git merge preparation command": {
+ "fr": "Git prepare merge - Commande de préparation merge git",
+ "en": "Git prepare merge - Git merge preparation command",
+ },
"Todo Add Command - Add a command to todo.py menu": {
"fr": "Todo Add Command - Ajouter une commande au menu todo.py",
"en": "Todo Add Command - Add a command to todo.py menu",