refactor: migrate MCP to aichat and integrate llm-functions
Replace LM Studio direct integration with aichat-based MCP server, enabling Qwen3-Coder to autonomously use filesystem, shell and web tools via llm-functions. Key changes: - Update server.py to launch aichat on a dynamic port and handle tool_calls loop - Add llm-functions directory with prebuilt tools (fs_cat, fs_ls, execute_command, etc.) - Revise delegation rules to remove file count limit and emphasize autonomous tool usage - Update agents and skills docs to reflect new architecture and workflow - Add setup instructions for aichat, argc, jq and llm-functions build step - Bump MCP server version to 3.0.0
This commit is contained in:
@@ -29,18 +29,20 @@ Programmeur expérimenté. Connaît parfaitement son projet, ses outils, et son
|
|||||||
|
|
||||||
## Heuristiques de délégation
|
## Heuristiques de délégation
|
||||||
|
|
||||||
Délègue à `qwen3-worker` si la tâche est **atomique** et satisfait TOUS les critères :
|
Délègue à `qwen3-worker` (via MCP `qwen3_task`) si la tâche satisfait TOUS les critères :
|
||||||
- ≤ 3 fichiers concernés
|
- Objectif clair et bien délimité
|
||||||
- Pas de modification d’API publique
|
- Pas de modification d’API publique (traits Rust, interfaces Go exportées)
|
||||||
- Pas de dépendance externe non encore importée
|
- Pas de vision cross-module requise
|
||||||
- Tâches typiques : génération de tests unitaires, reformatage, documentation inline,
|
- Tâches typiques : génération de tests, documentation inline, scaffolding de stubs,
|
||||||
conversion de types simples, scaffolding de stubs
|
reformatage, recherche web, exécution de scripts, création de fichiers
|
||||||
- Pour le bash, éviter les commandes destructrices (voir garde-fous)
|
|
||||||
|
Qwen3 dispose d’outils autonomes (filesystem, shell, web) — inutile de lui passer
|
||||||
|
le contenu des fichiers, il les lit lui-même.
|
||||||
|
|
||||||
Traite toi-même si :
|
Traite toi-même si :
|
||||||
- Refactoring cross-module
|
- Refactoring cross-module
|
||||||
- Conception d’architecture
|
- Conception d’architecture
|
||||||
- Debugging avec contexte multi-fichiers
|
- Debugging avec stacktrace multi-fichiers
|
||||||
- Modification de traits/interfaces publics
|
- Modification de traits/interfaces publics
|
||||||
|
|
||||||
## Garde-fous absolus
|
## Garde-fous absolus
|
||||||
|
|||||||
@@ -1,30 +1,56 @@
|
|||||||
---
|
---
|
||||||
name: qwen3-worker
|
name: qwen3-worker
|
||||||
description: >
|
description: >
|
||||||
Délègue à Qwen3-Coder via LM Studio les tâches atomiques sur 1-3 fichiers :
|
Délègue à Qwen3-Coder via le MCP tool `qwen3_task` les tâches de codage autonomes :
|
||||||
génération de tests unitaires, documentation inline, scaffolding de stubs,
|
génération de tests, documentation, scaffolding, reformatage, recherche web,
|
||||||
reformatage de code. N'utilise PAS pour du refactoring cross-module ou de
|
exécution de scripts. Qwen3 dispose d'outils filesystem, shell et web — il lit et
|
||||||
|
écrit les fichiers lui-même. N'utilise PAS pour du refactoring cross-module ou de
|
||||||
la conception d'architecture.
|
la conception d'architecture.
|
||||||
tools: [Read, Write, Bash]
|
tools: [mcp__qwen3__qwen3_task]
|
||||||
model: inherit
|
model: inherit
|
||||||
---
|
---
|
||||||
|
|
||||||
Tu es un assistant de codage spécialisé exécutant des tâches courtes et précises.
|
Tu es un orchestrateur qui délègue des tâches de codage à Qwen3-Coder via l'outil MCP `qwen3_task`.
|
||||||
|
|
||||||
## Comportement
|
## 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.
|
1. Formule une description précise de la tâche.
|
||||||
3. Écris le résultat avec Write.
|
2. Appelle `qwen3_task` avec le champ `task`. Le champ `files` est optionnel — ne l'utilise
|
||||||
4. Exécute le linter approprié (clippy pour Rust, golangci-lint pour Go) et corrige
|
que pour donner un contexte initial ; Qwen3 peut lire les fichiers dont il a besoin lui-même.
|
||||||
les erreurs éventuelles (max 2 tentatives).
|
3. Qwen3 opère de façon autonome : il lit, écrit, exécute des commandes et fait des requêtes
|
||||||
5. Retourne un résumé : fichiers modifiés, changements effectués, résultat du lint.
|
web via ses propres outils (llm-functions). Tu n'as pas à gérer ces étapes.
|
||||||
|
4. Retourne le résultat de `qwen3_task` à l'orchestrateur principal.
|
||||||
|
|
||||||
|
## Outils disponibles pour Qwen3
|
||||||
|
|
||||||
|
Qwen3 dispose des outils suivants (définis dans `.claude/llm-functions/tools.txt`) :
|
||||||
|
|
||||||
|
| Outil | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `fs_cat` | Lit un fichier |
|
||||||
|
| `fs_ls` | Liste un répertoire |
|
||||||
|
| `fs_mkdir` | Crée un répertoire |
|
||||||
|
| `fs_write` | Écrit/crée un fichier |
|
||||||
|
| `fs_patch` | Modifie un fichier par patch |
|
||||||
|
| `execute_command` | Exécute une commande shell |
|
||||||
|
| `fetch_url_via_curl` | Requête HTTP GET |
|
||||||
|
| `web_search_aichat` | Recherche web |
|
||||||
|
| `execute_py_code` | Exécute du code Python |
|
||||||
|
|
||||||
## Format de sortie
|
## Format de sortie
|
||||||
|
|
||||||
|
Qwen3 retourne une réponse en langage naturel. Résume-la ainsi :
|
||||||
|
|
||||||
```
|
```
|
||||||
STATUS: success|partial|failure
|
STATUS: success|partial|failure
|
||||||
FILES_MODIFIED: liste des fichiers
|
FILES_MODIFIED: liste des fichiers (si applicable)
|
||||||
SUMMARY: description des changements
|
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.
|
Si Qwen3 retourne une erreur (`ERROR:`), signale-la avec `STATUS: failure`.
|
||||||
|
|
||||||
|
## Contraintes
|
||||||
|
|
||||||
|
- Ne modifie pas les signatures publiques (traits Rust, interfaces Go exportées).
|
||||||
|
- Ne génère pas de fonctions non demandées.
|
||||||
|
- Max 2 re-délégations en cas d'échec partiel ; au-delà, remonte le problème.
|
||||||
|
|||||||
1
.claude/logs/subagent-decisions.log
Normal file
1
.claude/logs/subagent-decisions.log
Normal file
@@ -0,0 +1 @@
|
|||||||
|
2026-03-27T08:01:50Z agent=unknown
|
||||||
@@ -1,34 +1,56 @@
|
|||||||
#!/usr/bin/env -S .claude/venv/bin/python3
|
#!/usr/bin/env -S .claude/venv/bin/python3
|
||||||
"""
|
"""
|
||||||
MCP server stdio exposant un outil `qwen3_task`.
|
MCP server stdio exposant un outil `qwen3_task`.
|
||||||
|
Lance aichat en mode serveur au démarrage et gère la boucle tool_calls.
|
||||||
|
Les outils disponibles pour Qwen3 sont chargés depuis llm-functions/functions.json.
|
||||||
Appelé par Claude Code via : claude mcp add --transport stdio qwen3 -- .claude/venv/bin/python3 .claude/mcp/qwen3-mcp/server.py
|
Appelé par Claude Code via : claude mcp add --transport stdio qwen3 -- .claude/venv/bin/python3 .claude/mcp/qwen3-mcp/server.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import atexit
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import socket
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
SERVER = "http://localhost"
|
QWEN3_MODEL = "LMStudio:qwen/qwen3-coder-next"
|
||||||
PORT = 8888 # 1248
|
MAX_TOOL_ITERATIONS = 10
|
||||||
LMSTUDIO_URL = f"{SERVER}:{PORT}/v1/chat/completions"
|
|
||||||
QWEN3_MODEL = "qwen/qwen3-coder-next"
|
|
||||||
|
|
||||||
TOOLS = [
|
# Répertoire llm-functions relatif à ce script
|
||||||
|
_HERE = Path(__file__).parent
|
||||||
|
_LLM_FUNCTIONS_DIR = _HERE.parent.parent / "llm-functions"
|
||||||
|
_FUNCTIONS_JSON = _LLM_FUNCTIONS_DIR / "functions.json"
|
||||||
|
_BIN_DIR = _LLM_FUNCTIONS_DIR / "bin"
|
||||||
|
|
||||||
|
|
||||||
|
def _load_qwen3_tools() -> list[dict]:
|
||||||
|
"""Charge les outils depuis llm-functions/functions.json et les wrappe au format OpenAI."""
|
||||||
|
if not _FUNCTIONS_JSON.exists():
|
||||||
|
print(f"WARN: {_FUNCTIONS_JSON} introuvable — aucun outil disponible",
|
||||||
|
file=sys.stderr, flush=True)
|
||||||
|
return []
|
||||||
|
raw = json.loads(_FUNCTIONS_JSON.read_text())
|
||||||
|
return [{"type": "function", "function": tool} for tool in raw]
|
||||||
|
|
||||||
|
|
||||||
|
# Outil MCP exposé à Claude Code
|
||||||
|
MCP_TOOLS = [
|
||||||
{
|
{
|
||||||
"name": "qwen3_task",
|
"name": "qwen3_task",
|
||||||
"description": "Délègue une tâche de codage atomique à Qwen3-Coder via LM Studio.",
|
"description": "Délègue une tâche de codage à Qwen3-Coder via aichat. Qwen3 dispose d'outils filesystem, shell et web (llm-functions).",
|
||||||
"inputSchema": {
|
"inputSchema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"task": {
|
"task": {"type": "string", "description": "Description précise de la tâche"},
|
||||||
"type": "string",
|
|
||||||
"description": "Description précise de la tâche",
|
|
||||||
},
|
|
||||||
"files": {
|
"files": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {"type": "string"},
|
"items": {"type": "string"},
|
||||||
"description": "Chemins des fichiers concernés",
|
"description": "Fichiers à mettre en contexte initial (optionnel)",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"required": ["task"],
|
"required": ["task"],
|
||||||
@@ -37,40 +59,148 @@ TOOLS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def send(obj: dict):
|
# --- Gestion du processus aichat ---
|
||||||
print(json.dumps(obj), flush=True)
|
|
||||||
|
|
||||||
|
_aichat_proc: subprocess.Popen | None = None
|
||||||
|
_aichat_url: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def _find_free_port() -> int:
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||||
|
s.bind(("127.0.0.1", 0))
|
||||||
|
return s.getsockname()[1]
|
||||||
|
|
||||||
|
|
||||||
|
def _start_aichat() -> str:
|
||||||
|
global _aichat_proc, _aichat_url
|
||||||
|
|
||||||
|
if not shutil.which("aichat"):
|
||||||
|
raise RuntimeError("aichat introuvable dans le PATH")
|
||||||
|
|
||||||
|
port = _find_free_port()
|
||||||
|
address = f"127.0.0.1:{port}"
|
||||||
|
|
||||||
|
env = os.environ.copy()
|
||||||
|
if _LLM_FUNCTIONS_DIR.exists():
|
||||||
|
env["AICHAT_FUNCTIONS_DIR"] = str(_LLM_FUNCTIONS_DIR)
|
||||||
|
|
||||||
|
_aichat_proc = subprocess.Popen(
|
||||||
|
["aichat", "--serve", address],
|
||||||
|
stdin=subprocess.DEVNULL,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
|
atexit.register(_stop_aichat)
|
||||||
|
|
||||||
|
url = f"http://{address}"
|
||||||
|
for _ in range(30):
|
||||||
|
try:
|
||||||
|
requests.get(f"{url}/v1/models", timeout=1)
|
||||||
|
_aichat_url = url
|
||||||
|
return url
|
||||||
|
except requests.RequestException:
|
||||||
|
time.sleep(0.3)
|
||||||
|
|
||||||
|
_aichat_proc.kill()
|
||||||
|
raise RuntimeError(f"aichat n'a pas démarré sur {address}")
|
||||||
|
|
||||||
|
|
||||||
|
def _stop_aichat():
|
||||||
|
if _aichat_proc and _aichat_proc.poll() is None:
|
||||||
|
_aichat_proc.terminate()
|
||||||
|
try:
|
||||||
|
_aichat_proc.wait(timeout=5)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
_aichat_proc.kill()
|
||||||
|
|
||||||
|
|
||||||
|
# --- Exécution des outils via llm-functions/bin/ ---
|
||||||
|
|
||||||
|
def _execute_tool(name: str, args: dict) -> str:
|
||||||
|
"""Exécute un outil llm-functions via son binaire dans bin/.
|
||||||
|
Les binaires attendent le JSON des arguments comme premier argument positionnel.
|
||||||
|
"""
|
||||||
|
bin_path = _BIN_DIR / name
|
||||||
|
if not bin_path.exists():
|
||||||
|
return f"ERROR: outil '{name}' introuvable dans {_BIN_DIR}"
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
[str(bin_path), json.dumps(args)],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=60,
|
||||||
|
)
|
||||||
|
output = (result.stdout + result.stderr).strip()
|
||||||
|
return output or f"(exit code {result.returncode})"
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return f"ERROR: timeout lors de l'exécution de '{name}'"
|
||||||
|
except Exception as e:
|
||||||
|
return f"ERROR: {e}"
|
||||||
|
|
||||||
|
|
||||||
|
# --- Boucle tool_calls → Qwen3 ---
|
||||||
|
|
||||||
def call_qwen3(task: str, files: list[str]) -> str:
|
def call_qwen3(task: str, files: list[str]) -> str:
|
||||||
|
if _aichat_url is None:
|
||||||
|
raise RuntimeError("aichat non démarré")
|
||||||
|
|
||||||
|
qwen3_tools = _load_qwen3_tools()
|
||||||
|
|
||||||
context = ""
|
context = ""
|
||||||
for path in files:
|
for path in files:
|
||||||
try:
|
try:
|
||||||
with open(path) as f:
|
with open(path, encoding="utf-8") as f:
|
||||||
context += f"--- {path} ---\n{f.read()}\n\n"
|
context += f"--- {path} ---\n{f.read()}\n\n"
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
context += f"--- {path} --- ERREUR: {e}\n\n"
|
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.
|
user_content = f"TÂCHE: {task}"
|
||||||
|
if context:
|
||||||
|
user_content += f"\n\nFICHIERS:\n{context}"
|
||||||
|
|
||||||
TÂCHE: {task}
|
messages = [{"role": "user", "content": user_content}]
|
||||||
|
payload: dict = {
|
||||||
|
"model": QWEN3_MODEL,
|
||||||
|
"messages": messages,
|
||||||
|
"temperature": 0.2,
|
||||||
|
"max_tokens": 4096,
|
||||||
|
}
|
||||||
|
if qwen3_tools:
|
||||||
|
payload["tools"] = qwen3_tools
|
||||||
|
|
||||||
FICHIERS:
|
for _ in range(MAX_TOOL_ITERATIONS):
|
||||||
{context}
|
resp = requests.post(
|
||||||
|
f"{_aichat_url}/v1/chat/completions",
|
||||||
|
json=payload,
|
||||||
|
timeout=120,
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
data = resp.json()
|
||||||
|
choice = data["choices"][0]
|
||||||
|
message = choice["message"]
|
||||||
|
payload["messages"].append(message)
|
||||||
|
|
||||||
Retourne uniquement le code modifié ou généré, sans explication.
|
if choice["finish_reason"] != "tool_calls":
|
||||||
"""
|
return message.get("content") or ""
|
||||||
resp = requests.post(
|
|
||||||
LMSTUDIO_URL,
|
for tc in message.get("tool_calls", []):
|
||||||
json={
|
fn = tc["function"]
|
||||||
"model": QWEN3_MODEL,
|
args = json.loads(fn["arguments"])
|
||||||
"messages": [{"role": "user", "content": prompt}],
|
result = _execute_tool(fn["name"], args)
|
||||||
"temperature": 0.2,
|
payload["messages"].append({
|
||||||
"max_tokens": 4096,
|
"role": "tool",
|
||||||
},
|
"tool_call_id": tc["id"],
|
||||||
timeout=120,
|
"content": result,
|
||||||
)
|
})
|
||||||
resp.raise_for_status()
|
|
||||||
return resp.json()["choices"][0]["message"]["content"]
|
return "ERROR: nombre maximum d'itérations tool_calls atteint"
|
||||||
|
|
||||||
|
|
||||||
|
# --- Protocole MCP stdio ---
|
||||||
|
|
||||||
|
def send(obj: dict):
|
||||||
|
print(json.dumps(obj), flush=True)
|
||||||
|
|
||||||
|
|
||||||
def handle(req: dict):
|
def handle(req: dict):
|
||||||
@@ -78,21 +208,19 @@ def handle(req: dict):
|
|||||||
req_id = req.get("id")
|
req_id = req.get("id")
|
||||||
|
|
||||||
if method == "initialize":
|
if method == "initialize":
|
||||||
send(
|
send({
|
||||||
{
|
"jsonrpc": "2.0",
|
||||||
"jsonrpc": "2.0",
|
"id": req_id,
|
||||||
"id": req_id,
|
"result": {
|
||||||
"result": {
|
"protocolVersion": "2024-11-05",
|
||||||
"protocolVersion": "2024-11-05",
|
"capabilities": {"tools": {}},
|
||||||
"capabilities": {"tools": {}},
|
"serverInfo": {"name": "qwen3-mcp", "version": "3.0.0"},
|
||||||
"serverInfo": {"name": "qwen3-mcp", "version": "1.0.0"},
|
},
|
||||||
},
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
elif method == "notifications/initialized":
|
elif method == "notifications/initialized":
|
||||||
pass # notification, pas de réponse
|
pass
|
||||||
elif method == "tools/list":
|
elif method == "tools/list":
|
||||||
send({"jsonrpc": "2.0", "id": req_id, "result": {"tools": TOOLS}})
|
send({"jsonrpc": "2.0", "id": req_id, "result": {"tools": MCP_TOOLS}})
|
||||||
elif method == "tools/call":
|
elif method == "tools/call":
|
||||||
params = req.get("params", {})
|
params = req.get("params", {})
|
||||||
name = params.get("name", "")
|
name = params.get("name", "")
|
||||||
@@ -100,42 +228,31 @@ def handle(req: dict):
|
|||||||
if name == "qwen3_task":
|
if name == "qwen3_task":
|
||||||
try:
|
try:
|
||||||
text = call_qwen3(args["task"], args.get("files", []))
|
text = call_qwen3(args["task"], args.get("files", []))
|
||||||
send(
|
send({"jsonrpc": "2.0", "id": req_id, "result": {
|
||||||
{
|
"content": [{"type": "text", "text": text}]
|
||||||
"jsonrpc": "2.0",
|
}})
|
||||||
"id": req_id,
|
|
||||||
"result": {"content": [{"type": "text", "text": text}]},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
send(
|
send({"jsonrpc": "2.0", "id": req_id, "result": {
|
||||||
{
|
"content": [{"type": "text", "text": f"ERROR: {e}"}],
|
||||||
"jsonrpc": "2.0",
|
"isError": True,
|
||||||
"id": req_id,
|
}})
|
||||||
"result": {
|
|
||||||
"content": [{"type": "text", "text": f"ERROR: {e}"}],
|
|
||||||
"isError": True,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
send(
|
send({"jsonrpc": "2.0", "id": req_id,
|
||||||
{
|
"error": {"code": -32601, "message": f"Outil inconnu : {name}"}})
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": req_id,
|
|
||||||
"error": {"code": -32601, "message": f"Outil inconnu : {name}"},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
elif req_id is not None:
|
elif req_id is not None:
|
||||||
send(
|
send({"jsonrpc": "2.0", "id": req_id,
|
||||||
{
|
"error": {"code": -32601, "message": f"Méthode inconnue : {method}"}})
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": req_id,
|
|
||||||
"error": {"code": -32601, "message": f"Méthode inconnue : {method}"},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
# --- Démarrage ---
|
||||||
|
|
||||||
|
try:
|
||||||
|
_start_aichat()
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": f"Impossible de démarrer aichat : {e}"}),
|
||||||
|
file=sys.stderr, flush=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
for line in sys.stdin:
|
for line in sys.stdin:
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
if not line:
|
if not line:
|
||||||
|
|||||||
@@ -1,32 +1,43 @@
|
|||||||
---
|
---
|
||||||
name: delegation-rules
|
name: delegation-rules
|
||||||
description: Règles pour décider quand et comment déléguer à qwen3-worker ou qwen3 MCP
|
description: Règles pour décider quand et comment déléguer à qwen3-worker via le MCP qwen3_task
|
||||||
---
|
---
|
||||||
|
|
||||||
# Règles de Délégation à Qwen3
|
# Règles de Délégation à Qwen3
|
||||||
|
|
||||||
## Critères de délégation (TOUS requis)
|
Qwen3-Coder opère via le MCP tool `qwen3_task`. Il dispose d'outils filesystem, shell
|
||||||
- ≤ 3 fichiers source concernés
|
et web (llm-functions) — il peut lire/écrire des fichiers, exécuter des commandes,
|
||||||
- Tâche atomique, sans dépendance à résoudre
|
faire des recherches web et exécuter du code de façon autonome.
|
||||||
- Pas de modification de signature publique (trait, interface exportée)
|
|
||||||
- Contexte de fichier estimé < 8 000 tokens
|
## Critères de délégation (TOUS requis)
|
||||||
|
|
||||||
|
- Tâche bien définie, objectif clair
|
||||||
|
- Pas de modification de signature publique (trait, interface exportée)
|
||||||
|
- Pas de refactoring cross-module nécessitant une vision globale de l'architecture
|
||||||
|
|
||||||
|
## Tâches éligibles
|
||||||
|
|
||||||
## Tâches typiques éligibles
|
|
||||||
- Génération de tests unitaires pour une fonction donnée
|
- Génération de tests unitaires pour une fonction donnée
|
||||||
- Ajout de documentation (doc comments Rust, godoc Go)
|
- Ajout de documentation (doc comments Rust, godoc Go)
|
||||||
- Scaffolding de stubs à partir d'une interface
|
- Scaffolding de stubs à partir d'une interface
|
||||||
- Conversion de types internes simples
|
- Conversion de types internes simples
|
||||||
- Reformatage / réorganisation d'imports
|
- Reformatage / réorganisation d'imports
|
||||||
|
- Recherche d'information sur le web ou dans une URL
|
||||||
|
- Exécution et vérification d'un script Python
|
||||||
|
- Création ou modification de fichiers de configuration
|
||||||
|
|
||||||
## Procédure
|
## Procédure
|
||||||
|
|
||||||
1. Évalue les critères ci-dessus.
|
1. Évalue les critères ci-dessus.
|
||||||
2. Si éligible → utilise le subagent `qwen3-worker` en précisant la tâche et les fichiers.
|
2. Si éligible → utilise le subagent `qwen3-worker` avec une description précise de la tâche.
|
||||||
3. Attends le résumé de sortie (STATUS / FILES_MODIFIED / LINT).
|
Ne pas pré-charger les fichiers : Qwen3 les lit lui-même si nécessaire.
|
||||||
4. Si STATUS=failure → prends en charge toi-même.
|
3. Attends le résumé (STATUS / FILES_MODIFIED / SUMMARY).
|
||||||
5. Si STATUS=partial → corrige les points restants directement.
|
4. Si `STATUS: failure` → prends en charge toi-même.
|
||||||
|
5. Si `STATUS: partial` → corrige les points restants directement.
|
||||||
|
|
||||||
## Ne jamais déléguer
|
## Ne jamais déléguer
|
||||||
- Refactoring impliquant > 3 fichiers
|
|
||||||
|
- Refactoring impliquant une vision cross-module
|
||||||
- Changement d'architecture ou de design pattern
|
- Changement d'architecture ou de design pattern
|
||||||
- Debugging avec stacktrace à analyser
|
- Debugging avec stacktrace à analyser sur plusieurs fichiers
|
||||||
- Tâches nécessitant une recherche web ou du contexte externe
|
- Tâches nécessitant le contexte de la conversation en cours
|
||||||
|
|||||||
151
README.md
151
README.md
@@ -1,17 +1,38 @@
|
|||||||
# CrazyClaude
|
# 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.
|
Configuration Claude Code pour orchestrer des tâches de développement en déléguant les tâches atomiques à **Qwen3-Coder** via [aichat](https://github.com/sigoden/aichat).
|
||||||
|
|
||||||
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.
|
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, un serveur MCP et une collection d'outils prêts à l'emploi.
|
||||||
|
|
||||||
Pour le détail de l'architecture, voir [`rapport-orchestration-claude-code.md`](./rapport-orchestration-claude-code.md).
|
Pour le détail de l'architecture, voir [`rapport-orchestration-claude-code.md`](./rapport-orchestration-claude-code.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Claude Code (orchestrateur)
|
||||||
|
│
|
||||||
|
└── MCP qwen3 (stdio)
|
||||||
|
│
|
||||||
|
└── aichat --serve (port dynamique)
|
||||||
|
│
|
||||||
|
└── LM Studio → Qwen3-Coder-Next
|
||||||
|
+ outils llm-functions
|
||||||
|
(fs, shell, web, ...)
|
||||||
|
```
|
||||||
|
|
||||||
|
Le serveur MCP lance aichat au démarrage sur un port libre. Qwen3 dispose d'outils filesystem, shell et web fournis par [llm-functions](https://github.com/sigoden/llm-functions) — sans aucune logique d'outil dans le serveur lui-même.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Prérequis
|
## Prérequis
|
||||||
|
|
||||||
- [Claude Code](https://claude.ai/code) installé (`claude` disponible dans le PATH)
|
- [Claude Code](https://claude.ai/code) (`claude` dans le PATH)
|
||||||
- [LM Studio](https://lmstudio.ai/) avec le modèle `qwen/qwen3-coder-next` chargé, écoutant sur `http://localhost:1248`
|
- [aichat](https://github.com/sigoden/aichat) (`aichat` dans le PATH)
|
||||||
|
- [LM Studio](https://lmstudio.ai/) avec le modèle `qwen/qwen3-coder-next` chargé et accessible depuis aichat
|
||||||
|
- [argc](https://github.com/sigoden/argc) — runner de scripts pour llm-functions
|
||||||
|
- [jq](https://jqlang.github.io/jq/) — requis par llm-functions
|
||||||
- Python 3.11+ (pour le serveur MCP)
|
- Python 3.11+ (pour le serveur MCP)
|
||||||
- Outils selon votre stack : `cargo` + `clippy` (Rust), `golangci-lint` (Go)
|
- Outils selon votre stack : `cargo` + `clippy` (Rust), `golangci-lint` (Go)
|
||||||
|
|
||||||
@@ -28,18 +49,14 @@ Avec **Jujutsu** :
|
|||||||
jj git clone https://gargoton.petite-maison-orange.fr/eric/CrazyClaude.git .claude-crazy
|
jj git clone https://gargoton.petite-maison-orange.fr/eric/CrazyClaude.git .claude-crazy
|
||||||
```
|
```
|
||||||
|
|
||||||
Puis déplacez ou liez le contenu :
|
Puis copiez le contenu :
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Copier le répertoire .claude dans votre projet
|
|
||||||
cp -r .claude-crazy/.claude ./.claude
|
cp -r .claude-crazy/.claude ./.claude
|
||||||
|
cp -r .claude-crazy/llm-functions ./.claude/llm-functions # si présent dans le clone
|
||||||
# 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`),
|
> Avec jj colocalisé git, vous pouvez aussi utiliser subtree :
|
||||||
> vous pouvez aussi ajouter ce dépôt comme subtree :
|
|
||||||
>
|
>
|
||||||
> ```sh
|
> ```sh
|
||||||
> git subtree add --prefix .claude https://gargoton.petite-maison-orange.fr/eric/CrazyClaude.git main --squash
|
> git subtree add --prefix .claude https://gargoton.petite-maison-orange.fr/eric/CrazyClaude.git main --squash
|
||||||
@@ -53,24 +70,61 @@ python3 -m venv venv
|
|||||||
venv/bin/pip install -r mcp/qwen3-mcp/requirements.txt
|
venv/bin/pip install -r mcp/qwen3-mcp/requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Enregistrer le serveur MCP
|
### 3. Installer et construire llm-functions
|
||||||
|
|
||||||
|
Si `llm-functions/` n'est pas déjà présent dans `.claude/` :
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/sigoden/llm-functions .claude/llm-functions
|
||||||
|
```
|
||||||
|
|
||||||
|
Choisissez les outils à activer en éditant `.claude/llm-functions/tools.txt`, puis construisez :
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd .claude/llm-functions
|
||||||
|
argc build
|
||||||
|
```
|
||||||
|
|
||||||
|
Les outils activés par défaut :
|
||||||
|
|
||||||
|
```
|
||||||
|
fs_cat.sh
|
||||||
|
fs_ls.sh
|
||||||
|
fs_mkdir.sh
|
||||||
|
fs_write.sh
|
||||||
|
fs_patch.sh
|
||||||
|
execute_command.sh
|
||||||
|
fetch_url_via_curl.sh
|
||||||
|
web_search_aichat.sh
|
||||||
|
execute_py_code.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Configurer aichat
|
||||||
|
|
||||||
|
Vérifiez que aichat peut accéder à LM Studio et que le modèle `qwen/qwen3-coder-next` est disponible :
|
||||||
|
|
||||||
|
```sh
|
||||||
|
aichat --list-models | grep qwen
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Enregistrer le serveur MCP
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
claude mcp add --transport stdio qwen3 -- .claude/venv/bin/python3 .claude/mcp/qwen3-mcp/server.py
|
claude mcp add --transport stdio qwen3 -- .claude/venv/bin/python3 .claude/mcp/qwen3-mcp/server.py
|
||||||
```
|
```
|
||||||
|
|
||||||
Vérifier que la connexion est établie :
|
Vérifier :
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
claude mcp list
|
claude mcp list
|
||||||
# qwen3: ... - ✓ Connected
|
# qwen3: ... - ✓ Connected
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Adapter CLAUDE.md à votre projet
|
### 6. Adapter CLAUDE.md à votre projet
|
||||||
|
|
||||||
Éditez `.claude/CLAUDE.md` : mettez à jour la section `## Stack` et les heuristiques de délégation selon vos besoins.
|
Éditez `.claude/CLAUDE.md` : mettez à jour la section `## Stack` et les heuristiques de délégation.
|
||||||
|
|
||||||
### 5. Rendre les hooks exécutables
|
### 7. Rendre les hooks exécutables
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
chmod +x .claude/hooks/*.sh
|
chmod +x .claude/hooks/*.sh
|
||||||
@@ -85,7 +139,7 @@ chmod +x .claude/hooks/*.sh
|
|||||||
├── CLAUDE.md # contexte projet + heuristiques de délégation
|
├── CLAUDE.md # contexte projet + heuristiques de délégation
|
||||||
├── settings.json # hooks et permissions
|
├── settings.json # hooks et permissions
|
||||||
├── agents/
|
├── agents/
|
||||||
│ ├── qwen3-worker.md # délégation vers LM Studio (Qwen3)
|
│ ├── qwen3-worker.md # délégation vers Qwen3 via MCP
|
||||||
│ ├── code-reviewer.md # révision code (Claude Haiku)
|
│ ├── code-reviewer.md # révision code (Claude Haiku)
|
||||||
│ └── task-planner.md # planification complexe (Claude Opus)
|
│ └── task-planner.md # planification complexe (Claude Opus)
|
||||||
├── skills/
|
├── skills/
|
||||||
@@ -97,66 +151,81 @@ chmod +x .claude/hooks/*.sh
|
|||||||
│ └── pre-bash-guard.sh # blocage des commandes dangereuses
|
│ └── pre-bash-guard.sh # blocage des commandes dangereuses
|
||||||
├── mcp/
|
├── mcp/
|
||||||
│ └── qwen3-mcp/
|
│ └── qwen3-mcp/
|
||||||
│ ├── server.py # serveur MCP stdio (interface LM Studio)
|
│ ├── server.py # serveur MCP stdio : lance aichat + gère tool_calls
|
||||||
│ ├── agent_lm.py # agent loop CLI autonome
|
│ ├── agent_lm.py # agent loop CLI autonome (usage direct sans MCP)
|
||||||
│ └── requirements.txt
|
│ └── requirements.txt
|
||||||
|
├── llm-functions/ # collection d'outils pour Qwen3 (sous-repo)
|
||||||
|
│ ├── tools.txt # outils activés
|
||||||
|
│ ├── functions.json # déclarations générées (lues par server.py)
|
||||||
|
│ ├── bin/ # binaires générés par argc build
|
||||||
|
│ └── tools/ # scripts sources (.sh, .py, .js)
|
||||||
├── logs/ # produits par les hooks (ignorés par jj/git)
|
├── logs/ # produits par les hooks (ignorés par jj/git)
|
||||||
└── venv/ # virtualenv Python (ignoré par jj/git)
|
└── venv/ # virtualenv Python (ignoré par jj/git)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Configuration LM Studio
|
## Outils disponibles pour Qwen3
|
||||||
|
|
||||||
Le serveur MCP se connecte sur `http://localhost:1248` avec le modèle `qwen/qwen3-coder-next`.
|
Les outils sont définis dans `.claude/llm-functions/tools.txt` et buildés avec `argc build`.
|
||||||
|
|
||||||
Pour modifier ces valeurs, éditez `.claude/mcp/qwen3-mcp/server.py` :
|
| Outil | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `fs_cat` | Lit un fichier |
|
||||||
|
| `fs_ls` | Liste un répertoire |
|
||||||
|
| `fs_mkdir` | Crée un répertoire |
|
||||||
|
| `fs_write` | Écrit/crée un fichier |
|
||||||
|
| `fs_patch` | Modifie un fichier par patch |
|
||||||
|
| `execute_command` | Exécute une commande shell |
|
||||||
|
| `fetch_url_via_curl` | Requête HTTP GET |
|
||||||
|
| `web_search_aichat` | Recherche web |
|
||||||
|
| `execute_py_code` | Exécute du code Python |
|
||||||
|
|
||||||
|
Pour ajouter un outil : éditez `tools.txt` et relancez `argc build`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration du modèle
|
||||||
|
|
||||||
|
Le modèle Qwen3 est configuré dans `.claude/mcp/qwen3-mcp/server.py` :
|
||||||
|
|
||||||
```python
|
```python
|
||||||
LMSTUDIO_URL = "http://localhost:1248/v1/chat/completions"
|
QWEN3_MODEL = "LMStudio:qwen/qwen3-coder-next"
|
||||||
QWEN3_MODEL = "qwen/qwen3-coder-next"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Le format `Provider:model-id` suit la convention aichat. Adaptez selon votre installation LM Studio.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Fichiers à ignorer (jj / git)
|
## Fichiers à ignorer (jj / git)
|
||||||
|
|
||||||
Ajoutez ces entrées dans votre fichier d'ignore :
|
|
||||||
|
|
||||||
```
|
```
|
||||||
.claude/venv/
|
.claude/venv/
|
||||||
.claude/logs/
|
.claude/logs/
|
||||||
|
.claude/llm-functions/bin/
|
||||||
|
.claude/llm-functions/cache/
|
||||||
```
|
```
|
||||||
|
|
||||||
Avec Jujutsu :
|
Avec Jujutsu :
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
echo ".claude/venv/" >> .gitignore
|
printf ".claude/venv/\n.claude/logs/\n.claude/llm-functions/bin/\n.claude/llm-functions/cache/\n" >> .gitignore
|
||||||
echo ".claude/logs/" >> .gitignore
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Mise à jour
|
## Mise à jour
|
||||||
|
|
||||||
Avec Jujutsu (workflow colocalisé git) :
|
Avec Jujutsu colocalisé (`jj git init --colocate`), la mise à jour est gérée côté git :
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git subtree pull --prefix .claude https://gargoton.petite-maison-orange.fr/eric/CrazyClaude.git main --squash
|
git subtree pull --prefix .claude https://gargoton.petite-maison-orange.fr/eric/CrazyClaude.git main --squash
|
||||||
```
|
```
|
||||||
|
|
||||||
Ou si vous avez cloné séparément, tirez les changements puis recopiez :
|
jj voit le commit résultant comme un commit ordinaire dans son graphe.
|
||||||
|
|
||||||
|
Après mise à jour, reconstruire llm-functions si `tools.txt` a changé :
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd .claude-crazy && jj git fetch && jj new main
|
cd .claude/llm-functions && argc build
|
||||||
cp -r .claude/* ../.claude/
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Outils necessaire
|
|
||||||
|
|
||||||
### aichat
|
|
||||||
|
|
||||||
### jq
|
|
||||||
|
|
||||||
### argc
|
|
||||||
|
|||||||
Reference in New Issue
Block a user