diff --git a/hooks/README.md b/hooks/README.md new file mode 100644 index 0000000..20e9619 --- /dev/null +++ b/hooks/README.md @@ -0,0 +1,78 @@ +# Hooks — Installation multi-agent + +> Ce dossier contient les hooks universels de AI Native Dev Stack, installables sur tout agent IA. + +## Les 4 hooks universels + +| Hook | Event | Trigger | Agents | +|------|-------|---------|--------| +| `session-start-memory/` | SessionStart | démarrage session | Tous | +| `session-end-save/` | SessionEnd | fin de session | Tous | +| `posttool-ai-summary/` | PostToolUse | Edit ou Write | Claude Code, Codex, Cursor | +| `pretool-loc-gate/` | PreToolUse | avant Edit/Write | Mavis (PreToolUse), Claude Code, Codex | + +## Installation Mavis + +Mavis supporte nativement les hooks SessionStart et SessionEnd via `mavis hook create`. + +```powershell +# SessionStart memory loader (déjà installé) +mavis hook list --agent mavis --human + +# SessionEnd save (déjà installé) +mavis hook list --agent mavis --human +``` + +Pour le LOC gate (PreToolUse): +```powershell +# Créer manuellement via le fichier hook: +# C:\Users\barat\.mavis\agents\mavis\hooks\pretool-loc-gate.md +``` + +## Installation Claude Code + +### Global hooks +`~/.claude/hooks.json`: +```json +{ + "hooks": { + "SessionStart": [{ + "matcher": "", + "hooks": [{ + "type": "command", + "command": "node /hooks/session-start-memory/run.js" + }] + }], + "PostToolUse": [{ + "matcher": "Edit|Write", + "hooks": [{ + "type": "command", + "command": "bash /hooks/posttool-ai-summary/run_hook.sh" + }] + }] + } +} +``` + +### Vault sync quotidien +Dans `~/.claude/CLAUDE.md`, section début de session: +```powershell +pwsh -NoProfile -File 'D:/App/ai-native-dev-stack/scripts/vault_sync_once_daily.ps1' +``` + +## Installation Codex + +`~/.codex/hooks.json` — même format que Claude Code. + +## Installation Cursor + +Cursor utilise `.cursorrules` et les plugins MCP. +Vérifier la documentation Cursor pour l'équivalent de `hooks.json`. + +## Notes + +- API Obsidian locale: `http://127.0.0.1:27123` (override via `OBSIDIAN_API_URL`) +- API Key: lue depuis la variable d'environnement `OBSIDIAN_API_KEY` (jamais commitée). + Récupérer la clé dans Obsidian → plugin *Local REST API* → puis l'exporter : + `setx OBSIDIAN_API_KEY ""` (Windows) / `export OBSIDIAN_API_KEY=...` (bash) +- Prérequis: Obsidian ouvert avec le vault `IA_Dev_Brain` + plugin Local REST API activé diff --git a/hooks/permission-readonly-env/README.md b/hooks/permission-readonly-env/README.md new file mode 100644 index 0000000..d1c830a --- /dev/null +++ b/hooks/permission-readonly-env/README.md @@ -0,0 +1,69 @@ +# PermissionRequest Read-Only Env Prefix — Hook Universel + +## Objectif + +Auto-allow les commandes Bash read-only qui ont des préfixes de variables d'environnement. +Exemples : `RUST_LOG=debug cat file.txt`, `LANG=fr ls -la`, `DEBUG=1 grep pattern file`. + +Le hook strip le préfixe ENV_VAR=value, identifie la commande réelle, et si elle est +read-only (cat, ls, grep, etc.), émet `permissionDecision: allow`. + +## Ce que fait ce hook + +1. Lit `tool_input.command` du payload JSON +2. Skip les tokens `KEY=VALUE` au début +3. Identifie le premier mot non-env-var +4. Si dans la whitelist read-only → autorise + +## Sortie (format universel) + +```json +{ + "hookSpecificOutput": { + "hookEventName": "PermissionRequest", + "permissionDecision": "allow", + "permissionDecisionReason": "read-only command 'cat' with env var prefix" + } +} +``` + +## Whitelist + +```python +READONLY = { + 'ls', 'll', 'cat', 'head', 'tail', 'grep', 'rg', + 'wc', 'diff', 'echo', 'pwd', 'which', 'file', + 'stat', 'type', 'dir', 'find', 'awk', 'cd', +} +``` + +## Script + +Voir `run.py` (Python stdlib uniquement). + +## Installation par agent + +### Codex + +Dans `.codex/hooks.json` : +```json +{ + "hooks": { + "PermissionRequest": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "python3 /hooks/permission-readonly-env/run.py" + } + ] + } + ] + } +} +``` + +## Source originale + +Portée depuis `C:\Users\barat\.codex\hooks\readonly-env-prefix.py` (Erwan Barat). diff --git a/hooks/permission-readonly-env/run.py b/hooks/permission-readonly-env/run.py new file mode 100644 index 0000000..e9ff764 --- /dev/null +++ b/hooks/permission-readonly-env/run.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +""" +permission-readonly-env.py — PermissionRequest: auto-allow read-only commands with env var prefix. + +Example: RUST_LOG=debug cat file.txt → stripped = "cat file.txt" → allow + +Compatible: Claude Code, Codex (PermissionRequest), MiniMax Code. +Python stdlib uniquement. + +Usage: python3 run.py +stdin: payload JSON de l'agent (PermissionRequest event) +stdout: JSON { hookSpecificOutput: ... } ou vide (silence = pas d'autorisation automatique) +""" +import json +import sys + +READONLY = { + 'ls', 'll', 'cat', 'head', 'tail', 'grep', 'rg', + 'wc', 'diff', 'echo', 'pwd', 'which', 'file', + 'stat', 'type', 'dir', 'find', 'awk', 'cd', +} + + +def main(): + try: + payload = json.loads(sys.stdin.read()) + except Exception: + return + + tool_input = ( + payload.get('tool_input') + or payload.get('input', {}).get('tool_input') + or {} + ) + cmd = tool_input.get('command', '') + parts = cmd.split() + + # Skip leading ENV_VAR=value tokens + n = 0 + for p in parts: + if '=' in p: + key = p.split('=', 1)[0] + if key and key == key.upper() and key.replace('_', '').isalpha(): + n += 1 + continue + break + + first_word = parts[n] if n < len(parts) else '' + + if first_word in READONLY: + print(json.dumps({ + 'hookSpecificOutput': { + 'hookEventName': 'PermissionRequest', + 'permissionDecision': 'allow', + 'permissionDecisionReason': f"read-only command '{first_word}' with env var prefix", + } + })) + + +if __name__ == '__main__': + main() diff --git a/hooks/posttool-ai-summary/README.md b/hooks/posttool-ai-summary/README.md new file mode 100644 index 0000000..b7e15ca9 --- /dev/null +++ b/hooks/posttool-ai-summary/README.md @@ -0,0 +1,63 @@ +# PostToolUse AI_SUMMARY Generator — Hook Universel + +## Objectif + +Après un Edit ou Write de fichier source, régénérer le `AI_SUMMARY.md` du module concerné. +C'est le hook central du stack AI Native Dev — il maintient les summaries auto-générés à jour. + +## Principe de fonctionnement + +Le hook détecte le fichier modifié, trouve le `AI_CONTEXT.md` parent le plus proche, +puis appelle `generate_ai_summary.py` pour régénérer le `AI_SUMMARY.md` du module. + +## Dépendances + +- Python ≥ 3.8 (stdlib uniquement) +- Scripts AI Native Dev Stack: `tools/ai_docs/update_on_edit.py`, `tools/ai_docs/generate_ai_summary.py` + +## Installation par agent + +### Claude Code / Codex + +Ajouter dans `.claude/hooks.json` (projet) ou `~/.claude/hooks.json` (global): + +```json +{ + "hooks": { + "PostToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "bash /posttool-ai-summary/run_hook.sh" + } + ] + } + ] + } +} +``` + +### Mavis + +Mavis ne supporte pas nativement PostToolUse sur Edit/Write. +Alternative: utiliser un PreToolUse gate qui enregistre le fichier edité, +puis un cron job périodique qui régénère les summaries. + +Alternative viable: installer via MCP Obsidian un hook de notification +quand un fichier change, et régénérer les summaries en réponse. + +### Cursor / Continue + +Via `.cursorrules` ou plugin MCP Tools compatible. + +## Scripts + +- `run_hook.sh` — wrapper bash (Linux/macOS/Git Bash Windows) +- `run_hook.ps1` — wrapper PowerShell (Windows natif) + +## Configuration + +Le hook utilise `GRAPHIFY_BIN` et `OBSIDIAN_API_KEY` si définis. +Voir `tools/ai_docs/config.sh.example` pour la configuration. diff --git a/hooks/posttool-ai-summary/run_hook.ps1 b/hooks/posttool-ai-summary/run_hook.ps1 new file mode 100644 index 0000000..17f3c0f --- /dev/null +++ b/hooks/posttool-ai-summary/run_hook.ps1 @@ -0,0 +1,23 @@ +# run_hook.ps1 — PostToolUse AI_SUMMARY Generator (Windows natif) +# Wrapper PowerShell pour le hook PostToolUse. + +$INPUT = Get-Content -Raw +$SCRIPT_DIR = Split-Path -Parent $MyInvocation.MyCommand.Path +$STACK_ROOT = (Resolve-Path "$SCRIPT_DIR\..\..\..").Path +$UPDATE_SCRIPT = Join-Path $STACK_ROOT "tools\ai_docs\update_on_edit.py" + +# Trouver un Python +$PY = $null +foreach ($p in @("python", "python3", "py", "C:\Python312\python.exe", "C:\Python311\python.exe")) { + $pyCmd = Get-Command $p -ErrorAction SilentlyContinue + if ($pyCmd) { $PY = $pyCmd.Source; break } +} + +if ($PY -and (Test-Path $UPDATE_SCRIPT)) { + $result = @($INPUT | & $PY $UPDATE_SCRIPT 2>&1 | Out-String) + if ($result) { Write-Output $result } +} else { + # Silent skip +} + +exit 0 diff --git a/hooks/posttool-ai-summary/run_hook.sh b/hooks/posttool-ai-summary/run_hook.sh new file mode 100644 index 0000000..1bacd7b --- /dev/null +++ b/hooks/posttool-ai-summary/run_hook.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# run_hook.sh — PostToolUse AI_SUMMARY Generator +# Wrapper universel pour Claude Code / Codex PostToolUse hook. +# Lit stdin une fois, trouve un Python, délègue à update_on_edit.py. + +INPUT=$(cat) +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +STACK_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" + +# Chemin vers le script Python de AI Native Dev Stack +UPDATE_SCRIPT="$STACK_ROOT/tools/ai_docs/update_on_edit.py" +PYTHON_BIN="" + +# Trouver un Python disponible +for py in python3 python python3.11 python3.10 python3.9 python3.8; do + if command -v $py &>/dev/null; then + PYTHON_BIN=$py + break + fi +done + +if [ -n "$PYTHON_BIN" ] && [ -f "$UPDATE_SCRIPT" ]; then + echo "$INPUT" | PYTHONIOENCODING=utf-8 "$PYTHON_BIN" "$UPDATE_SCRIPT" 2>&1 +else + # Silent skip — ne pas bloquer l'agent + : +fi + +exit 0 diff --git a/hooks/pretool-graphify-inject/README.md b/hooks/pretool-graphify-inject/README.md new file mode 100644 index 0000000..28fa8fe --- /dev/null +++ b/hooks/pretool-graphify-inject/README.md @@ -0,0 +1,58 @@ +# PreToolUse Graphify Inject — Hook Universel + +## Objectif + +Quand un agent CLI tape une commande `grep`/`rg`/`find`/`ack`/`ag` sur un repo qui contient +`graphify-out/graph.json`, le hook injecte automatiquement le contexte du graphe de dépendances +dans la conversation — pour éviter de re-chercher dans le code alors qu'un graphe existe. + +## Ce que fait ce hook + +Lit la commande Bash entrante. Si elle contient un outil de recherche (`grep`, `rg`, etc.) +ET que `graphify-out/graph.json` existe dans le cwd, le hook émet un +`additionalContext` pointant vers `graphify-out/GRAPH_REPORT.md`. + +## Sortie (format universel) + +```json +{ + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "additionalContext": "graphify: Knowledge graph exists. Read graphify-out/GRAPH_REPORT.md for god nodes and community structure before searching raw files." + } +} +``` + +## Scripts + +- `run.js` (Node.js, stdlib uniquement) +- `run.py` (Python, stdlib uniquement) + +## Installation par agent + +### Claude Code / Codex / MiniMax Code + +Dans le `hooks.json` du projet (ou global) : + +```json +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "node /hooks/pretool-graphify-inject/run.js" + } + ] + } + ] + } +} +``` + +## Source originale + +Portée depuis `C:\Users\barat\.codex\hooks.json` (Erwan Barat) qui utilisait un +bash inline avec `case "$CMD"`. La version Node.js est multiplateforme sans dépendance bash. diff --git a/hooks/pretool-graphify-inject/run.js b/hooks/pretool-graphify-inject/run.js new file mode 100644 index 0000000..53899bf --- /dev/null +++ b/hooks/pretool-graphify-inject/run.js @@ -0,0 +1,60 @@ +/** + * pretool-graphify-inject.js — PreToolUse graphify inject + * + * Lit la commande Bash entrante. Si elle contient grep/rg/find/ack/ag + * ET que graphify-out/graph.json existe dans le cwd, injecte le contexte. + * + * Compatible: Claude Code, Codex, MiniMax Code, Aider (via prompt system). + * Node.js stdlib uniquement. + * + * Usage: node run.js + * stdin: payload JSON de l'agent avec tool_input.command + * stdout: JSON { hookSpecificOutput: ... } ou vide (silence = pas d'injection) + */ + +const fs = require('fs'); +const path = require('path'); + +const SEARCH_TOOLS = ['grep', 'rg', 'ripgrep', 'find', 'fd', 'ack', 'ag']; + +function readStdin() { + return new Promise((resolve) => { + let data = ''; + process.stdin.setEncoding('utf8'); + process.stdin.on('data', chunk => data += chunk); + process.stdin.on('end', () => resolve(data)); + }); +} + +async function main() { + let payload; + try { + const raw = await readStdin(); + payload = JSON.parse(raw); + } catch { + return; + } + + const command = (payload.tool_input || payload.input?.tool_input || {}).command || ''; + const cwd = payload.cwd || process.cwd(); + + const lower = command.toLowerCase(); + const isSearch = SEARCH_TOOLS.some(tool => { + const re = new RegExp(`(^|\\s|\\b)${tool}(\\s|$)`); + return re.test(lower); + }); + if (!isSearch) return; + + const graphPath = path.join(cwd, 'graphify-out', 'graph.json'); + if (!fs.existsSync(graphPath)) return; + + const message = 'graphify: Knowledge graph exists. Read graphify-out/GRAPH_REPORT.md for god nodes and community structure before searching raw files.'; + console.log(JSON.stringify({ + hookSpecificOutput: { + hookEventName: 'PreToolUse', + additionalContext: message, + }, + })); +} + +main(); diff --git a/hooks/pretool-graphify-inject/run.py b/hooks/pretool-graphify-inject/run.py new file mode 100644 index 0000000..2daf618 --- /dev/null +++ b/hooks/pretool-graphify-inject/run.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +""" +pretool-graphify-inject.py — version Python pour agents ne supportant pas Node.js. +Mêmes fonctionnalités que run.js. Lit stdin, injecte si search tool + graph existe. +""" +import json +import os +import re +import sys + +SEARCH_TOOLS = ['grep', 'rg', 'ripgrep', 'find', 'fd', 'ack', 'ag'] + + +def main(): + try: + payload = json.loads(sys.stdin.read()) + except Exception: + return + + tool_input = payload.get('tool_input') or payload.get('input', {}).get('tool_input') or {} + command = tool_input.get('command', '') + cwd = payload.get('cwd') or os.getcwd() + + lower = command.lower() + is_search = any(re.search(rf'(^|\s|\b){re.escape(tool)}(\s|$)', lower) for tool in SEARCH_TOOLS) + if not is_search: + return + + graph_path = os.path.join(cwd, 'graphify-out', 'graph.json') + if not os.path.exists(graph_path): + return + + print(json.dumps({ + 'hookSpecificOutput': { + 'hookEventName': 'PreToolUse', + 'additionalContext': 'graphify: Knowledge graph exists. Read graphify-out/GRAPH_REPORT.md for god nodes and community structure before searching raw files.', + }, + })) + + +if __name__ == '__main__': + main() diff --git a/hooks/pretool-loc-gate/README.md b/hooks/pretool-loc-gate/README.md new file mode 100644 index 0000000..cb65309 --- /dev/null +++ b/hooks/pretool-loc-gate/README.md @@ -0,0 +1,60 @@ +# PreToolUse LOC Gate — Hook Universel + +## Objectif + +Bloquer ou alerter quand un fichier dépasse les seuils LOC définis dans le CLAUDE.md global. + +## Seuils LOC (depuis CLAUDE.md global) + +| Condition | Action | +|-----------|--------| +| > 500 LOC (nouveau fichier) | Warning — proposer décomposition | +| > 800 LOC (fichier existant) | Warning — proposer extraction | +| > 1500 LOC (tout fichier) | **BLOCK** — refactoring obligatoire | + +## Comportement par agent + +### Mavis (PreToolUse) + +Retourne `{"_abort":{"reason":"..."}}` pour bloquer l'outil si > 1500 LOC. + +### Claude Code / Codex / Cursor + +Affiche un warning dans la sortie du hook. +Exit code 1 si > 1500 LOC (bloque le hook mais pas l'outil dans certains cas). + +## Installation + +### Mavis (PreToolUse gate) + +```powershell +# Créer via CLI ou fichier: +# C:\Users\barat\.mavis\agents\mavis\hooks\pretool-loc-gate.md +``` + +### Claude Code / Codex + +Ajouter dans `hooks.json`: +```json +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "node /pretool-loc-gate/run.js" + } + ] + } + ] + } +} +``` + +## Scripts + +- `run_gate.sh` — bash (Linux/macOS/Git Bash) +- `run_gate.ps1` — PowerShell (Windows) +- `run_gate.js` — Node.js (universel) diff --git a/hooks/pretool-loc-gate/run_gate.js b/hooks/pretool-loc-gate/run_gate.js new file mode 100644 index 0000000..aeffec7 --- /dev/null +++ b/hooks/pretool-loc-gate/run_gate.js @@ -0,0 +1,85 @@ +/** + * pretool-loc-gate.js — PreToolUse LOC Gate + * + * Vérifie la taille d'un fichier avant modification. + * Node.js stdlib uniquement — zéro dépendance externe. + * + * Usage: node run.js + * + * Seuils (depuis CLAUDE.md global): + * > 500 LOC (nouveau fichier) → warning + * > 800 LOC (fichier existant) → warning + * > 1500 LOC (tout fichier) → BLOCK (exit 1 + reason) + * + * Mavis PreToolUse: retourne { _abort: { reason: "..." } } + * Claude Code / Codex: exit code 1 = bloquant, affiche le reason + */ + +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +const THRESHOLDS = { + NEW_FILE_WARNING: 500, + EXISTING_FILE_WARNING: 800, + BLOCKING: 1500, +}; + +function countLines(filePath) { + try { + // PowerShell pour compatibilité Windows-native + const result = execSync( + `powershell.exe -NoProfile -Command "(Get-Content '${filePath.replace(/'/g, "''")}' -ErrorAction SilentlyContinue | Measure-Object -Line).Lines"`, + { encoding: 'utf8', timeout: 5000 } + ); + const lines = parseInt(result.trim(), 10); + return isNaN(lines) ? 0 : lines; + } catch { + return 0; + } +} + +function main() { + const filePath = process.argv[2]; + if (!filePath) { + console.log(JSON.stringify({ metadata: { locGate: 'no_file_specified' } })); + return; + } + + const absPath = path.resolve(filePath); + const exists = fs.existsSync(absPath); + const lines = countLines(absPath); + + const isNewFile = !exists; + + if (lines > THRESHOLDS.BLOCKING) { + const reason = `LOC GATE: ${absPath} a ${lines} LOC (limite bloquante: ${THRESHOLDS.BLOCKING}). Refactoring obligatoire avant modification. (Règle CLAUDE.md)`; + console.log(JSON.stringify({ + _abort: { reason }, + metadata: { locGate: 'blocked', file: absPath, lines, threshold: THRESHOLDS.BLOCKING } + })); + process.exit(1); + } + + if (isNewFile && lines > THRESHOLDS.NEW_FILE_WARNING) { + const reason = `LOC GATE: nouveau fichier ${absPath} à ${lines} LOC (>${THRESHOLDS.NEW_FILE_WARNING}). Proposer une décomposition immédiate.`; + console.log(JSON.stringify({ + metadata: { locGate: 'warning', file: absPath, lines, threshold: THRESHOLDS.NEW_FILE_WARNING, type: 'new_file' } + })); + process.exit(0); // Warning seulement, ne bloque pas + } + + if (exists && lines > THRESHOLDS.EXISTING_FILE_WARNING) { + const reason = `LOC GATE: fichier existant ${absPath} à ${lines} LOC (>${THRESHOLDS.EXISTING_FILE_WARNING}). Proposer extraction des responsabilités secondaires.`; + console.log(JSON.stringify({ + metadata: { locGate: 'warning', file: absPath, lines, threshold: THRESHOLDS.EXISTING_FILE_WARNING, type: 'existing' } + })); + process.exit(0); + } + + console.log(JSON.stringify({ + metadata: { locGate: 'pass', file: absPath, lines } + })); +} + +main(); diff --git a/hooks/session-end-save/README.md b/hooks/session-end-save/README.md new file mode 100644 index 0000000..a5cce4f --- /dev/null +++ b/hooks/session-end-save/README.md @@ -0,0 +1,31 @@ +# SessionEnd Memory Saver — Hook Universel + +## Objectif + +Sauvegarder automatiquement l'état de session à la fin de tout agent IA. + +## Ce que fait ce hook + +1. Appende une entrée dans `LOG.md` du vault avec date + résumé +2. Met à jour `_memory/memory.md` du projet actif (si identifiable) + +## API Obsidian Locale + +- Base URL: `http://127.0.0.1:27123` +- Auth: `Authorization: Bearer ` + +## Script + +Voir `run.js` (Node.js, stdlib uniquement). + +## Installation par agent + +### Mavis +```powershell +# Ce hook est déjà installé via: +# mavis hook create session-end-save --event SessionEnd --type script --agent mavis +# Fichier: C:\Users\barat\.mavis\agents\mavis\hooks\session-end-save.md +``` + +### Claude Code / Codex / Cursor +Comme pour session-start-memory, ajouter dans le fichier hooks.json de l'agent. diff --git a/hooks/session-end-save/run.js b/hooks/session-end-save/run.js new file mode 100644 index 0000000..d27855f --- /dev/null +++ b/hooks/session-end-save/run.js @@ -0,0 +1,111 @@ +/** + * session-end-save.js — SessionEnd Memory Saver + * + * Appende une entrée LOG.md à la fin de session. + * Node.js stdlib uniquement — zéro dépendance externe. + * + * Usage: node run.js + * Variables d'environnement: + * SESSION_ID - ID de session (optionnel) + * PROJECT_NAME - Nom du projet actif (optionnel) + * SESSION_SUMMARY - Résumé de session (optionnel) + */ + +const http = require('http'); +const fs = require('fs'); +const path = require('path'); + +// Obsidian Local REST API key — read from the environment, never hardcoded. +// Set OBSIDIAN_API_KEY in your shell/agent config (see README.md). +const API_KEY = process.env.OBSIDIAN_API_KEY || ''; +const VAULT_BASE = process.env.OBSIDIAN_API_URL || 'http://127.0.0.1:27123'; + +const SESSION_ID = process.env.SESSION_ID || 'unknown'; +const PROJECT_NAME = process.env.PROJECT_NAME || ''; +const SUMMARY = process.env.SESSION_SUMMARY || ''; + +/** + * Lit un fichier du vault via l'API REST locale. + */ +async function readVaultFile(vaultPath) { + return new Promise((resolve) => { + const url = `${VAULT_BASE}/vault/?path=${encodeURIComponent(vaultPath)}`; + const options = { headers: { 'Authorization': `Bearer ${API_KEY}` } }; + http.get(url, options, (res) => { + if (res.statusCode !== 200) { resolve(''); return; } + let data = ''; + res.on('data', c => data += c); + res.on('end', () => resolve(data)); + res.on('error', () => resolve('')); + }).on('error', () => resolve('')); + }); +} + +/** + * Écrit un fichier dans le vault via l'API REST locale. + */ +async function writeVaultFile(vaultPath, content) { + return new Promise((resolve) => { + const url = `${VAULT_BASE}/vault/`; + const body = JSON.stringify({ + path: vaultPath, + content: content, + }); + const options = { + hostname: '127.0.0.1', + port: 27123, + path: '/vault/', + method: 'POST', + headers: { + 'Authorization': `Bearer ${API_KEY}`, + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(body), + }, + }; + const req = http.request(options, (res) => { + let data = ''; + res.on('data', c => data += c); + res.on('end', () => resolve({ status: res.statusCode, data })); + }); + req.on('error', (err) => resolve({ status: 0, error: err.message })); + req.write(body); + req.end(); + }); +} + +/** + * Formate la date ISO en YYYY-MM-DD HH:MM. + */ +function formatDate() { + const now = new Date(); + return now.toISOString().replace('T', ' ').substring(0, 16); +} + +async function main() { + try { + const stamp = formatDate(); + const projectLine = PROJECT_NAME ? ` — [${PROJECT_NAME}]` : ''; + const summaryLine = SUMMARY ? `\n${SUMMARY}` : ''; + + // Construire l'entrée LOG + const logEntry = `\n## ${stamp}${projectLine}${summaryLine}\n\nSession: ${SESSION_ID}\n`; + + // Lire le LOG existant + const existingLog = await readVaultFile('LOG.md'); + const newLog = existingLog + ? existingLog.trimEnd() + logEntry + : `# LOG — Journal de session\n\n${logEntry.trimStart()}`; + + await writeVaultFile('LOG.md', newLog); + + console.log(JSON.stringify({ + metadata: { sessionSaved: stamp, project: PROJECT_NAME } + })); + } catch (err) { + console.log(JSON.stringify({ + metadata: { sessionSaveError: err.message } + })); + } +} + +main(); diff --git a/hooks/session-start-memory/README.md b/hooks/session-start-memory/README.md new file mode 100644 index 0000000..22c9684 --- /dev/null +++ b/hooks/session-start-memory/README.md @@ -0,0 +1,68 @@ +# SessionStart Memory Loader — Hook Universel + +## Objectif + +Charger automatiquement le contexte de session au démarrage de tout agent IA (Mavis, Claude Code, Codex, Cursor, etc.). + +## Ce que fait ce hook + +1. Lit `memory/user.md` (profil Erwan, projets, workflow) +2. Lit `_global/handoff.md` (état courant de tous les projets) +3. Affiche un résumé de session précédente (si existante) + +## API Obsidian Locale + +- Base URL: `http://127.0.0.1:27123` (override via `OBSIDIAN_API_URL`) +- Auth: `Authorization: Bearer ` +- API Key: lue depuis la variable d'environnement `OBSIDIAN_API_KEY` (jamais commitée) + +## Installation par agent + +### Mavis +```powershell +# Ce hook est déjà installé via: +# mavis hook create session-start-memory --event SessionStart --type script --agent mavis +# Fichier: C:\Users\barat\.mavis\agents\mavis\hooks\session-start-memory.md +``` + +### Claude Code +Ajouter dans `~/.claude/hooks.json` (global) ou `/.claude/hooks.json` (par projet): +```json +{ + "hooks": { + "SessionStart": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "node /session-start-memory/run.js" + } + ] + } + ] + } +} +``` + +### Codex +Ajouter dans `~/.codex/hooks.json`: +```json +{ + "hooks": { + "SessionStart": [ + { + "type": "command", + "command": "node /session-start-memory/run.js" + } + ] + } +} +``` + +### Cursor +Via le plugin MCP Tools ou `.cursor/mcp.json` (consulter documentation Cursor). + +## Script + +Voir `run.js` (Node.js, stdlib uniquement). diff --git a/hooks/session-start-memory/run.js b/hooks/session-start-memory/run.js new file mode 100644 index 0000000..ce070fb --- /dev/null +++ b/hooks/session-start-memory/run.js @@ -0,0 +1,75 @@ +/** + * session-start-memory.js — SessionStart Memory Loader + * + * Charge le contexte de session depuis le vault Obsidian local. + * Node.js stdlib uniquement — zéro dépendance externe. + * + * Usage: node run.js + * Retourne JSON sur stdout: { userMemory, handoff, lastSession } + */ + +const https = require('https'); +const http = require('http'); +const fs = require('fs'); +const path = require('path'); + +// Obsidian Local REST API key — read from the environment, never hardcoded. +// Set OBSIDIAN_API_KEY in your shell/agent config (see README.md). +const API_KEY = process.env.OBSIDIAN_API_KEY || ''; +const VAULT_BASE = process.env.OBSIDIAN_API_URL || 'http://127.0.0.1:27123'; + +/** + * Lit un fichier du vault Obsidian via l'API REST locale. + * @param {string} vaultPath - Chemin relatif dans le vault (ex: "_global/handoff.md") + * @returns {Promise} Contenu du fichier ou chaîne vide + */ +async function readVaultFile(vaultPath) { + return new Promise((resolve) => { + const url = `${VAULT_BASE}/vault/?path=${encodeURIComponent(vaultPath)}`; + const options = { + headers: { 'Authorization': `Bearer ${API_KEY}` }, + }; + http.get(url, options, (res) => { + if (res.statusCode !== 200) { + resolve(''); + return; + } + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => resolve(data)); + res.on('error', () => resolve('')); + }).on('error', () => resolve('')); + }); +} + +/** + * Extrait un résumé depuis un fichier markdown (N premières lignes). + * @param {string} content + * @param {number} maxLines + */ +function extractSummary(content, maxLines = 20) { + if (!content) return '(aucun contenu)'; + const lines = content.split('\n').filter(l => l.trim()); + return lines.slice(0, maxLines).join('\n'); +} + +async function main() { + try { + const [userMemory, handoff] = await Promise.all([ + readVaultFile('memory/user.md'), + readVaultFile('_global/handoff.md'), + ]); + + const result = { + userMemory: extractSummary(userMemory, 30), + handoff: extractSummary(handoff, 30), + loaded: !!(userMemory || handoff), + }; + + console.log(JSON.stringify({ metadata: { sessionContext: result } })); + } catch (err) { + console.log(JSON.stringify({ metadata: { sessionContext: { error: err.message } } })); + } +} + +main(); diff --git a/routing-guide.md b/routing-guide.md new file mode 100644 index 0000000..5dee32b --- /dev/null +++ b/routing-guide.md @@ -0,0 +1,172 @@ +# Routing Guide — Quel agent pour quelle tâche + +> Ce guide est une adaptation universelle de la section "Subagent vs lecture directe" du CLAUDE.md global d'Erwan Barat. Il fonctionne pour tout agent IA : Mavis, Claude Code, Codex, Cursor, etc. + +--- + +## Règle 0 — Diagnostic avant correctif + +**Toujours.** Avant de corriger, lire les fichiers impactés, identifier la cause racine, vérifier les call sites. Ne proposer un correctif que si le diagnostic est complet. Si le risque de régression est non nul et non maîtrisé, proposer sans appliquer et expliquer ce qu'il faut vérifier. + +--- + +## Règle 1 — Estimer le périmètre + +### Commandes d'estimation + +**PowerShell (Windows):** +```powershell +# Estimer taille tokens (÷4 = tokens estimés) +Get-ChildItem -Path . -Recurse -Include *.py,*.rs,*.cpp,*.c,*.h,*.hpp,*.ts,*.js ` + -Exclude node_modules,.git,target,build,dist,vendor ` + | Get-Content | Measure-Object -Line + +# Compter les fichiers source +Get-ChildItem -Path . -Recurse -Include *.py,*.rs,*.cpp,*.ts -Exclude node_modules,.git ` + | Measure-Object | Select-Object -ExpandProperty Count +``` + +**Bash (Linux/macOS/Git Bash):** +```bash +# Taille tokens estimés (÷4, variance ±20%) +git ls-files | grep -E '\.(py|rs|cpp|c|h|hpp|ts|js|go)$' | xargs wc -c 2>/dev/null | tail -1 + +# Nombre de fichiers source +git ls-files | grep -E '\.(py|rs|cpp|c|h|hpp|ts|js)$' | wc -l + +# Fallback hors git +find . -not -path '*/.git/*' -not -path '*/node_modules/*' \ + -not -path '*/target/*' -not -path '*/build/*' \ + \( -name '*.py' -o -name '*.rs' -o -name '*.cpp' -o -name '*.ts' \) \ + | xargs wc -c 2>/dev/null | tail -1 +``` + +--- + +## Règle 2 — Taille → Mode de travail + +``` +┌─────────────────────────────┐ +│ < 50k tokens source │ +│ → Lecture DIRECTE │ +│ → Couverture 100% │ +│ → agent principal only │ +└─────────────────────────────┘ + ↓ +┌─────────────────────────────┐ +│ 50k – 150k tokens │ +│ → Cartographie AST/ctags │ +│ → Lecture ciblée │ +│ → Couverture à déclarer │ +└─────────────────────────────┘ + ↓ +┌─────────────────────────────┐ +│ > 150k tokens │ +│ → Team plan multi-phases │ +│ → Phase 1: cartographie │ +│ → Phase 2: lecture directe │ +│ → Phase 3: synthèse │ +└─────────────────────────────┘ +``` + +--- + +## Règle 3 — Mode par type d'intention + +| Signal dans la demande | Mode | Stratégie | +|----------------------|------|-----------| +| "Où est X ?", "trouver Y" | **Lookup** | Subagent Explore | +| "Comment fonctionne X ?" | **Understanding** | Subagent + lecture ciblée | +| "Revue", "analyse l'architecture" | **Review** | Synthèse centrale + liste fichiers lus/non lus | +| "Exhaustif", "complet", "rien ne manque", "audit" | **Audit** | Workflow manifest-driven + coverage vérifié obligatoire | + +**Signal de complexité secondaire** : si `tokens < 50k` mais `fichiers > 100` → prioriser un tour de clarification. + +**Tour de clarification type:** +``` +"Je vois ~X tokens dans Y fichiers. Quel niveau d'analyse ? +[A] Vue d'ensemble rapide (Explore agent, ~2k tokens) +[B] Analyse ciblée (lecture directe, ~10k tokens) +[C] Audit exhaustif (lecture séquentielle, ~X tokens, coverage vérifié)" +``` + +--- + +## Règle 4 — Quand créer un team plan + +Un team plan (multi-session parallèle + verifiers) est justifié quand: + +1. **≥ 3 tracks parallèles** et indépendantes (ex: UI + data layer + API) +2. **Vérification indépendante** nécessaire (sécurité, permissions, calculs, données critiques) +3. **Haute valeur d'erreur** (un bug aurait un coût important) +4. **Chaîne de livraison multi-étapes** (research → analyze → write → verify) + +### Patterns de team plan + +``` +[impl-track-1] --\ +[impl-track-2] --+--> [integration-gate] +[impl-track-3] --/ + +tracks parallèles: pas de depends_on entre elles +integration gate: attend toutes les tracks avant de vérifier +``` + +--- + +## Règle 5 — Quand utiliser un subagent / worker + +| Cas | Utiliser subagent ? | +|-----|---------------------| +| Exploration de codebase inconnu | Oui — lecture en profondeur | +| Tâches indépendantes sur fichiers différents | Oui — exécution parallèle | +| Code review adversarial | Oui — regard neuf | +| Décision de routing elle-même | Non — agent principal | + +### Anti-patterns + +- **Ne PAS utiliser un subagent pour un Audit exhaustif** — la lecture directe dans le contexte principal garantit 100% de coverage +- **Ne PAS créer un team plan pour une tâche < 1h** — le overhead de coordination dépasse le gain + +--- + +## Règle 6 — Checklist de routing avant de commencer + +``` +1. Taille du périmètre ? (__ tokens / __ fichiers) +2. Intent ? (lookup | understanding | review | audit | direct-edit) +3. Mode ? + [ ] Directe (< 50k tokens, audit < scope) + [ ] Subagent (< 150k tokens, exploration ciblée) + [ ] Team plan (> 150k tokens ou ≥ 3 tracks) +4. Si team plan: tracks ? dépendances ? verifiers ? +5. Si directe: coverage attendu ? (% fichiers / tokens) +``` + +--- + +## Exemples concrets (projets d'Erwan) + +| Projet | Taille | Intent | Mode | +|--------|--------|--------|------| +| Seno Materia (~15k LOC, Rust) | < 50k | "ajoute la GUI" | Directe | +| VECTORA (~100k LOC, Rust) | 50k-150k | "analyse graph-engine" | Subagent + lecture ciblée | +| Seno DAW (~300k LOC, C++/Rust) | > 150k | "audit thread safety audio" | Team plan | +| HireLens (~20k LOC, Rust) | < 50k | "ajoute provider Ollama" | Directe | +| AI Native Dev Stack (~2k LOC, Python) | < 50k | "universalistion hooks" | Directe | + +--- + +## Annexe — Seuils LOC pour la qualité (depuis CLAUDE.md) + +| Métrique | Cible | Alerte | Bloquant | +|----------|-------|--------|----------| +| LOC / fichier | ≤ 500 | > 800 | > 1500 | +| LOC / fonction | ≤ 50 | > 100 | > 200 | +| Complexité cyclomatique | ≤ 10 | > 15 | > 25 | +| LOC / PR | ≤ 400 | — | Découper | + +--- + +*Source: adapté de `C:\Users\barat\.claude\CLAUDE.md` § "Règle — Subagent vs lecture directe (v2.1)"* +*Dernière mise à jour: 2026-06-15* diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..077feb0 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,57 @@ +# Scripts — Utilities AI Native Dev Stack + +## vault_sync_once_daily.ps1 + +Synchronise le vault Obsidian une fois par jour (première session). + +**Usage:** +```powershell +# Lancer au démarrage de session +pwsh -NoProfile -File vault_sync_once_daily.ps1 +``` + +**Sorties:** +- `✅ déjà synchronisé aujourd'hui` → continuer sans commentaire +- `✅ synchronisé (YYYY-MM-DD)` → première session du jour +- `⚠️ DIVERGENCE` → signaler et attendre instruction + +**Intégration CLAUDE.md global:** +Ajouter dans `~/.claude/CLAUDE.md` (section début de session): +```powershell +pwsh -NoProfile -File 'D:/App/ai-native-dev-stack/scripts/vault_sync_once_daily.ps1' +``` + +## vault_sync.ps1 + +Script de sync effectif (appelé par vault_sync_once_daily.ps1). +État actuel: placeholder — le vault `IA_Dev_Brain` est local uniquement (hors Git). + +## loc_gate.ps1 + +Gate LOC pour fichiers source — vérifie contre les seuils CLAUDE.md. + +**Usage:** +```powershell +# Check un fichier +pwsh -NoProfile -File scripts/loc_gate.ps1 -FilePath "src/main.rs" + +# Check fichiers git staged +pwsh -NoProfile -File scripts/loc_gate.ps1 -Staged + +# Check tous les fichiers source (scan) +pwsh -NoProfile -File scripts/loc_gate.ps1 +``` + +**Seuils:** +| Condition | Action | +|-----------|--------| +| > 500 LOC (nouveau) | Warning — proposer décomposition | +| > 800 LOC (existant) | Warning — proposer extraction | +| > 1500 LOC | **ERROR** — refactoring obligatoire | + +**Intégration pre-commit:** +```powershell +# Dans .git/hooks/pre-commit +pwsh -NoProfile -File D:/App/ai-native-dev-stack/scripts/loc_gate.ps1 -Staged +if ($LASTEXITCODE -ne 0) { exit 1 } +``` diff --git a/scripts/loc_gate.ps1 b/scripts/loc_gate.ps1 new file mode 100644 index 0000000..41070f8 --- /dev/null +++ b/scripts/loc_gate.ps1 @@ -0,0 +1,97 @@ +# loc_gate.ps1 — LOC Gate pour PreToolUse / pre-commit +# Vérifie la taille des fichiers source contre les seuils CLAUDE.md +# Seuils: +# > 500 LOC (nouveau fichier) → WARNING +# > 800 LOC (fichier existant) → WARNING +# > 1500 LOC (tout fichier) → ERROR (bloquant) + +param( + [Parameter(Mandatory=$false)] + [string]$FilePath, + + [Parameter(Mandatory=$false)] + [switch]$Staged # Check git staged files +) + +$THRESHOLDS = @{ + NEW_WARNING = 500 + EXISTING_WARNING = 800 + BLOCKING = 1500 +} + +$EXTENSIONS = @("*.py", "*.rs", "*.cpp", "*.c", "*.h", "*.hpp", "*.ts", "*.js", "*.go", "*.cs", "*.fs", "*.swift") +$EXCLUDE_DIRS = @("node_modules", ".git", "target", "build", "dist", "vendor", ".venv", "venv", "__pycache__", ".cache") + +function Get-LineCount { + param([string]$Path) + if (-not (Test-Path $Path)) { return 0 } + try { + (Get-Content $Path -ErrorAction SilentlyContinue | Measure-Object -Line).Lines + } catch { 0 } +} + +function Test-InExcludeDir { + param([string]$Dir) + foreach ($ex in $EXCLUDE_DIRS) { + if ($Dir -match "[/\\]$ex[/\\]?$") { return $true } + } + $false +} + +function Invoke-GateCheck { + param([string]$Path) + + $absPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) + $exists = Test-Path $absPath + $lines = Get-LineCount $absPath + $isNew = -not $exists + + if ($lines -gt $THRESHOLDS.BLOCKING) { + Write-Error "[LOC GATE] BLOQUANT: $absPath ($lines LOC > $($THRESHOLDS.BLOCKING)) — Refactoring obligatoire." + return $false + } + + if ($isNew -and $lines -gt $THRESHOLDS.NEW_WARNING) { + Write-Warning "[LOC GATE] WARNING: nouveau fichier $absPath ($lines LOC > $($THRESHOLDS.NEW_WARNING)) — proposer décomposition." + return $true # Warning seulement + } + + if ($exists -and $lines -gt $THRESHOLDS.EXISTING_WARNING) { + Write-Warning "[LOC GATE] WARNING: fichier existant $absPath ($lines LOC > $($THRESHOLDS.EXISTING_WARNING)) — proposer extraction." + return $true + } + + return $true # Pass +} + +$global:blocked = $false + +if ($Staged) { + # Check git staged files + try { + $staged = git diff --cached --name-only --diff-filter=ACM 2>$null + if ($staged) { + foreach ($f in $staged) { + $isExcluded = $false + foreach ($dir in $EXCLUDE_DIRS) { + if ($f -match $dir) { $isExcluded = $true; break } + } + if ($isExcluded) { continue } + if (-not (Invoke-GateCheck $f)) { $global:blocked = $true } + } + } + } catch { } +} elseif ($FilePath) { + if (-not (Invoke-GateCheck $FilePath)) { $global:blocked = $true } +} else { + # Check tous les fichiers source + foreach ($ext in $EXTENSIONS) { + Get-ChildItem -Recurse -Include $ext -File | ForEach-Object { + if (-not (Test-InExcludeDir $_.DirectoryName)) { + if (-not (Invoke-GateCheck $_.FullName)) { $global:blocked = $true } + } + } + } +} + +if ($global:blocked) { exit 1 } else { exit 0 } diff --git a/scripts/vault_sync.ps1 b/scripts/vault_sync.ps1 new file mode 100644 index 0000000..ff7637e --- /dev/null +++ b/scripts/vault_sync.ps1 @@ -0,0 +1,20 @@ +# vault_sync.ps1 +# Sync vault Obsidian: pull si modifications, push si divergence +# Copié depuis D:\scripts\vault_sync.ps1 +# IMPORTANT: le vault D:\Documents\Obsidian\IA_Dev_Brain est HORS GIT +# Ce script est préparé pour le cas où le vault serait synchronisé via Git +# Configuration actuelle: vault local uniquement (pas de sync Git) + +$VAULT_PATH = "D:\Documents\Obsidian\IA_Dev_Brain" +$LAST_SYNC = Join-Path $PSScriptRoot "vault_last_sync_date.txt" + +if (-not (Test-Path $VAULT_PATH)) { + Write-Host "vault: IA_Dev_Brain introuvable — skip" + exit 0 +} + +# État actuel: vault local uniquement, pas de Git sync +# Ce script est un placeholder pour le moment +# TODO: intégrer Obsidian Git plugin ou script de backup automatique +Write-Host "vault: sync vault local ($VAULT_PATH) — operation non configuree (vault hors Git)" +exit 0 diff --git a/scripts/vault_sync_once_daily.ps1 b/scripts/vault_sync_once_daily.ps1 new file mode 100644 index 0000000..10700e7 --- /dev/null +++ b/scripts/vault_sync_once_daily.ps1 @@ -0,0 +1,28 @@ +# vault_sync_once_daily.ps1 +# Sync vault Obsidian une fois par jour (première session de la journee) +# Copie depuis D:\scripts\vault_sync_once_daily.ps1 +# Chemin du vault: D:\Documents\Obsidian\IA_Dev_Brain + +$SCRIPT_DIR = Split-Path -Parent $MyInvocation.MyCommand.Path +$SCRIPT_SYNC = Join-Path $SCRIPT_DIR "vault_sync.ps1" +$SENTINEL = Join-Path $SCRIPT_DIR "vault_last_sync_date.txt" +$TODAY = (Get-Date -Format "yyyy-MM-dd") + +# Skip si deja synchronise aujourd'hui +if ((Test-Path $SENTINEL) -and ((Get-Content $SENTINEL -Raw).Trim() -eq $TODAY)) { + Write-Host "vault: deja synchronise aujourd'hui ($TODAY) — skip" + exit 0 +} + +# Premiere session de la journee → sync +if (Test-Path $SCRIPT_SYNC) { + & $SCRIPT_SYNC +} else { + Write-Host "vault: vault_sync.ps1 introuvable — skip" + exit 1 +} + +# Marquer la date apres sync reussi +Set-Content -Path $SENTINEL -Value $TODAY +Write-Host "vault: synchronise ($TODAY)" +exit 0