---
name: code-release-notes
description: >
  Generate user-facing release notes in up to three languages (FR, EN, IT) and three formats
  (markdown + Google Play 500-char + Apple App Store 4000-char) for the next release of a Pysae app,
  append them to the consolidated files at docs/release-notes/, then commit them to the current
  branch after manual validation. Languages, formats and an optional custom prompt are configurable
  per project via .pysae-ai-tools.yaml. Use when the user says /code-release-notes, /release-notes,
  'génère les release notes', 'écris les notes de version', 'rédige les release notes',
  'release notes pour <vX.Y.Z>', 'notes pour les stores', 'notes App Store et Play Store',
  'generate the release notes', 'write the store notes', 'draft the changelog for users',
  or when /ci-release or /code-review-pre-release needs user-facing notes before the tag.
  Distinct from /code-changelog (developer entry per change) and /code-get-next-version
  (which only computes the version number).
argument-hint: "<vX.Y.Z> [--root <path>] [--no-commit] [--no-push]"
allowed-tools:
  - Read
  - Edit
  - Write
  - Glob
  - Grep
  - Bash
  - AskUserQuestion
---

Synthesise user-facing release notes in **three languages** (FR, EN, IT) and **three formats** (canonical markdown, Google Play 500-char plain text, Apple App Store 4000-char plain text) from the commits and pending changelog entries since the latest tag, validate them with the user, then commit them to the **currently checked-out branch** — either the project's default branch (direct user invocation) or a `release/vX.Y.Z` branch (when delegated from `/ci-release` Step 6) — so `/ci-release` can create the tag.

## Required tools

- `glab`
- `jq`

Run `pysae-ai-tools tools require <tools>` before starting; if it exits non-zero, abort and report the install command from stderr to the user.

## File layout — up to nine consolidated files per repo

Every release adds a new section to each **configured** file (up to nine — the full default set is shown below) under `docs/release-notes/`, all at the same level, distinguished only by extension :

```
docs/release-notes/
├── release-notes.fr.md                    # canonical markdown
├── release-notes.fr.google-play.txt       # Google Play plain text (≤500 chars/release body)
├── release-notes.fr.apple-app-store.txt   # Apple App Store plain text (≤4000 chars/release body)
├── release-notes.en.md
├── release-notes.en.google-play.txt
├── release-notes.en.apple-app-store.txt
├── release-notes.it.md
├── release-notes.it.google-play.txt
└── release-notes.it.apple-app-store.txt
```

All configured files **accumulate** — newest release inserted at the top of each file, older sections preserved verbatim below.

| Aspect | `.md` (canonical) | `.google-play.txt` | `.apple-app-store.txt` |
| --- | --- | --- | --- |
| Audience | End users, support, ops (browsable rendered markdown) | Google Play Store « What's new » field | Apple App Store Connect « What's New » field |
| API constraint | None | **500 chars/release body** (Android Publisher API) | **4000 chars/release body** (App Store Connect API) |
| Heading style | Markdown `### Section` | Plain `Section :` label | Plain `Section :` label |
| Bullets | `* item` | `- item` | `- item` |
| Markdown allowed | Yes | **No** (stores render `#`/`*` literally) | **No** |
| Section labels | Always | **Always** (no shortcut for budget) | Always |
| Issue refs | Forbidden | Forbidden | Forbidden |
| Title line written by CLI | `## [vX.Y.Z] YYYY-MM-DD` | `> vX.Y.Z — YYYY-MM-DD` | `> vX.Y.Z — YYYY-MM-DD` |
| Per-language file header | `# Notes de version` / `# Release Notes` / `# Note di rilascio` | None | None |

## Translation contract — strict equivalence across languages and formats

**Within a format**, the three language files MUST be exact translations of one another : same items, same order, same emphasis, same `⚠️` flags on the same items.

**Across formats**, the three formats describe the same release but at different fidelities :
- `.md` is the canonical reference (every item, full prose).
- `.google-play.txt` is a tight summary of the same items, fitted to 500 chars.
- `.apple-app-store.txt` is a fuller plain-text version (4000 chars is generous — usually it can carry the entire `.md` content minus markdown syntax).

A change present in the `.md` must be present in both `.txt` formats (compressed if necessary), **except web-only items on a mixed web/native project**: those stay in the `.md` (which serves web users too) but are dropped from the two store `.txt` variants, whose audience is native-app only (see Pass 4b's native-only note). A change omitted from the `.md` (because it's purely internal, see classification below) is omitted from all three.

