commit c77673340239c3a15fc5923d50f16498d7d940b3 Author: Eric Coissac Date: Thu Mar 26 12:23:54 2026 +0100 feat: add Claude Code orchestration setup with MCP and subagents Add comprehensive Claude Code configuration for Rust/Go projects, including: - CLAUDE.md with delegation heuristics and guardrails - Subagents: qwen3-worker (LM Studio), code-reviewer, task-planner - Skills for delegation rules and Rust/Go conventions - Hooks: post-write-lint, pre-bash-guard, subagent-stop-log - MCP server (stdio) to interface with LM Studio - README and technical report documenting the architecture diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 0000000..1bc7420 --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,26 @@ +# Projet CrazyClaude + +## Stack +- Rust (edition 2024), Go 1.24 +- Tests : cargo test, go test +- Lint : clippy (Rust), golangci-lint (Go) + +## Heuristiques de délégation + +Délègue à `qwen3-worker` si la tâche est **atomique** et satisfait TOUS les critères : +- ≤ 3 fichiers concernés +- Pas de modification d'API publique +- Pas de dépendance externe non encore importée +- Tâches typiques : génération de tests unitaires, reformatage, documentation inline, + conversion de types simples, scaffolding de stubs + +Traite toi-même si : +- Refactoring cross-module +- Conception d'architecture +- Debugging avec contexte multi-fichiers +- Modification de traits/interfaces publics + +## Garde-fous absolus +- Ne jamais passer `rm -rf` sans confirmation explicite +- Ne jamais committer sans que les tests passent +- Toujours vérifier `git diff` avant un commit diff --git a/.claude/agents/code-reviewer.md b/.claude/agents/code-reviewer.md new file mode 100644 index 0000000..b27cc7c --- /dev/null +++ b/.claude/agents/code-reviewer.md @@ -0,0 +1,21 @@ +--- +name: code-reviewer +description: > + Révise du code Rust ou Go pour détecter bugs, régressions, problèmes de style. + Utilise après génération par qwen3-worker ou avant un commit. +tools: [Read, Grep, Glob, Bash] +model: claude-haiku-4-5-20251001 +--- + +Tu es un reviewer expérimenté en Rust et Go. + +Pour chaque fichier fourni : +1. Vérifie la cohérence des types et la gestion d'erreurs. +2. Identifie les patterns non idiomatiques. +3. Signale les tests manquants pour les chemins critiques. + +Format de sortie : +``` +VERDICT: approved|changes_requested +ISSUES: liste numérotée (vide si approved) +``` diff --git a/.claude/agents/qwen3-worker.md b/.claude/agents/qwen3-worker.md new file mode 100644 index 0000000..846f3ab --- /dev/null +++ b/.claude/agents/qwen3-worker.md @@ -0,0 +1,30 @@ +--- +name: qwen3-worker +description: > + Délègue à Qwen3-Coder via LM Studio les tâches atomiques sur 1-3 fichiers : + génération de tests unitaires, documentation inline, scaffolding de stubs, + reformatage de code. N'utilise PAS pour du refactoring cross-module ou de + la conception d'architecture. +tools: [Read, Write, Bash] +model: inherit +--- + +Tu es un assistant de codage spécialisé exécutant des tâches courtes et précises. + +## Comportement +1. Lis les fichiers nécessaires avec l'outil Read (ne reçois pas le contenu en entrée). +2. Effectue la transformation demandée. +3. Écris le résultat avec Write. +4. Exécute le linter approprié (clippy pour Rust, golangci-lint pour Go) et corrige + les erreurs éventuelles (max 2 tentatives). +5. Retourne un résumé : fichiers modifiés, changements effectués, résultat du lint. + +## Format de sortie +``` +STATUS: success|partial|failure +FILES_MODIFIED: liste des fichiers +SUMMARY: description des changements +LINT: passed|failed (+ détail si failed) +``` + +Ne génère pas de fonctions non demandées. Ne modifie pas les signatures publiques. diff --git a/.claude/agents/task-planner.md b/.claude/agents/task-planner.md new file mode 100644 index 0000000..b03b54c --- /dev/null +++ b/.claude/agents/task-planner.md @@ -0,0 +1,25 @@ +--- +name: task-planner +description: > + Planifie des tâches complexes impliquant plusieurs modules ou une refonte architecturale. + Utilise pour décomposer une demande en étapes atomiques avant exécution. +tools: [Read, Grep, Glob] +model: claude-opus-4-6 +--- + +Tu es un architecte logiciel expérimenté en Rust et Go. + +Pour la demande reçue : +1. Analyse le périmètre et les dépendances (lis les fichiers pertinents). +2. Décompose en étapes atomiques ordonnées. +3. Identifie lesquelles sont délégables à `qwen3-worker` (≤ 3 fichiers, sans changement d'API publique). +4. Estime les risques et propose des points de contrôle. + +Format de sortie : +``` +PLAN: + 1. [étape] — délégable: oui|non — raison + 2. ... +RISQUES: liste +POINTS_DE_CONTRÔLE: liste +``` diff --git a/.claude/hooks/post-write-lint.sh b/.claude/hooks/post-write-lint.sh new file mode 100755 index 0000000..62e2d96 --- /dev/null +++ b/.claude/hooks/post-write-lint.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# Lint automatique après écriture de fichier +FILE=$(echo "$CLAUDE_TOOL_INPUT" | python3 -c "import json,sys; print(json.load(sys.stdin).get('file_path',''))") + +if [[ "$FILE" == *.rs ]]; then + cargo clippy --quiet 2>&1 | tail -20 +elif [[ "$FILE" == *.go ]]; then + golangci-lint run "$FILE" 2>&1 | tail -20 +fi diff --git a/.claude/hooks/pre-bash-guard.sh b/.claude/hooks/pre-bash-guard.sh new file mode 100755 index 0000000..5050ab4 --- /dev/null +++ b/.claude/hooks/pre-bash-guard.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# Bloque les commandes destructives sans confirmation +CMD=$(echo "$CLAUDE_TOOL_INPUT" | python3 -c "import json,sys; print(json.load(sys.stdin).get('command',''))") + +if echo "$CMD" | grep -qE 'rm -rf|DROP TABLE|git push --force'; then + echo "BLOQUÉ : commande dangereuse détectée. Confirmez explicitement." + exit 2 # exit 2 = deny dans Claude Code +fi diff --git a/.claude/hooks/subagent-stop-log.sh b/.claude/hooks/subagent-stop-log.sh new file mode 100755 index 0000000..66aa391 --- /dev/null +++ b/.claude/hooks/subagent-stop-log.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# Observabilité : log chaque fin de subagent +TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") +PAYLOAD=$(cat) +AGENT=$(echo "$PAYLOAD" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('agent_name','unknown'))" 2>/dev/null) +echo "$TIMESTAMP agent=$AGENT" >> .claude/logs/subagent-decisions.log diff --git a/.claude/mcp/qwen3-mcp/agent_lm.py b/.claude/mcp/qwen3-mcp/agent_lm.py new file mode 100644 index 0000000..344e553 --- /dev/null +++ b/.claude/mcp/qwen3-mcp/agent_lm.py @@ -0,0 +1,182 @@ +#!/usr/bin/env -S .claude/venv/bin/python3 +""" +Agent loop local (~120 lignes) pour interagir avec LM Studio via l'API OpenAI-compatible. +Usage : python3 .claude/mcp/qwen3-mcp/agent_lm.py --task "..." --files file1.py file2.go +""" +import argparse +import json +import os +import sys +from pathlib import Path + +import requests + +LMSTUDIO_URL = "http://localhost:1248/v1/chat/completions" +LMSTUDIO_MODELS_URL = "http://localhost:1248/v1/models" +DEFAULT_MODEL = "qwen/qwen3-coder-next" + +SYSTEM_PROMPT = """Tu es un assistant de codage spécialisé. Tu effectues des tâches atomiques sur des fichiers source. +Règles : +- Ne modifie que ce qui est demandé +- Ne change pas les signatures publiques (traits Rust, interfaces Go exportées) +- Retourne uniquement le code, sans explication ni markdown +- Si tu ne peux pas accomplir la tâche, réponds avec: ERROR: +""" + + +def list_models() -> list[str]: + """Retourne la liste des modèles disponibles dans LM Studio.""" + try: + resp = requests.get(LMSTUDIO_MODELS_URL, timeout=10) + resp.raise_for_status() + return [m["id"] for m in resp.json().get("data", [])] + except requests.RequestException as e: + print(f"Impossible de contacter LM Studio : {e}", file=sys.stderr) + return [] + + +def read_files(paths: list[str]) -> str: + """Lit les fichiers et les formate pour le prompt.""" + context = "" + for path in paths: + try: + content = Path(path).read_text(encoding="utf-8") + context += f"=== {path} ===\n{content}\n\n" + except OSError as e: + context += f"=== {path} === ERREUR: {e}\n\n" + return context + + +def call_lmstudio(task: str, file_context: str, model: str) -> str: + """Envoie la tâche à LM Studio et retourne la réponse.""" + user_message = f"TÂCHE: {task}\n\nFICHIERS:\n{file_context}" if file_context else f"TÂCHE: {task}" + + payload = { + "model": model, + "messages": [ + {"role": "system", "content": SYSTEM_PROMPT}, + {"role": "user", "content": user_message}, + ], + "temperature": 0.2, + "max_tokens": 4096, + } + + try: + resp = requests.post(LMSTUDIO_URL, json=payload, timeout=120) + resp.raise_for_status() + return resp.json()["choices"][0]["message"]["content"] + except requests.Timeout: + return "ERROR: LM Studio timeout (>120s)" + except requests.RequestException as e: + return f"ERROR: {e}" + except (KeyError, IndexError) as e: + return f"ERROR: réponse inattendue de LM Studio : {e}" + + +def write_file(path: str, content: str) -> bool: + """Écrit le contenu dans un fichier, crée les répertoires si nécessaire.""" + try: + Path(path).parent.mkdir(parents=True, exist_ok=True) + Path(path).write_text(content, encoding="utf-8") + return True + except OSError as e: + print(f"Erreur écriture {path}: {e}", file=sys.stderr) + return False + + +def run_lint(file_path: str) -> tuple[bool, str]: + """Lance le linter approprié selon l'extension.""" + import subprocess + + if file_path.endswith(".rs"): + result = subprocess.run( + ["cargo", "clippy", "--quiet"], + capture_output=True, text=True, timeout=60 + ) + elif file_path.endswith(".go"): + result = subprocess.run( + ["golangci-lint", "run", file_path], + capture_output=True, text=True, timeout=60 + ) + else: + return True, "no linter for this file type" + + passed = result.returncode == 0 + output = (result.stdout + result.stderr).strip() + return passed, output + + +def agent_loop(task: str, input_files: list[str], output_file: str | None, model: str, max_retries: int = 2) -> dict: + """Boucle principale : génère, écrit, lint, corrige (max_retries fois).""" + file_context = read_files(input_files) if input_files else "" + result = {"status": "failure", "files_modified": [], "summary": "", "lint": "skipped"} + + for attempt in range(max_retries + 1): + response = call_lmstudio(task, file_context, model) + + if response.startswith("ERROR:"): + result["summary"] = response + break + + target = output_file or (input_files[0] if input_files else None) + if not target: + result["status"] = "success" + result["summary"] = response + result["lint"] = "skipped (no output file)" + break + + if write_file(target, response): + result["files_modified"] = [target] + + lint_ok, lint_output = run_lint(target) + result["lint"] = "passed" if lint_ok else f"failed: {lint_output[:500]}" + + if lint_ok: + result["status"] = "success" + result["summary"] = f"Attempt {attempt + 1}: task completed successfully" + break + elif attempt < max_retries: + task = f"{task}\n\nCORRECTION REQUISE (tentative {attempt + 1}):\n{lint_output}" + file_context = read_files([target]) + else: + result["status"] = "partial" + result["summary"] = f"Lint failed after {max_retries + 1} attempts" + + return result + + +def main(): + parser = argparse.ArgumentParser(description="Agent loop local pour LM Studio") + parser.add_argument("--task", required=True, help="Description de la tâche") + parser.add_argument("--files", nargs="*", default=[], help="Fichiers source à lire") + parser.add_argument("--output", help="Fichier de sortie (défaut: premier fichier input)") + parser.add_argument("--model", default=DEFAULT_MODEL, help="Modèle LM Studio") + parser.add_argument("--list-models", action="store_true", help="Liste les modèles disponibles") + parser.add_argument("--json", action="store_true", dest="json_output", help="Sortie JSON") + args = parser.parse_args() + + if args.list_models: + models = list_models() + if models: + print("Modèles disponibles :") + for m in models: + print(f" - {m}") + else: + print("Aucun modèle trouvé ou LM Studio inaccessible.") + return + + result = agent_loop(args.task, args.files, args.output, args.model) + + if args.json_output: + print(json.dumps(result, ensure_ascii=False, indent=2)) + else: + print(f"STATUS: {result['status']}") + print(f"FILES_MODIFIED: {', '.join(result['files_modified']) or 'none'}") + print(f"SUMMARY: {result['summary']}") + print(f"LINT: {result['lint']}") + + sys.exit(0 if result["status"] == "success" else 1) + + +if __name__ == "__main__": + main() diff --git a/.claude/mcp/qwen3-mcp/requirements.txt b/.claude/mcp/qwen3-mcp/requirements.txt new file mode 100644 index 0000000..0eb8cae --- /dev/null +++ b/.claude/mcp/qwen3-mcp/requirements.txt @@ -0,0 +1 @@ +requests>=2.31.0 diff --git a/.claude/mcp/qwen3-mcp/server.py b/.claude/mcp/qwen3-mcp/server.py new file mode 100644 index 0000000..88f3f72 --- /dev/null +++ b/.claude/mcp/qwen3-mcp/server.py @@ -0,0 +1,104 @@ +#!/usr/bin/env -S .claude/venv/bin/python3 +""" +MCP server stdio exposant un outil `qwen3_task`. +Appelé par Claude Code via : claude mcp add --transport stdio qwen3 -- .claude/venv/bin/python3 .claude/mcp/qwen3-mcp/server.py +""" +import sys +import json +import requests + +LMSTUDIO_URL = "http://localhost:1248/v1/chat/completions" +QWEN3_MODEL = "qwen/qwen3-coder-next" + +TOOLS = [{ + "name": "qwen3_task", + "description": "Délègue une tâche de codage atomique à Qwen3-Coder via LM Studio.", + "inputSchema": { + "type": "object", + "properties": { + "task": {"type": "string", "description": "Description précise de la tâche"}, + "files": {"type": "array", "items": {"type": "string"}, "description": "Chemins des fichiers concernés"} + }, + "required": ["task"] + } +}] + + +def send(obj: dict): + print(json.dumps(obj), flush=True) + + +def call_qwen3(task: str, files: list[str]) -> str: + context = "" + for path in files: + try: + with open(path) as f: + context += f"--- {path} ---\n{f.read()}\n\n" + except OSError as e: + context += f"--- {path} --- ERREUR: {e}\n\n" + + prompt = f"""Tu es un assistant de codage. Effectue la tâche suivante de manière précise. + +TÂCHE: {task} + +FICHIERS: +{context} + +Retourne uniquement le code modifié ou généré, sans explication. +""" + resp = requests.post( + LMSTUDIO_URL, + json={ + "model": QWEN3_MODEL, + "messages": [{"role": "user", "content": prompt}], + "temperature": 0.2, + "max_tokens": 4096, + }, + timeout=120, + ) + resp.raise_for_status() + return resp.json()["choices"][0]["message"]["content"] + + +def handle(req: dict): + method = req.get("method", "") + req_id = req.get("id") + + if method == "initialize": + send({ + "jsonrpc": "2.0", + "id": req_id, + "result": { + "protocolVersion": "2024-11-05", + "capabilities": {"tools": {}}, + "serverInfo": {"name": "qwen3-mcp", "version": "1.0.0"}, + }, + }) + elif method == "notifications/initialized": + pass # notification, pas de réponse + elif method == "tools/list": + send({"jsonrpc": "2.0", "id": req_id, "result": {"tools": TOOLS}}) + elif method == "tools/call": + params = req.get("params", {}) + name = params.get("name", "") + args = params.get("arguments", {}) + if name == "qwen3_task": + try: + text = call_qwen3(args["task"], args.get("files", [])) + send({"jsonrpc": "2.0", "id": req_id, "result": {"content": [{"type": "text", "text": text}]}}) + except Exception as e: + send({"jsonrpc": "2.0", "id": req_id, "result": {"content": [{"type": "text", "text": f"ERROR: {e}"}], "isError": True}}) + else: + send({"jsonrpc": "2.0", "id": req_id, "error": {"code": -32601, "message": f"Outil inconnu : {name}"}}) + elif req_id is not None: + send({"jsonrpc": "2.0", "id": req_id, "error": {"code": -32601, "message": f"Méthode inconnue : {method}"}}) + + +for line in sys.stdin: + line = line.strip() + if not line: + continue + try: + handle(json.loads(line)) + except Exception as e: + print(json.dumps({"error": str(e)}), file=sys.stderr, flush=True) diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..b20112b --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,58 @@ +{ + "hooks": { + "PostToolUse": [ + { + "matcher": "Write", + "hooks": [ + { + "type": "command", + "command": "bash .claude/hooks/post-write-lint.sh", + "async": true + } + ] + } + ], + "SubagentStop": [ + { + "matcher": ".*", + "hooks": [ + { + "type": "command", + "command": "bash .claude/hooks/subagent-stop-log.sh" + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "bash .claude/hooks/pre-bash-guard.sh" + } + ] + } + ] + }, + "permissions": { + "allow": [ + "Bash(cargo:*)", + "Bash(go:*)", + "Bash(git diff*)", + "Bash(git add*)", + "Bash(git commit*)", + "Bash(curl*)", + "Bash(chmod*)", + "Read(**/*.rs)", + "Read(**/*.go)", + "Write(**/*.rs)", + "Write(**/*.go)" + ], + "deny": [ + "Bash(rm -rf*)", + "Bash(curl * | bash*)", + "Bash(git push --force*)" + ] + } +} diff --git a/.claude/skills/delegation-rules.md b/.claude/skills/delegation-rules.md new file mode 100644 index 0000000..abfbf93 --- /dev/null +++ b/.claude/skills/delegation-rules.md @@ -0,0 +1,32 @@ +--- +name: delegation-rules +description: Règles pour décider quand et comment déléguer à qwen3-worker ou qwen3 MCP +--- + +# Règles de Délégation à Qwen3 + +## Critères de délégation (TOUS requis) +- ≤ 3 fichiers source concernés +- Tâche atomique, sans dépendance à résoudre +- Pas de modification de signature publique (trait, interface exportée) +- Contexte de fichier estimé < 8 000 tokens + +## Tâches typiques éligibles +- Génération de tests unitaires pour une fonction donnée +- Ajout de documentation (doc comments Rust, godoc Go) +- Scaffolding de stubs à partir d'une interface +- Conversion de types internes simples +- Reformatage / réorganisation d'imports + +## Procédure +1. Évalue les critères ci-dessus. +2. Si éligible → utilise le subagent `qwen3-worker` en précisant la tâche et les fichiers. +3. Attends le résumé de sortie (STATUS / FILES_MODIFIED / LINT). +4. Si STATUS=failure → prends en charge toi-même. +5. Si STATUS=partial → corrige les points restants directement. + +## Ne jamais déléguer +- Refactoring impliquant > 3 fichiers +- Changement d'architecture ou de design pattern +- Debugging avec stacktrace à analyser +- Tâches nécessitant une recherche web ou du contexte externe diff --git a/.claude/skills/rust-go-conventions.md b/.claude/skills/rust-go-conventions.md new file mode 100644 index 0000000..b6a6341 --- /dev/null +++ b/.claude/skills/rust-go-conventions.md @@ -0,0 +1,25 @@ +--- +name: rust-go-conventions +description: Conventions de style et patterns idiomatiques Rust (edition 2024) et Go 1.24 pour ce projet +--- + +# Conventions Rust / Go + +## Rust (edition 2024) +- Utilise `thiserror` pour les types d'erreur publics, `anyhow` dans les binaires. +- Préfère `?` à `unwrap()` sauf dans les tests. +- Les `pub` structs doivent avoir des doc comments (`///`). +- Nomme les types de retour complexes avec des type aliases. +- `clippy::pedantic` activé — corrige tous les lints avant commit. + +## Go 1.24 +- Gestion d'erreurs : toujours `if err != nil { return ..., err }`, jamais `_`. +- Interfaces en minuscule si non exportées. +- Docstring obligatoire pour toute fonction exportée (commence par le nom de la fonction). +- Utilise `errors.Is` / `errors.As` pour comparer les erreurs. +- `golangci-lint` avec preset `default` + `gocritic`. + +## Tests +- Rust : tests unitaires dans le même fichier (`#[cfg(test)]`), intégration dans `tests/`. +- Go : fichiers `_test.go`, table-driven tests avec `t.Run`. +- Couverture cible : 80 % sur les chemins critiques. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e4b5c35 --- /dev/null +++ b/README.md @@ -0,0 +1,153 @@ +# CrazyClaude + +Configuration Claude Code pour orchestrer des tâches de développement Rust/Go en déléguant les tâches atomiques à **Qwen3-Coder** via LM Studio. + +Ce dépôt est conçu pour être intégré comme sous-répertoire `.claude/` dans n'importe quel projet. Il fournit agents, hooks, skills et un serveur MCP prêts à l'emploi. + +Pour le détail de l'architecture, voir [`rapport-orchestration-claude-code.md`](./rapport-orchestration-claude-code.md). + +--- + +## Prérequis + +- [Claude Code](https://claude.ai/code) installé (`claude` disponible dans le PATH) +- [LM Studio](https://lmstudio.ai/) avec le modèle `qwen/qwen3-coder-next` chargé, écoutant sur `http://localhost:1248` +- Python 3.11+ (pour le serveur MCP) +- Outils selon votre stack : `cargo` + `clippy` (Rust), `golangci-lint` (Go) + +--- + +## Installation dans un projet existant + +### 1. Cloner ce dépôt + +Avec **Jujutsu** : + +```sh +# Depuis la racine de votre projet +jj git clone .claude-crazy +``` + +Puis déplacez ou liez le contenu : + +```sh +# Copier le répertoire .claude dans votre projet +cp -r .claude-crazy/.claude ./.claude + +# Ou, si vous préférez garder une référence au dépôt source : +# utilisez jj workspace add pour ajouter un workspace dédié +``` + +> Si vous utilisez git en sous-jacent avec jj (`jj git init --colocate`), +> vous pouvez aussi ajouter ce dépôt comme subtree : +> +> ```sh +> git subtree add --prefix .claude main --squash +> ``` + +### 2. Créer le virtualenv Python + +```sh +cd .claude +python3 -m venv venv +venv/bin/pip install -r mcp/qwen3-mcp/requirements.txt +``` + +### 3. Enregistrer le serveur MCP + +```sh +claude mcp add --transport stdio qwen3 -- .claude/venv/bin/python3 .claude/mcp/qwen3-mcp/server.py +``` + +Vérifier que la connexion est établie : + +```sh +claude mcp list +# qwen3: ... - ✓ Connected +``` + +### 4. Adapter CLAUDE.md à votre projet + +Éditez `.claude/CLAUDE.md` : mettez à jour la section `## Stack` et les heuristiques de délégation selon vos besoins. + +### 5. Rendre les hooks exécutables + +```sh +chmod +x .claude/hooks/*.sh +``` + +--- + +## Structure + +``` +.claude/ +├── CLAUDE.md # contexte projet + heuristiques de délégation +├── settings.json # hooks et permissions +├── agents/ +│ ├── qwen3-worker.md # délégation vers LM Studio (Qwen3) +│ ├── code-reviewer.md # révision code (Claude Haiku) +│ └── task-planner.md # planification complexe (Claude Opus) +├── skills/ +│ ├── delegation-rules.md # quand déléguer à Qwen3 +│ └── rust-go-conventions.md # conventions Rust/Go +├── hooks/ +│ ├── post-write-lint.sh # lint automatique après écriture +│ ├── subagent-stop-log.sh # log des décisions de délégation +│ └── pre-bash-guard.sh # blocage des commandes dangereuses +├── mcp/ +│ └── qwen3-mcp/ +│ ├── server.py # serveur MCP stdio (interface LM Studio) +│ ├── agent_lm.py # agent loop CLI autonome +│ └── requirements.txt +├── logs/ # produits par les hooks (ignorés par jj/git) +└── venv/ # virtualenv Python (ignoré par jj/git) +``` + +--- + +## Configuration LM Studio + +Le serveur MCP se connecte sur `http://localhost:1248` avec le modèle `qwen/qwen3-coder-next`. + +Pour modifier ces valeurs, éditez `.claude/mcp/qwen3-mcp/server.py` : + +```python +LMSTUDIO_URL = "http://localhost:1248/v1/chat/completions" +QWEN3_MODEL = "qwen/qwen3-coder-next" +``` + +--- + +## Fichiers à ignorer (jj / git) + +Ajoutez ces entrées dans votre fichier d'ignore : + +``` +.claude/venv/ +.claude/logs/ +``` + +Avec Jujutsu : + +```sh +echo ".claude/venv/" >> .gitignore +echo ".claude/logs/" >> .gitignore +``` + +--- + +## Mise à jour + +Avec Jujutsu (workflow colocalisé git) : + +```sh +git subtree pull --prefix .claude main --squash +``` + +Ou si vous avez cloné séparément, tirez les changements puis recopiez : + +```sh +cd .claude-crazy && jj git fetch && jj new main +cp -r .claude/* ../.claude/ +``` diff --git a/rapport-orchestration-claude-code.md b/rapport-orchestration-claude-code.md new file mode 100644 index 0000000..1e2e1b8 --- /dev/null +++ b/rapport-orchestration-claude-code.md @@ -0,0 +1,507 @@ +# Rapport Technique : Orchestration Robuste avec Claude Code et Délégation à un LLM Local + +**Auteur** : Eric Coissac +**Date** : 26 mars 2026 +**Contexte** : Ce document décrit une architecture robuste exploitant les mécanismes natifs de **Claude Code** (subagents, hooks, skills, MCP) pour déléguer des tâches à faible complexité à **Qwen3-Coder** (via LM Studio), tout en maintenant Claude Code comme orchestrateur central. + +--- + +## 1. Pourquoi cette architecture — et ce qu'elle n'est *pas* + +Le document original décrivait un système où Claude Code appelait un script Python externe (`delegate_to_qwen.py`) pour piloter Qwen3. C'est un anti-pattern : cela court-circuite les mécanismes natifs de Claude Code et réintroduit de la complexité là où l'écosystème propose déjà des solutions mieux intégrées. + +En mars 2026, Claude Code dispose de cinq systèmes fondamentaux : + +| Système | Rôle | +|---------|------| +| **CLAUDE.md** | Contexte permanent du projet (règles, conventions, heuristiques) | +| **Skills** | Instructions chargées à la demande selon la pertinence | +| **Subagents** | Agents spécialisés avec fenêtre de contexte isolée, outils restreints, modèle configurable | +| **Hooks** | Scripts shell ou prompts LLM déclenchés sur des événements du cycle de vie | +| **MCP servers** | Extensions vers des outils et services externes via protocole standardisé | + +La délégation à Qwen3 s'intègre naturellement via un **subagent pointant vers LM Studio** ou via un **MCP server stdio minimal**. Le script Python devient optionnel — il reste utile comme wrapper si LM Studio n'expose pas d'interface MCP, mais il ne doit pas être l'orchestrateur. + +--- + +## 2. Architecture cible + +``` +Utilisateur + │ + ▼ +Claude Code (orchestrateur) + │ lit CLAUDE.md au démarrage + │ charge les skills pertinents à la demande + │ + ├─── Subagent : qwen3-worker ──────► LM Studio (localhost:1234) + │ contexte isolé modèle Qwen3-Coder + │ outils : Read, Write, Bash + │ + ├─── Subagent : code-reviewer ──────► Claude Haiku (coût réduit) + │ validation syntaxe/style + │ + ├─── Hooks ──────────────────────────► scripts shell + │ PostToolUse : lint auto + │ SubagentStop : log décision + │ PreToolUse : garde-fous + │ + └─── MCP servers (optionnel) + qwen3-mcp (stdio) : wrapper LM Studio +``` + +**Principe clé** : Claude Code ne lit pas les fichiers sources pour les passer à Qwen3. C'est Qwen3 (via son subagent ou son agent loop) qui lit les fichiers dont il a besoin — le contenu ne transite pas par le contexte de Claude Code. + +--- + +## 3. Structure du projet + +``` +.claude/ +├── CLAUDE.md # contexte projet + heuristiques de délégation +├── settings.json # hooks et permissions +├── agents/ +│ ├── qwen3-worker.md # subagent → LM Studio +│ ├── code-reviewer.md # subagent → Haiku (validation) +│ └── task-planner.md # subagent → Opus (planification complexe) +├── skills/ +│ ├── delegation-rules.md # quand déléguer à Qwen3 +│ └── rust-go-conventions.md # conventions spécifiques au projet +└── hooks/ + ├── post-write-lint.sh # lint automatique après écriture + ├── subagent-stop-log.sh # observabilité + └── pre-bash-guard.sh # sécurité +mcp/ +└── qwen3-mcp/ + ├── server.py # MCP server stdio (optionnel) + └── agent_lm.py # agent loop local (~120 lignes) +``` + +--- + +## 4. CLAUDE.md — contexte et heuristiques + +```markdown +# Projet [nom] + +## Stack +- Rust (edition 2024), Go 1.24 +- Tests : cargo test, go test +- Lint : clippy (Rust), golangci-lint (Go) + +## Heuristiques de délégation + +Délègue à `qwen3-worker` si la tâche est **atomique** et satisfait TOUS les critères : +- ≤ 3 fichiers concernés +- Pas de modification d'API publique +- Pas de dépendance externe non encore importée +- Tâches typiques : génération de tests unitaires, reformatage, documentation inline, + conversion de types simples, scaffolding de stubs + +Traite toi-même si : +- Refactoring cross-module +- Conception d'architecture +- Debugging avec contexte multi-fichiers +- Modification de traits/interfaces publics + +## Garde-fous absolus +- Ne jamais passer `rm -rf` sans confirmation explicite +- Ne jamais committer sans que les tests passent +- Toujours vérifier `git diff` avant un commit +``` + +--- + +## 5. Subagent `qwen3-worker` + +Fichier : `.claude/agents/qwen3-worker.md` + +```markdown +--- +name: qwen3-worker +description: > + Délègue à Qwen3-Coder via LM Studio les tâches atomiques sur 1-3 fichiers : + génération de tests unitaires, documentation inline, scaffolding de stubs, + reformatage de code. N'utilise PAS pour du refactoring cross-module ou de + la conception d'architecture. +tools: [Read, Write, Bash] +model: inherit +--- + +Tu es un assistant de codage spécialisé exécutant des tâches courtes et précises. + +## Comportement +1. Lis les fichiers nécessaires avec l'outil Read (ne reçois pas le contenu en entrée). +2. Effectue la transformation demandée. +3. Écris le résultat avec Write. +4. Exécute le linter approprié (clippy pour Rust, golangci-lint pour Go) et corrige + les erreurs éventuelles (max 2 tentatives). +5. Retourne un résumé : fichiers modifiés, changements effectués, résultat du lint. + +## Format de sortie +``` +STATUS: success|partial|failure +FILES_MODIFIED: liste des fichiers +SUMMARY: description des changements +LINT: passed|failed (+ détail si failed) +``` + +Ne génère pas de fonctions non demandées. Ne modifie pas les signatures publiques. +``` + +> **Note importante** : le champ `model: inherit` signifie que ce subagent utilise le modèle courant de la session. Pour pointer vers LM Studio, il faut soit configurer `ANTHROPIC_BASE_URL` pour ce subagent, soit utiliser le MCP server décrit en section 7. + +--- + +## 6. Hooks + +### `settings.json` + +```json +{ + "hooks": { + "PostToolUse": [ + { + "matcher": "Write", + "hooks": [ + { + "type": "command", + "command": "bash .claude/hooks/post-write-lint.sh", + "async": true + } + ] + } + ], + "SubagentStop": [ + { + "matcher": ".*", + "hooks": [ + { + "type": "command", + "command": "bash .claude/hooks/subagent-stop-log.sh" + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "bash .claude/hooks/pre-bash-guard.sh" + } + ] + } + ] + } +} +``` + +### `post-write-lint.sh` + +```bash +#!/usr/bin/env bash +# Lint automatique après écriture de fichier +FILE=$(echo "$CLAUDE_TOOL_INPUT" | python3 -c "import json,sys; print(json.load(sys.stdin).get('file_path',''))") + +if [[ "$FILE" == *.rs ]]; then + cargo clippy --quiet 2>&1 | tail -20 +elif [[ "$FILE" == *.go ]]; then + golangci-lint run "$FILE" 2>&1 | tail -20 +fi +``` + +### `subagent-stop-log.sh` + +```bash +#!/usr/bin/env bash +# Observabilité : log chaque fin de subagent +TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") +PAYLOAD=$(cat) +AGENT=$(echo "$PAYLOAD" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('agent_name','unknown'))" 2>/dev/null) +echo "$TIMESTAMP agent=$AGENT" >> .claude/logs/subagent-decisions.log +``` + +### `pre-bash-guard.sh` + +```bash +#!/usr/bin/env bash +# Bloque les commandes destructives sans confirmation +CMD=$(echo "$CLAUDE_TOOL_INPUT" | python3 -c "import json,sys; print(json.load(sys.stdin).get('command',''))") + +if echo "$CMD" | grep -qE 'rm -rf|DROP TABLE|git push --force'; then + echo "BLOQUÉ : commande dangereuse détectée. Confirmez explicitement." + exit 2 # exit 2 = deny dans Claude Code +fi +``` + +--- + +## 7. MCP Server (optionnel — si LM Studio n'est pas accessible comme modèle) + +Si l'objectif est de router un subagent vers LM Studio plutôt que vers l'API Anthropic, le moyen le plus simple est un MCP server stdio minimal. + +### `mcp/qwen3-mcp/server.py` + +```python +#!/usr/bin/env python3 +""" +MCP server stdio minimaliste exposant un outil `qwen3_task`. +Appelé par Claude Code via : claude mcp add --transport stdio qwen3 -- python3 mcp/qwen3-mcp/server.py +""" +import sys +import json +import requests + +LMSTUDIO_URL = "http://localhost:1234/v1/chat/completions" +QWEN3_MODEL = "qwen3-coder" # nom du modèle dans LM Studio + + +def call_qwen3(task: str, files: list[str]) -> str: + context = "" + for path in files: + try: + with open(path) as f: + context += f"--- {path} ---\n{f.read()}\n\n" + except OSError as e: + context += f"--- {path} --- ERREUR: {e}\n\n" + + prompt = f"""Tu es un assistant de codage. Effectue la tâche suivante de manière précise. + +TÂCHE: {task} + +FICHIERS: +{context} + +Retourne uniquement le code modifié ou généré, sans explication. +""" + resp = requests.post( + LMSTUDIO_URL, + json={ + "model": QWEN3_MODEL, + "messages": [{"role": "user", "content": prompt}], + "temperature": 0.2, + "max_tokens": 4096, + }, + timeout=120, + ) + resp.raise_for_status() + return resp.json()["choices"][0]["message"]["content"] + + +def handle_tool_call(tool_name: str, args: dict) -> str: + if tool_name == "qwen3_task": + return call_qwen3(args["task"], args.get("files", [])) + return f"Outil inconnu : {tool_name}" + + +# Boucle MCP stdio (protocole JSON-RPC simplifié) +for line in sys.stdin: + try: + req = json.loads(line) + if req.get("method") == "tools/call": + result = handle_tool_call( + req["params"]["name"], req["params"].get("arguments", {}) + ) + print(json.dumps({"id": req["id"], "result": {"content": [{"type": "text", "text": result}]}})) + elif req.get("method") == "tools/list": + print(json.dumps({ + "id": req["id"], + "result": {"tools": [{ + "name": "qwen3_task", + "description": "Délègue une tâche de codage atomique à Qwen3-Coder via LM Studio.", + "inputSchema": { + "type": "object", + "properties": { + "task": {"type": "string", "description": "Description précise de la tâche"}, + "files": {"type": "array", "items": {"type": "string"}, "description": "Chemins des fichiers concernés"} + }, + "required": ["task"] + } + }]} + })) + elif req.get("method") == "initialize": + print(json.dumps({"id": req["id"], "result": {"protocolVersion": "2024-11-05", "capabilities": {"tools": {}}}})) + except Exception as e: + print(json.dumps({"error": str(e)}), file=sys.stderr) + sys.stdout.flush() +``` + +**Installation** : +```bash +claude mcp add --transport stdio qwen3 -- python3 mcp/qwen3-mcp/server.py +``` + +--- + +## 8. Skill de délégation + +Fichier : `.claude/skills/delegation-rules.md` + +```markdown +--- +name: delegation-rules +description: Règles pour décider quand et comment déléguer à qwen3-worker ou qwen3 MCP +--- + +# Règles de Délégation à Qwen3 + +## Critères de délégation (TOUS requis) +- ≤ 3 fichiers source concernés +- Tâche atomique, sans dépendance à résoudre +- Pas de modification de signature publique (trait, interface exportée) +- Contexte de fichier estimé < 8 000 tokens + +## Tâches typiques éligibles +- Génération de tests unitaires pour une fonction donnée +- Ajout de documentation (doc comments Rust, godoc Go) +- Scaffolding de stubs à partir d'une interface +- Conversion de types internes simples +- Reformatage / réorganisation d'imports + +## Procédure +1. Évalue les critères ci-dessus. +2. Si éligible → utilise le subagent `qwen3-worker` en précisant la tâche et les fichiers. +3. Attends le résumé de sortie (STATUS / FILES_MODIFIED / LINT). +4. Si STATUS=failure → prends en charge toi-même. +5. Si STATUS=partial → corrige les points restants directement. + +## Ne jamais déléguer +- Refactoring impliquant > 3 fichiers +- Changement d'architecture ou de design pattern +- Debugging avec stacktrace à analyser +- Tâches nécessitant une recherche web ou du contexte externe +``` + +--- + +## 9. Pipeline de validation native + +Claude Code valide via les hooks et les subagents dédiés — pas via du code de validation embarqué dans le script de délégation. + +| Étape | Mécanisme | Déclencheur | +|-------|-----------|-------------| +| Lint syntaxique | `post-write-lint.sh` (hook) | PostToolUse/Write automatique | +| Validation de style | subagent `code-reviewer` | explicitement demandé | +| Tests | `Bash: cargo test / go test` | dans le subagent ou directement | +| Contrôle des diffs | `Bash: git diff --stat` | avant tout commit | + +### Subagent `code-reviewer` + +```markdown +--- +name: code-reviewer +description: > + Révise du code Rust ou Go pour détecter bugs, régressions, problèmes de style. + Utilise après génération par qwen3-worker ou avant un commit. +tools: [Read, Grep, Glob, Bash] +model: claude-haiku-4-5-20251001 +--- + +Tu es un reviewer expérimenté en Rust et Go. + +Pour chaque fichier fourni : +1. Vérifie la cohérence des types et la gestion d'erreurs. +2. Identifie les patterns non idiomatiques. +3. Signale les tests manquants pour les chemins critiques. + +Format de sortie : +``` +VERDICT: approved|changes_requested +ISSUES: liste numérotée (vide si approved) +``` +``` + +--- + +## 10. Boucle de correction contrôlée + +La boucle de correction n'est pas un script externe — elle est gérée nativement par Claude Code via le prompt du subagent et les hooks. + +Séquence type : +1. Claude Code délègue à `qwen3-worker`. +2. Le subagent retourne `STATUS: failure` avec détail. +3. Claude Code lit le résumé, corrige lui-même ou re-délègue avec le feedback intégré dans le prompt. +4. Maximum 2 re-délégations ; au-delà, Claude Code traite directement. + +--- + +## 11. Sécurité + +Les garde-fous sont dans les **hooks** (non contournables par le contexte) plutôt que dans des fonctions Python. + +```json +{ + "permissions": { + "allow": [ + "Bash(cargo:*)", + "Bash(go:*)", + "Bash(git diff*)", + "Bash(git add*)", + "Bash(git commit*)", + "Read(**/*.rs)", + "Read(**/*.go)", + "Write(**/*.rs)", + "Write(**/*.go)" + ], + "deny": [ + "Bash(rm -rf*)", + "Bash(curl * | bash*)", + "Bash(git push --force*)" + ] + } +} +``` + +--- + +## 12. Observabilité + +Les logs sont produits par les hooks, pas par du code applicatif. + +``` +.claude/logs/ +├── subagent-decisions.log # timestamp + agent name à chaque SubagentStop +└── delegation-outcomes.log # STATUS de chaque délégation (si ajouté au hook) +``` + +Pour des métriques plus élaborées (taux de délégation, taux d'échec), un hook `SubagentStop` peut écrire dans un fichier JSON et un script externe peut agréger. + +--- + +## 13. Workflow complet — exemple + +**Requête** : "Génère les tests unitaires pour les fonctions `parse_header` et `validate_checksum` dans `src/parser.rs`." + +1. Claude Code lit `CLAUDE.md` → identifie les heuristiques de délégation. +2. Charge le skill `delegation-rules.md` → tâche éligible (1 fichier, atomique, pas d'API publique). +3. Délègue au subagent `qwen3-worker` : *"Génère des tests unitaires pour `parse_header` et `validate_checksum` dans `src/parser.rs`"*. +4. `qwen3-worker` lit `src/parser.rs` via l'outil Read (le contenu ne passe pas par le contexte principal). +5. Écrit les tests dans `src/parser.rs` (ou `tests/parser_tests.rs`). +6. Hook `post-write-lint.sh` déclenché → `cargo clippy` → résultat injecté dans le contexte du subagent. +7. Subagent retourne : `STATUS: success | FILES_MODIFIED: src/parser.rs | LINT: passed`. +8. Claude Code confirme à l'utilisateur. + +--- + +## 14. Ce qui a changé par rapport au document initial + +| Document initial | Cette version | +|-----------------|---------------| +| Script Python `delegate_to_qwen.py` comme orchestrateur | Claude Code natif comme orchestrateur | +| Validation syntaxique dans le script | Hook `post-write-lint.sh` (déterministe, non contournable) | +| Boucle de correction en Python | Gérée par le prompt du subagent + logique d'orchestration Claude | +| Sécurité dans `is_safe_file()` | Permissions natives (`settings.json`) + hook `pre-bash-guard.sh` | +| Logs en Python | Hooks shell sur les événements du cycle de vie | +| Tools MCP = scripts ad hoc | MCP server stdio standard + subagents natifs | + +--- + +## 15. Prochaines étapes recommandées + +1. **Bootstrapper la structure** : créer `.claude/agents/`, `settings.json`, `CLAUDE.md` avec les templates ci-dessus. +2. **Tester LM Studio** : vérifier que `curl http://localhost:1234/v1/models` répond avant d'activer le MCP server. +3. **Calibrer les heuristiques** : après 20-30 délégations, affiner les critères dans `delegation-rules.md` selon les résultats observés dans les logs. +4. **Envisager `model:` explicite** : si Claude Haiku est suffisant pour `qwen3-worker` (coût réduit, latence moindre), configurer `model: claude-haiku-4-5-20251001` à la place du LLM local. +5. **MCP Tool Search** : activer `ENABLE_TOOL_SEARCH=auto` pour réduire la consommation de contexte quand les MCP servers sont nombreux.