[{"data":1,"prerenderedAt":36},["ShallowReactive",2],{"doc:fr:plugins\u002Fevents":3},{"title":4,"lede":5,"html":6,"headings":7},"Les événements","Escouade dit ce qui s’y passe sous forme de faits : un agent créé, un tour fini, un ticket à tester… Un plugin s’abonne aux faits qu’il veut : son programme les reçoit, un webhook déclaré les envoie à une adresse sans code, ou une commande déclarée se lance.","\u003Cp>Les faits sont dits par le cœur d’Escouade à l’endroit où l’état change, jamais relus dans ce qu’affiche la fenêtre : ils ne changent pas quand l’interface change.\u003C\u002Fp>\n\u003Ch2 id=\"un-fait\">Un fait\u003C\u002Fh2>\n\u003Cp>Chaque fait a la même forme, en JSON :\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-json\">{\n  &quot;version&quot;: 1,\n  &quot;seq&quot;: 42,\n  &quot;at&quot;: &quot;2026-10-29T14:03:12.511Z&quot;,\n  &quot;name&quot;: &quot;ticket.column&quot;,\n  &quot;data&quot;: { &quot;from&quot;: &quot;doing&quot;, &quot;to&quot;: &quot;review&quot; },\n  &quot;project&quot;: { &quot;id&quot;: &quot;8c1f…&quot;, &quot;name&quot;: &quot;Escouade&quot;, &quot;private&quot;: false },\n  &quot;ticket&quot;: { &quot;id&quot;: &quot;e02a…&quot;, &quot;key&quot;: &quot;ESC-177&quot;, &quot;title&quot;: &quot;Les événements&quot; },\n  &quot;agent&quot;: { &quot;id&quot;: &quot;5b7d…&quot;, &quot;name&quot;: &quot;esc-177-les-evenements&quot; }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cul>\n\u003Cli>\u003Ccode class=\"key\">version\u003C\u002Fcode> : la version de la forme des faits. Un champ ajouté la garde ; un champ retiré ou qui change de sens passe à la suivante.\u003C\u002Fli>\n\u003Cli>\u003Ccode class=\"key\">seq\u003C\u002Fcode> : son numéro depuis le démarrage d’Escouade. Deux faits arrivent dans l’ordre de leurs numéros.\u003C\u002Fli>\n\u003Cli>\u003Ccode class=\"key\">name\u003C\u002Fcode> et \u003Ccode class=\"key\">data\u003C\u002Fcode> : ce qui s’est passé, et ce qu’il en dit (la table plus bas).\u003C\u002Fli>\n\u003Cli>\u003Ccode class=\"key\">project\u003C\u002Fcode>, \u003Ccode class=\"key\">ticket\u003C\u002Fcode>, \u003Ccode class=\"key\">agent\u003C\u002Fcode> : ce dont il parle, par leurs ids et leurs noms, quand il en parle. Le fait d’un agent qui travaille sur un ticket dit aussi son ticket ; le projet dit s’il est privé (\u003Ccode class=\"key\">private\u003C\u002Fcode>).\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>Un fait ne porte \u003Cstrong>jamais la conversation\u003C\u002Fstrong> : un plugin apprend qu’un tour est fini et comment, pas ce qui s’y est dit.\u003C\u002Fp>\n\u003Ch2 id=\"les-faits\">Les faits\u003C\u002Fh2>\n\u003Cdiv class=\"table\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Fait\u003C\u002Fth>\n\u003Cth>Quand\u003C\u002Fth>\n\u003Cth>\u003Ccode class=\"key\">data\u003C\u002Fcode>\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">agent.created\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Un agent est créé (à la main, pour un ticket, par un client du serveur MCP).\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">harness\u003C\u002Fcode>, \u003Ccode class=\"key\">model\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">agent.renamed\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Il est nommé (après son premier message, ou à la main) ; les faits suivants disent son nouveau nom.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">from\u003C\u002Fcode>, \u003Ccode class=\"key\">to\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">agent.status\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Son statut change.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">status\u003C\u002Fcode> et \u003Ccode class=\"key\">was\u003C\u002Fcode> : \u003Ccode class=\"key\">idle\u003C\u002Fcode>, \u003Ccode class=\"key\">running\u003C\u002Fcode>, \u003Ccode class=\"key\">waiting\u003C\u002Fcode> (une demande attend une réponse), \u003Ccode class=\"key\">done\u003C\u002Fcode>, \u003Ccode class=\"key\">error\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">agent.stopped\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Il est archivé (\u003Ccode>removed: false\u003C\u002Fcode>) ou supprimé (\u003Ccode>removed: true\u003C\u002Fcode>).\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">removed\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">agent.handover\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Il continue sur un autre harness.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">from\u003C\u002Fcode>, \u003Ccode class=\"key\">to\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">turn.ended\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Un tour se termine.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">end\u003C\u002Fcode> : \u003Ccode class=\"key\">finished\u003C\u002Fcode>, \u003Ccode class=\"key\">interrupted\u003C\u002Fcode>, \u003Ccode class=\"key\">limited\u003C\u002Fcode> (la limite d’usage), \u003Ccode class=\"key\">signIn\u003C\u002Fcode> (une connexion à refaire) ou \u003Ccode class=\"key\">error\u003C\u002Fcode>, avec \u003Ccode class=\"key\">error\u003C\u002Fcode> (sa première ligne)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">request.permission\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Une autorisation attend.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">requestId\u003C\u002Fcode>, \u003Ccode class=\"key\">tool\u003C\u002Fcode>, \u003Ccode class=\"key\">arg\u003C\u002Fcode> (ce sur quoi l’outil agit, résumé comme sur la carte)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">request.question\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Une question attend.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">requestId\u003C\u002Fcode>, \u003Ccode class=\"key\">questions\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">request.answered\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Une demande a sa réponse.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">requestId\u003C\u002Fcode>, \u003Ccode class=\"key\">kind\u003C\u002Fcode> (\u003Ccode class=\"key\">permission\u003C\u002Fcode> ou \u003Ccode class=\"key\">question\u003C\u002Fcode>), \u003Ccode class=\"key\">decision\u003C\u002Fcode> (\u003Ccode class=\"key\">allow\u003C\u002Fcode>, \u003Ccode class=\"key\">always\u003C\u002Fcode>, \u003Ccode class=\"key\">deny\u003C\u002Fcode>), \u003Ccode class=\"key\">by\u003C\u002Fcode> (le client du serveur MCP qui a répondu, sinon absent)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">ticket.created\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Un ticket est créé.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">column\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">ticket.column\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Il change de colonne.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">from\u003C\u002Fcode>, \u003Ccode class=\"key\">to\u003C\u002Fcode> : \u003Ccode class=\"key\">todo\u003C\u002Fcode>, \u003Ccode class=\"key\">doing\u003C\u002Fcode>, \u003Ccode class=\"key\">review\u003C\u002Fcode>, \u003Ccode class=\"key\">done\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">ticket.report\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Son agent rend le bilan de ses critères.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">met\u003C\u002Fcode>, \u003Ccode class=\"key\">total\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">ticket.approved\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Sa validation commence.\u003C\u002Ftd>\n\u003Ctd>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">ticket.rejected\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Il est renvoyé à son agent.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">comment\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">ticket.merged\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Il est mergé dans sa branche cible.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">target\u003C\u002Fcode>, \u003Ccode class=\"key\">commit\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">ticket.conflict\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Son merge rencontre un conflit.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">target\u003C\u002Fcode>, \u003Ccode class=\"key\">files\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">pr.opened\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Une pull request est ouverte pour lui.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">url\u003C\u002Fcode>, \u003Ccode class=\"key\">target\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">tests.result\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Les tests de sa validation sont passés ou non.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">passed\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">project.added\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Un projet est ouvert.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">path\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">project.closed\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Il est fermé.\u003C\u002Ftd>\n\u003Ctd>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">worktree.created\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Un worktree est créé pour un agent.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">branch\u003C\u002Fcode>, \u003Ccode class=\"key\">path\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">worktree.removed\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Il est retiré.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">branch\u003C\u002Fcode>, \u003Ccode class=\"key\">path\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">launch.started\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Un lancement de test d’un agent démarre.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">run\u003C\u002Fcode> (son nom)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">launch.ready\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Il répond.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">url\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">launch.stopped\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Il s’arrête.\u003C\u002Ftd>\n\u003Ctd>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">docs.written\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Une page des docs est écrite.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">page\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">docs.filed\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>La copie des docs d’un ticket est versée sur la branche des docs, à sa validation.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">pages\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">docs.gaps\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Une page est comparée au code.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">page\u003C\u002Fcode>, \u003Ccode class=\"key\">gaps\u003C\u002Fcode> (le nombre d’écarts)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">memory.written\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Un souvenir de la mémoire du projet est écrit.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">memory\u003C\u002Fcode>, \u003Ccode class=\"key\">new\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">memory.forgotten\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Il est oublié.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">memory\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">decision.proposed\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Une décision est proposée ou révisée.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">id\u003C\u002Fcode> (pas encore de numéro), \u003Ccode class=\"key\">title\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">decision.accepted\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Un humain l’accepte.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">number\u003C\u002Fcode> (donné alors), \u003Ccode class=\"key\">id\u003C\u002Fcode>, \u003Ccode class=\"key\">title\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">account.switched\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Un agent change de compte.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">from\u003C\u002Fcode>, \u003Ccode class=\"key\">to\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>workflow.step_entered\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Un agent entre dans une étape du workflow de son projet.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">workflow\u003C\u002Fcode>, \u003Ccode class=\"key\">step\u003C\u002Fcode>, \u003Ccode class=\"key\">door\u003C\u002Fcode> (la commande tapée, s’il y en a une), \u003Ccode class=\"key\">pass\u003C\u002Fcode> (son n-ième passage)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">workflow.step_left\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Il en sort : durée, coût, issue.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">workflow\u003C\u002Fcode>, \u003Ccode class=\"key\">step\u003C\u002Fcode>, \u003Ccode class=\"key\">durationMs\u003C\u002Fcode>, \u003Ccode class=\"key\">cost\u003C\u002Fcode>, \u003Ccode class=\"key\">outcome\u003C\u002Fcode> (\u003Ccode class=\"key\">done\u003C\u002Fcode>, \u003Ccode class=\"key\">back\u003C\u002Fcode>, \u003Ccode class=\"key\">failed\u003C\u002Fcode>, \u003Ccode class=\"key\">stopped\u003C\u002Fcode>, \u003Ccode class=\"key\">left\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">workflow.looped_back\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Il revient à une étape déjà passée.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">workflow\u003C\u002Fcode>, \u003Ccode class=\"key\">from\u003C\u002Fcode>, \u003Ccode class=\"key\">to\u003C\u002Fcode>, \u003Ccode class=\"key\">reason\u003C\u002Fcode> (la ligne où l’étape s’est arrêtée)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">workflow.left\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Il sort du workflow : il fait autre chose.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">workflow\u003C\u002Fcode>, \u003Ccode class=\"key\">doing\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">workflow.changed\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Une relecture des workflows du projet donne autre chose.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">workflows\u003C\u002Fcode> (les ids de ceux qui ont changé, apparu ou disparu)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">app.started\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Escouade démarre.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">version\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Ch2 id=\"s-abonner\">S’abonner\u003C\u002Fh2>\n\u003Cp>Un plugin ne reçoit que les faits qu’il demande, dans \u003Ccode class=\"key\">permissions.events\u003C\u002Fcode> de son manifeste. Un nom finissant par \u003Ccode class=\"key\">*\u003C\u002Fcode> les prend tous (\u003Ccode class=\"key\">ticket.*\u003C\u002Fcode>, ou \u003Ccode class=\"key\">*\u003C\u002Fcode> pour tout) :\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-json\">&quot;permissions&quot;: { &quot;events&quot;: [&quot;ticket.column&quot;, &quot;agent.status&quot;] }\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>La fenêtre d’installation les montre : « Reçoit ces événements : … ».\u003C\u002Fp>\n\u003Cp>Et seulement ceux des projets où il est \u003Cstrong>activé\u003C\u002Fstrong>. Un plugin est activé pour tous les projets, sauf les \u003Cstrong>projets privés\u003C\u002Fstrong> : un fait d’un projet privé ne va qu’à un plugin activé pour lui, un par un, dans Réglages › Plugins. Un fait sans projet (\u003Ccode class=\"key\">app.started\u003C\u002Fcode>) va à tout plugin activé pour l’app. Une permission \u003Ccode class=\"key\">events:…\u003C\u002Fcode> retirée dans l’onglet Permissions de sa fiche coupe ces faits, pour son programme comme pour ses webhooks et ses commandes.\u003C\u002Fp>\n\u003Ch2 id=\"dans-un-programme\">Dans un programme\u003C\u002Fh2>\n\u003Cp>Le programme d’un plugin (\u003Ccode class=\"key\">run\u003C\u002Fcode>) reçoit chaque fait par la notification \u003Ccode class=\"key\">events\u002Ffact\u003C\u002Fcode>, le fait entier en paramètres. Il n’y répond pas : Escouade n’attend jamais un plugin.\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-json\">{\n  &quot;jsonrpc&quot;: &quot;2.0&quot;,\n  &quot;method&quot;: &quot;events\u002Ffact&quot;,\n  &quot;params&quot;: {\n    &quot;version&quot;: 1,\n    &quot;seq&quot;: 42,\n    &quot;name&quot;: &quot;ticket.column&quot;,\n    &quot;data&quot;: { &quot;from&quot;: &quot;doing&quot;, &quot;to&quot;: &quot;review&quot; },\n    &quot;ticket&quot;: { &quot;id&quot;: &quot;e02a…&quot;, &quot;key&quot;: &quot;ESC-177&quot;, &quot;title&quot;: &quot;Les événements&quot; }\n  }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Pour le démarrer au premier fait, mets-le dans \u003Ccode class=\"key\">activation\u003C\u002Fcode> : \u003Ccode>&quot;activation&quot;: [&quot;event:ticket.column&quot;]\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Cp>Les faits d’un plugin passent par \u003Cstrong>une file à lui\u003C\u002Fstrong>, de 256 faits. Le suivant part quand son programme a pris le précédent sur son entrée ; un plugin lent ne retarde rien d’autre. Quand la file est pleine, le plus vieux fait est perdu, et son journal le dit : « the queue of facts is full (256): the oldest, ticket.column #41, is dropped ».\u003C\u002Fp>\n\u003Ch2 id=\"un-webhook-sans-code\">Un webhook, sans code\u003C\u002Fh2>\n\u003Cp>Un plugin \u003Cstrong>déclaratif\u003C\u002Fstrong> (sans \u003Ccode class=\"key\">run\u003C\u002Fcode>) appelle une adresse sur un fait. Chaque webhook de \u003Ccode class=\"key\">contributes.webhooks\u003C\u002Fcode> dit :\u003C\u002Fp>\n\u003Cdiv class=\"table\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Champ\u003C\u002Fth>\n\u003Cth>Obligatoire\u003C\u002Fth>\n\u003Cth>Ce qu’il dit\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">url\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>oui\u003C\u002Ftd>\n\u003Ctd>L’adresse, en \u003Ccode class=\"key\">http:\u002F\u002F\u003C\u002Fcode> ou \u003Ccode class=\"key\">https:\u002F\u002F\u003C\u002Fcode>, vers un hôte de \u003Ccode class=\"key\">permissions.network\u003C\u002Fcode>. Elle peut venir d’un secret du plugin : \u003Ccode class=\"key\">{{secrets.webhook}}\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">events\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>oui\u003C\u002Ftd>\n\u003Ctd>Les faits qui l’appellent (\u003Ccode class=\"key\">ticket.*\u003C\u002Fcode> les prend tous). Ils doivent aussi être dans \u003Ccode class=\"key\">permissions.events\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">when\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>non\u003C\u002Ftd>\n\u003Ctd>Ce que le fait doit dire, chemin par chemin : \u003Ccode>{ &quot;data.to&quot;: &quot;review&quot; }\u003C\u002Fcode>, ou une liste de valeurs : \u003Ccode>{ &quot;data.status&quot;: [&quot;waiting&quot;, &quot;error&quot;] }\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">body\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>non\u003C\u002Ftd>\n\u003Ctd>Le gabarit du corps : un objet JSON, ou un texte. Sans lui, le fait entier en JSON.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">contentType\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>non\u003C\u002Ftd>\n\u003Ctd>Le type du corps : \u003Ccode class=\"key\">application\u002Fjson\u003C\u002Fcode> pour un objet ou sans gabarit, \u003Ccode class=\"key\">text\u002Fplain\u003C\u002Fcode> pour un texte.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">headers\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>non\u003C\u002Ftd>\n\u003Ctd>Des en-têtes, qui peuvent prendre un secret : \u003Ccode>{ &quot;Authorization&quot;: &quot;Bearer {{secrets.token}}&quot; }\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>Un gabarit remplace \u003Ccode class=\"key\">{{chemin}}\u003C\u002Fcode> par la valeur du fait : \u003Ccode class=\"key\">{{name}}\u003C\u002Fcode>, \u003Ccode class=\"key\">{{ticket.key}}\u003C\u002Fcode>, \u003Ccode class=\"key\">{{ticket.title}}\u003C\u002Fcode>, \u003Ccode class=\"key\">{{project.name}}\u003C\u002Fcode>, \u003Ccode class=\"key\">{{agent.name}}\u003C\u002Fcode>, \u003Ccode class=\"key\">{{data.to}}\u003C\u002Fcode>… Dans un objet, un texte qui n’est qu’un \u003Ccode class=\"key\">{{chemin}}\u003C\u002Fcode> prend la valeur telle quelle (une liste reste une liste) ; dans un texte JSON, les valeurs sont échappées. Un secret ne va jamais dans le corps.\u003C\u002Fp>\n\u003Cp>Le plugin Discord, en entier : un webhook pour un ticket passé « À tester », un autre pour un agent qui attend.\u003C\u002Fp>\n\u003Cp>\u003Ccode>plugins\u002Fdiscord\u002Fescouade-plugin.json\u003C\u002Fcode>\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-json\">{\n  &quot;$schema&quot;: &quot;https:\u002F\u002Fescouade.dev\u002Fschemas\u002Fplugin-1.json&quot;,\n  &quot;id&quot;: &quot;dev.escouade.discord&quot;,\n  &quot;name&quot;: &quot;Discord&quot;,\n  &quot;version&quot;: &quot;1.1.0&quot;,\n  &quot;description&quot;: {\n    &quot;fr&quot;: &quot;Les tickets à tester et les agents qui attendent, dans un salon Discord : un webhook, sans code.&quot;,\n    &quot;en&quot;: &quot;Tickets to test and agents waiting, in a Discord channel: a webhook, no code.&quot;\n  },\n  &quot;author&quot;: { &quot;name&quot;: &quot;Escouade&quot;, &quot;url&quot;: &quot;https:\u002F\u002Fescouade.dev&quot; },\n  &quot;license&quot;: &quot;MIT&quot;,\n  &quot;repository&quot;: &quot;https:\u002F\u002Fgithub.com\u002Fguillaume-gagnaire\u002Fescouade\u002Ftree\u002Fmain\u002Fplugins\u002Fdiscord&quot;,\n  &quot;escouade&quot;: { &quot;api&quot;: &quot;1&quot;, &quot;min&quot;: &quot;2.1.0&quot; },\n  &quot;permissions&quot;: {\n    &quot;events&quot;: [&quot;ticket.column&quot;, &quot;agent.status&quot;],\n    &quot;network&quot;: [&quot;discord.com&quot;],\n    &quot;secrets&quot;: [&quot;webhook&quot;]\n  },\n  &quot;contributes&quot;: {\n    &quot;secrets&quot;: [{ &quot;key&quot;: &quot;webhook&quot;, &quot;title&quot;: { &quot;fr&quot;: &quot;Adresse du webhook Discord&quot;, &quot;en&quot;: &quot;Discord webhook URL&quot; } }],\n    &quot;webhooks&quot;: [\n      {\n        &quot;url&quot;: &quot;{{secrets.webhook}}&quot;,\n        &quot;events&quot;: [&quot;ticket.column&quot;],\n        &quot;when&quot;: { &quot;data.to&quot;: &quot;review&quot; },\n        &quot;body&quot;: {\n          &quot;content&quot;: &quot;🧪 **{{ticket.key}}** to test: {{ticket.title}} ({{project.name}})&quot;,\n          &quot;allowed_mentions&quot;: { &quot;parse&quot;: [] }\n        }\n      },\n      {\n        &quot;url&quot;: &quot;{{secrets.webhook}}&quot;,\n        &quot;events&quot;: [&quot;agent.status&quot;],\n        &quot;when&quot;: { &quot;data.status&quot;: &quot;waiting&quot; },\n        &quot;body&quot;: {\n          &quot;content&quot;: &quot;✋ **{{agent.name}}** waits for you ({{project.name}})&quot;,\n          &quot;allowed_mentions&quot;: { &quot;parse&quot;: [] }\n        }\n      }\n    ]\n  }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>Un webhook ne part que vers un hôte de \u003Ccode class=\"key\">permissions.network\u003C\u002Fcode>\u003C\u002Fstrong>, celui que la fenêtre d’installation montre, que son adresse soit écrite en clair, tirée d’un secret ou faite d’une valeur du fait. Escouade refuse au chargement un manifeste dont un webhook écrit en clair va ailleurs, et ne fait pas un appel dont l’adresse, une fois faite, va ailleurs (le journal le dit) : un titre de ticket comme \u003Ccode class=\"key\">evil.dev\u002Fx\u003C\u002Fcode> n’envoie rien. Un hôte se compare sans tenir compte de la casse, sans l’utilisateur ni le port de l’adresse. \u003Ccode class=\"key\">hooks.slack.com\u003C\u002Fcode> ne couvre que lui-même ; \u003Ccode class=\"key\">*.exemple.dev\u003C\u002Fcode> couvre ses sous-domaines à toute profondeur (\u003Ccode class=\"key\">hooks.exemple.dev\u003C\u002Fcode>, \u003Ccode class=\"key\">a.b.exemple.dev\u003C\u002Fcode>), mais pas \u003Ccode class=\"key\">exemple.dev\u003C\u002Fcode> lui-même, à déclarer à part, ni \u003Ccode class=\"key\">evilexemple.dev\u003C\u002Fcode>. Le webhook a 10 s pour répondre, sans redirection. Son journal note chaque appel par son hôte et sa réponse, jamais l’adresse entière (celle de Discord contient un jeton).\u003C\u002Fp>\n\u003Ch2 id=\"une-commande-sur-un-fait\">Une commande sur un fait\u003C\u002Fh2>\n\u003Cp>\u003Ccode class=\"key\">contributes.commands\u003C\u002Fcode> lance une ligne de commande sur un fait, dans le shell de l’utilisateur, dans le dossier du projet du fait :\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-json\">&quot;contributes&quot;: {\n  &quot;commands&quot;: [\n    { &quot;id&quot;: &quot;fin-du-tour&quot;, &quot;events&quot;: [&quot;turn.ended&quot;], &quot;when&quot;: { &quot;data.end&quot;: &quot;finished&quot; }, &quot;run&quot;: &quot;say \\&quot;Claude a fini\\&quot;&quot; }\n  ]\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cul>\n\u003Cli>\u003Cstrong>Rien ne tourne avant l’accord\u003C\u002Fstrong> de l’utilisateur, comme les commandes de \u003Ccode class=\"key\">.escouade.yml\u003C\u002Fcode> : l’onglet Permissions de sa fiche, dans Réglages › Plugins, les montre sous « Lance ces commandes » avec « Accepter ces commandes », et l’accord vaut pour elles telles qu’elles sont ; « Retirer l’accord » les arrête. Une commande, un fait ou une condition qui change, et il faut l’accord à nouveau. Le journal dit chaque commande refusée faute d’accord.\u003C\u002Fli>\n\u003Cli>Le fait n’entre \u003Cstrong>jamais dans la ligne de commande\u003C\u002Fstrong> : un titre de ticket ne lance rien. La commande le trouve dans ses variables : \u003Ccode class=\"key\">ESCOUADE_FACT\u003C\u002Fcode> (le fait entier en JSON), \u003Ccode class=\"key\">ESCOUADE_FACT_NAME\u003C\u002Fcode>, \u003Ccode class=\"key\">ESCOUADE_PROJECT\u003C\u002Fcode>, \u003Ccode class=\"key\">ESCOUADE_TICKET\u003C\u002Fcode> (sa clé) et \u003Ccode class=\"key\">ESCOUADE_AGENT\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>Elle a 60 s. Son journal note sa fin (\u003Ccode>exit 0\u003C\u002Fcode>) et, si elle échoue, ses dernières lignes.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"le-lecteur-de-workflow\">Le lecteur de workflow\u003C\u002Fh2>\n\u003Cp>Escouade lit seule les workflows d’un projet (GSD, Superpowers, Ralph…). Un plugin peut lui en apprendre un qu’elle ne connaît pas, avec \u003Ccode>contributes.workflowReaders\u003C\u002Fcode> :\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-json\">&quot;contributes&quot;: {\n  &quot;workflowReaders&quot;: [\n    { &quot;id&quot;: &quot;bmad&quot;, &quot;name&quot;: &quot;BMAD&quot;, &quot;files&quot;: [&quot;_bmad\u002F**\u002F*.md&quot;, &quot;docs\u002F*.md&quot;] }\n  ]\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cul>\n\u003Cli>Escouade appelle \u003Ccode class=\"key\">workflow\u002Fread\u003C\u002Fcode> avec \u003Ccode>{ &quot;reader&quot;: &quot;bmad&quot;, &quot;root&quot;: &quot;…&quot;, &quot;repos&quot;: [], &quot;files&quot;: [{ &quot;path&quot;: &quot;_bmad\u002Fbrief.md&quot;, &quot;text&quot;: &quot;…&quot; }] }\u003C\u002Fcode> : \u003Cstrong>seulement les fichiers que ses globs désignent\u003C\u002Fstrong>, depuis le dossier du projet (et celui de chaque dépôt pour un projet de plusieurs), en texte, au plus 200 fichiers de 256 Ko. Un glob avec \u003Ccode class=\"key\">..\u003C\u002Fcode> ou un chemin absolu ne trouve rien.\u003C\u002Fli>\n\u003Cli>Le plugin rend le graphe : \u003Ccode>{ &quot;workflows&quot;: [{ &quot;id&quot;, &quot;name&quot;, &quot;steps&quot;: [{ &quot;name&quot;, &quot;command&quot;, &quot;description&quot;, &quot;path&quot;, &quot;agents&quot;, &quot;reads&quot;, &quot;writes&quot; }], &quot;transitions&quot;: [{ &quot;from&quot;, &quot;to&quot;, &quot;back&quot;, &quot;condition&quot;, &quot;max&quot; }], &quot;exit&quot; }] }\u003C\u002Fcode>. Une étape est signalée par sa \u003Ccode class=\"key\">command\u003C\u002Fcode> tapée, par les sous-agents qu’elle nomme ou les fichiers qu’elle écrit ; une transition \u003Ccode class=\"key\">back\u003C\u002Fcode> est un retour en arrière, avec sa \u003Ccode class=\"key\">condition\u003C\u002Fcode> pour raison quand l’étape n’en donne pas.\u003C\u002Fli>\n\u003Cli>Le workflow se suit comme ceux qu’Escouade lit : l’onglet Workflow, la carte, les faits \u003Ccode class=\"key\">workflow.*\u003C\u002Fcode>. Son id est \u003Ccode class=\"key\">plugin:&lt;plugin&gt;:&lt;id&gt;\u003C\u002Fcode>, donc il ne prend jamais la place d’un autre.\u003C\u002Fli>\n\u003Cli>Un graphe qui ne tient pas (une étape sans nom, deux du même nom, une transition vers une étape absente, plus de 50 workflows) est refusé en entier, avec la raison, dans le journal. L’appel a 60 s.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"le-journal\">Le journal\u003C\u002Fh2>\n\u003Cp>Le journal de chaque plugin (Réglages › Plugins, et \u003Ccode class=\"key\">plugin-logs\u002F&lt;id&gt;.log\u003C\u002Fcode> dans les données d’Escouade) dit les webhooks appelés et leurs réponses, les commandes lancées et leur fin, ce qui a été refusé (un secret qui n’est pas le sien, une adresse non déclarée, une commande sans accord) et les faits perdus d’une file pleine.\u003C\u002Fp>\n\u003Ch2 id=\"essayer\">Essayer\u003C\u002Fh2>\n\u003Cp>\u003Ccode>escouade-plugin dev &lt;dossier&gt;\u003C\u002Fcode> lance ton plugin hors d’Escouade et lui envoie les faits que tu choisis ; \u003Ccode>escouade-plugin test &lt;dossier&gt;\u003C\u002Fcode> vérifie qu’il reçoit un fait sans planter. Voir \u003Ca href=\"\u002Fdocs\u002Fplugins\u002Ftesting\u002F\">Tester un plugin\u003C\u002Fa>.\u003C\u002Fp>\n",[8,12,15,18,21,24,27,30,33],{"id":9,"text":10,"depth":11},"un-fait","Un fait",2,{"id":13,"text":14,"depth":11},"les-faits","Les faits",{"id":16,"text":17,"depth":11},"s-abonner","S’abonner",{"id":19,"text":20,"depth":11},"dans-un-programme","Dans un programme",{"id":22,"text":23,"depth":11},"un-webhook-sans-code","Un webhook, sans code",{"id":25,"text":26,"depth":11},"une-commande-sur-un-fait","Une commande sur un fait",{"id":28,"text":29,"depth":11},"le-lecteur-de-workflow","Le lecteur de workflow",{"id":31,"text":32,"depth":11},"le-journal","Le journal",{"id":34,"text":35,"depth":11},"essayer","Essayer",1793889379555]