## Mode detection

```bash
CTX=$(pysae-ai-tools internal detect-context --local)
IS_CI=$(echo "$CTX" | jq -r '.is_ci')
```

CI mode (`IS_CI == "true"`): this skill is **not expected to run in CI** — the validation step is by design a manual Claude Code interaction (the user reads the synthesised text and approves it before it ships to main). If invoked in CI, emit `RELEASE_NOTES_RESULT` with `status: "skipped"` and exit cleanly.

## Step 1 — Parse arguments

Required: a positional version `vMAJOR.MINOR.PATCH`, optionally with a prerelease suffix `-<label>.N` (validated with `^v\d+\.\d+\.\d+(-[0-9A-Za-z][0-9A-Za-z.-]*)?$`). Optional:

- `--root <path>` — project root (default: current directory)
- `--no-commit` — synthesise + write the files only; skip git commit/push
- `--no-push` — commit but don't push

If the version is missing or malformed, stop and ask the user — never invent it.

**Betas (`vX.Y.Z-<label>.N` with an alpha/beta/rc label, e.g. `v6.0.0-beta.1`) are markdown-only.** Betas never publish to the stores, so synthesise and write **only** the `markdown` variant — skip Pass 4b/4c and the `.google-play.txt` / `.apple-app-store.txt` files entirely, regardless of `release.stores`. The store variants are produced only at the final `vX.Y.Z` release. `merge` enforces this from the tag, but scope your synthesis to markdown up front so you don't waste work. Coalescing into a single section per version line (the latest beta's section is renamed/replaced, not stacked) is handled by `merge` automatically.

**A hotfix (`vX.Y.Z-hotfix.N` or the bare `vX.Y.Z-N`, e.g. `v5.3.16-1`) is NOT a beta — it works like a brand-new final release.** It is a store-bound post-release rebuild of the same `X.Y.Z` (mirrors the Android versionCode scheme). So synthesise and write **every** configured variant, including the store `.google-play.txt` / `.apple-app-store.txt` files, exactly as for a final `vX.Y.Z`. **No coalescing applies to a hotfix**: each hotfix gets its own fresh section (it never absorbs a predecessor, and a later beta/final never drops it) — `merge` enforces this from the tag.

## Step 2 — Verify repo state

Before touching any file under `docs/release-notes/`, resolve the **target branch** (the branch the commit will land on) and validate it. The target is whatever branch is currently checked out, but it must be either the project's default branch (direct invocation) **or** a `release/vX.Y.Z` branch (when delegated from `/ci-release` Step 6) — never a feature branch :

```bash
DEFAULT_BRANCH=$(pysae-ai-tools internal detect-context --local | jq -r '.default_branch // "main"')

[ -z "$(git -C "$ROOT" status --porcelain)" ] || { echo "✗ working tree dirty"; exit 1; }
TARGET_BRANCH=$(git -C "$ROOT" branch --show-current)

case "$TARGET_BRANCH" in
  "$DEFAULT_BRANCH"|release/v[0-9]*.[0-9]*.[0-9]*|release/[0-9]*.[0-9]*.[0-9]*)
    ;;
  *)
    echo "✗ not on $DEFAULT_BRANCH or a release/vX.Y.Z branch (current: $TARGET_BRANCH)"
    exit 1
    ;;
esac

git -C "$ROOT" fetch origin "$TARGET_BRANCH" --quiet
LOCAL=$(git -C "$ROOT" rev-parse HEAD)
REMOTE=$(git -C "$ROOT" rev-parse "origin/$TARGET_BRANCH")
[ "$LOCAL" = "$REMOTE" ] || { echo "✗ local $TARGET_BRANCH is behind origin — pull first"; exit 1; }
```

If any check fails, stop and tell the user how to fix it. Do **not** auto-fix.

The rest of the skill pushes to `$TARGET_BRANCH` — Step 7 commits and Step 7's `git push` both use the resolved target rather than hardcoding the default branch.

## Step 3 — Gather raw material

```bash
MATERIAL=$(pysae-ai-tools code release-notes gather --root "$ROOT")
LATEST_TAG=$(echo "$MATERIAL" | jq -r '.latest_tag')
```

