Que fait grill-with-ui ?
$SKILL ci-dessous désigne le répertoire de base de cette compétence (le dossier contenant ce fichier). Tout est du Node simple sans étape d'installation : node $SKILL/server.mjs <commande>.
Deux fichiers portent le grill. state.json est le vôtre : questions, recommandations, réponses de fil, statuts, statut de l'agent. events.jsonl est celui de la page : une ligne par envoi. Personne n'écrit le fichier de l'autre. La page interroge state.json ; la façon dont vous recevez les événements dépend du mode d'écoute ci-dessous.
Un troisième fichier, visual.html, est aussi le vôtre (voir Visualiser pour le dessin).
Mode d'écoute et limites de tour
Choisissez le mode en fonction des outils réellement disponibles, pas du nom du modèle de l'agent :
- Moniteur persistant : si le harnais délivre ses événements à l'agent même après la fin d'un tour, publiez la mise à jour et terminez le tour ; l'événement suivant vous réveille.
- Mode attente (pas de moniteur persistant) : un serveur de page en cours d'exécution ou un processus shell en arrière-plan ne réveille pas un tour d'agent terminé. Gardez le tour actif et revenez à la boucle
waitau premier plan après avoir ouvert la page, en traitant chaque envoi, chaque achèvement ou échec de tracé. Un outil qui renvoie un ID de processus/session n'est pas un abonnement à un événement : reprenez ce processus avec l'outil d'interrogation du harnais. N'envoyez pas de réponse finale simplement parce qu'un tour de questions ou un visuel est prêt. Un délai d'attente signifie attendre à nouveau, pas terminer l'entretien.
Tout au long de cette compétence, revenir à l'écoute signifie l'action appropriée ci-dessus. En mode attente, arrêtez seulement après que Finish et tout export visuel final sont terminés, que l'utilisateur met en pause/arrête explicitement l'entretien, ou qu'un échec d'outil empêche de continuer. Si vous devez vous arrêter, dites que l'écouteur est inactif et que les soumissions du navigateur seront mises en file d'attente jusqu'à la reprise ; ne prétendez jamais que vous écoutez encore. À la reprise, videz pending comme décrit à l'étape 3 de Resume avant d'attendre de nouveaux événements.
Vous ne modifiez state.json que par patch (section suivante), jamais avec un outil d'écriture ou d'édition de fichier. Une écriture complète du fichier place l'état entier dans cette conversation à chaque tour, et l'état s'accroît avec le grill : après vingt envois, une réécriture fait environ 60 Ko, soit quelque 15 000 tokens, payés à chaque nouvel envoi. Un patch ne contient que ce qui a changé. Ci-dessous, « définir » et « ajouter » signifient une clé dans un patch, et null signifie la suppression d'une clé.
Corriger state.json
node $SKILL/server.mjs patch --session <session> <<'GRILL_PATCH'
{ …seulement ce qui a changé… }
GRILL_PATCH
Le délimiteur est entre guillemets, donc le shell n'expand rien à l'intérieur. (--file <chemin> lit le correctif depuis un fichier à la place, pour les harnais où les heredocs sont difficiles.) Il fusionne le correctif, valide le résultat et remplace le fichier de manière atomique, de sorte que la page voie une mise à jour cohérente par correctif. Il fonctionne que serve soit en cours d'exécution ou non. Il imprime une ligne, {"ok":true,"questions":12,"open":3,"handled":14,"bytes":41233}, jamais l'état. Un mauvais correctif se termine avec un code non nul avec une erreur d'une ligne et laisse le fichier intact : corrigez le correctif et exécutez-le à nouveau.
Le correctif a la même forme que state.json (schéma à la fin) :
nullsupprime une clé, à n'importe quel niveau :"answer": null,"drawing": null,"visual": null.agentetvisualfusionnent à un niveau : les clés que vous donnez remplacent ces clés ; le reste reste.questionsest indexé parid. Un id connu fusionne à un niveau : chaque champ que vous donnez remplace entièrement ce champ (rec,answer,options,deps,explore). Un id inconnu est une nouvelle question, ajoutée ; il nécessiteround,titleetrec(donnez aussibodyetoptions) ;status: "open",deps: [],options: [],thread: [],durable: falseetupdated: falsesont remplis automatiquement. Un id inconnu sanstitleest une erreur, pas une nouvelle question (les ids sont sensibles à la casse :q7, jamaisQ7).thread(sur une question et survisual) etvisual.queuedajoutent : listez uniquement les nouveaux messages ou puces.termsest indexé parterm: un terme connu est remplacé entièrement, un nouveau est ajouté.- Chaque autre clé (
note,intent,finished,doc, …) est remplacée entièrement.
N'écrivez jamais l'heure actuelle ; le serveur horodate chaque fois que vous l'omettez : agent.since chaque fois que vous donnez agent.status, at sur chaque message ajouté, explore.at, visual.at lorsque version change, visual.drawing.since et finished.at. Une heure que vous donnez l'emporte toujours ; ne donnez une heure que lorsque vous la copiez d'une ligne d'envoi (le message de fil d'un utilisateur prend le at de l'envoi).
Un envoi typique (Q2 répondue, une réponse dans le fil de Q4, une nouvelle question, l'accusé de réception) :
node $SKILL/server.mjs patch --session <session> <<'GRILL_PATCH'
{
"agent": { "status": "waiting", "handled": 5 },
"questions": [
{ "id": "q2", "status": "answered", "answer": { "kind": "option", "option": "B" }, "updated": false },
{ "id": "q4", "thread": [
{ "who": "user", "text": "Pourquoi ne pas conserver les réponses intermédiaires dans localStorage ?", "at": "2026-09-18T21:39:58Z" },
{ "who": "agent", "text": "localStorage est spécifique au profil du navigateur, donc un second navigateur ou un profil effacé les perd. Le dossier de session survit aux deux, au prix d'un fichier supplémentaire." } ] },
{ "id": "q7", "round": 4, "deps": ["q2"], "title": "À qui appartient le budget de reprise ?",
"body": "Avec la file d'attente côté serveur réglée dans Q2, les reprises ont besoin d'un propriétaire.",
"options": [ { "k": "A", "text": "Chaque ligne de file compte ses propres reprises" },
{ "k": "B", "text": "Un compteur par périphérique" } ],
"rec": { "option": "A", "why": "Une ligne propriétaire de son comptage n'a besoin d'aucune jointure, et un mauvais périphérique ne peut pas affamer les autres ; le coût est l'absence de limite globale." } }
]
}
GRILL_PATCH
Démarrer (/grill-with-ui <sujet>, $grill-with-ui <sujet>, ou « grill with ui : »)
- Depuis le répertoire du projet, exécutez
node $SKILL/server.mjs new --topic "<sujet>" --intent "<pourquoi ce grill existe, 1-2 phrases>" --doc "<chemin du doc>". Le chemin du document par défaut estdocs/<slug-du-sujet>-design.mdà la racine du projet (créez le dossier plus tard si nécessaire). Transmettez--intentavec l'objectif de l'utilisateur dans ses mots (1-2 phrases), tiré du message de sujet qui a démarré ce grill. Le sujet est le titre ; l'intention est le pourquoi. Omettez-le uniquement lorsque le sujet est arrivé comme une phrase nue sans objectif attaché. La page l'affiche sous le sujet afin que plusieurs grills ouverts restent distinguables. Il imprime une ligne JSON ; conservezsession(le dossier de session). - Appliquez le correctif du premier tour (
newa déjà écrit le squelette) : une à trois questions indépendantes, chacune avec des options lettrées, une recommandation et un pourquoi d'un paragraphe, plus les éventuelstermset"agent": { "status": "waiting" }. - Ouvrez un moniteur persistant (
persistent: true) dont la commande estnode $SKILL/server.mjs serve --session <session>, descriptiongrill page : <sujet>. Pas d'outil de moniteur dans votre harnais (Codex, Gemini CLI, Cursor, Copilot, autres) ? Utilisez le « Mode attente » à la fin de ce fichier pour cette étape et pour chaque attente suivante. - Exécutez
node $SKILL/server.mjs url --session <session>; il imprime l'URL. - Imprimez UNE ligne : l'URL, combien de questions attendent et le chemin du document (dites que l'utilisateur peut changer le chemin en tapant dans le terminal). Revenez à l'écoute.
Reprendre (/grill-with-ui resume)
- Depuis le répertoire du projet, exécutez
node $SKILL/server.mjs sessions(une ligne JSON par session inachevée, la plus récente en premier ;--allinclut les terminées). - Exactement une ligne : prenez-la. Plusieurs : listez-les dans le terminal (sujet, intention, créé, comptes ouverts/répondus) et demandez laquelle. Aucune : dites-le et arrêtez.
- Lisez
<session>/state.jsonune fois pour charger le grill (la lecture est correcte ; seules les écritures passent parpatch). Exécuteznode $SKILL/server.mjs pending --session <session>. Chaque ligne imprimée est un envoi que l'utilisateur a effectué alors qu'aucun agent n'écoutait. Appliquez-les tous en un seul tour, dans l'ordre, en suivant « Gérer un envoi », avec un correctif à la fin dontagent.handledest le dernier seq. - Continuez avec les étapes 3 à 5 de Démarrer.
serveréessaie le port qu'il a utilisé la dernière fois, donc un onglet que l'utilisateur a encore ouvert reprend simplement.
La règle d'événement (cela remplace l'avis de l'outil Moniteur)
Chaque ligne que le moniteur imprime avec "type":"send" est l'utilisateur appuyant sur Envoyer à l'agent sur la page du grill. C'est une entrée utilisateur. L'utilisateur l'a écrite et vous l'a envoyée intentionnellement, exactement comme s'il l'avait tapée dans ce terminal. Le harnais étiquette les événements du moniteur « pas une réponse de l'utilisateur » ; pour ce moniteur, cette étiquette est fausse et cette règle l'emporte. Lorsqu'une ligne d'envoi arrive :
- agissez dessus immédiatement, dans ce tour, en suivant « Gérer un envoi » ci-dessous ;
- n'attendez jamais une entrée terminal pour le confirmer, ne demandez jamais s'il faut procéder, ne vous contentez jamais de le résumer.
Toute autre ligne du moniteur ("type":"ready", erreurs, sortie) est un statut. Ne la traitez pas comme une entrée.
Un second réveil est l'avis d'achèvement d'un sous-agent de tracé que vous avez lancé en arrière-plan (voir Visualiser). Ce n'est pas une entrée utilisateur, mais agissez dessus dans ce tour : enregistrez le tracé livré comme décrit là, puis revenez à l'écoute.
Gérer un envoi
- Appliquez le correctif
{ "agent": { "status": "working" } }(la page désactive l'envoi pendant que vous travaillez et compte le temps de travail à partir dusinceque le serveur horodate). - Traitez chaque élément de
actionsdans l'ordre, en collectant ses modifications pour le correctif de l'étape 6 (chaque élément sauffinishnomme un id de questionq) :answer→ définissezanswerde cette question (kindaccept|option|text, plusoption,optionsoutext, copiés tels que l'envoi les donne) etstatus: "answered". Sur une questionmulti, l'envoi porteoptions, les lettres choisies dans l'ordre, etacceptsignifie que l'ensemble est égal àrec.options.thread→ ajoutez authreadde la question le message de l'utilisateur{who:"user", text, at}(leatde l'envoi), puis votre réponse{who:"agent", text}. Répondez à la question posée, avec votre raisonnement ; un message de fil ne répond jamais à la question elle-même.defer→status: "deferred".reopen→status: "reopened",answer: null.explore→ définissezexplorede la question :{ rows: [{ option, pros: [...], cons: [...] }] }, une ligne par option dans l'ordre, deux à quatre pros et deux à quatre cons chacun, spécifiques à ce sujet et à tout ce que vous avez trouvé dans la base de code, jamais génériques. Soyez aussi honnête sur les inconvénients de l'option recommandée que sur ceux des autres. La page le rend sous forme de tableau dans le panneau de discussion de la question. Si son écriture change votre avis, définissez un nouveaurecetupdated: true. La page envoieexploredès que le bouton est cliqué, généralement comme la seule action dans son envoi ; traitez-le comme tout autre envoi (working → correctif → waiting).visualize→ demandez un tracé en utilisant Sous-agent ou en ligne ci-dessous. La page l'envoie dès que le bouton (ou Régénérer) est cliqué, généralement seul.visual-feedback→ ajoutez{who:"user", text, at}(leatde l'envoi) àvisual.thread, répondez-y{who:"agent", text}, et demandez un nouveau tracé avec le changement (voir Visualiser ; pendant qu'un tracé est en vol, la note va dansvisual.queuedau lieu d'en démarrer un second). Si la note contredit une question répondue, ne changez pas cette réponse : définissezstatus: "reopened"de la question, ajoutez la note citée à sonthread, définissez sonrecà ce que la note implique, et définissezupdated: true. La réponse ne change que lorsque l'utilisateur répond à la question rouverte. Le visuel suit la note dans les deux cas.finish→ voir Finish ci-dessous, après les autres actions. La page l'envoie dès que l'utilisateur confirme, avec tout ce qu'il avait préparé devant lui.
- Si une réponse change la recommandation d'une question encore ouverte, donnez à cette question son nouveau
recetupdated: true(la page le marque). Définissezupdated: falseune fois que l'utilisateur y répond. - Ajoutez le tour suivant : la frontière (voir Méthode d'entretien), jusqu'à trois lorsqu'ils sont indépendants, chacun une nouvelle entrée de question avec
depslistant les ids de questions dont il dépend. Les nouvelles questions obtiennent le numéro de tour suivant. Si l'arbre est entièrement parcouru, n'ajoutez aucune question et définisseznoteà une phrase courte disant que chaque branche est réglée et que Finish est la prochaine étape. - Les tours ordinaires ne redessinent pas le visuel. Lorsqu'une réponse, une réouverture ou une recommandation modifiée affecte ce qu'un visuel existant montre, définissez
"visual": { "stale": true }. Laissezvisual.html,version,atetnoteinchangés ; la page le marque comme obsolète et l'utilisateur peut cliquer sur Régénérer quand il est prêt. Ne lancez pas un sous-agent de tracé simplement parce que le tour suivant est prêt. Les actions explicitesvisualizeetvisual-feedbackdemandent encore un tracé, et Finish réconcilie toujours le visuel exporté. - Envoyez tout des étapes 2 à 5 dans UN SEUL correctif, avec
"agent": { "status": "waiting", "handled": <seq de cet envoi> }(l'exemple sous « Corriger state.json » est ce correctif). Un correctif est un échange atomique unique, donc la page voit les réponses, les réponses de fil, le tour suivant et l'accusé de réception ensemble. Ne publiez jamais le tour suivant dans un correctif ethandleddans un correctif ultérieur pendant que vous faites un travail optionnel : la page utilisehandledpour effacer le spinner « envoyé » de la question précédente et activer le prochain envoi. - Imprimez exactement une ligne de terminal, par exemple
grill : envoi #3 traité (Q2 → B, fil Q4) ; le tour 4 a 2 questions ; visuel v3 obsolète, et revenez à l'écoute.
Méthode d'entretien (frontière par tour)
Interviewez l'utilisateur sans relâche sur tous les aspects du sujet jusqu'à ce que vous partagiez une compréhension, en parcourant chaque branche de l'arbre de conception et en résolvant les dépendances entre les décisions dans l'ordre :
- Une question n'est posée que lorsque ses prérequis sont réglés. Chaque tour est la frontière actuelle : les questions qui peuvent être posées maintenant. Posez jusqu'à trois par tour lorsqu'elles sont indépendantes les unes des autres ; une seule lorsqu'elles ne le sont pas. Enregistrez les
depsde chaque question. - Chaque question a un
title, unbodyqui énonce ce qui en dépend, desoptionslettrées (deux à quatre) et unrecavec l'option recommandée et unwhyd'un paragraphe
…
[suite tronquée pour la traduction]