Spec : système memory_sessions
Fichier auto-porté — contient tout le nécessaire pour installer ce système dans n'importe quel projet Claude Code.
Modèle d'installation
Modèle recommandé pour installer cette spec : Sonnet
L'installation nécessite d'adapter la liste des projets, de créer les répertoires et de modifier settings.json — un peu de jugement contextuel est utile.
Présentation
Nom : memory_sessions
Type : Stop hook + 2 skills
Rôle : Mémoire automatique de session — après chaque tour Claude, Haiku analyse l'échange et met à jour des logs quotidiens et une mémoire long terme par projet.
Modèle utilisé : claude-haiku-4-5-20251001 (rapide, économique, suffisant pour extraction/résumé)
Pourquoi ce système ?
Un projet Claude Code actif accumule du contexte : décisions techniques, comportements à éviter, état d'avancement de tâches longues. Sans mémoire persistante, chaque nouvelle session repart de zéro.
Les solutions existantes (CLAUDE.md, mémoire auto de Claude Code) ont des limites :
- CLAUDE.md : règles statiques, pas de trace d'activité quotidienne
- Mémoire auto Claude Code (~/.claude/...) : globale, non versionnée, non portable entre machines
Ce système apporte une mémoire structurée par projet, versionnée dans le dépôt, consultable via /memory_load, consolidable via /memory_promotion.
Inspiration : OpenClaw
Inspiré de l'architecture mémoire d'OpenClaw (agent Claude open-source) :
- Trois niveaux : mémoire immédiate → logs quotidiens → mémoire long terme
- Déclenchement automatique en fin de session via hook
- Fichiers plats Markdown, lisibles et éditables directement
Différences délibérées par rapport à OpenClaw :
- Les logs quotidiens sont modifiables (OpenClaw les rend append-only) — permet à Haiku de réorganiser l'entrée du jour
- Structure par projet (pas une mémoire globale) — adapté aux workspaces multi-projets
- Le hook appelle claude depuis /tmp pour éviter que le CLAUDE.md du projet hôte interfère avec la réponse Haiku
Architecture
Structure de fichiers
{project_root}/
memory_sessions/ ← mémoire du projet père
.gitkeep
long_memory.md ← mémoire long terme (condensé permanent)
logs/
2026-03-21.md ← log quotidien structuré par sections
2026-03-20.md
.debug/ ← JSON brut Haiku (gitignored)
2026-03-21_190023.json
archives/ ← logs > 30 jours (créé par /memory_promotion)
projects/{sous-projet}/
memory_sessions/ ← même structure, par sous-projet
.gitkeep
long_memory.md
logs/
.debug/
# Log session — 2026-03-21
## Contexte
- Sujet ou système sur lequel porte la session (1-2 phrases, stable dans la journée)
## Objectif
- Intention de l'échange
## Problèmes rencontrés
- Bugs, blocages avec cause/solution
## Connaissances acquises
- Faits techniques/métier durables
## Travail produit
- Ce qui a été livré, créé, modifié
## Décisions prises
- Choix techniques ou fonctionnels actés
## À suivre
- Points ouverts, prochaines étapes
## Finalisé
- ✅ Points résolus (déplacés depuis "À suivre" via OK-done)
{
"updates": [
{
"project": "cmms_doc",
"sections": {
"contexte": ["Refonte du hook memory-session.sh pour passer au format JSON structuré."],
"objectif": ["Vérifier la découverte des skills."],
"problemes": ["Recherche des skills échouée : aucun skill trouvé."],
"connaissances": [],
"travail": ["Rédaction spec mémoire de session."],
"decisions": [],
"a_suivre": ["Revalider découverte skills."],
"a_suivre_done": []
},
"long_memory_update": null
}
]
}
| Clé JSON |
Titre Markdown |
Contenu |
Comportement fusion |
contexte |
## Contexte |
Sujet/système de la session (1-2 phrases) |
Append (dédupliqué) — plusieurs contextes possibles par jour |
objectif |
## Objectif |
Ce sur quoi porte la session |
Append (dédupliqué) |
problemes |
## Problèmes rencontrés |
Bugs, blocages, erreurs — avec cause/solution si connue |
Append (dédupliqué) |
connaissances |
## Connaissances acquises |
Faits techniques ou métier appris/confirmés |
Append (dédupliqué) |
travail |
## Travail produit |
Ce qui a été livré, créé, modifié |
Append (dédupliqué) |
decisions |
## Décisions prises |
Choix techniques ou fonctionnels actés |
Append (dédupliqué) |
a_suivre |
## À suivre |
Points ouverts, prochaines étapes |
Append (dédupliqué) |
a_suivre_done |
(pas de section propre) |
Points de "À suivre" à marquer comme résolus → déplacés vers "Finalisé" |
Retrait de "À suivre" + ✅ dans "Finalisé" |
Gestion "OK-done"
Quand l'utilisateur envoie exactement OK-done (insensible à la casse) :
- Haiku reçoit une instruction spécifique pour marquer les points de "À suivre" comme finalisés
- Les items correspondants sont retirés de "## À suivre" et déplacés dans "## Finalisé" avec un préfixe ✅
- Toutes les autres sections sont vides ([])
Debug JSON
Le JSON brut retourné par Haiku est copié dans memory_sessions/logs/.debug/YYYY-MM-DD_HHMMSS.json (un fichier par appel, horodaté). Ce répertoire est dans .gitignore (**/.debug/).
Règles de taille
| Fichier |
Règle |
logs/YYYY-MM-DD.md |
Structuré par sections — append de nouveaux bullets sans duplication |
long_memory.md |
Maximum 300 lignes — résumé automatique si dépassement (via /memory_promotion) |
| Archive |
Logs > 30 jours déplacés vers logs/archives/ |
.debug/ |
JSON brut Haiku, gitignored, utile pour diagnostic |
Composants
| Composant |
Fichier |
Déclenchement |
| Stop hook |
.claude/hooks/memory-session.sh |
Automatique — fin de chaque tour Claude |
| Skill load |
.claude/skills/memory_load/SKILL.md |
Manuel — /memory_load [projet] |
| Skill promotion |
.claude/skills/memory_promotion/SKILL.md |
Manuel — /memory_promotion [projet] |
Flux du hook
Claude répond
↓
Stop hook déclenché (settings.json)
↓
memory-session.sh reçoit JSON stdin
↓
stop_hook_active = true ? → exit 0 (anti-boucle)
↓
Extraire transcript_path du JSON stdin
↓
Étape 1 — Parser le JSONL : trouver le dernier échange
(ignorer les messages tool_result/thinking/tool_use — texte vide)
↓
Étape 2 — Lire les logs existants du jour pour chaque projet
+ Détecter si l'utilisateur a envoyé "OK-done"
+ Construire prompt structuré pour Haiku
↓
Étape 3 — Appeler Haiku depuis /tmp
(depuis /tmp : évite que CLAUDE.md du projet interfère)
↓
Haiku retourne JSON structuré par sections :
{ project, sections: { objectif, problemes, connaissances,
travail, decisions, a_suivre, a_suivre_done }, long_memory_update }
↓
Étape 4 — Python fusionne les nouvelles entrées dans le log existant
+ Parse les sections Markdown existantes
+ Append les nouveaux bullets sous chaque section
+ Gère a_suivre_done → déplace vers "Finalisé" avec ✅
+ Copie le JSON brut dans .debug/ pour diagnostic
+ Écrit long_memory.md si nécessaire
(avec flock si disponible, dégradation gracieuse sinon)
↓
exit 0
Le transcript est un fichier JSONL. Les messages utiles ont type: "user" ou type: "assistant" avec message.content :
{
"type": "user",
"message": {
"role": "user",
"content": [
{ "type": "text", "text": "le texte du message" }
]
}
}
Important : beaucoup de messages intermédiaires ont type: "tool_result", "thinking", "tool_use" avec text vide — il faut filtrer sur content[].type == "text" et ignorer les messages sans texte.
Anti-boucle
Quand le Stop hook appelle claude, cette invocation se termine → déclenche à nouveau le Stop hook. Claude Code injecte stop_hook_active: true dans le JSON stdin de la seconde invocation. Le hook vérifie ce flag en premier et fait exit 0 immédiatement.
Choix d'implémentation
| Choix |
Décision |
Raison |
| Modèle |
Haiku 4.5 |
Rapide (~1-2s), coût minimal, suffisant pour extraction/classification |
Appel Claude depuis /tmp |
Oui |
Le CLAUDE.md du projet hôte interdit à Haiku de répondre en JSON pur |
| Format intermédiaire |
JSON structuré par sections → Python formate le Markdown |
Séparation claire extraction/formatage, debug facile |
| Déduplication |
Haiku relit le log existant + append sous la même section |
Pas de réécriture/fusion — robuste et simple |
flock |
Optionnel (dégradation gracieuse) |
Non installé par défaut sur macOS — brew install util-linux |
| Fichiers temporaires |
mktemp -d avec trap EXIT |
Évite l'injection de variables shell dans les heredocs Python |
| Debug JSON |
Copie dans .debug/ (gitignored) |
Diagnostic facile sans polluer le dépôt |
| Mémoire par projet |
Oui |
Un workspace peut contenir plusieurs sous-projets indépendants |
long_memory.md (pas memory.md) |
Oui |
Évite la collision avec les memory.md existants dans les sous-projets |
memory_sessions/ (pas memory/) |
Oui |
Évite la collision avec les répertoires memory/ existants |
| Versionnement |
Oui (dans git) |
Portabilité entre machines, historique des mémoires |
Prérequis
| Prérequis |
Détail |
Installation |
| Claude Code CLI |
claude disponible dans PATH |
claude.ai/code |
| Python 3 |
Stdlib uniquement (json, pathlib, re) — aucun package tiers |
pré-installé macOS / brew install python |
flock |
Optionnel (dégradation gracieuse si absent) |
brew install util-linux (macOS) / pré-installé Linux |
| Homebrew |
Gestionnaire de paquets macOS — nécessaire pour util-linux |
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" |
| Dépôt git |
Pour la portabilité des fichiers memory_sessions/ |
git init ou clone |
Installation complète sur macOS
# 1. Homebrew (si absent)
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# 2. util-linux (pour flock)
brew install util-linux
# 3. Vérifier que claude est dans PATH
claude --version
Installation dans un nouveau projet
Étape 1 — Copier le hook
Créer .claude/hooks/memory-session.sh avec le contenu ci-dessous (voir section Code complet).
Rendre exécutable :
chmod +x .claude/hooks/memory-session.sh
Étape 2 — Adapter la liste des projets
Dans le hook, modifier la variable PROJECT_PATHS :
PROJECT_PATHS = {
"root": "memory_sessions", # projet père — toujours présent
"mon_projet": "projects/mon_projet/memory_sessions",
# ajouter autant de sous-projets que nécessaire
}
Et dans le prompt Haiku, mettre à jour la liste :
## Projets disponibles
root, mon_projet, autre_projet
Étape 3 — Enregistrer le hook dans settings.json
Ajouter dans .claude/settings.json :
{
"hooks": {
"Stop": [
{
"matcher": "",
"hooks": [
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/memory-session.sh" }
]
}
]
}
}
mkdir -p memory_sessions/logs
touch memory_sessions/.gitkeep
# Répéter pour chaque sous-projet
mkdir -p projects/mon_projet/memory_sessions/logs
touch projects/mon_projet/memory_sessions/.gitkeep
Ajouter dans .gitignore :
# Debug JSON des appels Haiku (memory-session hook)
**/.debug/
Étape 5 — Copier les skills (optionnel mais recommandé)
Créer .claude/skills/memory_load/SKILL.md et .claude/skills/memory_promotion/SKILL.md (voir section Code complet).
Skills
/memory_load [projet]
Charge la mémoire en début de session. Lit long_memory.md + les logs du jour et de la veille. Affiche un résumé consolidé.
/memory_load cmms_doc
/memory_load all
Consolide les logs récents vers long_memory.md. Archive les logs > 30 jours. À lancer manuellement (hebdomadaire ou mensuel).
Code complet
.claude/hooks/memory-session.sh
#!/usr/bin/env bash
# memory-session.sh — Stop hook : met à jour la mémoire de session après chaque tour Claude
# Inspiré d'OpenClaw : mémoire courte (log quotidien) + longue (long_memory.md) par projet
set -euo pipefail
# Anti-boucle : si Claude a déjà traité ce hook, sortir immédiatement
STDIN_DATA=$(cat)
STOP_HOOK_ACTIVE=$(echo "$STDIN_DATA" | python3 -c "
import sys, json
try:
d = json.load(sys.stdin)
print('true' if d.get('stop_hook_active') else 'false')
except:
print('false')
" 2>/dev/null || echo "false")
[ "$STOP_HOOK_ACTIVE" = "true" ] && exit 0
# Extraire transcript_path depuis stdin
TRANSCRIPT_PATH=$(echo "$STDIN_DATA" | python3 -c "
import sys, json
try:
d = json.load(sys.stdin)
print(d.get('transcript_path', ''))
except:
print('')
" 2>/dev/null || echo "")
[ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ] && exit 0
PROJECT_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
TODAY=$(date +%Y-%m-%d)
NOW=$(date +%Y-%m-%d_%H%M%S)
# Répertoire temporaire nettoyé automatiquement à la sortie
TMP_HOOK=$(mktemp -d)
trap 'rm -rf "$TMP_HOOK"' EXIT
# --- Étape 1 : extraire le dernier échange humain+assistant ---
EXCHANGE_FILE="$TMP_HOOK/exchange.json"
python3 - "$TRANSCRIPT_PATH" "$EXCHANGE_FILE" <<'PYEOF'
import sys, json
from pathlib import Path
transcript_path = sys.argv[1]
output_file = sys.argv[2]
messages = []
with open(transcript_path) as f:
for line in f:
line = line.strip()
if not line:
continue
try:
d = json.loads(line)
if d.get("type") in ("user", "assistant"):
msg = d.get("message", {})
content = msg.get("content", "")
if isinstance(content, list):
content = " ".join(
c.get("text", "") for c in content
if isinstance(c, dict) and c.get("type") == "text"
)
messages.append({"role": d["type"], "content": content})
except:
pass
def extract_text(content):
"""Extrait uniquement le texte réel (ignore tool_result, thinking, tool_use)."""
if isinstance(content, str):
return content.strip()
if isinstance(content, list):
return " ".join(
c.get("text", "") for c in content
if isinstance(c, dict) and c.get("type") == "text" and c.get("text", "").strip()
).strip()
return ""
human_msg = ""
assistant_msg = ""
for m in reversed(messages):
role = m.get("role", "")
text = extract_text(m.get("content", ""))
if not text:
continue
if not assistant_msg and role == "assistant":
assistant_msg = text[:2000]
elif assistant_msg and not human_msg and role == "user":
human_msg = text[:1000]
break
Path(output_file).write_text(json.dumps({"human": human_msg, "assistant": assistant_msg}))
PYEOF
# Vérifier que l'échange est non vide
HUMAN_LEN=$(python3 -c "
import json, sys
with open('$EXCHANGE_FILE') as f:
d = json.load(f)
print(len(d.get('human', '')))
" 2>/dev/null || echo "0")
[ "$HUMAN_LEN" = "0" ] && exit 0
# --- Étape 2 : construire le prompt structuré pour Haiku (avec log existant) ---
PROMPT_FILE="$TMP_HOOK/prompt.txt"
python3 - "$EXCHANGE_FILE" "$PROMPT_FILE" "$PROJECT_ROOT" "$TODAY" <<'PYEOF'
import sys, json
from pathlib import Path
exchange_file = sys.argv[1]
prompt_file = sys.argv[2]
project_root = sys.argv[3]
today = sys.argv[4]
with open(exchange_file) as f:
d = json.load(f)
human = d["human"][:800]
assistant = d["assistant"][:1200]
PROJECT_PATHS = {
"root": "memory_sessions",
"mon_projet": "projects/mon_projet/memory_sessions",
# ← ADAPTER ICI
}
# Lire les logs existants du jour pour chaque projet
existing_logs = {}
for proj, path in PROJECT_PATHS.items():
log_file = Path(project_root) / path / "logs" / f"{today}.md"
if log_file.exists():
content = log_file.read_text().strip()
if content:
existing_logs[proj] = content
if existing_logs:
existing_section = "\n\n".join(
f"### {proj}\n{content}" for proj, content in existing_logs.items()
)
else:
existing_section = "(aucun log existant)"
# Détection OK-done
is_ok_done = human.strip().lower().replace("-", " ").replace("_", " ").strip() in ("ok done", "okdone")
if is_ok_done:
special = """
ATTENTION : L'utilisateur a envoyé "OK-done" = le point en cours est finalisé.
Retourne UNIQUEMENT des items dans "a_suivre_done" pour marquer les points résolus.
Toutes les autres sections doivent être des tableaux vides [].
"""
else:
special = ""
prompt = """Tu es un assistant de mémoire de session Claude. Analyse cet échange et classe les informations.
## Échange
Humain: {human}
Claude: {assistant}
{special}
## Projets disponibles
root, mon_projet ← ADAPTER ICI
## Log existant du jour
{existing}
## Sections à remplir
Pour chaque projet concerné, retourne un JSON avec les sections pertinentes.
Chaque section est un tableau de strings (1 string = 1 bullet point).
Un tableau vide [] = rien à ajouter dans cette section.
Règles :
- Ne répète JAMAIS un point déjà présent dans le log existant.
- Sois concret et actionnable : "Test échoué : aucun skill trouvé, cause = extraction texte vide" plutôt que "Test échoué".
- "contexte" : 1-2 phrases décrivant LE SUJET ou LE SYSTÈME sur lequel porte la session (ex: "Refonte du hook memory-session.sh pour passer au format JSON structuré."). Plusieurs contextes possibles par jour — ne pas répéter un contexte déjà présent dans le log existant.
- "objectif" : l'intention de l'échange (un jour peut avoir plusieurs objectifs).
- "a_suivre" : seulement si un point reste explicitement ouvert.
- "a_suivre_done" : si l'échange résout un point listé dans "À suivre" du log existant. Reprends le texte EXACT.
- "connaissances" : uniquement les faits techniques/métier durables.
- "decisions" : choix techniques ou fonctionnels actés.
## Format JSON strict (aucun texte autour)
{{"updates":[{{"project":"...","sections":{{"contexte":[],"objectif":[],"problemes":[],"connaissances":[],"travail":[],"decisions":[],"a_suivre":[],"a_suivre_done":[]}},"long_memory_update":null}}]}}
""".format(human=human, assistant=assistant, existing=existing_section, special=special)
Path(prompt_file).write_text(prompt)
PYEOF
# --- Étape 3 : appel Haiku ---
OUTPUT_FILE="$TMP_HOOK/haiku_output.txt"
# Appel depuis /tmp pour éviter que le CLAUDE.md du projet interfère
(cd /tmp && claude --model claude-haiku-4-5-20251001 --print) \
< "$PROMPT_FILE" > "$OUTPUT_FILE" 2>/dev/null \
|| echo '{"updates":[]}' > "$OUTPUT_FILE"
# --- Étape 4 : écrire les fichiers mémoire (structurés par sections) ---
write_memory() {
python3 - "$PROJECT_ROOT" "$TODAY" "$OUTPUT_FILE" "$NOW" <<'PYEOF'
import sys, json, re, collections, shutil
from pathlib import Path
project_root = sys.argv[1]
today = sys.argv[2]
output_file = sys.argv[3]
now_ts = sys.argv[4]
PROJECT_PATHS = {
"root": "memory_sessions",
"mon_projet": "projects/mon_projet/memory_sessions",
# ← ADAPTER ICI
}
SECTION_ORDER = [
("contexte", "## Contexte"),
("objectif", "## Objectif"),
("problemes", "## Problèmes rencontrés"),
("connaissances", "## Connaissances acquises"),
("travail", "## Travail produit"),
("decisions", "## Décisions prises"),
("a_suivre", "## À suivre"),
("finalise", "## Finalisé"),
]
def parse_sections(md_text):
"""Parse un Markdown en sections ordonnées : titre -> [lignes bullet]."""
sections = collections.OrderedDict()
current_title = None
for line in md_text.split("\n"):
if line.startswith("## "):
current_title = line.strip()
if current_title not in sections:
sections[current_title] = []
elif current_title is not None:
stripped = line.strip()
if stripped.startswith("- "):
sections[current_title].append(stripped)
return sections
def merge_log(existing_md, update, today_str):
"""Fusionne les nouvelles entrées dans le log existant."""
sections = parse_sections(existing_md) if existing_md else collections.OrderedDict()
new_sections = update.get("sections", {})
# Ajouter les nouvelles entrées par section
for key, title in SECTION_ORDER:
if key == "finalise":
continue
items = new_sections.get(key, [])
if items:
if title not in sections:
sections[title] = []
for item in items:
bullet = f"- {item}"
if bullet not in sections[title]:
sections[title].append(bullet)
# Gérer a_suivre_done : retirer de "À suivre", ajouter à "Finalisé"
done_items = new_sections.get("a_suivre_done", [])
if done_items and "## À suivre" in sections:
remaining = []
finalized = []
for line in sections["## À suivre"]:
matched = False
for done in done_items:
if done.lower() in line.lower():
finalized.append(line.replace("- ", "- ✅ ", 1))
matched = True
break
if not matched:
remaining.append(line)
sections["## À suivre"] = remaining
if finalized:
sections.setdefault("## Finalisé", []).extend(finalized)
# Réécrire le Markdown
lines = [f"# Log session — {today_str}", ""]
known_titles = {t for _, t in SECTION_ORDER}
for _, title in SECTION_ORDER:
if title in sections and sections[title]:
lines.append(title)
for item in sections[title]:
lines.append(item)
lines.append("")
# Sections inconnues (au cas où)
for title, items in sections.items():
if title not in known_titles and items:
lines.append(title)
for item in items:
lines.append(item)
lines.append("")
return "\n".join(lines).rstrip() + "\n"
# --- Main ---
raw = Path(output_file).read_text()
try:
match = re.search(r'\{.*\}', raw, re.DOTALL)
data = json.loads(match.group()) if match else {"updates": []}
except Exception:
data = {"updates": []}
for update in data.get("updates", []):
project = update.get("project", "")
if project not in PROJECT_PATHS:
continue
mem_dir = Path(project_root) / PROJECT_PATHS[project]
logs_dir = mem_dir / "logs"
logs_dir.mkdir(parents=True, exist_ok=True)
# Lire le log existant du jour
log_file = logs_dir / f"{today}.md"
existing_md = log_file.read_text() if log_file.exists() else ""
# Fusionner et réécrire si des sections sont non vides
has_sections = update.get("sections") and any(
update["sections"].get(k, []) for k in
["contexte", "objectif", "problemes", "connaissances", "travail", "decisions", "a_suivre", "a_suivre_done"]
)
if has_sections:
merged = merge_log(existing_md, update, today)
log_file.write_text(merged)
# Mémoire long terme (inchangé)
long_memory = (update.get("long_memory_update") or "").strip()
if long_memory:
long_file = mem_dir / "long_memory.md"
if not long_file.exists():
long_file.write_text(f"# Mémoire long terme — {project}\n\n")
with open(long_file, "a") as f:
f.write(f"\n## {today}\n\n{long_memory}\n")
# Debug : copier le JSON brut de Haiku
debug_dir = logs_dir / ".debug"
debug_dir.mkdir(exist_ok=True)
shutil.copy2(output_file, debug_dir / f"{now_ts}.json")
PYEOF
}
# Verrou flock si disponible, sinon continuer sans (dégradation gracieuse)
LOCK_FILE="/tmp/.memory-session-product-agents.lock"
if command -v flock >/dev/null 2>&1; then
(flock -x 200; write_memory) 200>"$LOCK_FILE"
else
write_memory
fi
exit 0
.claude/skills/memory_load/SKILL.md
# /memory_load
Charge la mémoire de session des projets actifs.
## Étape 1 — Identifier les projets
Si argument passé → ce projet uniquement. Sinon → demander via AskUserQuestion.
`all` → tous les projets.
## Étape 2 — Lire les fichiers
Pour chaque projet (chemin : `memory_sessions/` ou `projects/{nom}/memory_sessions/`) :
1. Lire `long_memory.md` (si existant)
2. Lire `logs/{YYYY-MM-DD}.md` du jour courant
3. Lire `logs/{YYYY-MM-DD}.md` du jour précédent
## Étape 3 — Afficher le résumé
✓ Mémoire chargée pour : {projets}
[{Projet}]
Long terme : {contenu ou "vide"}
Aujourd'hui : {entrées ou "aucune"}
Hier : {entrées ou "aucune"}
# /memory_promotion
Consolide les logs vers long_memory.md et archive les anciens.
## Étape 1 — Périmètre
Si argument → ce projet. Sinon → demander. `all` → tous.
## Étape 2 — Analyser les logs (30 derniers jours)
Lire `memory_sessions/logs/*.md` (hors archives/).
## Étape 3 — Mettre à jour long_memory.md
Intégrer les faits durables. Maximum 300 lignes — résumer si dépassement.
## Étape 4 — Archiver
Déplacer logs > 30 jours vers `memory_sessions/logs/archives/`.
## Étape 5 — Résumé
Afficher : logs analysés, faits promus, fichiers archivés, taille long_memory.md.
Bugs connus et solutions
| Bug |
Cause |
Solution |
--stop-hook-active invalide |
Ce flag n'existe pas dans le CLI claude — il est uniquement dans le JSON stdin du hook |
Supprimé du call CLI |
| CLAUDE.md interfère |
Quand claude est appelé depuis le projet, le CLAUDE.md du projet est injecté et modifie la réponse Haiku |
Appeler claude depuis /tmp |
| Messages vides extraits |
Le transcript JSONL contient beaucoup de messages tool_result/thinking avec text vide |
Filtrer sur content[].type == "text" et ignorer les messages sans texte réel |
flock absent (macOS) |
flock vient de util-linux, non installé par défaut sur macOS |
Dégradation gracieuse : if command -v flock avant d'utiliser |
| Variable shell dans heredoc Python |
"""$VAR""" dans un heredoc casse si $VAR contient des guillemets ou caractères spéciaux |
Utiliser des fichiers temporaires (mktemp) passés en arguments |
--no-streaming invalide |
Ce flag n'existe pas dans le CLI claude |
Supprimé — --print suffit |
Spec : skill token_audit
Fichier auto-porté — contient tout le nécessaire pour installer ce skill dans un projet Claude Code.
Modèle d'installation
Modèle recommandé pour installer cette spec : Sonnet 4.6
Plus de 3 fichiers à créer ou modifier (SKILL.md, docs/skills/token-audit.md, docs/skills/README.md, docs/audits/token/) — nécessite un peu de propagation dans les fichiers existants.
Présentation
Nom du skill : token_audit
Déclenchement : /token_audit <nom_du_skill>
Rôle : Analyse un skill existant et produit un rapport d'optimisation des tokens consommés
Modèle recommandé : Sonnet 4.6
Prérequis projet
Le projet doit disposer de :
- docs/skills/README.md — index des skills (pour lister les skills disponibles et y ajouter l'entrée)
- .claude/skills/ — répertoire des skills à auditer
- docs/audits/token/ — créé automatiquement par le skill si absent
- Outil standard wc disponible dans le shell (aucune dépendance externe supplémentaire)
Fichiers à créer
1. .claude/skills/token_audit/SKILL.md
---
name: token_audit
metadata:
project: parent
description: Analyse un skill existant et produit un rapport d'optimisation des tokens consommés
---
# /token_audit
Analyser un skill existant pour produire un **rapport d'optimisation des tokens** consommés, avec des recommandations concrètes et chiffrées.
---
## Entrée
- Argument : `<nom_du_skill>` (ex: `/token_audit mag_add_magazine`)
- Si aucun nom fourni → lire `docs/skills/README.md`, afficher les skills disponibles, demander lequel analyser
---
## Étape 1 — Charger le skill cible
1. Lire `.claude/skills/<nom_du_skill>/SKILL.md`
2. Si le fichier n'existe pas → afficher "Skill introuvable." + lister les skills disponibles, s'arrêter
3. Lire la doc associée `docs/skills/` (chercher le fichier correspondant en kebab-case)
4. Identifier tous les fichiers référencés dans le skill : scripts Python, templates, fichiers lus/écrits
---
## Étape 2 — Inventorier les opérations consommatrices
Pour chaque étape du skill, classifier l'opération dans ce tableau :
| Type | Tokens | Description |
|------|--------|-------------|
| **Bash/Python** | Input (résultat) | Le résultat de la commande entre dans le contexte. Estimer avec `wc -c <output>` / 4 si volumineux. Ex : `git log` 50 commits ≈ 500 tok, `Grep` 20 matches ≈ 150 tok. Commandes sans output (`mv`, `mkdir`, `chmod`) → 0 tok. |
| **Read** (Claude lit un fichier) | Input | Lecture de TXT, JSON, CSV, PDF |
| **Write/génération** (Claude écrit) | Output (coût 5x) | Génération de JSON, Markdown, tout fichier |
| **Raisonnement** (Claude réfléchit) | Output (coût 5x) | Résumés, classification, décisions sémantiques |
| **Contexte système** | Input fixe | SKILL.md, CLAUDE.md, fichiers de principes |
| **AskUserQuestion** | Input + Output | Output : ~100 tok (question affichée). Input : réponse utilisateur ~50-500 tok. |
| **WebFetch** | Input | Contenu HTML parsé entrant dans le contexte. Estimer : `wc -c <contenu>` / 4. Souvent 1 000-5 000 tok. |
Lister chaque étape avec son type dans un tableau intermédiaire.
---
## Étape 3 — Mesurer les volumes
Pour chaque opération Read ou Write identifiée :
1. Chercher un fichier exemple existant dans le projet (output d'une exécution précédente du skill)
2. Mesurer sa taille avec `wc -c` et estimer les tokens selon le type de contenu :
- Prose française : `taille_octets / 5`
- JSON (clés/valeurs courtes) : `taille_octets / 2.5`
- Code source (PHP, JS, Vue) : `taille_octets / 3.5`
- Markdown mixte (prose + code) : `taille_octets / 4`
3. Si pas d'exemple disponible → estimer depuis le SKILL.md avec les catégories : petit (<500 tokens), moyen (500-5000), gros (>5000)
Pour le contexte système, estimer :
- SKILL.md du skill cible : `wc -c` / 4
- Fichiers de principes/config : mesurer avec `wc -c` / 4
**Cache** : dans Claude Code, le contexte système est mis en cache après le 1er tour. Pour les exécutions répétées, multiplier le coût du contexte système par 0.1. Mentionner dans le rapport "(sans cache)" vs "(en régime établi)" si l'écart dépasse 10% du coût total.
---
## Étape 4 — Calculer la répartition des coûts
Appliquer la formule de coût pondéré :
coût_input = tokens_input × 1
coût_output = tokens_output × 5
coût_total = somme de tous les coûts
%_étape = coût_étape / coût_total × 100
**Accumulation de contexte** : dans une conversation multi-tours, chaque step injecte ses outputs dans le contexte de tous les steps suivants. Pour les skills longs (>5 étapes), inclure une ligne "Contexte accumulé" dans le tableau :
- Estimer : somme des tokens output des étapes précédentes × 1 (input)
- Cette ligne représente souvent 15-40% du coût total réel
- C'est la principale source d'écart entre estimation step-par-step et coût réel
Produire le tableau de répartition par étape, en incluant la ligne "Contexte accumulé" si le skill comporte plus de 5 étapes.
---
## Étape 5 — Identifier les optimisations
Pour chaque opération dont le coût dépasse **15% du total ET 1 500 unités**, évaluer ces stratégies par ordre de priorité :
### 5.1 Déléguer à Python (priorité haute)
L'opération peut-elle être réalisée par un script sans LLM ?
- Copie verbatim de contenu → oui
- Transformation de format (TXT→JSON structure) → partiellement
- Calculs, tri, filtrage → oui
### 5.2 Lecture conditionnelle (priorité haute)
L'opération peut-elle être ignorée si une condition préalable n'est pas remplie ?
- WebFetch → uniquement si la documentation interne est insuffisante
- Lecture fichiers code source → uniquement si la doc ne répond pas
- Lecture d'un sous-répertoire → uniquement si le scoring de pertinence est positif
Différent de "réduire l'input" : ici on ne lit pas du tout, plutôt que de lire moins.
### 5.3 Réduire l'input (priorité moyenne)
Claude lit-il des données inutiles ?
- Fichier entier lu alors que seules certaines sections sont nécessaires
- Contexte système trop volumineux pour la tâche
### 5.4 Paralléliser les lectures (priorité haute)
Les lectures sont-elles déclenchées séquentiellement alors qu'elles sont indépendantes ?
- Lire N fichiers en 1 seul tour (N appels Read en parallèle) au lieu de N tours séquentiels
- Élimine (N-1) couches d'accumulation de contexte
- Applicable quand : lecture de plusieurs fichiers sans dépendance entre eux
- Gain typique : -10 à -30% sur le coût total pour les skills > 5 étapes
### 5.5 Réduire l'output (priorité haute)
Claude génère-t-il du contenu qu'un script pourrait produire ?
- **Pattern diff/patch** : le LLM produit uniquement les sections modifiées (format structuré), un script Python applique les modifications. Gain typique : -40 à -70% sur les tokens output. Applicable quand : mise à jour de documents existants, enrichissement partiel.
- Structures JSON répétitives → déléguer à Python
- Messages/récapitulatifs longs → condenser
### 5.6 Choisir un modèle adapté (priorité basse)
La tâche nécessite-t-elle le modèle actuel ?
- Extraction structurée simple → Haiku 4.5 suffit
- Synthèse/résumé → Sonnet 4.6 minimum
- Raisonnement complexe multi-étapes → Opus 4.6
**Calcul du gain** : pour chaque optimisation identifiée :
gain_% = (coût_pondéré_étape_actuel - coût_pondéré_étape_optimisé) / coût_total × 100
---
## Étape 6 — Produire le rapport
1. Créer le dossier `docs/audits/token/` s'il n'existe pas
2. Écrire le rapport dans `docs/audits/token/token_audit_<nom_du_skill>.md`
Format du rapport :
```markdown
---
date_creation: YYYY-MM-DD
skill_audite: <nom_du_skill>
---
# Audit tokens : <nom_du_skill>
## Résumé
| Métrique | Valeur |
|----------|--------|
| Tokens input estimés | X |
| Tokens output estimés | X |
| Coût pondéré total | X unités |
| Étape la plus coûteuse | X (Y%) |
| Modèle recommandé | Haiku / Sonnet / Opus |
**Base de mesure** : {décrire le scénario retenu — nb commits, nb fichiers lus, taille typique des outputs, modèle hypothétique d'exécution}
## Détail par étape
| # | Étape | Type | Tokens in | Tokens out | Coût pondéré | % total |
|---|-------|------|-----------|------------|-------------|---------|
| 0 | Contexte système | Contexte système | 3 500 | 0 | 3 500 | X% |
| 1 | ... | Bash (git log) | 500 | 0 | 500 | X% |
| 2 | ... | Read | X | 0 | X | Y% |
| 3 | ... | Write | 0 | X | 5X | Y% |
| — | Contexte accumulé | Input | X | 0 | X | Y% |
## Optimisations proposées
### 1. [Titre] — Gain estimé : X%
- **Problème** : description du gaspillage identifié
- **Solution** : action concrète à réaliser
- **Complexité** : faible / moyenne / élevée
- **Modèle recommandé** : Haiku / Sonnet / Opus
## Justification du modèle
{Expliquer pourquoi le modèle recommandé est nécessaire — quelles étapes justifient ce niveau de capacité, et pourquoi le modèle inférieur serait insuffisant (risques concrets : mapping incorrect, enrichissement manqué, synthèse approximative, etc.)}
## Bilan
| Scénario | Tokens in | Tokens out | Coût pondéré | Gain |
|----------|-----------|------------|-------------|------|
| Actuel | X | X | X | — |
| Optimisé | X | X | X | -Y% |
- Afficher le résumé et le chemin du rapport généré
Étape 7 — Mettre à jour l'index README.md
- Lire tous les fichiers
docs/audits/token/token_audit_*.md
- Pour chaque fichier, extraire :
date_creation (frontmatter)
skill_audite (frontmatter)
- Modèle recommandé (ligne "Modèle recommandé" du tableau Résumé)
- Coût pondéré total (ligne "Coût pondéré total" du tableau Résumé — extraire la valeur numérique)
- Projet : lire
.claude/skills/{skill_audite}/SKILL.md → extraire metadata.project. Si absent ou fichier sans frontmatter → parent.
- Formater les coûts avec espaces de milliers et suffixe "unités" (ex:
826 000 unités)
- Écrire
docs/audits/token/README.md avec ce format :
# Token Audit
Généré automatiquement par `/token_audit`. Dernière mise à jour : YYYY-MM-DD.
## Par coût décroissant
| Date audit | Projet | Skill | Modèle recommandé | Coût pondéré total |
| --- | --- | --- | --- | --- |
| YYYY-MM-DD | projet | skill_name | Sonnet 4.6 | 826 000 unités |
| YYYY-MM-DD | projet | skill_name | Haiku 4.5 | 24 502 unités |
## Par projet
| Date audit | Projet | Skill | Modèle recommandé | Coût pondéré total |
| --- | --- | --- | --- | --- |
| YYYY-MM-DD | blog | skill_name | Haiku 4.5 | 7 817 unités |
| YYYY-MM-DD | cmms_doc | skill_name | Sonnet 4.6 | 826 000 unités |
Le tableau "Par coût décroissant" est trié par Coût pondéré total décroissant.
Le tableau "Par projet" est trié par Projet ASC puis Skill ASC.
- Si
docs/audits/token/README.md existe déjà, l'écraser entièrement avec la version recalculée
Gestion des erreurs
- Skill introuvable → lister les skills disponibles depuis
docs/skills/README.md, demander de choisir
- Pas de fichier exemple trouvé → signaler dans le rapport avec mention "estimation sans données réelles"
- Skill trivial (coût total estimé < 1 500 unités, ex:
/help) → afficher "Ce skill consomme très peu de tokens, l'optimisation n'est pas pertinente." et s'arrêter sans générer de rapport. (Aligne le seuil trivial avec le double seuil de l'étape 5.)
### 2. `docs/skills/token-audit.md`
```markdown
---
date_creation: 2026-03-07
date_maj: 2026-03-07
---
# Skill : token_audit
## Rôle
Analyse un skill existant et produit un rapport d'optimisation des tokens consommés, avec recommandations concrètes et chiffrées.
## Déclenchement
`/token_audit <nom_du_skill>`
Exemple : `/token_audit mon_skill`
## Fichiers impliqués
- [.claude/skills/token_audit/SKILL.md](../../.claude/skills/token_audit/SKILL.md) — définition du skill
- [docs/skills/token-audit.md](token-audit.md) — cette documentation
- [docs/audits/](../audits/) — rapports générés (sortie)
## Flux
1. Charger le SKILL.md et la doc du skill cible
2. Inventorier chaque étape : Bash (0 token), Read (input), Write (output 5x), Raisonnement (output)
3. Mesurer les volumes réels via `wc -c` sur les fichiers exemples existants
4. Calculer la répartition des coûts pondérés (output x 5)
5. Identifier les optimisations : déléguer à Python, réduire input/output, modèle adapté
6. Écrire le rapport dans `docs/audits/`
## Particularités
- Seuil de pertinence : si le coût total estimé < 1 500 tokens, le skill est considéré trivial et aucun rapport n'est généré
- Les estimations de tokens utilisent des ratios par type de contenu (prose française : /5, JSON : /2.5, Markdown mixte : /4)
- Le rapport inclut un bilan comparatif avant/après optimisation
- La ligne "Contexte accumulé" est ajoutée pour les skills > 5 étapes
3. Ligne à ajouter dans docs/skills/README.md
| parent | [/token\_audit](token-audit.md) | Analyse un skill existant et produit un rapport d'optimisation des tokens consommés |
Instructions d'installation
- Créer le dossier
.claude/skills/token_audit/
- Y copier le contenu du bloc SKILL.md ci-dessus dans un fichier
SKILL.md
- Créer le fichier
docs/skills/token-audit.md avec le contenu du bloc correspondant
- Ajouter la ligne dans
docs/skills/README.md (section parent, ordre alphabétique)
- Créer le dossier
docs/audits/token/ (créé automatiquement à la 1ère exécution, mais peut être créé à l'avance)
- Vérifier que les prérequis listés ci-dessus sont présents dans le projet cible
Test de validation
- Lancer
/token_audit commit (skill léger, exécution rapide)
- Vérifier que le fichier
docs/audits/token/token_audit_commit.md est créé
- Vérifier que le frontmatter contient
skill_audite: commit et date_creation
- Vérifier que le tableau Résumé contient les lignes "Coût pondéré total" et "Modèle recommandé"
- Vérifier que
docs/audits/token/README.md est mis à jour avec une entrée pour commit