Payload :
- `latest_tag` — previous semver tag, e.g. `v1.4.2`. Empty for the first release.
- `commits[]` — every non-merge commit between `latest_tag` and `HEAD`, with `sha`, `subject`, `body`.
- `changelog_entries[]` — pending entries in `<root>/changelogs/*.md` (highest-signal source).
- `config` — the resolved project configuration (see Step 3.5).

If both lists are empty, stop with a friendly message — there is nothing to release.

## Step 3.5 — Read the project config

The release-notes config lives in the repo's `.pysae-ai-tools.yaml` (`release.notes` + `release.stores`), resolved by the commands themselves — `gather`/`merge` read it via `release_config`, and `pysae-ai-tools code release-notes config --root "$ROOT"` prints the resolved `{languages, variants, prompt}` as JSON.

- **`languages`**: `release.notes.languages` (default: all three — fr, en, it).
- **`variants`**: always `markdown`; `google-play` / `apple-app-store` are added only when the matching `release.stores` flag is on (mobile apps). A backend with no stores → markdown only.
- **`prompt`**: `release.notes.prompt` (optional extra LLM guidance).

When the file or a field is absent, the schema defaults apply (all languages, markdown only). `release.notes.enabled: false` makes `merge` a no-op (handled centrally by the command). No `init-config` step — the file is created/owned by `pysae-ai-tools project init`.

```jsonc
"config": {
  "languages": ["fr", "en", "it"],            // subset to generate (default: all three)
  "variants":  ["markdown", "google-play", "apple-app-store"], // subset of formats (default: all three)
  "prompt":    ""                              // optional extra generation guidance (default: empty)
}
```

This config **governs the rest of the skill**:
- **Generate only** the `languages` × `variants` listed. The set of files is `languages × variants` (e.g. `["fr"] × ["markdown"]` = a single `release-notes.fr.md`). Skip every language/format not selected — don't synthesise, validate, show, merge, or commit it.
- **`prompt`** — when non-empty, treat it as additional, project-specific instructions for the textual synthesis in Step 4 (tone, recurring mentions, wording rules…). It **augments** the canonical rules below; it never overrides hard constraints (section labels, char caps, no issue refs, plain-text store rules).

Throughout the rest of this skill, "the configured set" means exactly these `languages × variants` pairs. The examples below show the full set (nine files, both stores on) — scale them down to the configured set.

## Step 4 — Synthesise in three passes

**Scope to the config (Step 3.5):** only synthesise the `languages` and `variants` selected — skip Pass 4b if `google-play` isn't selected, skip a language everywhere if it isn't listed, etc. **Apply the config `prompt`** (when non-empty) as extra guidance across all passes, on top of the canonical rules below (never replacing the hard constraints).

### Pass 4a — Canonical markdown (French first, then EN + IT translations)

**Canonical sections** (same four for every release, in this exact order, h3 level, omit empty ones — never write « Aucun ») :

| # | FR | EN | IT |
| --- | --- | --- | --- |
| 1 | `### ⚠️ Important` | `### ⚠️ Important` | `### ⚠️ Importante` |
| 2 | `### Nouveautés` | `### What's new` | `### Novità` |
| 3 | `### Améliorations` | `### Improvements` | `### Miglioramenti` |
| 4 | `### Corrections` | `### Fixes` | `### Correzioni` |

Bullets use `* ` (asterisk + space), same convention as `CHANGELOG.md`. Hard-wrap every line at 120 characters max ; continuation lines use a two-space indent so they align under the bullet text.

**Typographie FR — exception aux [règles de rédaction](../references/french-writing-rules.md) :** dans les release notes françaises, utiliser un **espace normal** (`U+0020`) avant `:` `;` `!` `?`, **jamais** l'espace insécable `U+00A0`. Les release notes partent vers Google Play, l'App Store et des fichiers plain-text dont le comptage de caractères et le rendu gèrent mal le `U+00A0` ; l'espace normal est la valeur sûre. Cette exception vaut pour les trois formats FR (markdown, Google Play, App Store).

**Classification — every commit / changelog entry must land somewhere** (except the two genuinely-internal types listed at the bottom) :

