Les tâches¶
Comptabilité du dispatch : coût par tâche résolue et pass^k¶
grimoire dispatch stats est la sœur de providers history : même idée
(compter, ne pas classifier), autre source et un autre angle. providers
history lit le Mission Ledger au grain de la tentative et compte des
taux d'escalade par couple (type de tâche, classe) ; dispatch stats lit le
journal de traces au grain de la cascade entière (un événement
dispatch.outcome par grimoire task dispatch ou par node de
grimoire flow run --executor dispatch réellement exécuté — jamais pour un
refus avant tout appel ni un --dry-run) et répond à deux questions que
seules des campagnes d'évals manuelles répondaient jusque-là (issue #442,
point 5 de l'audit de positionnement du 2026-09-12) : combien coûte une
tâche résolue, et à quel point une même tâche rejouée plusieurs fois
réussit-elle systématiquement ?
| Commande | Description |
|---|---|
grimoire dispatch stats [--since 30d] [--json] |
Coût par tâche résolue, taux d'escalade, part d'inexécutable — pour l'ensemble, par classe de vérifiabilité, par fournisseur et par flow — et pass^k sur les nœuds rejoués |
Coût par tâche résolue : le coût total de toutes les tentatives (vertes
et rouges confondues, un check rouge a quand même coûté un appel) divisé
par le nombre de dispatchs résolus — jamais divisé par le nombre total de
tentatives, qui sous-estimerait le coût réel d'un succès quand la cascade a
dû escalader. None sans aucun dispatch résolu, jamais une division par
zéro qui se lirait à tort comme un coût nul.
pass^k : quand un même nœud (le node d'un blueprint de flow, identifié par blueprint + id de node — ou, hors flow, directement l'identifiant de la tâche) a été dispatché au moins deux fois, cette répétition forme une « série ». Une série compte comme entièrement verte seulement si toutes ses exécutions ont résolu la tâche — la fiabilité au sens strict (k exécutions, toutes réussies), pas une moyenne de taux de réussite individuels. Le taux affiché est la part de séries entièrement vertes parmi celles observées au moins deux fois ; une tâche jamais rejouée n'entre dans aucun dénominateur.
--since 30d (ou 12h) ne garde que les dispatchs dont l'horodatage est
plus récent que cette ancienneté. Sans journal de traces sous
_grimoire-output/traces/, la commande n'échoue jamais : elle affiche qu'il
n'y a rien à montrer ({"overall": null} en JSON).
Le contrôle du standard dispatch.cost_slo (voir
Contrôles gouvernés) lit exactement les
mêmes agrégats — cette commande en est la vue humaine, jamais un second
calcul.
Tâches agentiques¶
Le task board gouverné est une projection du Mission Ledger, pas une source
(voir ADR-005). Éditer
_grimoire/standard/task-board.yaml à la main ne change rien au ledger, et le
prochain export l'écrase.
| Commande | Description |
|---|---|
grimoire task board export [.] |
Régénérer le board depuis le Mission Ledger |
grimoire task board export . --dry-run |
Afficher la projection sans écrire |
grimoire task board export . --mission <id> |
N'exporter qu'une mission |
grimoire task board export . -o <chemin> |
Écrire ailleurs que dans le board du standard |
grimoire task add "<titre>" -a "<critère>" [--owner <qui>] [--ready] |
Ouvrir une tâche (un critère d'acceptation au moins) ; --ready la rend réclamable tout de suite, gate proposed_to_ready compris |
grimoire task add "<titre>" -a "<critère>" --description "<brief>" ou --description-file <fichier> |
Donner à la tâche son contexte (pourquoi, pistes) au-delà du titre : texte brut, 4000 caractères au plus ; rendu par task show, l'outil MCP task_show, task_context (bundle) et la carte du cockpit (#714) |
grimoire task add "<titre>" -a "<critère>" --finition maquette\|peaufine |
Poser la finition du livrable (R24). Sans l'option, la tâche hérite de la phase de project-context.yaml (clé phase ou project.phase : exploration → maquette, construction et exploitation → peaufine) ; sans phase déclarée ou reconnue, le champ reste vide (le kit ne devine pas). task show rend finition. La valeur machine est sans accent ; peaufiné est accepté en entrée. Une valeur inconnue est refusée avant toute écriture. L'outil MCP task_add porte le même paramètre finition |
grimoire task add --intake "<texte>" |
Proposer, sans rien écrire, le type de mission, le risque, le périmètre, les contrôles de politique attendus et les tâches d'une demande (JSON sur stdout, classement déterministe sans modèle) ; ne se combine ni avec un titre ni avec -a (R07). Les tâches se créent ensuite une à une par task add |
grimoire task import <plan.json> [--mission <id>] [--dry-run] |
Importer un plan JSON (P50) : crée les tâches (titre, critères, preuves, priorité) et leurs dépendances (task link), puis rend la table clé du plan → clé du ledger sur stdout (clé<TAB>GAO-…<TAB>action). Le champ "plan" (identité du plan, sans blanc) est obligatoire ; les clés de tâche n'ont ni blanc ni caractère de contrôle (100 caractères au plus) ; au plus 200 tâches et 1 Mo par fichier. Le plan est validé en entier avant d'écrire : toute anomalie (champ inconnu, critère manquant, clé en double, dépendance absente, cycle, mission inconnue, plafond) refuse tout, rien n'est écrit, code 1. Idempotent et sûr en concurrence : la relecture du ledger, les créations et les liens se font sous un verrou inter-processus, donc N imports simultanés d'un même plan créent un seul jeu de tâches ; une entrée déjà importée (extra.import = {plan, key}) n'est pas recréée. Si son titre ou ses critères diffèrent du ledger (plan modifié, ou autre plan qui réutilise l'identifiant), le plan entier est refusé (code 1, rien d'écrit) : aucune tâche n'est écrasée. Une entrée {"key": …, "existing": "GAO-…"} désigne une tâche déjà au ledger, sautée (jamais modifiée) mais utilisable comme cible de depends_on. La relance complète les liens manquants des tâches importées par ce plan (reprise après interruption) et ne touche à aucun autre champ. --dry-run montre ce qui serait créé sans toucher au ledger |
grimoire task list [--status <état>] [--mission <id>] |
Lister les tâches et leur colonne de board |
grimoire task wave-checkpoint <mission> <vague> [--action status\|request\|require\|approve\|reject] [--fingerprint <empreinte>] |
Checkpoint humain d'une vague, lié à l'empreinte du contenu fusionné (arbres base, tête et merge de chaque tâche). request présente l'empreinte, approve/reject la citent et exigent un tour humain attesté postérieur (W2-01) ; require ferme le claim des vagues suivantes tant que la vague n'est pas approved. Une vague dont le contenu change devient stale. Le numéro de vague est celui de task waves (niveau absolu, vagues closes comprises) ; le verrou s'applique à toute transition vers claimed |
grimoire task show <id> |
Détailler une tâche et ce que chaque prochain pas exigera |
grimoire task claim <id> [--actor <qui>] [--host <où>] |
Réclamer une tâche prête (ready → claimed). Une tâche claimed/running dont le bail est perdu (expires_at passé, battement de cœur perdu) se réclame directement : barrière du bail, retour ready journalisé (reclaimed), nouveau claim au nouvel epoch ; un autre acteur que le titulaire seulement si l'hôte du claim a un frein vérifié, sinon grimoire task reap pose la question à l'humain. Le WIP ne compte que les claims vivants |
grimoire task move <id> --to <état> |
Déplacer une tâche, si la machine à états et le gate le permettent |
grimoire task block <id> --reason "<motif>" |
Bloquer en disant pourquoi |
grimoire task close <id> |
Fermer une tâche vérifiée (verdict accepté exigé ; depuis needs_verification, reçu de revue frais exigé) |
grimoire task review <id> --verdict READY\|NOT-READY [--finding "résumé :: commande"] |
Écrire le reçu de revue, lié à l'empreinte présente de l'arbre (_grimoire-output/reviews/<id>/review-receipt.json) |
grimoire task verify <id> [--evidence <fichier> ...] |
Lancer les commandes déclarées de la tâche, écrire le pack et le verdict (0 passed, 1 failed, 2 aucune commande, 3 commandes non approuvées). Pour une tâche sans commande, --evidence (répétable) écrit un pack déclaratif : chaque fichier doit exister sous la racine du projet, il entre haché (SHA-256, taille, URI file:<chemin>), le verdict est skipped (created_by: declarative, contrôle declarative-only) et le code de sortie 4 : la revue peut s'ouvrir (needs_verification), la clôture reste refusée (review_gate) — une preuve déclarative n'est jamais un vert mécanique |
grimoire task accept <id> --reason "<ce qui a été relu>" [--no-close] |
Accepter la preuve déclarative d'une tâche en revue (canal humain seulement, refusé depuis une session d'agent comme task answer) : pack d'attestation (les éléments du dernier pack plus l'acceptation, qui nomme chaque critère), verdict passed signé created_by: human:<acteur>, reçu de revue READY (reviewer_id: human:<acteur>), puis clôture ; --no-close s'arrête avant (issue #865) |
grimoire task prioritize <id> --to low\|medium\|high\|critical [--reason "<motif>"] |
Changer la priorité (issue #638) — l'historique reste au ledger, le board se retrie |
grimoire task comment <id> "<texte>" [--kind comment\|directive] |
Poser un commentaire ou une consigne ; la session qui tient la tâche la lit au prochain UserPromptSubmit |
grimoire task cancel <id> --reason "<motif>" [--force] |
Annuler avec sa raison (obligatoire) ; tenue par une autre session, elle exige --force |
grimoire task ack <id> <dir-…> |
Accuser réception d'une consigne de l'orchestrateur |
grimoire task link <id> --depends-on <id> |
Déclarer une dépendance |
grimoire task unlink <id> --depends-on <id> --reason "<motif>" [--kind <nature>] |
Retirer une dépendance, motif journalisé. Une dépendance annulée bloque le claim pour toujours (elle ne sera jamais close) : si elle a été remplacée, on retire le lien puis on relie la remplaçante avec task link |
grimoire task ask <id> "<question>" [--kind <nature>] [-o <option>] [--non-blocking] [--why "<pourquoi>"] [--key <clé>] |
Poser une question à l'humain : une bloquante gare la tâche en waiting(human) et remonte au cockpit ; --non-blocking l'inscrit sans garer la tâche |
grimoire task ask <id> --from <lot.yaml\|-> |
Poser un lot de questions (JSON ou YAML) en une interruption et une notification ; une question déjà répondue (même clé) n'est pas reposée |
grimoire task answer <id> "<réponse>" [--question <q-…>] |
Répondre (canal humain seulement) ; la tâche n'est rendue que quand toutes les bloquantes du lot ont leur réponse |
grimoire task questions [<id>] [--open] |
Le registre des questions : statut, bloquante ou non, pourquoi, qui a répondu, par quel canal ou sur quelle source, et le nombre d'interruptions |
grimoire task resolve <id> <q-…> "<réponse>" --source <d'où> |
Déduire la réponse d'une question non bloquante, source citée ; une bloquante est refusée |
grimoire task decisions |
Les décisions qui attendent un humain (un lot = une décision, ses questions ouvertes listées) |
grimoire task context <id> |
Produire le context bundle d'une tâche réelle |
grimoire task trace <id> [--causes] |
Timeline unifiée d'une tâche : transitions, outils refusés, gates rouges, checkpoints, abort, preuves, incidents |
grimoire task approve-check <id> [--cmd "<commande>"] [--allow-unattested] |
Approuver par empreinte les commandes de vérification de la tâche, pour cette tâche seule (governed/production : sans approbation, task verify et le dispatch de flow ne lancent rien) ; l'acteur est attesté par un tour humain postérieur au challenge de cet ensemble exact de commandes ; sans attestation (terminal nu), l'approbation est refusée sauf --allow-unattested (journalisée unattested) ; les caractères cachés (U+202E, U+200B) sont refusés |
grimoire task trace-export [<dest>] [--format otel\|langfuse\|otlp] [--endpoint <url>] [--header NOM=VALEUR] [--mission-id <id>] |
Exporte le TraceLedger (plus, pour OTel et OTLP, les spans des runs de flow lus dans le journal du kernel, avec parentSpanId) : JSONL aux conventions OTel GenAI ou Langfuse, ou --format otlp --endpoint vers un collecteur OTLP/HTTP (échoue si le lot n'est pas accepté) |
grimoire task recall <id> |
Ce que la mémoire du projet sait de cette tâche et de ses voisines — borné en tokens |
grimoire task dispatch <id> --check "<commande>" [--check ...] [--dry-run] [--max-tier cheap\|mid\|strong] [--start-tier cheap\|mid\|strong] [--provider <id>] [--timeout <s>] |
Déléguer la tâche en cascade par palier de fournisseur |
Une attente porte sur sa tâche, pas sur la session¶
grimoire task wait <id> --reason ci|quota|paused et grimoire task ask <id> "<question>" garent
la tâche (waiting). Ils ne gèlent ni la session ni ses sous-agents : le frein du bail
(hook PreToolUse) se tait sur une session dont la tâche est garée, qui est jugée comme une session
sans tâche — mêmes consignes que le repli normal (grimoire task claim|attach <autre-tâche>).
Restent possibles grimoire task … sur n'importe quelle tâche et grimoire cockpit serve (pour que
l'humain réponde). Ce qui ne change pas : aucune écriture ne se fait au nom de la tâche garée, et une
question humaine bloquante ne se lève que par l'humain — task resume est refusé sur un
waiting(human). La réponse se donne ainsi, exécutable telle quelle (à lancer avec le binaire du
projet, .venv/bin/grimoire si grimoire n'est pas dans le PATH) :
task wait et task ask (et l'action MCP wait / ask) préviennent quand des sous-agents de la
session tiennent encore leurs propres claims : garer le coordinateur ne les arrête pas. L'action MCP
task_update(action="resume") reprend sans epoch quand la session de l'environnement est celle qui
porte le claim (comme la CLI avec --epoch omis) ; sinon l'erreur « aucun epoch à présenter » reste.
Sans ledger, la commande d'export refuse et sort en erreur plutôt que d'écrire un board vide — écraser le travail déclaré par du néant serait pire que ne rien faire.
Chaque transition d'état (claim, move, block, close, cancel) franchit deux
portes avant de toucher le ledger : la machine à états, puis le gate de preuve de
_grimoire/standard/evidence-gates.yaml. Un refus nomme la preuve manquante et le
remède ; rien n'est écrit. Après une écriture acceptée, le board du standard est
reprojeté depuis le ledger si le projet est enrôlé (_grimoire/standard/ présent)
— plus besoin de relancer task board export pour qu'un claim se voie.
Les mêmes gestes sont exposés aux agents par le serveur MCP (task_add,
task_intake pour add --intake, task_list_ready, task_show, task_waves, task_claim, task_update, task_context) :
même service (TaskService.add pour l'ouverture), même gate, même refus. Voir
Intégration MCP.
Questions structurées¶
Une question posée à l'humain est un objet du Mission Ledger, pas une phrase dans le
fil de discussion (W6-07, issue #564). Chaque tâche porte le registre de ses questions
(task show --json, champ questions ; task questions le rend à plat avec son statut) :
- bloquante ou non : une bloquante gare la tâche en
waiting(human); une non bloquante ne la gare pas, l'humain y répond quand il veut ; - par lot :
task ask --from(ou l'outil MCPtask_updateavecquestions) pose plusieurs questions en une attente, une carte du cockpit et une notification ; la tâche n'est rendue qu'une fois toutes les bloquantes du lot tranchées ; - pourquoi (
why) : la raison de la question, montrée au cockpit et dans le registre ; - qui a répondu :
answeredvient du canal humain (cockpit, CLI interactive) ;deducedvient de l'agent, source citée (task resolve, ouansweretsourcedans le lot), et n'est permis que pour une non bloquante ;not_awaitedest une bloquante restée sans réponse que la tâche n'attend pas ou plus (run de flow abandonné, tâche annulée) : elle n'est pas comptée comme répondue ; - reprise : une question du lot dont la clé (
key, ou le texte normalisé) est déjà répondue ou déduite n'est pas reposée ; sa réponse est rendue dansreused.
# lot.yaml — grimoire task ask <id> --from lot.yaml
questions:
- text: Maquette ou version peaufinée ?
options: [maquette, peaufiné]
why: la finition n'est pas déterminable depuis le dépôt
- text: Supprimer la table users_v1 ?
kind: permission
why: arbitrage irréversible
- text: Quel gestionnaire de paquets ?
blocking: false
answer: uv
source: uv.lock
Un agent ne répond jamais à une question bloquante, ni par la CLI hors terminal humain,
ni par le MCP : task ask sans les options du lot garde le comportement d'origine
(une question, reposée telle quelle si on la repose).
Vérifiabilité d'une tâche¶
grimoire task show <id> et la projection du board (verifiability) portent
une classe dérivée — jamais déclarée — depuis le texte des critères
d'acceptation et des preuves attendues : V0 si tous nomment un verdict
qu'un programme rend seul (test, lint, schéma, gate, code de sortie, build/CI,
fichier attendu), V1 si au moins un critère nomme une revue ou une
validation par une personne ou un agent sans qu'aucun autre ne reste ambigu,
V2 sinon — y compris une tâche sans aucun critère. Un critère formulé de
façon vague (« le code est propre ») ne peut jamais produire un V0 : un seul
critère ambigu suffit à faire monter la classe, jamais à la faire descendre.
task show affiche, critère par critère, le motif reconnu — c'est ce qui rend
lisible pourquoi une tâche n'est pas au niveau qu'on lui prêtait.
Route de cérémonie et profondeur de test¶
grimoire ceremony suggest calcule, sans LLM, la cérémonie qu'une tâche
mérite : une route parmi cinq, et la profondeur de test exigée à la fermeture.
Deux appels sur les mêmes entrées rendent les mêmes octets (--json, avec
l'empreinte inputs_digest des entrées et des seuils).
| Commande | Description |
|---|---|
grimoire ceremony suggest <id> [-f <fichier>…] [--diff-base <ref>] [--impact <n> \| --measure-impact] [--json] |
Route d'une tâche du ledger |
grimoire ceremony suggest --title "<titre>" -a "<critère>" [--check "<cmd>"] [--risk <profil>] … |
Route d'une tâche décrite en ligne |
grimoire ceremony budget [--file evals/ceremony-budget.yaml] [--json] |
Vérifier un budget de cérémonie ; sortie 1 au premier écart (job CI du kit) |
grimoire task add "<titre>" -a "<critère>" --test-depth <floor\|standard\|strict\|auto> |
Exiger une profondeur de test à la fermeture (auto : celle de la route) |
Les routes, de la plus légère à la plus lourde : express (verdict mécanique
seul, profondeur floor), checked (tests puis relecture par un agent,
standard), reviewed (plus une relecture humaine, standard), guarded
(tests et analyse statique, double relecture, checkpoint humain, strict),
session (jugement ou conception en session avec une personne, strict).
Chaque règle donne un plancher et la route est le maximum des planchers :
V1 → checked, V2 → session, risque strict → reviewed, risque
security_critical ou release → guarded, fichier de secret, de workflow CI,
de migration ou de publication → guarded, plus de files_wide fichiers ou
plus de impact_wide importeurs directs des .py touchés (--measure-impact, lu par ast ; le code graph du kit ne résout pas ses arêtes d'import) → checked. Des fichiers
inconnus montent la route à checked ; un impact non mesuré ne la monte pas,
et la sortie le dit dans limits. Les seuils portent calibrated et leur
date : aucun n'est calibré sur des runs réels aujourd'hui.
La profondeur se mesure sur les commandes du dernier verdict
grimoire task verify, pas sur celles que la tâche promet : floor (une
commande verte), standard (dont une commande de test : celle que le projet
déclare en needs.commands.test-runner, ou un outil reconnu comme pytest,
go test, npm test), strict (en plus une analyse statique verte : lint,
typecheck déclarés, ou ruff, mypy, eslint, tsc…). Une tâche qui
déclare test_depth ne passe en accepted ou released qu'à cette profondeur ;
sinon le gate refuse avec acceptance.depth_floor_unmet (bloquant selon la
sévérité du profil, comme les autres preuves). Une profondeur inconnue est un
refus. Le routeur ne choisit pas de modèle : il choisit la cérémonie.
Ouvrir une mission¶
| Commande | Description |
|---|---|
grimoire mission open "<titre>" [-d "<visée>"] [--scope-repo <dépôt>] [--scope-surface <surface>] [--scope-pack <pack>] |
Ouvre une mission dans le Mission Ledger et rend son identifiant (MIS-…) sur stdout ; les --scope-* sont répétables |
task add --mission <identifiant> y rattache ensuite des tâches (une mission
inconnue est refusée). Deux titres homonymes — à la casse et aux espaces près —
sont refusés avec l'identifiant de la mission existante, y compris entre deux processus simultanés (la vérification et la création se font sous un même verrou) ; un titre vide aussi ; rien
n'est écrit sur un refus (code 1). Le périmètre (scope.repos, scope.surfaces,
scope.packs) est écrit au ledger sans doublon ni entrée blanche, et l'outil MCP
task_context le rend sous mission_scope avec la tâche (R25, P49).
Rapport de mission¶
| Commande | Description |
|---|---|
grimoire mission report <mission> [--json] |
Coût, acceptance, palier, preuves et rejeu d'une mission, chaque ligne avec sa source |
grimoire mission report rassemble ce que trois journaux gardaient chacun de leur
côté : les tentatives de la cascade (task.dispatched, Mission Ledger), le résumé
de cascade et l'usage de tokens replié (TraceLedger) et les packs et verdicts
(EvidenceService). Chaque tentative porte son cost.status (exact ou
unknown), l'issue de l'acceptance (passed, failed, unrunnable, not_run)
et le trace_id de sa cascade ; chaque ligne nomme sa source (ledger:,
traces:, evidence:).
- Jamais de chiffre inventé. Sans aucun coût connu, le rapport n'a aucun total
numérique et écrit « coût inconnu ». Un agrégat partiel est une borne basse
(
>= X USD), et une tâche sans donnée de coût est comptée à part. - Le journal de session et les tentatives ne s'additionnent pas. Ce sont deux
vues des mêmes appels : une tâche retient le journal replié par
task fold-usages'il existe, sinon les tentatives, et les deux vues restent affichées. - Le lien tentative vers trace est exact ou absent. Une cascade est rattachée à
l'unique trace
dispatch.outcomeécrite entre sa dernière tentative et la première de la suivante ; sinontrace_idvautnull(trace_link: none). - Lecture seule et déterministe. Aucun dossier n'est créé, aucune date de génération n'est écrite : deux exécutions sur les mêmes journaux donnent les mêmes octets. Mission inconnue ou ledger absent : sortie 1.
Registre des plans¶
| Commande | Description |
|---|---|
grimoire plans list [--status active\|absorbed\|archive\|incubator] [--check] [--registry <fichier>] |
Lister les plans du registre (deprecated-plans-registry.yaml) : identifiant, statut, titre, et le plan qui absorbe un plan absorbé (R08) |
Lecture seule. Le registre se cherche à
_grimoire-runtime-output/planning-artifacts/deprecated-plans-registry.yaml sous la
racine du projet (--registry en désigne un autre) ; absent ou illisible, la commande
le dit et sort en 1, elle n'en invente pas un. La liste part sur stdout (colonnes
séparées par une tabulation), grimoire -o json plans list rend le registre entier (l'option -o json est globale, il n'y a pas de --json propre à la commande). --check valide aussi
le registre (ids uniques, statuts connus, absorbed_by renseigné, chemins présents,
au moins un plan actif) et sort en 1 s'il est incohérent.
Plafonds MAST par instance de workflow¶
Chaque instance créée par le RuntimeKernel (create_instance) porte deux
plafonds indépendants, surchargeables à la création (max_tool_calls=50,
max_budget=100 par défaut) : le nombre d'appels d'outils que
mediate_tool médiera — la seule unité de « tour » que le kernel observe —
et un budget d'unités de coût consommées par ces mêmes appels (1 unité par
appel par défaut ; un appelant qui connaît un coût réel, en tokens ou en
devise, peut passer cost= à chaque appel). Le kernel ne voit aujourd'hui
passer ni tokens ni coût ailleurs, d'où ce choix par défaut. Au premier appel
qui dépasserait l'un des deux plafonds, l'instance passe dans l'état terminal
refused : un checkpoint est écrit, un événement workflow.refused est
journalisé, et l'appel — comme tout appel suivant sur cette instance — rend
False sans jamais lever d'exception ni laisser l'instance continuer en
silence. Voir src/grimoire/runtime/kernel.py et schemas.py
(WorkflowStatus.REFUSED, RunEventType.WORKFLOW_REFUSED).
Pourquoi une tâche s'est arrêtée¶
grimoire task trace <id> lit les journaux qui portent chacun le task_id
— le Mission Ledger (transitions, incidents), le TraceLedger des hooks (outils
autorisés ou refusés par la policy, clôtures refusées, gates de
transition rouges, dispatch d'agent ou non-choix), le RuntimeKernel (run
events, checkpoints, abort/refus et leur raison), l'EvidenceService
(packs, verdicts) et, s'il existe, l'export OTel du TraceLedger (grimoire
task trace-export --format otel, #322) — et les trie dans le temps. Cette
dernière source lit un fichier déjà écrit par trace-export (emplacement
conventionnel : <traces>/otel-export.jsonl) ; elle ne relance jamais l'export
elle-même. Les entrées qui expliquent un arrêt sont marquées et reprises dans
une section « Cause(s) d'arrêt » ; --causes n'affiche qu'elles ; --output
json rend la timeline complète avec ses sources. Une source absente est
nommée comme telle ; rien n'est créé, rien n'est inventé. bootstrap se trace
aussi — c'est là que les hooks écrivent tant qu'aucune tâche n'est réclamée.
Le cockpit et l'atelier affichent la même timeline dans l'espace Exécuter
de la vue de travail (onglet Timeline, drill-down depuis une carte du board),
et l'espace Observer y renvoie depuis un span OTel qui porte un
grimoire.task_id — voir la page du cockpit, section
« Le board (espace Exécuter) ».
GAO-livrer-la-ti-001 — Livrer la timeline (running)
21:14:39 ledger ready → claimed par claude (claim claude@claude-code)
✗ 21:14:40 hooks Bash refusé par la policy (args 3f2a9c1e0b7d4e11)
✗ 21:14:41 gate gate « in_progress_to_review » refuse la transition — manque : evidence_pack, decision_trace
✗ 21:14:42 runtime workflow WFI-recipe-livraison-001 abandonné — raison : suite pytest rouge
Cause(s) d'arrêt : 3
Dispatch par cascade¶
grimoire task dispatch <id> délègue une tâche à un fournisseur LLM du
registre (llm-provider-registry.yaml), en cascadant par palier de coût
(cheap → mid → strong) selon la classe de vérifiabilité de la tâche : une
tâche V2 est refusée avant tout appel (aucun verdict mécanique ne peut la
juger), une V0 cascade depuis cheap, une V1 cascade depuis mid et,
au vert, passe en needs_verification au lieu d'être considérée close — le
check mécanique n'y est qu'un indice, la classe exige encore un regard
humain. Au moins un --check <commande> est obligatoire (code de sortie 0 =
vert, toutes doivent l'être) ; sans lui, la classe dit que le verdict est
mécanique, encore faut-il dire lequel.
Un échec d'appel (429, timeout, code de sortie non nul) passe au fournisseur
suivant du même palier et pose un refroidissement (grimoire providers
status) ; un --check rouge passe au palier suivant. Chaque tentative laisse
un événement task.dispatched au Mission Ledger (palier, fournisseur,
modèle, durée, verdict, coût si la sortie est un JSON portant
total_cost_usd, type de tâche, classe de vérifiabilité, palier de départ et
sa raison). --dry-run montre la classe, la chaîne de paliers prévue et le
prompt sans rien appeler ; --max-tier borne la cascade ; --provider la
restreint à un seul fournisseur.
Code de sortie : 0 si la chaîne finit au vert, 1 si elle s'épuise sans
verdict vert, 2 sur un refus (V2, aucun --check, aucun fournisseur
invocable). Ce lot ne worktree pas automatiquement : travaillez sur une
branche propre avant de dispatcher — le fournisseur délégué écrit dans le
dépôt courant.
Palier de départ ajusté par l'historique¶
Le palier de départ de la cascade n'est plus fixe : sauf --start-tier
explicite, il vient de l'historique des dispatchs passés pour le couple
(type de tâche, classe de vérifiabilité) — voir grimoire providers
history (issue #312). Par défaut, cheap pour V0 et
mid pour V1 (le plancher que la classe impose, jamais franchi vers le
bas) ; ajusté vers mid, puis strong, quand un couple escalade trop
souvent depuis son palier habituel, et re-sondé vers le bas quand il cesse
d'escalader et que le palier inférieur n'a pas été retenté depuis un
moment. Le rapport (start_tier, start_tier_reason) et l'événement
task.dispatched disent tous deux quel palier a été retenu et pourquoi ;
--start-tier cheap|mid|strong l'impose sans consulter l'historique.
Le pilote : combien dépenser par node (issue #209)¶
Une fonction de décision — jamais une couche — appelée avant chaque node
dispatché par flow run --executor dispatch : le graphe dit quoi, le
pilote dit combien. Une politique de projet optionnelle,
_grimoire/standard/pilot.yaml, la paramètre ; absente, le comportement est
exactement celui décrit ci-dessus (palier de départ ajusté par
l'historique, aucun plafond).
start_tier:
V0: mid # ne peut jamais descendre sous le plancher de la classe
max_escalations: 1 # 0 = aucune escalade permise, absent = pas de plafond posé ici
max_cost_usd_per_node: 0.50
on_unknown_cost: stop # défaut ; ou continue_flagged
max_cost_usd_per_node s'applique dans missions.dispatch.run_dispatch
lui-même (max_cost_usd) : entre deux paliers, jamais au milieu d'une
tentative ni après un vert déjà acquis — seule l'escalade vers un palier
plus cher est abandonnée. Le rapport porte alors cost_capped (un message
nommé) plutôt qu'une chaîne simplement épuisée ; côté flow status, le node
apparaît avec le verdict cost_capped. Un pilot.yaml malformé refuse
nommément le chargement plutôt que de retomber en silence sur « aucun
plafond » — contrairement à orchestration-policy.yaml, un plafond de coût
est un mécanisme de sécurité.
Un fournisseur qui ne rend pas son coût (aucun JSON portant total_cost_usd,
le cas de la plupart des fournisseurs headless) laisse ce coût inconnu :
il n'est jamais compté pour zéro. on_unknown_cost: stop (défaut,
fail-closed) arrête l'escalade dès qu'un palier a répondu sans coût alors
qu'un plafond est posé — le rapport porte cost_capped avec la raison
cost_unknown (cost_cap_reason). continue_flagged laisse escalader ;
le coût reste alors signalé lower_bound (>= 0.4200 USD (1 non pricés)) ou
unknown dans le rapport, la trace dispatch.outcome, grimoire dispatch
stats, flow status, flow list et le contrôle dispatch.cost_slo
(tous rendent le même format, avec le décompte des appels sans prix). Dans un
flow, cost_unknown ne sert qu'à l'escalade, à l'enchaînement des nodes d'un
sous-flow et aux passes optionnelles de budget (non lancées sous stop,
skipped_reason: cost_unknown) : un coût inconnu ne jette jamais un node déjà vert ni les éléments
frères d'un fanout, verify-panel, loop-until-dry, judge ou replay-diff
(seul un montant connu qui atteint le plafond, cost_reached, les arrête).
Ces nodes continuent donc sous stop malgré un coût inconnu, mais ne passent pas pour
« sous le plafond » : le node porte cost_cap_unevaluated: true (plafond posé, politique
stop, appels sans prix — le plafond n'a pas pu être jugé), à lire avec cost_status.
Le node porte cost_cap_reason (cost_reached | cost_unknown). La politique
assume_tier_ceiling (estimer au plafond du palier) n'existe pas encore.
Classe de relisibilité¶
Un dispatch qui finit au vert classe aussi le diff qu'il a produit
(git diff --name-only dans le projet) : review_required s'il touche une
surface sensible, review_optional sinon. Surfaces par défaut, génériques : exports publics (*/__init__.py), surfaces d'entrée (*/cli/*, */mcp/*, */api/*), schémas (*schema*), standard du projet (_grimoire/standard/*, framework/agentic-standard/*), vocabulaire de décision (*/verifiability.py), politiques, sécurité et hooks (*/policies/*, */security/*, */hooks/*). Un diff confiné à tests/ ou
docs/ reste toujours review_optional. La liste se surcharge projet par
projet dans _grimoire/standard/orchestration-policy.yaml (clé
review_surfaces, une liste de globs). Un projet qui n'est pas un dépôt git
ne permet pas de calculer le diff : la classe reste review_optional, avec
une note qui le dit plutôt qu'un silence trompeur. Le rapport (review,
review_files) et l'événement task.dispatched portent tous deux le
résultat ; task show affiche celui du dernier dispatch.
Incertitudes déclarées¶
Le prompt envoyé à l'ouvrier délégué se termine par une consigne : rendre un
bloc délimité ```grimoire-uncertainties contenant une liste JSON
d'objets {"where": ..., "what": ..., "why": ...} — le canal d'escalade le
moins cher qui existe, lu ici mécaniquement plutôt qu'en prose. Le dispatch
l'extrait de la sortie de l'ouvrier (le texte brut, ou le champ result si la
sortie est un JSON qui l'enveloppe) et stocke le résultat sous uncertainties
dans le rapport et dans l'événement task.dispatched. Un bloc absent est une
liste vide, silencieusement — la plupart des ouvriers n'y répondront pas
encore ; un bloc présent mais illisible (JSON invalide, pas une liste) est
aussi une liste vide, mais avec un avertissement dans uncertainty_warnings ;
un objet du bloc sans les trois clés est ignoré avec son propre avertissement,
les autres objets du même bloc restent gardés. Rien de tout cela ne change le
verdict ni le code de sortie du dispatch. task show affiche les incertitudes
du dernier dispatch.
Pilote automatique (P82)¶
Un interrupteur par projet : l'orchestrateur tranche aussi les cas bloqués (merge, resume_branch,
retry_exhausted, escalades du merger et du relecteur, tâches faites à la main, « abandonner » avec raison),
sauf une liste fermée qui reste à l'humain. Sans pilote, rien ne change.
grimoire autonomy autopilot on # attesté par un tour humain, comme le mandat ; exige un mandat actif
grimoire autonomy autopilot status
grimoire autonomy autopilot off # même canal humain (révoquer le mandat le coupe aussi)
- Autorité : l'état (activé ou coupé, par qui, par quel canal, quand, pour quel mandat) est au journal
d'autorité. Il ne s'active et ne se coupe que par le canal humain : la CLI attestée par un tour humain, ou le
cockpit (
POST autonomy/autopilot, derrière le jeton et la clé de contrôle). Le pilote est lié au mandat : mandat révoqué, expiré ou remplacé, il est coupé, et un nouveau mandat ne le rallume pas. - Liste fermée, dans le code : identifiants, secrets, dépense payante, accès hors du dépôt (
permissionhors du dépôt ou sans chemin), suppression de données utilisateur, mandat, réglages du dépôt. Le classement lit la question, ses options, sa raison et la tâche, sans accents ni casse. La sectionautopilot.reserveddeorchestration-policy.yamlpeut ajouter des réservations (catégories d'escalade ou mots) ; toute autre clé ou forme rend la politique invalide et le pilote ne décide plus rien. - Comment il décide : d'abord l'option recommandée par la question (
task ask --recommended <option> --why "…", ou le champrecommendedd'un lot), appliquée par le tick sans session. Sinon une session tranche avec les critères, les directives et la mémoire, et chaque réponse porte--why "<justification>" --confidence high|medium|low(task answer). Confiance basse, non déclarée ou sans justification : la question reste à l'humain (autopilot.deferred, que le cockpit signale). - Traçabilité : chaque décision est un événement
autopilot.decided(question, option, justification, confiance, source de la recommandation, mandat). Sans borne de présentations (le plafond de sessions du jour et le disjoncteur bornent encore). - Limites : une annulation décidée par le pilote est terminale (l'état
cancelledn'a pas de sortie) ; la raison est au ledger. L'acceptation sous dérogation n'est pas traitée (aucune dérogation n'existe dans le kit).
Mandat de l'orchestrateur (#876, lot 1)¶
Les gestes de validation (task accept, task answer, task approve-check, orchestrate resume,
queue release) exigent l'origine humaine. Un mandat ajoute un second principal, orchestrator, qui
n'agit qu'à l'intérieur de ses bornes. Sans mandat, rien ne change.
grimoire autonomy mandate grant --gesture accept --gesture approve-check --task-type implementation \
--max-sessions-per-day 5 --until +7d # attesté par un tour humain, comme `autonomy grant`
grimoire autonomy mandate status
grimoire autonomy mandate revoke --reason "fin du pilote"
- Session ouverte dans un autre projet : le journal des tours humains n'est écrit que par le hook du projet
où tourne la session. Pour accorder un mandat sur un projet piloté depuis la session d'un autre, désigne ce
dernier :
grimoire autonomy mandate grant … --project-root <cible> --attest-from <projet de ta session>. Sans l'option, la demande sort enhuman_authority_unprovableet le refus propose--attest-from. Rien n'est déduit du dossier courant ni de l'environnement. Les autres règles ne changent pas : même session, touruserpostérieur au challenge, un tour pour un seul octroi. Le challenge et le mandat restent ceux de la cible ; le mandat inscritattest_from. Le tour est consommé dans le registre de la source (_grimoire-runtime-output/flows/authority/@consumed-turns.authority.jsonl, entréefor_project), après vérification dans celui de la source et dans celui de la cible : le registre suit le journal qu'il protège, si bien qu'un même tour ne sert ni à deux cibles ni à la source elle-même. Un dossier absent, sans journal ou dont le journal est illisible est refusé (code 1) avant tout challenge ; le tour est consommé avant l'inscription du mandat, donc un registre qui ne s'écrit pas n'inscrit rien. -
Registre des tours de l'utilisateur : toute consommation de tour, avec ou sans
--attest-from(octroi, mandat, checkpoint de flow,approve-check), est aussi inscrite dans~/.grimoire/control/consumed-turns.authority.jsonl, clé(session_id, ts, prompt_sha256), indépendante du chemin du journal ; toute attestation le consulte. Une copie du journal des tours (dossier source recopié ailleurs) ne réatteste donc pas un tour déjà consommé. La consommation revérifie la clé sous le verrou de ce registre : de deux décisions concurrentes qui ont retenu le même tour, la seconde est refusée (« déjà consommé », code 1) sans rien inscrire. Limite : ce registre vit dans la zone de contrôle de l'utilisateur ; un autre compte (autreHOME), ou un agent qui exécute un script arbitraire pour y écrire, n'est pas couvert. -
Borné : gestes couverts, types de tâche, plafond de sessions de worker par jour, durée. Un mandat expiré, révoqué, hors plafond ou qui ne couvre pas le geste est refusé.
- Journal d'autorité :
~/.grimoire/control/mandates.authority.jsonl, chaîné par hash. Un journal illisible ou rompu ne porte aucun mandat. Chaque geste pris sous mandat y est inscrit avecorigin: orchestrator, l'identifiant du mandat et sa provenance (règle, verdict ou reçu). La troncature de la queue est vue par une tête (<journal>.head) et un marqueur par révocation (revoked/<mandat>) : un journal plus court que sa tête, ou sans tête, est rompu. Pour repartir d'un journal rompu, l'humain supprime lui-même les fichiersmandates.*et le dossierrevoked/de~/.grimoire/control/. - Jamais couverts : modifier le mandat et modifier la politique d'escalade.
- Politique d'escalade : section
escalationde_grimoire/standard/orchestration-policy.yaml, par catégories génériques (release,secrets,paid,security,merge,accept), chacunehumanouorchestrator. Le gabarit livré garde tout à l'humain. Le dépôt du kit délèguemergeetacceptet garderelease,secretsetpaid: cette configuration vit dans_grimoire/standard/orchestration-policy.yamldu dépôt qui porte le pilote (le dépôt du kit n'en versionne pas aujourd'hui). La modifier après l'octroi révoque le mandat. - Sous mandat : un reçu de revue ne vaut que si son relecteur diffère de l'auteur et si la session du
relecteur figure au journal d'autorité ; les
--checkdoivent être ceux scellés à la création de la tâche, un check modifié depuis ouvre un événement « disjoncteur ». - Principal : en CLI,
GRIMOIRE_PRINCIPAL=orchestratorrevendique le principal etGRIMOIRE_PROVENANCE=<rule|verdict|receipt>:<réf>cite la provenance ; la revendication ne vaut que si la session appelante est inscrite au journal d'autorité en rôleorchestrator(register_orchestrator_session(projet, session_id), appelée par l'étape orchestrateur du tick, voir « Session orchestrateur » plus bas) et n'est celle d'aucun worker ni relecteur de la tâche. Sans session inscrite : refus. Le tick, qui est du code et non une session, appelle l'API Python danswith acting_as_orchestrator():(grimoire.missions.mandate), jamais par l'environnement. - Cockpit :
GET /api/workspace/autonomy/mandate,POST …/mandate/grant(jeton de session et clé de contrôle~/.grimoire/control/cockpit-key) etPOST …/mandate/revoke. - Zone protégée :
~/.grimoire/control/(journal d'autorité, preuve de contrat d'hôte, clé du cockpit) est refusée en écriture aux outils d'agent par le freinPreToolUse, et la clé en lecture. Les chemins sont résolus ($HOME,..,cdpuis chemin relatif, globs, liens symboliques existants, composants cités). Limite : un agent qui lance un script arbitraire, ou dont le chemin est assemblé par le shell ($X), contourne une garde de chemin.
Fusion par le merger (#876, lot 4)¶
Le rôle merger est une étape de orchestrate tick --apply, après le relecteur. Il est désactivé par défaut :
# _grimoire/standard/orchestration-policy.yaml
orchestrator:
roles:
merger:
enabled: true
realign_max: 1 # PR réalignées sur la cible à la fois (le débit CI)
ignored_checks: ["codecov/*"] # hors du verdict, seulement quand gh dit qu'aucun check n'est requis
max_cycles: 2 # retours au worker (CI rouge, conflit) avant l'escalade
protected_paths: [] # chemins qui reviennent à l'humain, en plus des défauts
La file prend les tâches lancées par le tick, en revue, dont la dernière relecture est jugée READY, dans
l'ordre des jugements. Le merger n'ouvre pas de PR : il attend celle du worker sur task/<id>. Le dépôt GitHub de la
tâche et l'URL de origin sont figés au journal d'autorité au lancement, avant le worker : chaque gh du merger vise
ce dépôt (-R). La tête et la base de la PR viennent de gh pr view, ses fichiers de l'API REST, anciens noms d'un
renommage compris ; jamais de refs locales. À chaque tick, le merger vérifie
dans cet ordre, et chaque garde échoue fermée :
| Étape | Ce qui est vérifié | Sinon |
|---|---|---|
| Worktree | celui du ledger, sur task/<id>, du même dépôt que le projet |
refus |
| PR | ouverte, du même dépôt (pas d'un fork), pas en brouillon, vers la cible du worktree | attente, ou refus |
| READY | la tête GitHub est la tête scellée au jugement (ou un réalignement noté au même journal) ; reçu et pack inchangés à l'octet | tête relue pas encore poussée : attente ; sinon nouvelle relecture |
| CI | checks requis (gh pr checks --required) ; tous hors ignored_checks seulement si gh dit qu'aucun n'est requis ; au moins un pass ; vert au tick précédent aussi |
rouge : retour au worker ; en attente, sans check ou gh en panne : attente |
| Cible | la base GitHub contenue dans la tête | réalignement en plomberie, poussé sans forcer, puis nouvelle CI |
| Fusion | tête de file seulement (WIP 1), mandat qui couvre merge, politique qui le délègue, aucune catégorie gardée, aucun fichier qui escalade, reçu attesté au journal d'autorité |
question non bloquante « fusionner ? » : l'humain fusionne lui-même |
| Acceptation | fusion revérifiée, tête fusionnée couverte par le sceau, aucun fichier qui escalade, mandat qui couvre accept |
question non bloquante « accepter ? » avec la commande task accept |
- Sceau du READY (
missions/review_seal.py) : au lancement du relecteur, la tête detask/<id>est notée au journal d'autorité (~/.grimoire/control/). Au jugement, sont scellés la même tête (une branche qui a bougé pendant la relecture n'est pas scellée), son arbre git, un worktree propre, et les empreintes du reçu et du pack. Le merger ettask acceptsous mandat comparent une tête git à ce sceau, jamais le fichier reçu seul. - Chemins qui escaladent : le code des gardes (
src/grimoire/missions/*,src/grimoire/hosts/decisions/*,src/grimoire/core/*et l'attestation des flows), la politique (_grimoire/standard/*), les workflows et hooks (.github/*,.githooks/*,.husky/*), le packaging (pyproject.toml,setup.cfget le script setup de setuptools),.gitattributes,.gitmodules, les fichiers conftest de pytest,.claude/*,.mcp.json,.vscode/*,.pre-commit-config.yamlet les fichiers compilés. La cléprotected_pathss'y ajoute. La comparaison se fait encasefold(), sur la liste de fichiers que GitHub annonce ; une liste tronquée fait escalader. - Réalignement :
git merge-tree, puiscommit-tree -p <tête GitHub> -p <base GitHub>, sur objets : le worktree du worker n'est pas touché. Les objets sont récupérés à refspec explicite versrefs/grimoire/tick/…, depuis l'URL deoriginvérifiée égale au dépôt de la PR. Un conflit limité àCHANGELOG.mdest résolu par l'union des deux côtés, refusée si une ligne de la cible manque. - Git du tick (
grimoire.core.safe_git) : seulement quand un rôle autonome est activé, et le temps du tick, l'environnement neutralise pour tous les git du processus ce qui exécute du code. Sont toujours coupés : les hooks, le fsmonitor, les signatures et les transportsext::etfd::. Sont aussi neutralisés, quand le dépôt ou un worktree les déclare localement : les filtres, les pilotes de fusion,core.sshCommandetcredential.helper(remis aux valeurs globales de l'utilisateur). Rien n'est refusé pour une configuration ordinaire, et l'identité et les identifiants globaux ne sont pas touchés. Hors de ce cas, les git du kit sont ceux de toujours. - Le geste humain : le merger ne lit aucune réponse du ledger. Ce qu'il ne fait pas sous mandat, l'humain le fait
lui-même : il fusionne la PR, puis lance
grimoire task accept <id> --project-root <worktree> --reason "…". - Limites :
- les sessions tournent sous le même utilisateur du système : un processus qui écrit ce que le tick lit avant lui contourne une garde de fichier ; seule une isolation par utilisateur du système la fermerait ;
- un filtre posé par le worker pendant le tick tourne le temps de ce tick ;
- les pilotes de diff ne sont pas neutralisés, et les git que
ghlance lui-même n'héritent que de la neutralisation par l'environnement ; - la liste des chemins qui escaladent est courte ;
- la branche locale du worker n'est pas avancée par le réalignement ;
- une PR sans aucun check attend indéfiniment.
Session orchestrateur (#962)¶
Sous mandat, les questions des workers et les reprises n'attendent plus l'humain toute la nuit. Le rôle
orchestrator est une étape de orchestrate tick --apply, après le merger. Il est désactivé par défaut :
# _grimoire/standard/orchestration-policy.yaml
orchestrator:
roles:
orchestrator:
enabled: true
model: sonnet # défaut
wip: 1 # sessions orchestrateur en même temps
sessions_per_day: 12 # plafond de lancements par jour (UTC)
# permission_mode : imposé, dontAsk ; toute autre valeur rend la politique illisible
Le tick n'a pas de LLM et ne tranche rien. Quand une question éligible attend et qu'un mandat actif la couvre, il
lance une session de fond, claude --bg --name grimoire-orchestrator, dans la racine du projet. Sa consigne porte
les questions dans un bloc JSON, déclaré donnée non fiable : tâche, identifiant, nature, geste, texte, options,
critères d'acceptation, dernières consignes et, pour une reprise, l'état de la session du worker. Les délimiteurs de
bloc sont retirés des textes du worker, et ses notes libres n'y entrent pas. Pour chaque question, la session tranche
dans le périmètre des critères, ou ne répond pas et la question reste à l'humain :
GRIMOIRE_PRINCIPAL=orchestrator GRIMOIRE_PROVENANCE=rule:critere-2 \
grimoire task answer <tâche> "<réponse>" --question <q-…>
Une commande par question, à cette forme littérale : la liste blanche de la session (dontAsk) refuse une
boucle, un ;, && ou |, un set --, une variable ou un sous-shell. La consigne donne l'exemple de la première
question et demande de reformuler seule une commande refusée par les permissions plutôt que d'abandonner (premier
lancement réel, 3.69.0 : 8 questions jugées, une boucle refusée, rien d'écrit).
| Question ouverte d'une tâche lancée par le tick | Source (événement écrit par le kit) | Geste du mandat |
|---|---|---|
choice (avec ou sans options) |
task.question_asked du worker |
answer |
resume_unfenced |
task.reap_deferred de la récolte |
resume |
| « reprendre ou abandonner ? » d'un désaccord de relecture | task.review_escalated |
resume |
checkpoint de flow (origin: flow) ou autre origine que agent/tick, merge, permission, resume_branch, retry_exhausted, escalade du merger (task.merge_escalated), source inconnue |
— | jamais : humain |
- Éligible : la tâche est en
waiting(human), porte un lancement du tick, et le mandat autorise le geste sur elle (geste couvert, type de tâche, politique d'escalade). La source se lit dans les événements que le kit écrit, jamais dans l'acteur qu'un appelant CLI choisit (--actor). Une question présentée à une session encore ouverte n'est pas relistée. Présentée à une session terminée sans qu'aucune réponse de l'orchestrateur ne soit enregistrée, elle est représentée une fois à une nouvelle session (au plus 2 présentations) ; au-delà, le silence est une escalade et la question reste à l'humain. - Pas de boucle : au plus 2 reprises (geste
resume) de l'orchestrateur par tâche depuis la dernière réponse humaine ; au-delà, la question reste à l'humain. Une réponse de l'orchestrateur ne remet à zéro ni le compteur de tentatives de la récolte, ni les cycles du relecteur et du merger : seule une réponse humaine le fait. - Identité : la vraie identité de la session est lue dans
claude agents --jsonaprès le lancement (#966), puis inscrite au journal d'autorité en rôleorchestrator, bornée au canaltask answeret datée (lancement + 30 minutes). Sa fin (terminée, arrêtée, délai dépassé) est aussi inscrite au journal : son identifiant ne vaut plus. Identité introuvable ou inscription refusée : la session est arrêtée (claude stop), la tentative notée au ledger (orchestrator.session_launch_failed) et comptée au disjoncteur. Un arrêt non confirmé compte au WIP et se retente à chaque tick. - Gardes de
task answer: une réponse d'origineorchestratorest refusée, avant le mandat (le journal ne dit jamais « pris » d'un geste refusé), pour une question réservée à l'humain, pour la réponseabandonner(qui annule la tâche), au-delà du plafond de reprises, depuis un worktree de tâche, l'un de ses sous-dossiers ou le dossier des worktrees, quand un identifiant présenté (GRIMOIRE_SESSION_ID,CLAUDE_CODE_SESSION_ID) a tenu le claim de la tâche ou l'a relue, sur une tâche que le tick n'a pas lancée, et pour une question que le tick n'a pas présentée à cette session. Le mandat exige ensuite une session inscrite orchestrateur, non échue et non terminée, qui n'est celle d'aucun worker ni relecteur, et un geste (answerouresume) et un type de tâche couverts. Une inscription illisible refuse. Chaque réponse acceptée est inscrite au journal d'autorité (geste, mandat, provenance) et au ledger (task.human_answered,origin: orchestrator,mandate_id,provenance,gesture). - Jamais : la session inscrite ne peut ni accepter, ni fusionner, ni approuver un check, ni libérer la file, ni
lever une pause (
orchestrate resume), même si le mandat couvre ces gestes : ces commandes n'ouvrent pas le canaltask answer. Elle est lancée en--restricted: les réglages utilisateur, projet et local sont ignorés, donc leurspermissions.allowne s'ajoutent pas etbypassPermissionsest refusé. S'y ajoutent--strict-mcp-config(aucun serveur MCP),--tools Read,Bash(aucun autre outil),--permission-mode dontAsk, imposé quelle que soit la politique, et une liste blanche--allowed-tools:Read,grimoire task show|list|questionsetgrimoire task answer. Tout autre appel est refusé sans demander. Contrepartie assumée : les hooks des réglages utilisateur et projet ne tournent pas dans cette session, dont les seuls gestes passent partask answer, gardé dans le code du kit. - Fin : une session absente de
claude agents, ou qui a rendu son tour, est terminée au tick suivant. Une session encore au travail après 30 minutes est arrêtée. Les questions qu'elle a laissées ouvertes remontent dans l'attention du tick. Une lecture declaude agentsen échec ne termine rien et ne lance rien. - Limites :
- la session est un LLM : elle peut trancher faux dans le périmètre des critères. Tout est journalisé avec sa
provenance, et l'humain garde la révocation du mandat (
grimoire autonomy mandate revoke) ; - l'identité de session n'est pas un secret, comme pour le relecteur : un worker peut lire celle de l'orchestrateur
dans
claude agentset la présenter. Il est refusé depuis un worktree de tâche ou l'un de ses sous-dossiers, s'il présente aussi son identifiant, sur toute question non présentée à cette session, et après la fin ou l'échéance de l'inscription. Reste ouvert : sorti de tout worktree (cd /,--project-rootvers la racine), avec le seul identifiant volé et pendant la vie de la session, toute question présentée à cette session, y compris la sienne. Seule une preuve de l'appelant par le hook de l'hôte le fermerait ; - elle tourne sous le même utilisateur du système : la liste blanche la borne côté hôte, pas côté système ;
- entre son lancement et son inscription (quelques secondes), ses réponses sont refusées : la consigne lui dit de réessayer une fois ;
- une réponse « reprendre » rend la tâche
ready: le tick relance un worker, comme après une réponse humaine.
Ce que le claim rappelle¶
grimoire task recall <id> (#141) assemble ce qu'une tâche et ses voisines ont
déjà traversé : son propre historique (tentatives précédentes bloquées ou
échouées), ses tâches voisines — liées par task link, de la même mission, ou
au titre proche — avec la cause de leur arrêt même si elles ont depuis été
rouvertes, et ce que la mémoire du projet a consolidé sur des sujets voisins
quand un backend mémoire est configuré (project-context.yaml). Sans lui, le
rappel reste utile depuis le seul Mission Ledger. Le texte rendu est borné en
tokens : c'est un rappel, pas un rapport.
[Grimoire — rappel de tâche] GAO-migrer-le-se-002 :
Tâches voisines :
- GAO-migrer-le-se-001 (même mission, failed)
cause : connexion refusée par la base cible : pool épuisé
Le même rappel est injecté par le hook SessionStart, entre la persona
d'entrée et la directive du standard, seulement pour une tâche réclamée
(task.claim posé — une tâche proposed ou ready ne rappelle rien) ; et par
l'outil MCP task_recall, qui résout la tâche active de la même manière que
task_context quand aucun identifiant n'est donné. À la clôture ou au
blocage d'une tâche, TaskService consolide ce qui a été appris dans la
mémoire du projet (decisions à la clôture, failures au blocage) — jamais
sur un mouvement ordinaire.
Quelle tâche la session porte¶
Les hooks (SessionStart, UserPromptSubmit, Stop) et grimoire standard
activation-context résolvent la tâche courante dans cet ordre :
GRIMOIRE_TASK_ID— un opérateur dit de quelle tâche la session parle ;- le claim actif du Mission Ledger : l'unique tâche
claimedourunning, restreinte aux claims deGRIMOIRE_ACTORquand cette variable est posée ; - l'unique carte
in_progressdu board (projet sans ledger) ; bootstrap.
Deux claims simultanés (ou deux cartes in_progress) sont ambigus : le niveau
est sauté plutôt que deviné, et GRIMOIRE_TASK_ID tranche. task_context sans
argument, côté MCP, rend la tâche résolue et la règle qui l'a choisie.
Neuf états côté ledger se projettent sur les huit colonnes du standard. Les
fusions sont décidées et testées : claimed et running deviennent
in_progress, needs_verification devient review, failed devient blocked
(avec un motif), closed devient accepted, cancelled devient archived.