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
This commit is contained in:
2026-03-26 12:23:54 +01:00
commit c776733402
15 changed files with 1187 additions and 0 deletions

26
.claude/CLAUDE.md Normal file
View File

@@ -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

View File

@@ -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)
```

View File

@@ -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.

View File

@@ -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
```

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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: <raison>
"""
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()

View File

@@ -0,0 +1 @@
requests>=2.31.0

View File

@@ -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)

58
.claude/settings.json Normal file
View File

@@ -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*)"
]
}
}

View File

@@ -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

View File

@@ -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.

153
README.md Normal file
View File

@@ -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 <url-du-depot> .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 <url-du-depot> 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 <url-du-depot> 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/
```

View File

@@ -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.