| Source type | Default section | Notes |
| --- | --- | --- |
| `feat` | Nouveautés | Describe the feature / the application impersonally ("Il est maintenant possible de…", "La carte affiche désormais…") — never address the user directly ("Vous pouvez…"). |
| `fix` | Corrections | Phrase the now-correct behaviour as a **positive affirmation** — see the Corrections wording rule below. |
| `perf` | Améliorations | "Plus rapide / plus fluide / consomme moins". |
| `tech` / `chore` / `ci` / `build` / `style` / `test` / `bump` | **Améliorations** | Rephrased in user-perceivable terms — even an infra bump means improved stability or speed for the user. |
| Breaking change / action required (any type) | **⚠️ Important** (override) | State the required user action explicitly. |
| `refactor` | **omit** | Pure code restructuring with no user impact. |
| `docs` | **omit** | Internal docs — Pysae's user-facing help center is Intercom, not the repo. |

The user has explicitly chosen this — **everything that has any user-perceivable impact gets transcribed**, even tech tickets, even dependency bumps. The only omissions are `refactor` (no user impact by definition) and `docs` (handled elsewhere). When in doubt, include it under Améliorations, rephrased.

**Wording rule — every section, every language, every format :** the section title already states the *kind* of change, so **never echo it at the start of a bullet** — no « Correction de… » / "Fix for…" under Corrections, no « Ajout de… » / « Nouveau… » under Nouveautés, no « Amélioration de… » under Améliorations. It's redundant. Instead, write each item as a **positive affirmation, in the present tense, describing the new state** — what now works, what is now possible, what is now faster — never the old broken state and never the act of changing it.

**Speak of the feature / the application, never address the user directly.** Phrase each item around *what the product now does* or *what is now possible*, not « Vous pouvez désormais… » / "You can now…". Prefer impersonal turns : « Il est maintenant possible de… », « La page X affiche désormais… », « L'export PDF est disponible… ». The bad/good contrast : ❌ « Vous pouvez désormais archiver un groupe puis le réactiver à tout moment. » → ✅ « Il est maintenant possible d'archiver un groupe puis de le réactiver à tout moment (archivage réversible). » Same rule in EN ("It is now possible to…" / "The X page now shows…", not "You can now…") and IT ("Ora è possibile…", not "Ora puoi…").

