[{"data":1,"prerenderedAt":36},["ShallowReactive",2],{"doc:en:plugins\u002Fevents":3},{"title":4,"lede":5,"html":6,"headings":7},"Events","Escouade tells what happens in it as facts: an agent created, a turn over, a ticket to test… A plugin subscribes to the facts it wants: its program receives them, a declared webhook sends them to an address without code, or a declared command runs.","\u003Cp>Facts are told by Escouade’s core where the state changes, never read back from what the window shows: they do not change when the interface does.\u003C\u002Fp>\n\u003Ch2 id=\"a-fact\">A fact\u003C\u002Fh2>\n\u003Cp>Every fact has the same shape, in 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;Events&quot; },\n  &quot;agent&quot;: { &quot;id&quot;: &quot;5b7d…&quot;, &quot;name&quot;: &quot;esc-177-events&quot; }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cul>\n\u003Cli>\u003Ccode class=\"key\">version\u003C\u002Fcode>: the version of the facts’ shape. A field added keeps it; a field removed or changed in meaning moves to the next one.\u003C\u002Fli>\n\u003Cli>\u003Ccode class=\"key\">seq\u003C\u002Fcode>: its number since Escouade started. Two facts come in the order of their numbers.\u003C\u002Fli>\n\u003Cli>\u003Ccode class=\"key\">name\u003C\u002Fcode> and \u003Ccode class=\"key\">data\u003C\u002Fcode>: what happened, and what it says of it (the table below).\u003C\u002Fli>\n\u003Cli>\u003Ccode class=\"key\">project\u003C\u002Fcode>, \u003Ccode class=\"key\">ticket\u003C\u002Fcode>, \u003Ccode class=\"key\">agent\u003C\u002Fcode>: what it is about, by their ids and their names, when it is about them. The fact of an agent working on a ticket also says its ticket; the project says whether it is private (\u003Ccode class=\"key\">private\u003C\u002Fcode>).\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>A fact \u003Cstrong>never carries the conversation\u003C\u002Fstrong>: a plugin learns that a turn is over and how, not what was said in it.\u003C\u002Fp>\n\u003Ch2 id=\"the-facts\">The facts\u003C\u002Fh2>\n\u003Cdiv class=\"table\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Fact\u003C\u002Fth>\n\u003Cth>When\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>An agent is created (by hand, for a ticket, by a client of the MCP server).\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>It is named (after its first message, or by hand); the facts after it say its new name.\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>Its status changes.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">status\u003C\u002Fcode> and \u003Ccode class=\"key\">was\u003C\u002Fcode>: \u003Ccode class=\"key\">idle\u003C\u002Fcode>, \u003Ccode class=\"key\">running\u003C\u002Fcode>, \u003Ccode class=\"key\">waiting\u003C\u002Fcode> (a request waits for an answer), \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>It is archived (\u003Ccode>removed: false\u003C\u002Fcode>) or deleted (\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>It goes on with another 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>A turn ends.\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> (the usage limit), \u003Ccode class=\"key\">signIn\u003C\u002Fcode> (a sign-in to do again) or \u003Ccode class=\"key\">error\u003C\u002Fcode>, with \u003Ccode class=\"key\">error\u003C\u002Fcode> (its first line)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">request.permission\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>A permission waits.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">requestId\u003C\u002Fcode>, \u003Ccode class=\"key\">tool\u003C\u002Fcode>, \u003Ccode class=\"key\">arg\u003C\u002Fcode> (what the tool acts on, summed up as on the card)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">request.question\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>A question waits.\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>A request has its answer.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">requestId\u003C\u002Fcode>, \u003Ccode class=\"key\">kind\u003C\u002Fcode> (\u003Ccode class=\"key\">permission\u003C\u002Fcode> or \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> (the client of the MCP server that answered, else absent)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">ticket.created\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>A ticket is created.\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>It moves to another column.\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>Its agent reports on its criteria.\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>Its validation starts.\u003C\u002Ftd>\n\u003Ctd>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">ticket.rejected\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>It is sent back to its 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>It is merged into its target branch.\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>Its merge meets a conflict.\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>A pull request is opened for it.\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>The tests of its validation passed or not.\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>A project is opened.\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>It is closed.\u003C\u002Ftd>\n\u003Ctd>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">worktree.created\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>A worktree is created for an 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>It is removed.\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>A test launch of an agent starts.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">run\u003C\u002Fcode> (its name)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">launch.ready\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>It answers.\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>It stops.\u003C\u002Ftd>\n\u003Ctd>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">docs.written\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>A page of the docs is written.\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>A ticket’s copy of the docs is poured on the docs’ branch, at its 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>A page is compared to the code.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">page\u003C\u002Fcode>, \u003Ccode class=\"key\">gaps\u003C\u002Fcode> (how many gaps)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">memory.written\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>A memory of the project’s memory is written.\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>It is forgotten.\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>A decision is proposed or revised.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">id\u003C\u002Fcode> (no number yet), \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>A human accepts it.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">number\u003C\u002Fcode> (given then), \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>An agent changes account.\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>An agent enters a step of its project’s workflow.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">workflow\u003C\u002Fcode>, \u003Ccode class=\"key\">step\u003C\u002Fcode>, \u003Ccode class=\"key\">door\u003C\u002Fcode> (the command typed, if any), \u003Ccode class=\"key\">pass\u003C\u002Fcode> (its nth stay there)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">workflow.step_left\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>It leaves it: duration, cost, outcome.\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>It goes back to a step already passed.\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> (the line the step ended on)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">workflow.left\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>It leaves the workflow: it does something else.\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>A new reading of the project’s workflows came out different.\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">workflows\u003C\u002Fcode> (the ids of those that changed, appeared or went)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">app.started\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Escouade starts.\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=\"subscribing\">Subscribing\u003C\u002Fh2>\n\u003Cp>A plugin only receives the facts it asks for, in \u003Ccode class=\"key\">permissions.events\u003C\u002Fcode> of its manifest. A name ending with \u003Ccode class=\"key\">*\u003C\u002Fcode> takes them all (\u003Ccode class=\"key\">ticket.*\u003C\u002Fcode>, or \u003Ccode class=\"key\">*\u003C\u002Fcode> for everything):\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>The install window shows them: “Receives these events: …”.\u003C\u002Fp>\n\u003Cp>And only those of the projects it is \u003Cstrong>turned on\u003C\u002Fstrong> for. A plugin is on for every project, except \u003Cstrong>private projects\u003C\u002Fstrong>: a private project’s fact only goes to a plugin turned on for it, one by one, in Settings › Plugins. A fact of no project (\u003Ccode class=\"key\">app.started\u003C\u002Fcode>) goes to every plugin on for the app. An \u003Ccode class=\"key\">events:…\u003C\u002Fcode> permission taken back in the Permissions tab of its page cuts those facts, for its program as for its webhooks and its commands.\u003C\u002Fp>\n\u003Ch2 id=\"in-a-program\">In a program\u003C\u002Fh2>\n\u003Cp>A plugin’s program (\u003Ccode class=\"key\">run\u003C\u002Fcode>) receives each fact by the \u003Ccode class=\"key\">events\u002Ffact\u003C\u002Fcode> notification, the whole fact as its parameters. It does not answer: Escouade never waits for a 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;Events&quot; }\n  }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>To start it at the first fact, put it in \u003Ccode class=\"key\">activation\u003C\u002Fcode>: \u003Ccode>&quot;activation&quot;: [&quot;event:ticket.column&quot;]\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Cp>A plugin’s facts go through \u003Cstrong>a queue of its own\u003C\u002Fstrong>, of 256 facts. The next one goes once its program took the last one on its input; a slow plugin holds nothing else back. When the queue is full, the oldest fact is lost, and its journal says so: “the queue of facts is full (256): the oldest, ticket.column #41, is dropped”.\u003C\u002Fp>\n\u003Ch2 id=\"a-webhook-without-code\">A webhook, without code\u003C\u002Fh2>\n\u003Cp>A \u003Cstrong>declarative\u003C\u002Fstrong> plugin (without \u003Ccode class=\"key\">run\u003C\u002Fcode>) calls an address on a fact. Each webhook of \u003Ccode class=\"key\">contributes.webhooks\u003C\u002Fcode> says:\u003C\u002Fp>\n\u003Cdiv class=\"table\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Field\u003C\u002Fth>\n\u003Cth>Required\u003C\u002Fth>\n\u003Cth>What it says\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">url\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>yes\u003C\u002Ftd>\n\u003Ctd>The address, \u003Ccode class=\"key\">http:\u002F\u002F\u003C\u002Fcode> or \u003Ccode class=\"key\">https:\u002F\u002F\u003C\u002Fcode>, to a host of \u003Ccode class=\"key\">permissions.network\u003C\u002Fcode>. It may come from a secret of the 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>yes\u003C\u002Ftd>\n\u003Ctd>The facts that call it (\u003Ccode class=\"key\">ticket.*\u003C\u002Fcode> takes them all). They must also be in \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>no\u003C\u002Ftd>\n\u003Ctd>What the fact must say, path by path: \u003Ccode>{ &quot;data.to&quot;: &quot;review&quot; }\u003C\u002Fcode>, or a list of values: \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>no\u003C\u002Ftd>\n\u003Ctd>The body’s template: a JSON object, or a text. Without it, the whole fact in JSON.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">contentType\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>no\u003C\u002Ftd>\n\u003Ctd>The body’s type: \u003Ccode class=\"key\">application\u002Fjson\u003C\u002Fcode> for an object or without a template, \u003Ccode class=\"key\">text\u002Fplain\u003C\u002Fcode> for a text.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">headers\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>no\u003C\u002Ftd>\n\u003Ctd>Headers, which may take a 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>A template replaces \u003Ccode class=\"key\">{{path}}\u003C\u002Fcode> with the fact’s value: \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>… In an object, a text that is only a \u003Ccode class=\"key\">{{path}}\u003C\u002Fcode> takes the value as it is (a list stays a list); in a JSON text, the values are escaped. A secret never goes in the body.\u003C\u002Fp>\n\u003Cp>The Discord plugin, whole: one webhook for a ticket moved to “To review”, another for an agent waiting.\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>A webhook only goes to a host of \u003Ccode class=\"key\">permissions.network\u003C\u002Fcode>\u003C\u002Fstrong>, the one the install window shows, whether its address is written whole, taken from a secret or made of a value of the fact. Escouade refuses at load a manifest whose webhook written whole goes elsewhere, and makes no call whose address, once made, goes elsewhere (the journal says so): a ticket title like \u003Ccode class=\"key\">evil.dev\u002Fx\u003C\u002Fcode> sends nothing. A host is compared without regard to case, without the address’s user or port. \u003Ccode class=\"key\">hooks.slack.com\u003C\u002Fcode> covers itself alone; \u003Ccode class=\"key\">*.example.dev\u003C\u002Fcode> covers its subdomains at any depth (\u003Ccode class=\"key\">hooks.example.dev\u003C\u002Fcode>, \u003Ccode class=\"key\">a.b.example.dev\u003C\u002Fcode>), but not \u003Ccode class=\"key\">example.dev\u003C\u002Fcode> itself, to declare apart, nor \u003Ccode class=\"key\">evilexample.dev\u003C\u002Fcode>. The webhook has 10 s to answer, without redirects. Its journal notes each call by its host and its answer, never the whole address (Discord’s holds a token).\u003C\u002Fp>\n\u003Ch2 id=\"a-command-on-a-fact\">A command on a fact\u003C\u002Fh2>\n\u003Cp>\u003Ccode class=\"key\">contributes.commands\u003C\u002Fcode> runs a command line on a fact, in the user’s shell, in the folder of the fact’s project:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-json\">&quot;contributes&quot;: {\n  &quot;commands&quot;: [\n    { &quot;id&quot;: &quot;turn-over&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 is done\\&quot;&quot; }\n  ]\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cul>\n\u003Cli>\u003Cstrong>Nothing runs before the user agrees\u003C\u002Fstrong>, as with the commands of \u003Ccode class=\"key\">.escouade.yml\u003C\u002Fcode>: the Permissions tab of its page, in Settings › Plugins, shows them under “Runs these commands” with “Agree to these commands”, and the agreement holds for them as they are; “Take the agreement back” stops them. A command, a fact or a condition that changes, and it takes agreeing again. The journal tells each command refused for want of agreement.\u003C\u002Fli>\n\u003Cli>The fact \u003Cstrong>never goes in the command line\u003C\u002Fstrong>: a ticket’s title runs nothing. The command finds it in its variables: \u003Ccode class=\"key\">ESCOUADE_FACT\u003C\u002Fcode> (the whole fact in JSON), \u003Ccode class=\"key\">ESCOUADE_FACT_NAME\u003C\u002Fcode>, \u003Ccode class=\"key\">ESCOUADE_PROJECT\u003C\u002Fcode>, \u003Ccode class=\"key\">ESCOUADE_TICKET\u003C\u002Fcode> (its key) and \u003Ccode class=\"key\">ESCOUADE_AGENT\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>It has 60 s. Its journal notes its end (\u003Ccode>exit 0\u003C\u002Fcode>) and, when it fails, its last lines.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"the-workflow-reader\">The workflow reader\u003C\u002Fh2>\n\u003Cp>Escouade reads the workflows of a project by itself (GSD, Superpowers, Ralph…). A plugin can teach it one it does not know, with \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 calls \u003Ccode class=\"key\">workflow\u002Fread\u003C\u002Fcode> with \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>only the files its globs match\u003C\u002Fstrong>, from the project’s folder (and from each repository’s of a project of several), as text, at most 200 files of 256 KB. A glob with \u003Ccode class=\"key\">..\u003C\u002Fcode> or an absolute path finds nothing.\u003C\u002Fli>\n\u003Cli>The plugin answers the graph: \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>. A step is signalled by its \u003Ccode class=\"key\">command\u003C\u002Fcode> typed, by the sub-agents it names or the files it writes; a transition with \u003Ccode class=\"key\">back\u003C\u002Fcode> is a way back, with its \u003Ccode class=\"key\">condition\u003C\u002Fcode> as the reason when the step says none.\u003C\u002Fli>\n\u003Cli>The workflow is followed like those Escouade reads: the Workflow tab, the card, the \u003Ccode class=\"key\">workflow.*\u003C\u002Fcode> facts. Its id is \u003Ccode class=\"key\">plugin:&lt;plugin&gt;:&lt;id&gt;\u003C\u002Fcode>, so it never takes the place of another.\u003C\u002Fli>\n\u003Cli>A graph that does not hold (a step without a name, two of one name, a transition to a step that is not there, more than 50 workflows) is refused whole, with why, in the journal. The call has 60 s.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"the-journal\">The journal\u003C\u002Fh2>\n\u003Cp>Each plugin’s journal (Settings › Plugins, and \u003Ccode class=\"key\">plugin-logs\u002F&lt;id&gt;.log\u003C\u002Fcode> in Escouade’s data) tells the webhooks called and their answers, the commands run and how they ended, what was refused (a secret not its own, an address not declared, a command not agreed to) and the facts lost from a full queue.\u003C\u002Fp>\n\u003Ch2 id=\"trying-it\">Trying it\u003C\u002Fh2>\n\u003Cp>\u003Ccode>escouade-plugin dev &lt;folder&gt;\u003C\u002Fcode> runs your plugin outside Escouade and sends it the facts you pick; \u003Ccode>escouade-plugin test &lt;folder&gt;\u003C\u002Fcode> checks that it takes a fact without crashing. See \u003Ca href=\"\u002Fen\u002Fdocs\u002Fplugins\u002Ftesting\u002F\">Testing a plugin\u003C\u002Fa>.\u003C\u002Fp>\n",[8,12,15,18,21,24,27,30,33],{"id":9,"text":10,"depth":11},"a-fact","A fact",2,{"id":13,"text":14,"depth":11},"the-facts","The facts",{"id":16,"text":17,"depth":11},"subscribing","Subscribing",{"id":19,"text":20,"depth":11},"in-a-program","In a program",{"id":22,"text":23,"depth":11},"a-webhook-without-code","A webhook, without code",{"id":25,"text":26,"depth":11},"a-command-on-a-fact","A command on a fact",{"id":28,"text":29,"depth":11},"the-workflow-reader","The workflow reader",{"id":31,"text":32,"depth":11},"the-journal","The journal",{"id":34,"text":35,"depth":11},"trying-it","Trying it",1793889382274]