Déléguer une tâche à un autre agent CLI
Exécute une tâche sur un agent CLI externe en arrière-plan et renvoie un résultat normalisé.
Pilote : delegate.mjs, dans le propre répertoire de cette compétence (Node ≥ 18, aucune dépendance).
Le comportement est défini par trois contrats dans contracts/ — lisez-les avant de modifier le pilote.
start renvoie immédiatement un run_id ; collect récupère le résultat. Rien ne vous bloque.
Résoudre d'abord le pilote et le répertoire des runs
Là où cette compétence est installée décide où se trouve delegate.mjs, et les artefacts de run
se placent par défaut à côté du pilote. Définissez les deux une fois par session, puis utilisez
$DELEGATE dans chaque commande ci-dessous :
# Installation du plugin Claude Code (agent-tools@sanduq)
DELEGATE="$CLAUDE_PLUGIN_ROOT/skills/delegate-task/delegate.mjs"
# Installation via npx skills, ou copie commitée dans le dépôt
DELEGATE=".claude/skills/delegate-task/delegate.mjs"
# Conserver les artefacts de run dans le projet en cours, pas à côté du pilote
export DELEGATE_RUNS_DIR="$PWD/.delegate/runs"
DELEGATE_RUNS_DIR est obligatoire pour une installation via plugin : sans lui, les artefacts de
chaque projet s'accumulent dans le répertoire partagé du plugin, et ils contiennent le texte de votre
tâche et la sortie brute du harnais. Ajoutez .delegate/ au .gitignore du projet. Chaque commande
qui touche un run — start, status, collect, list, prune — doit voir la même valeur.
Vérifier d'abord ce qui est disponible
node "$DELEGATE" doctor
doctor sonde la version de chaque CLI, ce qui distingue non installé de installé mais
cassé — et imprime ce que --sandbox demande réellement à chacun.
| Harnais | id | niveau | reprise | demande --sandbox |
|---|---|---|---|---|
| Claude Code | claude | vérifié | vérifié | --permission-mode plan |
| OpenAI Codex | codex | vérifié | vérifié | -s read-only (imposé par la CLI) |
| OpenCode | opencode | vérifié | vérifié | --agent plan |
| GitHub Copilot | copilot | expérimental | expérimental | --mode plan |
| Pi | pi | expérimental | expérimental | --no-approve --tools read,grep,find,ls |
Préférez claude, codex, ou opencode.
Lancer une délégation
node "$DELEGATE" start \
--harness codex \
--cwd /chemin/vers/espace-de-travail \
--timeout 600 \
--task "Ajouter une validation d'entrée à parse_config() et un test couvrant le chemin d'erreur."
Affiche {"run_id": "codex-…", "state": "running", ...} et se termine. Ensuite :
node "$DELEGATE" collect codex-… --wait 300
--wait N interroge jusqu'à N secondes. Sans lui, collect se termine avec le code 3 si le run est
encore en cours.
Autres commandes :
node "$DELEGATE" status <run_id> # starting|running|stalled|adrift|lost|done
node "$DELEGATE" collect <run_id> --json # le résultat lisible par machine — analysez-le
node "$DELEGATE" list # tous les runs, du plus ancien au plus récent
node "$DELEGATE" prune --keep 20 --yes # supprime les anciens artefacts de run
node "$DELEGATE" contract # drapeaux, champs, codes de sortie lisibles par machine
Lecture du résultat
Statut SUCCESSFUL
Parce que harness_telemetry
Modifications
mesurées greeting.txt
déclarées greeting.txt
Permission demandée=bypass appliquée=codex: --dangerously-bypass-approvals-and-sandbox
Trois champs portent l'essentiel du sens :
status—successful/failed/abandoned.status_provenance.primary— pourquoi ce statut, en un mot :timeout,spawn_error,signal,exit_code,parse_error,telemetry_incomplete,harness_telemetry,self_report,head_moved,index_moved,relay_aborted,supervisor_lost,legacy. Ne rapportez jamais un statut sans savoir lequel de ceux-ci l'a produit.evidenceliste toutes les observations qui étaient vraies, pas seulement celle qui a gagné.dirty_paths_changed— ce que git a mesuré, par opposition àfiles_claimed, qui est ce que le délégué a dit.files_mismatchsignale un désaccord.
Permissions
Par défaut, contournement total. Le délégué exécute des commandes arbitraires sans supervision.
--sandbox demande le mode lecture seule réel de chaque CLI (tableau ci-dessus) et active le fil
coup-de-poing : read_only_violation est true / false / null. null signifie inconnu — git
était indisponible ou la couverture d'empreinte était incomplète. Il n'est jamais réduit à false.
false ne signifie pas "aucune écriture n'a eu lieu". Cela signifie qu'aucun delta d'état final
couvert n'a été détecté. Il ne peut pas voir une écriture puis annulation, quoi que ce soit dans
.gitignore, ni aucune écriture en dehors du dépôt. true ne signifie pas que le délégué l'a fait
— une modification concurrente dans la même arborescence de travail semble identique.
L'enveloppe demande au délégué de ne pas commiter, mais c'est une politique, pas une application.
Ce qui l'applique, c'est la mesure : si HEAD bouge pendant la fenêtre du run, le résultat est forcé
en révision requise (head_moved) sauf si vous avez passé --allow-commit.
Continuer un run
node "$DELEGATE" start --harness codex --cwd <même répertoire> --resume <run_id> \
--task "La correction est bonne, mais utilise le vrai fixture au lieu d'un mock."
N'envoyez que le delta. Resume obtient un nouveau run_id avec parent_run_id défini, et refuse
si le harnais, le répertoire de travail ou l'identifiant de session du parent ne correspondent pas.
Un envoi repris répète chaque règle de politique plutôt que de supposer que la session parente les honore toujours.
Drapeaux
| drapeau | usage |
|---|---|
--cwd DIR | répertoire de travail — toujours le définir explicitement |
--model M | remplacer le modèle |
--timeout SEC | par défaut 1800 ; validé en amont ; à l'expiration, l'arborescence est tuée → abandoned |
--deliverable TEXT | ce qui doit être remis ("un diff unifié, ne pas commiter") |
--constraint TEXT | règle supplémentaire répétable |
--sandbox | demander le mode lecture seule du harnais |
--allow-commit | autoriser le commit, et cesser de traiter un HEAD déplacé comme révision requise |
--resume <run_id> | continuer la session d'un run terminé avec une tâche delta |
--clean-env / --keep-env N | ne donner à la CLI que les bases d'exécution ; répéter --keep-env par variable nécessaire à l'authentification |
--raw | envoyer la tâche textuellement (perd le statut/résumé ; incompatible avec --resume) |
Pièges
- Le code de sortie 0 ne signifie pas que la tâche est terminée — et jusqu'à ce refactoring, un
code de sortie non nul ne signifiait pas un échec. Les adaptateurs recevaient le code de sortie
et aucun ne le lisait, donc un événement en forme de succès plus un code de sortie 1 était publié
comme
successful. Corrigé ; verrouillé par le casREPAIR-D1detest/run.mjs. Lisez toujoursstatus_provenance, jamaisexit_codeseul. - Les champs de tokens ont des significations différentes selon le harnais. Les
input_tokens/cache_creation/cache_readde Claude sont disjoints — additionnez-les (une sonde a rapportéinput_tokens : 2pour une requête qui en envoyait vraiment 58 065). Lesinput_tokensde Codex sont inclusifs decached_input_tokens— soustrayez pour le compte frais. Utilisezinput_total/input_fresh;usage_rawconserve les chiffres propres du harnais pour audit mais n'est jamais affiché comme total. - Les totaux ne sont pas comparables entre harnais. OpenCode émet un
step_finishpar étape et le pilote les additionne, donc le contexte renvoyé à chaque étape est compté à chaque fois. Même tâche triviale : 66 341 (codex), 117 014 (claude), 426 523 (opencode). Pas un classement d'efficacité. - Déléguer à
claudea un plancher d'environ 58k tokens.claude -pcharge CLAUDE.md et chaque compétence découverte avant de lire la tâche. - Codex hérite de vos serveurs MCP. Si codex semble lent, lisez
runs/<run_id>/stderr.log— le délai vient généralement du MCP, pas du modèle. - Codex émet des événements
item.type: "error"lors de runs réussis. Seulsturn.failedou unturn.completedmanquant comptent. - Le contournement total est vraiment non isolé. Un délégué OpenCode a un jour écrit un fichier
à l'extérieur de son
--cwd. Utilisez--sandbox, ou un répertoire jetable. - Tous les harnais ne rapportent pas tous les champs. Copilot n'émet aucun nombre de tokens
(
fidelity : "unavailable"— des zéros signifient inconnu, pas gratuit). Codex n'émet ni coût ni modèle nulle part dans son JSONL, donccost_usdetmodelsont null par nature, pas par une erreur d'analyse —cost_reported/model_reporteddisent lesquels, et le résultat imprimé indique "non rapporté par ce harnais". Claude rapporte les deux ; OpenCode rapporte le coût. - Le texte de la tâche n'atteint jamais un shell. Chaque CLI est résolue en un exécutable réel
et lancée avec
shell:false. pipersiste une session dans son propre espace de stockage.--no-sessionn'est volontairement pas passé — sans session, il n'y a rien à reprendre. Les sessions atterrissent dans~/.pi/agent/sessions/<slug-cwd>/, indexées par répertoire de travail ;prunen'y touche pas. L'authentification de Pi est basée sur fichier (~/.pi/agent/auth.json), donc--clean-envne la casse pas.- Un run dont le superviseur est tué est
lost, pas bloqué.collectsynthétise un résultat terminal à partir du journal et des logs. Son delta git estnull: aucune base de début de run n'a survécu, et rapporterfalseserait une assurance qu'aucune mesure n'a été faite. - Les identifiants de run sont validés. Ils doivent correspondre à
<harnais>-<14 chiffres>-<6 hex>, et doivent se résoudre directement sous le répertoire des runs. Un identifiant fourni par l'appelant ne peut pas en sortir par traversée. - Un superviseur mort dont le harnais est encore vivant est
adrift, paslost.collectrefuse de publier, car un résultat écrit à ce moment instantané serait un run encore en mouvement. Attendez l'enfant, ou tuez le pid questatusnomme.prunene supprimera pas non plus un run adrift. DELEGATE_BIN_<HARNASS>remplace l'emplacement où un binaire est trouvé — par exempleDELEGATE_BIN_CODEX=/opt/codex/bin/codex. C'est aussi ainsi que la suite de tests pilote un faux harnais de bout en bout sans dépenser un token.
Dépannage
| symptôme | cause / correctif |
|---|---|
<bin> introuvable dans PATH | cette CLI n'est pas installée ; exécutez doctor |
trouvé <chemin> mais impossible de résoudre ce qu'il lance | la CLI est installée derrière un shim .cmd non-npm que ce pilote ne peut pas décoder |
doctor affiche BROKEN: probe timed out | généralement un démarrage à froid ; réexécutez doctor avant de conclure quoi que ce soit |
le superviseur n'a pas démarré (code 5) | le superviseur détaché n'a jamais accusé réception ; vérifiez runs/<id>/journal.jsonl. Le run est finalisé failed avec containment_evidence: "le harnais n'a jamais été lancé" ; si le journal ne contient que created et terminal et que le pid du superviseur est parti, aucun agent n'a tourné |
run <id> encore en cours d'exécution (code 3) | ajoutez --wait <secondes> |
Refus de publier … toujours en cours (code 6) | le run est adrift : le superviseur est mort mais le harnais vit. Attendez, ou tuez le pid qu'il nomme |
un autre processus finalise ce run (code 7) | un écrivain actif détient la revendication ; réessayez dans un instant |
écrit par un pilote différent (code 8) | le résultat porte un schéma inconnu ; cette version ne devinera pas |
statut abandoned, provenance supervisor_lost | le superviseur a été tué ; inspectez l'arborescence de travail avant de redéléguer |
statut failed, provenance head_moved / index_moved | quelque chose a commité ou stagé pendant le run ; l'arborescence est préservée, examinez-la |
Résumé indique (aucun résumé produit) | le délégué n'a jamais émis le bloc de résultat ; lisez runs/<run_id>/stdout.log |
Fichiers
Les artefacts de run se trouvent dans $DELEGATE_RUNS_DIR/<run_id>/ (prompt.txt, meta.json, journal.jsonl,
stdout.log, stderr.log, result.json). Non défini, DELEGATE_RUNS_DIR par défaut est runs/ à côté
du pilote — ce qui est le répertoire partagé du plugin lors d'une installation via plugin, donc définissez-le.
Limitez la taille des logs avec DELEGATE_MAX_LOG_BYTES (par défaut 32 Mo par flux) et la fenêtre de
rétention après limite avec DELEGATE_LOG_TAIL_BYTES (par défaut 512 Ko).
Passé la limite, le pilote conserve des enregistrements analysés complets plutôt qu'un suffixe d'octets,
donc la télémétrie reste analysable. Les enregistrements que la fenêtre ne peut pas contenir sont comptés dans
records_dropped — une valeur non nulle signifie que le statut est toujours fiable mais que la réponse
finale du délégué peut être incomplète.
⚠️ Les artefacts contiennent le texte de votre tâche, l'invite complète et la sortie brute du harnais —
traitez-les comme sensibles. La finalisation masque les formes de tokens et les paires NOM=valeur
ressemblant à des identifiants dans stdout.log et stderr.log (pas dans result.json). Sur POSIX,
le répertoire des runs est chmod 0700 ; sur Windows, les permissions sont héritées et ce pilote ne peut
pas les restreindre. Un collect réussi supprime les runs terminés plus vieux que DELEGATE_PRUNE_DAYS
jours (par défaut 30 ; 0 désactive) ; nettoyez plus tôt avec prune.
Aller plus loin
-
Pour un lot de vérifications CLI locales connues, lisez le contrat natif du runner de vérifications. Il couvre les plans d'argv JSON, les tentatives immuables, la collecte bornée et l'invocation PowerShell. Utilisez la même référence pour
collect --summary, les diagnostics uniquement par battement de cœur et les délais de démarrage optionnels. -
contracts/status-precedence.md— comment un statut est décidé, et le produit croisé à tester -
contracts/git-fields.md— ce que le fil coup-de-poing mesure et ce qu'il ne peut pas voir -
contracts/result-schema-v2.md— la forme du résultat, lecture arrière v1, journal, mono-écrivain -
references/writing-the-brief.md— comment rédiger une tâche que le délégué peut exécuter à l'aveugle -
references/reviewing-the-result.md— comment examiner ce qui revient, y compris le balayage -
references/task-queues.md— exécuter plusieurs tâches dépendantes sans perdre le fil
Tests : node test/run.mjs, depuis le répertoire de cette compétence.
Régressions d'exécution : node --test test/execution.test.mjs (fixtures CLI Node/Python).