| Section | ❌ Redundant / process-focused (don't) | ✅ Positive affirmation of the new state (do) |
| --- | --- | --- |
| Corrections | Correction du prochain départ qui pouvait être masqué par la barre de navigation système sur Android. | Le prochain départ ne peut plus être masqué par la barre de navigation système sur Android (fiche arrêt). |
| Corrections | Correction de l'icône de l'écran de démarrage déformée sur Android 12 et versions ultérieures. | L'icône de l'écran de démarrage n'est plus déformée sur Android 12 et versions ultérieures. |
| Corrections | Correction de l'affichage de la page profil qui débordait sur le bord droit de l'écran. | La page profil ne déborde plus sur le bord droit de l'écran. |
| Nouveautés | Ajout de l'export PDF des feuilles de route. | Les feuilles de route peuvent désormais être exportées au format PDF. |
| Nouveautés | Vous pouvez désormais archiver un groupe puis le réactiver à tout moment. | Il est maintenant possible d'archiver un groupe puis de le réactiver à tout moment (archivage réversible). |
| Améliorations | Amélioration du temps de chargement de la carte. | La carte se charge plus rapidement. |

For a fix, the « ne … plus » / "no longer" turn of phrase is the natural default; for a new feature, « Il est maintenant possible de… » / « La fonctionnalité X est disponible… » / "It is now possible to…" — impersonal, centred on the product, never « Vous pouvez… » / "You can…". Keep the same fidelity rules — name the part of the product that changed, never extrapolate.

**Fidelity rule — never extrapolate** :

Stay strictly faithful to what the commits and changelog entries actually describe. Do **not** invent capabilities, infer benefits the change does not deliver, or generalise a narrow fix into « a full overhaul ».

If a commit subject or changelog entry is too terse, **read the actual code change** (`git show <sha> -- <files>`) before writing the bullet. **When in doubt, stay a bit technical rather than invent a feature.** It is always preferable to leak a little jargon (« *Le calcul d'horaire prévisionnel est désormais correct sur les courses récurrentes* ») than to dress up a confident user-facing claim that turns out to be wrong (« *Nouveau module de planification* » when there is no such module).

**Précis ET compréhensible — audience = un simple utilisateur** (chauffeur, régulateur, référent réseau, intégrateur). Nommer la partie *du produit* qui change (page, menu, bouton, type d'objet) en français courant ; éviter les noms de classes, tables, jobs, namespaces, buckets, etc.

**Application names — always the official commercial name.** When a bullet references another Pysae application, use its **official commercial name** from that repo's config — resolve it live with `pysae-ai-tools project show --project pysae/<repo> --json` (field `.project.name`), never the repo slug, K8s deployment name, or an internal alias. The shape of `project.name` decides localisation across the FR/EN/IT bodies :
- **plain string** (descriptive names, e.g. `GTFS Validator`, `GTFSRT -> Siri Converter`) — translate like any other phrase per language (e.g. FR « Validateur GTFS », IT « Validatore GTFS »).
- **`{lang: value}` mapping** (brand names, e.g. `Driver`, `Info`, `Editor`, `Display`, `Shift`, `App`, `Services`) — **invariant across all three languages**, never translate, localise, or paraphrase (no « le conducteur » for `Driver`); the EN and IT bodies carry the exact same string as the FR one.

**Translation rules** (EN and IT translate the French body line-by-line — do not re-synthesise from raw material) :
- Same canonical section titles (translated using the table above).
- Same number of bullets per section, in the same order.
- Same `⚠️` flag placement.
- Keep Pysae-specific names untranslated when no natural equivalent exists.

### Pass 4b — Google Play plain-text (≤500 chars/release body)

For each language, produce a **plain-text** version capped at **500 characters** (validated by `pysae-ai-tools code release-notes validate <lang> <body> --kind=google-play`). This is the body that lands in the Google Play « What's new » field via the Android Publisher API.

> **Native-only — drop web-only items (mixed web/native projects).** The store variants reach **only** users of the **native app** (Google Play → Android, Apple App Store → iOS). For a project that ships both a web version and native apps, **remove from the store variants (Pass 4b *and* Pass 4c) every item that concerns only the web version** — a change to a web-only screen, a browser-specific fix, a desktop-only flow, etc. Keep all cross-platform and native items. The canonical `.md` is **not** filtered (it also serves web users, support and ops) — only the two `.txt` store bodies are. Judge from the change's scope (the part of the product it touches); **when unsure whether an item reaches the native app, keep it**. If, after filtering, a section becomes empty, omit that section's label; if the whole store body would be empty, fall back to the localised maintenance note (Step 6).

**Canonical labels** (mandatory — labels are *not* sacrificed for budget) :

| FR | EN | IT |
| --- | --- | --- |
| `⚠️ Important :` (normal space `U+0020` before `:`, **not** the non-breaking space) | `⚠️ Important:` | `⚠️ Importante:` |
| `Nouveautés :` | `What's new:` | `Novità:` |
| `Améliorations :` | `Improvements:` | `Miglioramenti:` |
| `Corrections :` | `Fixes:` | `Correzioni:` |

**Format** :
```
Nouveautés :
- Bullet 1
- Bullet 2

Corrections :
- Bullet 1
```

**Process** :
1. **Start from the markdown body** and convert to plain text without rewriting (strip `### `, convert `* ` → `- `, drop `[text](url)` syntax, keep blank lines, replace FR section headings with their `Section :` plain-text equivalents). **Drop web-only items first** (see the native-only note above) before measuring length.
2. **Measure.** If body length is ≤500 chars → keep as-is.
3. **If over budget** → summarise to fit, aiming as close to 500 as possible without going over. Compress sentences, drop the least-important bullets first, but **never drop a canonical section label** and **never drop a `⚠️ Important` item**.
4. Re-measure until ≤500. The CLI rejects 501-char bodies outright.

### Pass 4c — Apple App Store plain-text (≤4000 chars/release body)

Same plain-text rules as Google Play, but the budget is 4000 characters. In practice, this almost always allows the **full markdown content** converted to plain text — no summarisation needed. Use Pass 4b's conversion algorithm without the compression step.

Validate with `pysae-ai-tools code release-notes validate <lang> <body> --kind=apple-app-store`.

## Step 5 — Validate with the user (local only)

Skip if `IS_CI == "true"`.

Show **all configured bodies** (the `languages × variants` from Step 3.5 — up to 3 languages × 3 formats) grouped by language so the user can spot inconsistencies between the markdown and the plain-text variants of the same release. Use `AskUserQuestion` :

> Release notes pour `<vX.Y.Z>` (`<latest_tag>` → `<vX.Y.Z>`) :
>
> **Français :**
> ```
> [markdown]      <full FR markdown body, no header, no `## [vX.Y.Z]` line>
> [Google Play]   <full FR plain-text body — N chars>
> [App Store]     <full FR plain-text body — N chars>
> ```
>
> *(same for English then Italian)*
>
> - **Valider et committer** — écrit les neuf fichiers et pousse sur `<TARGET_BRANCH>` (la branche courante).
> - **Modifier** — l'utilisateur fournit les corrections en réponse.
> - **Annuler** — sort sans rien écrire.

Self-check before showing the prompt :
- Three markdown bodies have the same set of headings (translated) and the same number of bullets per section.
- Three Google Play bodies fit ≤500 chars **counted exactly** and use the canonical FR/EN/IT labels.
- Three Apple App Store bodies fit ≤4000 chars and use the canonical labels.
- Same `⚠️ Important` content appears across formats and languages — **except** web-only items, which the native-only filter drops from the store variants (see Pass 4b). The store variants may therefore carry fewer items than the `.md`, but the **same** web-only items must be dropped consistently across FR/EN/IT.

If any contract isn't met, fix it before asking the user — don't push the burden of structural review onto them. If **Annuler**, emit `RELEASE_NOTES_RESULT` with `status: "cancelled"` and stop.

## Step 6 — Merge into the consolidated files

> **No user-facing change?** If a release ships only technical work (or everything
> was excluded from a partial release) and there is genuinely nothing user-facing
> to write, leave the bodies **empty** — do not invent content. The merge command
> detects an empty body and writes a « Version de maintenance — aucun changement
> visible pour les utilisateurs » placeholder (localised per language) so the
> `## [vX.Y.Z]` section still exists in every file and the release verification
> passes. Empty bodies skip the canonical-template validation.

Write each validated body to its own temp file, then invoke the merge command. **Pass only the files for the configured set** (Step 3.5) — `merge` reads the same `.pysae-ai-tools.yaml` and writes exactly `languages × variants`; any flag for a non-selected pair is ignored, and a flag missing for a *selected* pair is an error. Passing all nine is fine when the config selects all nine (both stores on).

```bash
declare -A T
for v in fr en it; do
  T[$v.md]=$(mktemp -t rn-$v-XXXXXX.md)
  T[$v.gp]=$(mktemp -t rn-$v-gp-XXXXXX.txt)
  T[$v.as]=$(mktemp -t rn-$v-as-XXXXXX.txt)
done
trap 'rm -f "${T[@]}"' EXIT

# … cat each validated body into the right temp file …

# Default config (all 9). Drop the flags for any language/variant not selected.
pysae-ai-tools code release-notes merge "$NEW_VERSION" \
  --fr-file              "${T[fr.md]}" \
  --en-file              "${T[en.md]}" \
  --it-file              "${T[it.md]}" \
  --fr-google-play-file  "${T[fr.gp]}" \
  --en-google-play-file  "${T[en.gp]}" \
  --it-google-play-file  "${T[it.gp]}" \
  --fr-apple-app-store-file "${T[fr.as]}" \
  --en-apple-app-store-file "${T[en.as]}" \
  --it-apple-app-store-file "${T[it.as]}" \
  --root "$ROOT"
```

What the CLI does, all-or-nothing (over the configured set) :

1. Validates the **configured** bodies (canonical template for `.md`, ≤500-char plain text for `.google-play.txt`, ≤4000-char plain text for `.apple-app-store.txt`) — aborts before any write if any violation.
2. **Markdown** : inserts the new section at the top of each `docs/release-notes/release-notes.<lang>.md` (per-language file header on first creation).
3. **Google Play** : inserts a new `> <NEW_VERSION> — YYYY-MM-DD` section at the top of each `docs/release-notes/release-notes.<lang>.google-play.txt`.
4. **Apple App Store** : inserts the same shape into each `docs/release-notes/release-notes.<lang>.apple-app-store.txt`.

The `> ` prefix on the title line gives a single-character grep anchor (`grep '^> v' release-notes.fr.google-play.txt`) and stands out clearly in a terminal viewer.

If the user passed `--no-commit`, stop here, emit the result block with `status: "written"`, and tell the user to commit by hand.

## Step 7 — Commit and push directly to the target branch

Stage **only the files `merge` actually wrote** (the configured set — `merge` prints them; the default is the nine below). The simplest robust approach is to stage the whole directory, which picks up exactly what changed:

```bash
git -C "$ROOT" add docs/release-notes/
git -C "$ROOT" commit -m "docs(release-notes): add notes for $NEW_VERSION"
```

If `--no-push`, stop here. Otherwise :

```bash
git -C "$ROOT" push origin "$TARGET_BRANCH"
```

A direct push to `$TARGET_BRANCH` is deliberate — `docs/release-notes/` is a write-only artefact for users and stores, outside the code-review surface. The manual validation gate is the AskUserQuestion at Step 5. If the push is rejected, do **not** rebase silently — surface to the user.

When `$TARGET_BRANCH` is a `release/vX.Y.Z` branch, the commit lands on that release branch (not on the default branch). `/ci-release` Step 10 will back-merge the release branch into the default branch once the production deploy is validated, which is when the notes finally reach `main`.

## Step 8 — Emit the result block

```
RELEASE_NOTES_RESULT:
{
  "status": "committed" | "pushed" | "written" | "cancelled" | "skipped" | "failed",
  "version": "<vX.Y.Z>",
  "latest_tag": "<vX.Y.Z previous>",
  "files": [
    // the files actually written (the configured set); full default shown here
    "docs/release-notes/release-notes.fr.md",
    "docs/release-notes/release-notes.en.md",
    "docs/release-notes/release-notes.it.md",
    "docs/release-notes/release-notes.fr.google-play.txt",
    "docs/release-notes/release-notes.en.google-play.txt",
    "docs/release-notes/release-notes.it.google-play.txt",
    "docs/release-notes/release-notes.fr.apple-app-store.txt",
    "docs/release-notes/release-notes.en.apple-app-store.txt",
    "docs/release-notes/release-notes.it.apple-app-store.txt"
  ],
  "commit_sha": "<sha or null>"
}
```

Status semantics :
- `pushed` — happy path, all configured files on `$TARGET_BRANCH` on origin (default branch on direct invocation, `release/vX.Y.Z` when delegated from `/ci-release`). `commit_sha` set.
- `committed` — committed locally but not pushed (`--no-push`). `commit_sha` set.
- `written` — files written but not committed (`--no-commit`). `commit_sha` null.
- `cancelled` — user picked Annuler at Step 5. Files untouched.
- `skipped` — CI mode or nothing to release.
- `failed` — anything unrecoverable (dirty tree, off-branch, behind origin, push rejected, validation failure).

The closing `}` must be on its own unindented line so callers can extract with `sed -n '/^RELEASE_NOTES_RESULT:/,/^}/p'`.

## Rules

- **Config-driven scope.** `.pysae-ai-tools.yaml` drives scope: `release.notes.languages` (default: all three), variants derived from `release.stores` (always markdown; google-play/apple-app-store per store flag), and optional `release.notes.prompt`. Only ever generate/validate/merge/commit the configured set. All files accumulate newest-first under `docs/release-notes/`.
- **The config `prompt` augments, never overrides.** Apply it as extra synthesis guidance; it cannot relax hard constraints (canonical labels, char caps, plain-text store rules, no issue refs).
- **Mandatory canonical section labels** in every body of every format. Labels are not negotiable, even when fighting the 500-char budget.
- **Mandatory line wrap at 120 chars** in the markdown body.
- **Stores accept plain text only** — no markdown headings, no `*` bullets, no `[text](url)` links in `.txt` files.
- **Read-only on raw material** — `gather` reads commits and changelog files; never mutates them.
- **Write-only on `docs/release-notes/`** — never touch `CHANGELOG.md`, `changelogs/`, or any other file.
- **Direct push to the target branch is allowed** (and expected) for this specific commit. The target is either the project's default branch (direct invocation) or a `release/vX.Y.Z` branch (delegated from `/ci-release` Step 6). Do not open a MR.
- **Manual validation is non-negotiable** — never skip Step 5 outside of `--no-commit` previews.
- **Application names = official commercial name.** Reference other Pysae apps by their `project.name` from `pysae-ai-tools project show --project pysae/<repo> --json`, never the repo slug or K8s name. A `{lang: value}` mapping is a brand name (e.g. `Driver`, `Shift`), identical in FR/EN/IT; a plain string (e.g. `GTFS Validator`) is translated per language.
- **No issue refs, no MR links, no SHAs** in any final body.

$ARGUMENTS
