@sjawhar/pi-legion 8.4.2 → 8.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/legion.js CHANGED
@@ -13918,7 +13918,7 @@ var AGENT_STREAM_LIMITS = {
13918
13918
  var SESSION_ID_PATTERN = "[^/?#\\s\\p{Z}\\p{Cc}<>\"'`\\[\\]|]+";
13919
13919
  var sessionIdOnly = new RegExp(`^${SESSION_ID_PATTERN}$`, "u");
13920
13920
  // ../contracts/src/dispatch-tools.ts
13921
- var ISSUE_REFERENCE = "An issue is a native KEY or external owner/repo#n reference. An external reference addresses an existing Dispatch issue, including one linked to that GitHub pull request; only dispatch_issue with external creates a native issue.";
13921
+ var ISSUE_REFERENCE = "An issue is a native KEY or external owner/repo#n reference. An external reference addresses an existing Dispatch issue, including one linked to that GitHub pull request; only dispatch issue with --external creates a native issue.";
13922
13922
  var OWNER_REFERENCE = "Exactly one of issue and project is required. An issue is a native KEY or external owner/repo#n reference; a project is a project key such as CORE and addresses an unlinked project document named by artifact.";
13923
13923
  var ASK_QUESTION_CONTRACT = "The question carries the problem the reader recognises and why it matters now, what constrains " + "the answer, and the recommendation with its reason. It asks how to solve the problem or which " + "outcome is wanted; never enumerate choices in the question.";
13924
13924
  var ASK_OPTIONS_CONTRACT = "Options carry the genuinely different approaches. Each option has a label, and its description " + "says what that approach costs.";
@@ -14019,7 +14019,7 @@ var dispatchToolSpecs = [
14019
14019
  {
14020
14020
  name: "dispatch_issue",
14021
14021
  example: { project: "DSP", title: "Native workspace" },
14022
- description: "Create a native Dispatch issue for newly tracked work. Search first with dispatch_search; if potentially duplicate issues exist, this returns 409 POSSIBLE_DUPLICATE unless force is true after reading them. " + "A spec holding an ask block whose body breaks its content rule (one or more question paragraphs, then at most one bullet list of options, last) is refused with 400 INVALID_ASK_BLOCK. " + `Do not use it when an existing issue already covers the work; read or update that issue instead. ${ISSUE_REFERENCE}`,
14022
+ description: "Create a native Dispatch issue for newly tracked work. Search first with dispatch search; if potentially duplicate issues exist, this returns 409 POSSIBLE_DUPLICATE unless --force is set after reading them. " + "A spec holding an ask block whose body breaks its content rule (one or more question paragraphs, then at most one bullet list of options, last) is refused with 400 INVALID_ASK_BLOCK. " + `Do not use it when an existing issue already covers the work; read or update that issue instead. ${ISSUE_REFERENCE}`,
14023
14023
  arguments: (z2) => ({
14024
14024
  project: z2.string().describe("Project key for the new issue."),
14025
14025
  title: z2.string().describe("Concise issue title."),
@@ -14066,7 +14066,7 @@ var dispatchToolSpecs = [
14066
14066
  {
14067
14067
  name: "dispatch_claim",
14068
14068
  example: { issue: "DSP-1" },
14069
- description: "Claim a Dispatch issue before you start implementing it, so no other session takes the same work, " + "and release it when you stop. Pass the issue alone to claim it, or release: true to give it up. " + "Your claim records your own session and shows on every read of the issue: the dashboard header, the " + "issue list and board, dispatch_read, and dispatch_issues. Claiming is refused with 409 ISSUE_CLAIMED " + "when another session holds the issue and is still running; the refusal names that session, so talk to " + "it instead of working the same issue in parallel. When a human holds the claim the refusal names the " + "person, not a session: there is nothing running to message, so ask them on the issue rather than " + "taking it. 409 CLAIM_CONTENDED means the issue changed " + "hands twice while your call ran, so nothing was applied and nobody's liveness was checked: read " + "the issue and decide again. A claim whose session is no longer running may be " + "taken: the takeover is recorded on the issue and the session that lost it is told. A claim is not the " + "issue's status \u2014 claiming moves nothing, so also move the issue to in_progress with " + "dispatch_issue_update when you start. A claim is released by its holder or any human, and by " + "any agent once the holder's session is no longer running. " + ISSUE_REFERENCE,
14069
+ description: "Claim a Dispatch issue before you start implementing it, so no other session takes the same work, " + "and release it when you stop. Pass the issue alone to claim it, or --release to give it up. " + "Your claim records your own session and shows on every read of the issue: the dashboard header, the " + "issue list and board, dispatch read, and dispatch issues. Claiming is refused with 409 ISSUE_CLAIMED " + "when another session holds the issue and is still running; the refusal names that session, so talk to " + "it instead of working the same issue in parallel. When a human holds the claim the refusal names the " + "person, not a session: there is nothing running to message, so ask them on the issue rather than " + "taking it. 409 CLAIM_CONTENDED means the issue changed " + "hands twice while your call ran, so nothing was applied and nobody's liveness was checked: read " + "the issue and decide again. A claim whose session is no longer running may be " + "taken: the takeover is recorded on the issue and the session that lost it is told. A claim is not the " + "issue's status \u2014 claiming moves nothing, so also move the issue to in_progress with " + "dispatch issue-update when you start. A claim is released by its holder or any human, and by " + "any agent once the holder's session is no longer running. " + ISSUE_REFERENCE,
14070
14070
  arguments: (z2) => ({
14071
14071
  issue: z2.string().describe(ISSUE_REFERENCE),
14072
14072
  release: z2.boolean().describe("Give up your claim instead of taking it; the issue's status does not change.").optional()
@@ -14089,7 +14089,7 @@ var dispatchToolSpecs = [
14089
14089
  }
14090
14090
  ]
14091
14091
  },
14092
- description: "Open a to-do or permission only a human can give, or a decision that has no document to " + "live in. A question about the design an issue's document records is not this tool: write " + "it into that document as a decision block (dispatch_doc_edit inserting an ask block at the " + "end of the section it concerns), at every phase, approved spec or not; the block reaches " + "the Inbox and its answer lands next to its context. Never give an ask an Approve option: a " + "document is approved through dispatch_request_approval. Do not use this tool for a status " + "update or discussion; use dispatch_message instead. " + ASK_QUESTION_CONTRACT + " " + ASK_OPTIONS_CONTRACT + " For an action only a human can perform, state what it changes and risks as constraints. " + "Anything that requires a human to do, including a credential or grant renewal, is an ask, " + "never a message. " + "Anchor a to-do about a document passage, thread reply_to/reply_to_ask, or cite a dispatch:// " + `reference \u2014 it must be answerable from its own text and anchor alone, never "see above". ` + `A quote anchor is pinned to its block. Question is at most ${ASK_QUESTION_MAX} characters, ` + `the lines images appends included, and has at most 8 options. ${OWNER_REFERENCE}`,
14092
+ description: "Open a to-do or permission only a human can give, or a decision that has no document to " + "live in. A question about the design an issue's document records is not this tool: write " + "it into that document as a decision block (dispatch doc-edit inserting an ask block at the " + "end of the section it concerns), at every phase, approved spec or not; the block reaches " + "the Inbox and its answer lands next to its context. Never give an ask an Approve option: a " + "document is approved through dispatch request-approval. Do not use this tool for a status " + "update or discussion; use dispatch message instead. " + ASK_QUESTION_CONTRACT + " " + ASK_OPTIONS_CONTRACT + " For an action only a human can perform, state what it changes and risks as constraints. " + "Anything that requires a human to do, including a credential or grant renewal, is an ask, " + "never a message. " + "Anchor a to-do about a document passage, thread reply_to/reply_to_ask, or cite a dispatch:// " + `reference \u2014 it must be answerable from its own text and anchor alone, never "see above". ` + `A quote anchor is pinned to its block. Question is at most ${ASK_QUESTION_MAX} characters, ` + `the lines images appends included, and has at most 8 options. ${OWNER_REFERENCE}`,
14093
14093
  arguments: (z2) => ({
14094
14094
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14095
14095
  project: z2.string().describe("Project key owning the document.").optional(),
@@ -14163,7 +14163,7 @@ var dispatchToolSpecs = [
14163
14163
  {
14164
14164
  name: "dispatch_resolve_comment",
14165
14165
  example: { comment: "01234567-0000-4000-8000-000000000001" },
14166
- description: "Resolve a review comment thread once it has been addressed - typically your own comment " + "after the document was fixed. Any session or human may resolve any open comment on an " + "open issue or project document; reopening a resolved comment is human-only (the dashboard). " + "Not for asks: use dispatch_resolve_ask.",
14166
+ description: "Resolve a review comment thread once it has been addressed - typically your own comment " + "after the document was fixed. Any session or human may resolve any open comment on an " + "open issue or project document; reopening a resolved comment is human-only (the dashboard). " + "Not for asks: use dispatch resolve-ask.",
14167
14167
  arguments: (z2) => ({
14168
14168
  comment: z2.string().describe("Comment id (uuid), or a dispatch://KEY/comment/<id> or " + "dispatch://PROJECT/artifact/<slug>/comment/<id> reference; a reference accepts an " + "8+ character id prefix that is unique on its owner.")
14169
14169
  }),
@@ -14182,7 +14182,7 @@ var dispatchToolSpecs = [
14182
14182
  {
14183
14183
  name: "dispatch_comment",
14184
14184
  example: { issue: "DSP-1", body: "Looks good." },
14185
- description: "Add review feedback to an issue or project document quote, or reply to a question asked with dispatch_ask. " + "Do not use it for an exact replacement; use " + `dispatch_suggest instead. A quote anchor is pinned to its block. Body is at most 2,000 characters, the lines images appends included. ${OWNER_REFERENCE}`,
14185
+ description: "Add review feedback to an issue or project document quote, or reply to a question asked with dispatch ask. " + "Do not use it for an exact replacement; use " + `dispatch suggest instead. A quote anchor is pinned to its block. Body is at most 2,000 characters, the lines images appends included. ${OWNER_REFERENCE}`,
14186
14186
  arguments: (z2) => ({
14187
14187
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14188
14188
  project: z2.string().describe("Project key owning the document.").optional(),
@@ -14206,7 +14206,7 @@ var dispatchToolSpecs = [
14206
14206
  quote: "old wording",
14207
14207
  replace_with: "new wording"
14208
14208
  },
14209
- description: "Propose an exact replacement for quoted document text. Do not use it for general feedback; use " + `dispatch_comment instead. Optional explanation is at most 2,000 characters. ${OWNER_REFERENCE}`,
14209
+ description: "Propose an exact replacement for quoted document text. Do not use it for general feedback; use " + `dispatch comment instead. Optional explanation is at most 2,000 characters. ${OWNER_REFERENCE}`,
14210
14210
  arguments: (z2) => ({
14211
14211
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14212
14212
  project: z2.string().describe("Project key owning the document.").optional(),
@@ -14222,7 +14222,7 @@ var dispatchToolSpecs = [
14222
14222
  {
14223
14223
  name: "dispatch_message",
14224
14224
  example: { issue: "DSP-1", body: "Implementation started." },
14225
- description: "Post a note humans must read now: a reply to a human's message or a deliverable that landed. A to-do only a human can " + "complete is an ask (dispatch_ask), so it reaches their inbox. Never progress or status updates - Dispatch is a high-signal " + "record, not a log. Not a design decision (write it as a decision block in the document) or document feedback (dispatch_comment). " + "To answer a human's direct message to this session - one sent from the Agents page, which names no issue - pass that message's bare id as " + "in_reply_to and no issue; the reply lands in that conversation. Another call with the same in_reply_to and new text posts a follow-up, " + "threaded under this session's first reply; the same text again posts nothing. dispatch_read({message}) reads " + "that conversation back. Every other message names its issue. " + `Body is at most 2,000 characters, the lines images appends included. ${ISSUE_REFERENCE}`,
14225
+ description: "Post a note humans must read now: a reply to a human's message or a deliverable that landed. A to-do only a human can " + "complete is an ask (dispatch ask), so it reaches their inbox. Never progress or status updates - Dispatch is a high-signal " + "record, not a log. Not a design decision (write it as a decision block in the document) or document feedback (dispatch comment). " + "To answer a human's direct message to this session - one sent from the Agents page, which names no issue - pass that message's bare id as " + "in_reply_to and no issue; the reply lands in that conversation. Another call with the same in_reply_to and new text posts a follow-up, " + "threaded under this session's first reply; the same text again posts nothing. dispatch read --message <id> reads " + "that conversation back. Every other message names its issue. " + `Body is at most 2,000 characters, the lines images appends included. ${ISSUE_REFERENCE}`,
14226
14226
  arguments: (z2) => ({
14227
14227
  issue: z2.string().describe(`${ISSUE_REFERENCE} Omit it only when in_reply_to answers a human's direct message to this session.`).optional(),
14228
14228
  body: z2.string({ max: DISPATCH_BODY_MAX }).describe("Update text, at most 2,000 characters."),
@@ -14239,7 +14239,7 @@ var dispatchToolSpecs = [
14239
14239
  ops: [{ op: "delete_column", block: "table-123", index: 1 }],
14240
14240
  precondition: { blocks: [{ id: "table-123", token: "sha256:current-table-token" }] }
14241
14241
  },
14242
- description: "Apply deterministic document edits: replace or delete quoted text, insert markdown at an anchor, retype an identified paragraph or typed block into a schema-declared typed block, delete or move a whole block by its id, or delete a table row or column in place. " + "Do not use it for review feedback or for reading; use dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. " + `For replace, delete, and quote anchors, find text as rendered: inline Markdown (**bold**, \`code\`) is tolerated and must be balanced; a leading '# ' matches a heading at any level. replace is inline: with is the new text of the matched span, so a marker of a different kind from the block's own stays literal text ('4. Design' written into a heading). A with that opens with a marker of the same kind as the matched block's own would write it twice and is INVALID_OP - including prose that merely looks like one ('1999. was a year' into an ordered item), which you write as text by escaping it ('1999\\. was a year'). The exception is a heading rename whose find carried a heading marker: replace(find="## Old", with="## New") gives '## New', and a different level applies only when find named the heading's actual level (find "## Old" with "### New" makes it an h3), since '# ' selects a heading without naming its level. A task item (\`- [ ]\` / \`- [x]\`) is ticked the same way: when find matches from the start of the item's text and with opens with '[x] ' or '[ ] ', the opening sets the item's box and the rest is its new text (replace(find="Write it", with="[x] Write it") ticks it); the same opening over a plain list item or a paragraph stays literal text. Any non-empty with that renders to no text - a line indented four spaces or a tab, which markdown reads as a code block, or whitespace alone - is INVALID_OP rather than a silent deletion; pass an empty with to delete the matched text on purpose - a list item, quote, typed block or footnote definition left holding only the emptied paragraph keeps it. ` + "with cannot open a new block: after a hard line break inside with (two trailing spaces, or a backslash, before the newline) a heading, bullet, '1.'/'1)' ordered, or '>' blockquote marker is INVALID_OP too, since that line would stay escaped text inside the matched block - use insert, plus delete for what it replaces, to add the block. A hard break in with is itself INVALID_OP when the matched text is in a heading or a table cell, which are written on one line. " + "A delete whose find is a block's entire text removes the block (a list emptied of its items goes too); delete with block removes any block by id, and move with block relocates one. A delete or retype that would take an ask block out of the document while its ask is open is refused, with nothing sent. delete_row and delete_column take a table block and a zero-based index, preserving the table block id and refusing to remove cells with open asks or unresolved comments. " + 'Insert and move anchors also accept "start", "end", "heading:<exact heading text>", and "block:<id>"; block ids and their tokens come from GET /api/v1/artifacts/{artifact UUID}/blocks (the route takes the artifact UUID, not its slug). ' + "Optionally require the state just read: precondition selects exactly one of a document token from dispatch_doc_read, or block {id, token} values from /blocks. A block guard must include every block the batch changes; Dispatch resolves quote targets and rejects an uncovered batch rather than applying it. Use a document token for insert or move, which depend on document order. Prefer block tokens when the covered content blocks are independent sections. Tokens include inline marks, so a fresh human comment also makes a stale edit fail. PRECONDITION_FAILED means re-read; EDIT_QUEUE_FULL means back off before retrying. " + "The result carries the document token this edit produced, so a chain of guarded edits passes each result's token as the next edit's precondition with no dispatch_doc_read between them. " + "A batch that leaves the document exactly as it was mints no version, named or not, and the result says nothing changed and names each operation that did nothing. " + "A change a browser removes while the edit is in flight is never reported as applied: EDIT_LOST_TO_CONCURRENT_CHANGE means the write was refused and nothing was written, so re-read the document and decide again, as with PRECONDITION_FAILED; lost_ops on a successful result names operations whose text the live document no longer has, because the deletion landed after the version was written. " + `The spec (or any document) holds requirements, design, and decisions - never progress, status, or timestamps. ${OWNER_REFERENCE} ${SPEC_WRITING_POINTER}`,
14242
+ description: "Apply deterministic document edits: replace or delete quoted text, insert markdown at an anchor, retype an identified paragraph or typed block into a schema-declared typed block, delete or move a whole block by its id, or delete a table row or column in place. " + "Do not use it for review feedback or for reading; use dispatch comment, dispatch suggest, or dispatch doc-read instead. " + `For replace, delete, and quote anchors, find text as rendered: inline Markdown (**bold**, \`code\`) is tolerated and must be balanced; a leading '# ' matches a heading at any level. replace is inline: with is the new text of the matched span, so a marker of a different kind from the block's own stays literal text ('4. Design' written into a heading). A with that opens with a marker of the same kind as the matched block's own would write it twice and is INVALID_OP - including prose that merely looks like one ('1999. was a year' into an ordered item), which you write as text by escaping it ('1999\\. was a year'). The exception is a heading rename whose find carried a heading marker: replace(find="## Old", with="## New") gives '## New', and a different level applies only when find named the heading's actual level (find "## Old" with "### New" makes it an h3), since '# ' selects a heading without naming its level. A task item (\`- [ ]\` / \`- [x]\`) is ticked the same way: when find matches from the start of the item's text and with opens with '[x] ' or '[ ] ', the opening sets the item's box and the rest is its new text (replace(find="Write it", with="[x] Write it") ticks it); the same opening over a plain list item or a paragraph stays literal text. Any non-empty with that renders to no text - a line indented four spaces or a tab, which markdown reads as a code block, or whitespace alone - is INVALID_OP rather than a silent deletion; pass an empty with to delete the matched text on purpose - a list item, quote, typed block or footnote definition left holding only the emptied paragraph keeps it. ` + "with cannot open a new block: after a hard line break inside with (two trailing spaces, or a backslash, before the newline) a heading, bullet, '1.'/'1)' ordered, or '>' blockquote marker is INVALID_OP too, since that line would stay escaped text inside the matched block - use insert, plus delete for what it replaces, to add the block. A hard break in with is itself INVALID_OP when the matched text is in a heading or a table cell, which are written on one line. " + "A delete whose find is a block's entire text removes the block (a list emptied of its items goes too); delete with block removes any block by id, and move with block relocates one. A delete or retype that would take an ask block out of the document while its ask is open is refused, with nothing sent. delete_row and delete_column take a table block and a zero-based index, preserving the table block id and refusing to remove cells with open asks or unresolved comments. " + 'Insert and move anchors also accept "start", "end", "heading:<exact heading text>", and "block:<id>"; block ids and their tokens come from GET /api/v1/artifacts/{artifact UUID}/blocks (the route takes the artifact UUID, not its slug). ' + "Optionally require the state just read: precondition selects exactly one of a document token from dispatch doc-read, or block {id, token} values from /blocks. A block guard must include every block the batch changes; Dispatch resolves quote targets and rejects an uncovered batch rather than applying it. Use a document token for insert or move, which depend on document order. Prefer block tokens when the covered content blocks are independent sections. Tokens include inline marks, so a fresh human comment also makes a stale edit fail. PRECONDITION_FAILED means re-read; EDIT_QUEUE_FULL means back off before retrying. " + "The result carries the document token this edit produced, so a chain of guarded edits passes each result's token as the next edit's precondition with no dispatch doc-read between them. " + "A batch that leaves the document exactly as it was mints no version, named or not, and the result says nothing changed and names each operation that did nothing. " + "A change a browser removes while the edit is in flight is never reported as applied: EDIT_LOST_TO_CONCURRENT_CHANGE means the write was refused and nothing was written, so re-read the document and decide again, as with PRECONDITION_FAILED; lost_ops on a successful result names operations whose text the live document no longer has, because the deletion landed after the version was written. " + `The spec (or any document) holds requirements, design, and decisions - never progress, status, or timestamps. ${OWNER_REFERENCE} ${SPEC_WRITING_POINTER}`,
14243
14243
  arguments: (z2) => ({
14244
14244
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14245
14245
  project: z2.string().describe("Project key owning the document.").optional(),
@@ -14259,7 +14259,7 @@ var dispatchToolSpecs = [
14259
14259
  attributes: z2.unknown().describe("Typed block attributes for retype; a CR LF or a lone carriage return in a string value is written as a line feed.").optional()
14260
14260
  }, { strict: true })).describe("Flat tagged edits; an operation takes only the keys below, a misspelled one is refused rather than ignored, and the server validates the fields its op requires."),
14261
14261
  precondition: z2.object({
14262
- document: z2.string({ min: 1 }).describe("Token for the exact canonical document returned by dispatch_doc_read.").optional(),
14262
+ document: z2.string({ min: 1 }).describe("Token for the exact canonical document returned by dispatch doc-read.").optional(),
14263
14263
  blocks: z2.array(z2.object({
14264
14264
  id: z2.string({ min: 1 }).describe("Stable block id from GET /api/v1/artifacts/{id}/blocks."),
14265
14265
  token: z2.string({ min: 1 }).describe("That block's full-state token, including inline marks.")
@@ -14272,7 +14272,7 @@ var dispatchToolSpecs = [
14272
14272
  {
14273
14273
  name: "dispatch_doc_read",
14274
14274
  example: { issue: "DSP-1" },
14275
- description: "Read a live document or a named document version, or the text of an uploaded file at its latest or named version. " + "Do not use it for issue status, asks, or events; " + "use dispatch_read instead. Supply ref, issue, or project plus artifact; issue plus an omitted artifact reads the primary document. " + "A live read returns its document token for an optional dispatch_doc_edit precondition; use /blocks for per-block tokens. " + "A picture (an uploaded PNG, JPEG, GIF or WebP of at most 3,750,000 bytes, 5 MB once base64-encoded) comes back as an image you see, with its name, type, size and version, every time you ask, including one a read already showed this session; " + "any other file that is not UTF-8 text is described, with the route that serves its bytes. " + OWNER_REFERENCE,
14275
+ description: "Read a live document or a named document version, or the text of an uploaded file at its latest or named version. " + "Do not use it for issue status, asks, or events; " + "use dispatch read instead. Supply ref, issue, or project plus artifact; issue plus an omitted artifact reads the primary document. " + "A live read returns its document token for an optional dispatch doc-edit precondition; use /blocks for per-block tokens. " + "A picture (an uploaded PNG, JPEG, GIF or WebP of at most 3,750,000 bytes, 5 MB once base64-encoded) comes back as an image you see, with its name, type, size and version, every time you ask, including one a read already showed this session; " + "any other file that is not UTF-8 text is described, with the route that serves its bytes. " + OWNER_REFERENCE,
14276
14276
  arguments: (z2) => ({
14277
14277
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14278
14278
  project: z2.string().describe("Project key owning the document.").optional(),
@@ -14285,7 +14285,7 @@ var dispatchToolSpecs = [
14285
14285
  {
14286
14286
  name: "dispatch_request_approval",
14287
14287
  example: { issue: "DSP-1", summary: "A live sync replaces the nightly export." },
14288
- description: "Ask a human to approve a document at its current version. Opens an approval ask (Approve / " + "Request changes) in the human's Inbox whose question names the document and version, " + "followed by the summary; the answer pins a review to that version and arrives as " + "artifact.approved or artifact.changes_requested. An open request follows the document: a " + "later version moves it to that version and leaves it waiting on you, as a human's reply in " + "its thread does. Only the first move since the request was opened or handed back sends an " + "event, and never to the session whose version made it; dispatch_doc_read shows whom it " + "waits on. Once the revision is complete and " + HUMAN_AGREED_TO_DOCUMENT + ", call this again to hand that same Inbox row back. The request carries nothing new. A " + "call while it already waits on the human hands nothing back: the same summary changes " + "nothing, and a different one is refused, since it would rewrite the card the human is " + "reading. Approve and Request changes each close the request, so the next call opens a new " + "one. Call it once per revision, when the revision is complete, never after each edit. An " + "approval goes stale when the document changes after it: request approval again once that " + "revision is complete and " + HUMAN_AGREED_TO_DOCUMENT + ". A new version of a Legion root spec closes its armed design gate until a human approves " + "it. " + "Refused, with nothing sent, while the document holds an open decision block, even when a " + "human asked for approval: the refusal names each block; ask the human to answer or waive " + "it first. " + OWNER_REFERENCE,
14288
+ description: "Ask a human to approve a document at its current version. Opens an approval ask (Approve / " + "Request changes) in the human's Inbox whose question names the document and version, " + "followed by the summary; the answer pins a review to that version and arrives as " + "artifact.approved or artifact.changes_requested. An open request follows the document: a " + "later version moves it to that version and leaves it waiting on you, as a human's reply in " + "its thread does. Only the first move since the request was opened or handed back sends an " + "event, and never to the session whose version made it; dispatch doc-read shows whom it " + "waits on. Once the revision is complete and " + HUMAN_AGREED_TO_DOCUMENT + ", call this again to hand that same Inbox row back. The request carries nothing new. A " + "call while it already waits on the human hands nothing back: the same summary changes " + "nothing, and a different one is refused, since it would rewrite the card the human is " + "reading. Approve and Request changes each close the request, so the next call opens a new " + "one. Call it once per revision, when the revision is complete, never after each edit. An " + "approval goes stale when the document changes after it: request approval again once that " + "revision is complete and " + HUMAN_AGREED_TO_DOCUMENT + ". A new version of a Legion root spec closes its armed design gate until a human approves " + "it. " + "Refused, with nothing sent, while the document holds an open decision block, even when a " + "human asked for approval: the refusal names each block; ask the human to answer or waive " + "it first. " + OWNER_REFERENCE,
14289
14289
  arguments: (z2) => ({
14290
14290
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14291
14291
  project: z2.string().describe("Project key owning the document.").optional(),
@@ -14298,7 +14298,7 @@ var dispatchToolSpecs = [
14298
14298
  name: "dispatch_artifact",
14299
14299
  example: { issue: "DSP-1", name: "design.md", content: `# Design
14300
14300
  ` },
14301
- description: "Attach a local file or inline text as an issue artifact or project document. Do not use it to edit a live document; use " + "dispatch_doc_edit instead. Exactly one of path or content is required; a markdown document is at most 1 MiB and any other file at most 25 MiB. " + "Markdown is also refused with 413 CAP_EXCEEDED when it makes more than 65,536 elements (a block weighs 3, and an empty list item, quote, footnote definition or typed block 3 more for the empty paragraph it holds; a table cell 4, a hard line break 3, an image 3, a piece of inline HTML 2, an autolink 2, a footnote reference 2; inline syntax, marks, lines of text and backslash escapes or character references 1 each), or when a new version would leave the document's stored markdown (what its text reads back as) longer than 1 MiB or making more than 65,536 elements, and longer or heavier than before; shorten it or split it across documents. " + "Markdown holding an ask block whose body breaks its content rule (one or more question paragraphs, then at most one bullet list of options, last) is refused with 400 INVALID_ASK_BLOCK; a new version of a document is held to it only for the asks it writes or changes. " + `${OWNER_REFERENCE}`,
14301
+ description: "Attach a local file or inline text as an issue artifact or project document. Do not use it to edit a live document; use " + "dispatch doc-edit instead. Exactly one of path or content is required; a markdown document is at most 1 MiB and any other file at most 25 MiB. " + "Markdown is also refused with 413 CAP_EXCEEDED when it makes more than 65,536 elements (a block weighs 3, and an empty list item, quote, footnote definition or typed block 3 more for the empty paragraph it holds; a table cell 4, a hard line break 3, an image 3, a piece of inline HTML 2, an autolink 2, a footnote reference 2; inline syntax, marks, lines of text and backslash escapes or character references 1 each), or when a new version would leave the document's stored markdown (what its text reads back as) longer than 1 MiB or making more than 65,536 elements, and longer or heavier than before; shorten it or split it across documents. " + "Markdown holding an ask block whose body breaks its content rule (one or more question paragraphs, then at most one bullet list of options, last) is refused with 400 INVALID_ASK_BLOCK; a new version of a document is held to it only for the asks it writes or changes. " + `${OWNER_REFERENCE}`,
14302
14302
  arguments: (z2) => ({
14303
14303
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14304
14304
  project: z2.string().describe("Project key for an unlinked document.").optional(),
@@ -14318,7 +14318,7 @@ var dispatchToolSpecs = [
14318
14318
  {
14319
14319
  name: "dispatch_read",
14320
14320
  example: { issue: "DSP-1" },
14321
- description: "Read an issue or project-document summary, targeted ask, or targeted comment reply chain, or the conversation " + "a message belongs to. Do not use it for document contents; use dispatch_doc_read instead. Supply ref, issue, " + "or project plus artifact; or message alone, which reads a human's direct message to this session and every " + "reply to it (they belong to no issue). " + "An anchored comment or ask also says where its quote sits, as `Position:`: the block's path from the top, " + "and in a table the row (0 is the header), the cells before the anchored one, and the column's header; " + "`Position: unavailable (<code>)` when Dispatch could not read the document: `DOC_SERVICE_UNAVAILABLE` " + "(try again shortly), `DOC_SCHEMA` (the document needs repair), `DOCUMENT_UNLOADABLE` (the document " + "needs a rebuild) or `INTERNAL`. " + "An issue read carries a `Progress:` line, `tasks 3/7, children 2/5`: its spec's task-list items " + "(`- [ ]` / `- [x]`, nested lists included) and its direct children, each done of total and each only " + "when it has any (`Progress: none` otherwise). " + "Every read ends with `Referenced by:` (what cites or hangs off this node, each with its dispatch:// address, " + "an excerpt, and when) and `Links:` (what it cites), so tracing provenance is one call. " + "The pictures the shown messages, asks and comments embed come back as images you see, newest first, at most 8 " + "and 10 MiB of them per read, each a PNG, JPEG, GIF or WebP of at most 3,750,000 bytes (5 MB of base64); a `Pictures:` section names each " + "one shown, in order, and the rest by reference, for dispatch_doc_read. A picture this session was already shown is named, not sent again " + "(every request carries the session's history, and the provider refuses one over 32 MB); dispatch_doc_read shows it again. " + OWNER_REFERENCE,
14321
+ description: "Read an issue or project-document summary, targeted ask, or targeted comment reply chain, or the conversation " + "a message belongs to. Do not use it for document contents; use dispatch doc-read instead. Supply ref, issue, " + "or project plus artifact; or message alone, which reads a human's direct message to this session and every " + "reply to it (they belong to no issue). " + "An anchored comment or ask also says where its quote sits, as `Position:`: the block's path from the top, " + "and in a table the row (0 is the header), the cells before the anchored one, and the column's header; " + "`Position: unavailable (<code>)` when Dispatch could not read the document: `DOC_SERVICE_UNAVAILABLE` " + "(try again shortly), `DOC_SCHEMA` (the document needs repair), `DOCUMENT_UNLOADABLE` (the document " + "needs a rebuild) or `INTERNAL`. " + "An issue read carries a `Progress:` line, `tasks 3/7, children 2/5`: its spec's task-list items " + "(`- [ ]` / `- [x]`, nested lists included) and its direct children, each done of total and each only " + "when it has any (`Progress: none` otherwise). " + "Every read ends with `Referenced by:` (what cites or hangs off this node, each with its dispatch:// address, " + "an excerpt, and when) and `Links:` (what it cites), so tracing provenance is one call. " + "The pictures the shown messages, asks and comments embed come back as images you see, newest first, at most 8 " + "and 10 MiB of them per read, each a PNG, JPEG, GIF or WebP of at most 3,750,000 bytes (5 MB of base64); a `Pictures:` section names each " + "one shown, in order, and the rest by reference, for dispatch doc-read. A picture this session was already shown is named, not sent again " + "(every request carries the session's history, and the provider refuses one over 32 MB); dispatch doc-read shows it again. " + OWNER_REFERENCE,
14322
14322
  arguments: (z2) => ({
14323
14323
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14324
14324
  project: z2.string().describe("Project key owning the document.").optional(),
@@ -14353,7 +14353,7 @@ var dispatchToolSpecs = [
14353
14353
  {
14354
14354
  name: "dispatch_issues",
14355
14355
  example: { project: "PROJ", limit: 250, offset: 250 },
14356
- description: "List a project's issues for a roadmap or backlog pass: every issue in one project, each carrying " + "its status, priority, parent, labels, open-ask count, progress (its spec's task-list items and " + "its direct children, each done of total, as `tasks 3/7 \xB7 children 2/5`, each only when it has " + "any), and route with whether it reaches anyone, " + "so you can see backlog shape without opening every issue. Optionally filter by status, parent, " + "label, priority, route status, or how recently it changed; priority takes one or more of 0-3 " + "(P0-P3) and null for an issue with no priority, so an owner's P0/P1 audit is priority [0, 1]. " + 'route_status "no_holder" lists every open issue whose route names a role nobody holds or a ' + "session that is not running at the moment of the read, whatever its priority. A restarting " + "session is absent for minutes, so an issue is unowned only when a read ten minutes later agrees. " + "Do not use it to search by keyword or phrase; dispatch_search remains the keyword surface. " + "Dispatch pages the list: limit sets the page size (default " + `${DEFAULT_ISSUE_PAGE_LIMIT}, max ${MAX_ISSUE_PAGE_LIMIT}) and offset selects where it starts ` + "(default 0), and the answer names how many issues match, so repeat with the next offset to " + "walk every matching issue. A walk is exact only while the list does not change: an issue " + "that enters or leaves what the filters match, or whose status or rank changes, between two " + "pages shifts rows across a page boundary, so one issue can come back twice and another never.",
14356
+ description: "List a project's issues for a roadmap or backlog pass: every issue in one project, each carrying " + "its status, priority, parent, labels, open-ask count, progress (its spec's task-list items and " + "its direct children, each done of total, as `tasks 3/7 \xB7 children 2/5`, each only when it has " + "any), and route with whether it reaches anyone, " + "so you can see backlog shape without opening every issue. Optionally filter by status, parent, " + "label, priority, route status, or how recently it changed; priority takes one or more of 0-3 " + "(P0-P3) and null for an issue with no priority, so an owner's P0/P1 audit is priority [0, 1]. " + 'route_status "no_holder" lists every open issue whose route names a role nobody holds or a ' + "session that is not running at the moment of the read, whatever its priority. A restarting " + "session is absent for minutes, so an issue is unowned only when a read ten minutes later agrees. " + "Do not use it to search by keyword or phrase; dispatch search remains the keyword surface. " + "Dispatch pages the list: limit sets the page size (default " + `${DEFAULT_ISSUE_PAGE_LIMIT}, max ${MAX_ISSUE_PAGE_LIMIT}) and offset selects where it starts ` + "(default 0), and the answer names how many issues match, so repeat with the next offset to " + "walk every matching issue. A walk is exact only while the list does not change: an issue " + "that enters or leaves what the filters match, or whose status or rank changes, between two " + "pages shifts rows across a page boundary, so one issue can come back twice and another never.",
14357
14357
  arguments: (z2) => ({
14358
14358
  project: z2.string().describe("Project key to list issues from."),
14359
14359
  status: z2.enum(ISSUE_STATUSES).describe("Optional lifecycle status filter.").optional(),
@@ -14480,6 +14480,13 @@ function textHead(text) {
14480
14480
  return flat.length > HEAD_LENGTH ? `${flat.slice(0, HEAD_LENGTH)}\u2026` : flat;
14481
14481
  }
14482
14482
 
14483
+ // ../envoy-client/src/dispatch-command.ts
14484
+ function commandName(tool) {
14485
+ return tool.replace(/^dispatch_/, "").replaceAll("_", "-");
14486
+ }
14487
+ var toolsByCommand = new Map(dispatchToolSpecs.map((spec) => [commandName(spec.name), spec.name]));
14488
+ var surfaces = new Map;
14489
+
14483
14490
  // ../envoy-client/src/dispatch-cwd.ts
14484
14491
  import { execFile } from "child_process";
14485
14492
  import { promisify } from "util";
@@ -14962,7 +14969,7 @@ var picturesShown = new Map;
14962
14969
 
14963
14970
  // ../envoy-client/src/dispatch-execute.ts
14964
14971
  var triageAdviceShown = new Set;
14965
- var refGrammarProblem = "ref must be a valid dispatch:// reference such as dispatch://KEY-1, " + "dispatch://KEY-1/ask/<uuid>, dispatch://KEY-1/comment/<uuid>, " + "dispatch://KEY-1/message/<uuid>, dispatch://KEY-1/artifact/<slug>, " + "dispatch://PROJECT/artifact/<document-ref> (an artifact id, slug, or filename), or " + "dispatch://agent/<session id>/artifact/<slug> (a picture in a conversation on the Agents page, " + "for dispatch_doc_read); a dashboard URL on this Dispatch server is accepted too";
14972
+ var refGrammarProblem = "ref must be a valid dispatch:// reference such as dispatch://KEY-1, " + "dispatch://KEY-1/ask/<uuid>, dispatch://KEY-1/comment/<uuid>, " + "dispatch://KEY-1/message/<uuid>, dispatch://KEY-1/artifact/<slug>, " + "dispatch://PROJECT/artifact/<document-ref> (an artifact id, slug, or filename), or " + "dispatch://agent/<session id>/artifact/<slug> (a picture in a conversation on the Agents page, " + "for dispatch doc-read); a dashboard URL on this Dispatch server is accepted too";
14966
14973
  var toolSchemas = new Map;
14967
14974
  function artifactByReference(artifacts, artifactReference, owner, canonical) {
14968
14975
  const bySlug = artifacts.find((candidate) => candidate.slug === artifactReference);
@@ -15032,6 +15039,51 @@ function dispatchAnswered(error48, status) {
15032
15039
  return error48 instanceof DispatchServiceError && error48.fromDispatch && error48.status === status;
15033
15040
  }
15034
15041
 
15042
+ // ../envoy-client/src/dispatch-session-state.ts
15043
+ import {
15044
+ appendFileSync,
15045
+ closeSync,
15046
+ fstatSync,
15047
+ mkdirSync,
15048
+ openSync,
15049
+ readdirSync,
15050
+ readFileSync as readFileSync3,
15051
+ readSync,
15052
+ renameSync,
15053
+ rmSync,
15054
+ statSync,
15055
+ writeFileSync
15056
+ } from "fs";
15057
+ import { homedir as homedir2 } from "os";
15058
+ import { join as join2 } from "path";
15059
+ var SESSION_ID = /^[A-Za-z0-9._-]{1,128}$/;
15060
+ var PRIVATE_DIR = 448;
15061
+ var PRIVATE_FILE = 384;
15062
+ var IDLE_SESSION_MS = 14 * 86400000;
15063
+ function sessionStateRoot(env) {
15064
+ if (env.DISPATCH_STATE_DIR)
15065
+ return env.DISPATCH_STATE_DIR;
15066
+ return join2(env.XDG_STATE_HOME || join2(homedir2(), ".local", "state"), "dispatch");
15067
+ }
15068
+ function sessionDirectory(env, sessionId) {
15069
+ if (!SESSION_ID.test(sessionId) || sessionId === "." || sessionId === "..") {
15070
+ throw new Error(`dispatch: the session id ${JSON.stringify(sessionId)} is not 1-128 letters, digits, ".", "_" or "-"`);
15071
+ }
15072
+ return join2(sessionStateRoot(env), "sessions", sessionId);
15073
+ }
15074
+ function ensureDirectory(dir) {
15075
+ mkdirSync(dir, { recursive: true, mode: PRIVATE_DIR });
15076
+ }
15077
+ function writeAtomically(path2, text) {
15078
+ const temporary = `${path2}.${process.pid}.${Date.now()}.tmp`;
15079
+ writeFileSync(temporary, text, { mode: PRIVATE_FILE });
15080
+ renameSync(temporary, path2);
15081
+ }
15082
+ function writeSessionTitle(dir, title) {
15083
+ ensureDirectory(dir);
15084
+ writeAtomically(join2(dir, "title"), title);
15085
+ }
15086
+
15035
15087
  // ../pi-shared/src/interface.ts
15036
15088
  import { logger } from "@oh-my-pi/pi-utils";
15037
15089
  var ENVOY_PLUGIN_INTERFACE_VERSION = 1;
@@ -15110,6 +15162,70 @@ function matchInjectedUserTurn(sessionID, message) {
15110
15162
  return sent.messageId;
15111
15163
  }
15112
15164
 
15165
+ // ../pi-shared/src/shell-command.ts
15166
+ var HEREDOC_OPENING = /^(.*?)[ \t]*<<(-?)[ \t]*'([A-Za-z_][A-Za-z0-9_]*)'[ \t]*$/;
15167
+ var BLANK_LINE = /^[ \t]*$/;
15168
+ var REFUSED_UNQUOTED = "\n;&|$`<>()#\\";
15169
+ var REFUSED_DOUBLE_QUOTED = "$`\\";
15170
+ function dispatchCommandHead(command) {
15171
+ if (typeof command !== "string" || hasControlCharacter(command))
15172
+ return;
15173
+ const all = command.split(`
15174
+ `);
15175
+ const first = all.findIndex((line) => !BLANK_LINE.test(line));
15176
+ if (first === -1)
15177
+ return;
15178
+ const last = all.findLastIndex((line) => !BLANK_LINE.test(line));
15179
+ const lines = all.slice(first, last + 1);
15180
+ let head = lines[0] ?? "";
15181
+ if (lines.length > 1) {
15182
+ const opening = HEREDOC_OPENING.exec(head);
15183
+ if (opening === null)
15184
+ return;
15185
+ const stripTabs = opening[2] === "-";
15186
+ const delimiter = opening[3];
15187
+ const end = lines.findIndex((line, index) => index > 0 && (stripTabs ? line.replace(/^\t+/, "") : line) === delimiter);
15188
+ if (end !== lines.length - 1)
15189
+ return;
15190
+ head = opening[1] ?? "";
15191
+ }
15192
+ head = head.replace(/^[ \t]+|[ \t]+$/g, "");
15193
+ return firstWord(head) === "dispatch" && headScan(head) ? head : undefined;
15194
+ }
15195
+ function firstWord(head) {
15196
+ return head.split(/[ \t]/, 1)[0] ?? "";
15197
+ }
15198
+ function hasControlCharacter(text) {
15199
+ for (let index = 0;index < text.length; index += 1) {
15200
+ const code = text.charCodeAt(index);
15201
+ if (code < 32 && code !== 10 && code !== 9 || code === 127)
15202
+ return true;
15203
+ }
15204
+ return false;
15205
+ }
15206
+ function headScan(head) {
15207
+ let quote;
15208
+ for (const char of head) {
15209
+ if (quote === "'") {
15210
+ if (char === "'")
15211
+ quote = undefined;
15212
+ continue;
15213
+ }
15214
+ if (quote === '"') {
15215
+ if (char === '"')
15216
+ quote = undefined;
15217
+ else if (REFUSED_DOUBLE_QUOTED.includes(char))
15218
+ return false;
15219
+ continue;
15220
+ }
15221
+ if (char === "'" || char === '"')
15222
+ quote = char;
15223
+ else if (REFUSED_UNQUOTED.includes(char))
15224
+ return false;
15225
+ }
15226
+ return quote === undefined;
15227
+ }
15228
+
15113
15229
  // ../pi-shared/src/subagent-session.ts
15114
15230
  import fs from "fs";
15115
15231
  import path2 from "path";
@@ -15193,7 +15309,7 @@ function onEnvoyRoleRegained(listener) {
15193
15309
  // package.json
15194
15310
  var package_default = {
15195
15311
  name: "@sjawhar/pi-legion",
15196
- version: "8.4.2",
15312
+ version: "8.5.1",
15197
15313
  license: "Apache-2.0",
15198
15314
  type: "module",
15199
15315
  omp: {
@@ -15205,7 +15321,7 @@ var package_default = {
15205
15321
  ]
15206
15322
  },
15207
15323
  legion: {
15208
- daemonApiVersion: 16
15324
+ daemonApiVersion: 17
15209
15325
  },
15210
15326
  repository: {
15211
15327
  type: "git",
@@ -15228,7 +15344,7 @@ var package_default = {
15228
15344
  "@legion/contracts": "workspace:*",
15229
15345
  "@legion/envoy-client": "workspace:*",
15230
15346
  "@legion/pi-shared": "workspace:*",
15231
- "@oh-my-pi/pi-utils": "^18.1.15",
15347
+ "@oh-my-pi/pi-utils": "^18.6.0",
15232
15348
  "@types/bun": "latest",
15233
15349
  typescript: "^5.3.0",
15234
15350
  zod: "4.3.6"
@@ -16223,7 +16339,7 @@ function toolSchema(pi) {
16223
16339
  ...HANDOFF_OPERATIONS
16224
16340
  ]),
16225
16341
  issue: z2.string().optional(),
16226
- artifactId: z2.string().describe("register_gate's root spec document: its artifact id, slug, or filename, as the Dispatch tools take it").optional(),
16342
+ artifactId: z2.string().describe("register_gate's root spec document: its artifact id, slug, or filename, as a dispatch command's --artifact takes it").optional(),
16227
16343
  version: z2.number().optional(),
16228
16344
  issues: z2.array(z2.string()).optional(),
16229
16345
  to: z2.enum(LEGION_PHASE_BACKWARD_TARGETS).optional(),
@@ -16386,8 +16502,9 @@ function setLegionBootstrapExitForTests(hook) {
16386
16502
  }
16387
16503
  var GH_RESOLVED_URL = /(?:^|[\s;,"])(?:pr|issue):\/\//i;
16388
16504
  function needsGrant({ toolName, input }) {
16389
- if (toolName === "bash")
16390
- return typeof input.command === "string";
16505
+ if (toolName === "bash") {
16506
+ return typeof input.command === "string" && dispatchCommandHead(input.command) === undefined;
16507
+ }
16391
16508
  if (toolName === "github")
16392
16509
  return true;
16393
16510
  const paths = Array.isArray(input.paths) ? input.paths : [input.path];
@@ -16637,8 +16754,13 @@ function legionExtension(pi) {
16637
16754
  });
16638
16755
  const titleSession = async (context) => {
16639
16756
  const title = legionSessionTitle(classifySession(process.env), process.env.LEGION_PROJECT);
16640
- if (title !== undefined)
16641
- await applySessionTitle(pi, context, title);
16757
+ if (title === undefined)
16758
+ return;
16759
+ await applySessionTitle(pi, context, title);
16760
+ const sessionID = context.sessionManager.getSessionId();
16761
+ if (sessionID === "")
16762
+ return;
16763
+ writeSessionTitle(sessionDirectory(process.env, sessionID), context.sessionManager.getSessionName?.() ?? title);
16642
16764
  };
16643
16765
  pi.on("session_start", async (_event, context) => {
16644
16766
  if (await checkSubagentSession(context))
@@ -16678,7 +16800,7 @@ function legionExtension(pi) {
16678
16800
  toolName: toolCall.toolName
16679
16801
  });
16680
16802
  paneRules ??= PANE_RULES[classifySession(process.env).kind] ?? [];
16681
- const commands = toolCall.toolName === "bash" && typeof toolCall.input.command === "string" ? splitShellCommands(toolCall.input.command) : undefined;
16803
+ const commands = toolCall.toolName === "bash" && typeof toolCall.input.command === "string" ? splitShellCommands(dispatchCommandHead(toolCall.input.command) ?? toolCall.input.command) : undefined;
16682
16804
  const refusal = paneRuleRefusal(toolCall, paneRules, commands);
16683
16805
  if (refusal !== undefined)
16684
16806
  return { block: true, reason: refusal };
@@ -23,7 +23,7 @@ separate coordinator to finish necessary work.
23
23
  product, scope, design, and cross-phase decisions the same way: `envoy_publish` to your own
24
24
  encoded token. You decide whether one needs the human and write its decision block yourself
25
25
  (section 1 says what one does to an approved root spec); a worker never writes one. Any role may
26
- use `dispatch_ask` directly for a standalone to-do only a human can complete, and replies return
26
+ use `dispatch ask` directly for a standalone to-do only a human can complete, and replies return
27
27
  to the asking session.
28
28
  - The daemon starts each role as its own process with the issue's context already in its
29
29
  environment. Never hand-format a role token: the daemon encodes one as
@@ -33,7 +33,7 @@ separate coordinator to finish necessary work.
33
33
  the `roleToken` helper from `@legion/contracts` exactly the way the daemon does.
34
34
  - There is no label vocabulary. Dispatch status replaces the board, and the design gate
35
35
  is a human approving the root spec document at a version in Dispatch, requested with
36
- `dispatch_request_approval` — not a label and not an ask. Never attempt to apply a label.
36
+ `dispatch request-approval` — not a label and not an ask. Never attempt to apply a label.
37
37
  - Deferring necessary work is failure. The sole valid deferral is a new child issue you
38
38
  create and continue to own. Re-file a genuinely independent child through the
39
39
  controller rather than treating it as an abandoned dependency.
@@ -59,35 +59,32 @@ exercise a criterion end to end, building that path is a child issue of this tre
59
59
  - **No children:** choose a single-issue tree only when its acceptance criteria can be
60
60
  completed and integrated as one unit. Otherwise create complete child issues with:
61
61
 
62
- ```text
63
- dispatch_issue({
64
- project: "<project>",
65
- parent: "<root issue>",
66
- title: "<child outcome>",
67
- spec: "<acceptance criteria, scope, and context>"
68
- })
62
+ ```bash
63
+ dispatch issue --project <project> --parent <root issue> --title '<child outcome>' --spec-file - <<'EOF'
64
+ <acceptance criteria, scope, and context>
65
+ EOF
69
66
  ```
70
67
 
71
- `project` is the issue key's prefix before `-<n>` (e.g. `LEGSMOKE-3` → `LEGSMOKE`) — not
68
+ `--project` is the issue key's prefix before `-<n>` (e.g. `LEGSMOKE-3` → `LEGSMOKE`) — not
72
69
  the role-token `<project>` (the daemon's own project, e.g. `acme`), a different string. A
73
70
  root session has `LEGION_TREE == LEGION_ISSUE`. The daemon establishes the sub-issue
74
- relationship from `parent`. Keep the returned issue keys in ordered waves; a child is
75
- inert until released. Do not pass `assignee`: the default keeps the tree's questions in one
71
+ relationship from `--parent`. Keep the returned issue keys in ordered waves; a child is
72
+ inert until released. Do not pass `--assignee`: the default keeps the tree's questions in one
76
73
  Inbox — under the shared token a child inherits its parent's assignee (the human who
77
74
  answers the tree's asks); under a personal token it goes to that token's owner, whom
78
- `dispatch_whoami` names. Set it only when a human told you a specific person owns that child.
75
+ `dispatch whoami` names. Set it only when a human told you a specific person owns that child.
79
76
 
80
77
  Specifications written into Dispatch follow `skill://dispatch`'s [Writing a spec](skill://dispatch/SKILL.md#writing-a-spec).
81
78
  Wave releases, child closures, and your own status are visible from the issue tree and the
82
- handoffs; do not narrate them into the spec or a `dispatch_message`. A to-do only a human can
83
- clear is a `dispatch_ask`.
79
+ handoffs; do not narrate them into the spec or a `dispatch message`. A to-do only a human can
80
+ clear is a `dispatch ask`.
84
81
 
85
82
  The issue's primary document **is** the root specification. Extend it in place: a new version
86
83
  that adds only the evidence each decision needs and what the human decides, each as a
87
84
  [decision block](skill://dispatch/SKILL.md#decision-blocks). The decomposition and its waves, how each
88
85
  outcome is proven, and the integration test are your own calls: they go in the child issues and
89
86
  the planner's `.legion/<issue>/plan.json`, not the root spec. Never post a second "spec" artifact beside
90
- it (`dispatch_artifact` with the primary document's name replaces the human's document; do not do
87
+ it (`dispatch artifact` with the primary document's name replaces the human's document; do not do
91
88
  that).
92
89
  The design gate's approval step runs only when the `Design gate policy` sentence of your
93
90
  `Legion addressing` line says `gates.design: root-issues`. When it says `gates.design: off`, write
@@ -97,47 +94,46 @@ sub-architect on a child issue has no policy line and never runs the gate: the r
97
94
  covers the tree. When the gate is armed, run this exact sequence **before the tree's work starts**:
98
95
 
99
96
  ```text
100
- dispatch_doc_edit({ issue: "<root issue>", ... }) // extend the primary document in place
101
- // then settle its decision blocks (below)
102
- result = dispatch_request_approval({
103
- issue: "<root issue>", // the primary document by default
104
- summary: "<what the human is approving>",
105
- })
97
+ dispatch doc-edit --issue <root issue> --ops-json-file - <<'EOF' # extend the primary document in place
98
+ ...
99
+ EOF
100
+ # then settle its decision blocks (below)
101
+ dispatch request-approval --issue <root issue> --summary '<what the human is approving>'
102
+ # the primary document by default; prints "Approval requested for spec.md (document id <UUID>) at version <N> …"
106
103
  legion({
107
104
  op: "register_gate",
108
105
  issue: "<root issue>",
109
- artifactId: result.details.artifact, // the document id, a UUID such as 4e0aca36-77b3-43bd-96cf-d58890ae64e4
110
- version: result.details.version, // the version number the human is asked to approve
106
+ artifactId: "<UUID>", // the document id from that text, such as 4e0aca36-77b3-43bd-96cf-d58890ae64e4
107
+ version: <N>, // the version number the human is asked to approve
111
108
  })
112
109
  ```
113
110
 
114
111
  The decision blocks come first: settle every one as
115
112
  [Approval of a spec](skill://dispatch/SKILL.md#approval-of-a-spec) says before you request approval;
116
- `dispatch_request_approval` refuses while one is open. Each answer reaches you, since you follow
113
+ `dispatch request-approval` refuses while one is open. Each answer reaches you, since you follow
117
114
  every ask you open. An approval request carries nothing new: request it only once the human has
118
115
  agreed to every point in the spec, so a point they have not agreed to gets its own decision block
119
- first, or comes out of the spec. `summary` says in one to three sentences what the human is
116
+ first, or comes out of the spec. `--summary` says in one to three sentences what the human is
120
117
  approving and nothing else: no commentary and no open question.
121
118
 
122
- `dispatch_request_approval` opens a system question on the document with the fixed options
119
+ `dispatch request-approval` opens a system question on the document with the fixed options
123
120
  `Approve` and `Request changes`; a human answers it from the Inbox or approves from the
124
- document's own header. Never open a `dispatch_ask` with an `Approve` option yourself: an
121
+ document's own header. Never open a `dispatch ask` with an `Approve` option yourself: an
125
122
  ordinary question is not a gate and the daemon ignores its answer. Copy `artifactId` and
126
- `version` from the result of `dispatch_request_approval` — its text reads "Approval requested for
127
- spec.md (document id <UUID>) at version <N> (ask <id>)", followed by the question the human's
128
- Inbox shows, and its `details.artifact` / `details.version` carry the same two values. The
129
- document id is never the slug or file name you passed in (`spec`, `spec.md`): the daemon
123
+ `version` from the text `dispatch request-approval` prints: "Approval requested for spec.md
124
+ (document id <UUID>) at version <N> (ask <id>)", followed by the question the human's Inbox shows.
125
+ The document id is never the slug or file name you passed in (`spec`, `spec.md`): the daemon
130
126
  recognizes the document's approval events by that id and takes no other value; the `legion` tool
131
- looks a slug or file name up in Dispatch and hands the daemon the id. Calling `dispatch_request_approval` again while that request
132
- waits on the human, with the same `summary`, changes nothing and returns it (its text says
133
- it "already waits on the human"), so it is safe to repeat; a different `summary` is refused then.
127
+ looks a slug or file name up in Dispatch and hands the daemon the id. Calling `dispatch request-approval` again while that request
128
+ waits on the human, with the same `--summary`, changes nothing and returns it (its text says
129
+ it "already waits on the human"), so it is safe to repeat; a different `--summary` is refused then.
134
130
  A newer version of the spec moves the open request to that version and leaves it waiting on you,
135
131
  with no wake when the edit was yours: once the human has agreed to every point in it, call again
136
132
  to hand the same request back at the latest version. If its text instead reads "spec.md (document
137
133
  id <UUID>) is already approved at version <N>" — a human approved from the document header before
138
134
  you asked — still call `register_gate` with that id and version: the daemon reads the approval
139
135
  from Dispatch as it registers, opens the gate, and delivers `design-approved` at once. The same
140
- read covers a human who answers the question between your `dispatch_request_approval` and
136
+ read covers a human who answers the question between your `dispatch request-approval` and
141
137
  `register_gate` calls, so an approval is never lost to timing; you never approve anything
142
138
  yourself.
143
139
 
@@ -196,7 +192,7 @@ The daemon moves each released child to `todo` and, while the root's design gate
196
192
  its phases itself from its fixed workflow table, each phase worker as its own process with the
197
193
  child's context already in its environment; it starts no sub-architect, and you start no worker.
198
194
  Park while children are in flight. On each child closure, re-scope open work, take obsolete work
199
- out of the workflow with `park_child` (saying why on the issue with `dispatch_comment`), and
195
+ out of the workflow with `park_child` (saying why on the issue with `dispatch comment`), and
200
196
  release the next wave only when it now makes sense. There is no inter-child
201
197
  dependency mechanism to encode.
202
198
 
@@ -238,12 +234,12 @@ the task.
238
234
  Wait for the implementer to report its durable retro result. Retro output is
239
235
  `docs/solutions/`, its last commit removing the issue's `.legion/<issue>/` from the head a human
240
236
  merges, the PR body content the repository's instructions derive from the pull request's changed
241
- paths at that head, and one `dispatch_message` on the issue; it must write no handoff or
237
+ paths at that head, and one `dispatch message` on the issue; it must write no handoff or
242
238
  rewrite the reviewer-approved head. When the implementer reports that it cannot
243
239
  compute that body content or do the work a line of it affirms, that its read of the body failed or
244
240
  came back empty, or that GitHub refused the body, it has pushed nothing and is still in its phase.
245
241
  Answer it with `envoy_publish` to its role topic, saying how from the deployment instructions;
246
- when they do not say, open a `dispatch_ask` naming the missing instruction.
242
+ when they do not say, open a `dispatch ask` naming the missing instruction.
247
243
 
248
244
  ## 6. Architect sign-off and merge
249
245
 
@@ -276,7 +272,7 @@ The daemon keeps this order from its fixed table; you start none of its steps:
276
272
  It drives the changed path in production through the user's own access path and records
277
273
  what it saw on the pull request and on this issue. Sign off only after the implementer's production
278
274
  report exists. A defect it finds is a corrective child issue of this tree, not a note on a
279
- closed one; if the implementer cannot perform the deploy, it opens a `dispatch_ask` that starts
275
+ closed one; if the implementer cannot perform the deploy, it opens a `dispatch ask` that starts
280
276
  with the production gap and why it matters, then names the required step, its risk, and
281
277
  outcome-named options. The issue waits for that answer.
282
278
 
@@ -299,12 +295,12 @@ This merge happens only when GitHub reports `CONFLICTING`
299
295
  wake — `phase-finished`, `catch-up`, `checks-red`, `review-stuck` — because a `CONFLICTING`
300
296
  PR gets no CI and no wake announces it. The moment you see it, tell the worker holding the issue's
301
297
  phase (`envoy_publish` to its role topic) to move the issue back to `implementing` with
302
- `request_backward_move`; in `awaiting_merge`, where no worker holds a phase, open a `dispatch_ask`
298
+ `request_backward_move`; in `awaiting_merge`, where no worker holds a phase, open a `dispatch ask`
303
299
  naming the conflict for the human who merges. Do not let the merger send a READY packet for an
304
300
  obsolete approval.
305
301
 
306
302
  If a worker reports that `legion threads resolve` exited 1 naming a review thread GitHub refused
307
- to resolve, open a `dispatch_ask` that names the thread's URL and GitHub's message for a human to
303
+ to resolve, open a `dispatch ask` that names the thread's URL and GitHub's message for a human to
308
304
  resolve it by hand, with options for resolved / could not; the merger does not complete while it
309
305
  is open. That is the one review-thread step a human takes: the review App cannot resolve a thread
310
306
  on a pull request the implementer opened, and the implementer's and merger's runs of the command
@@ -323,7 +319,9 @@ After the merge result and the implementer's production report are recorded, pos
323
319
  close this issue with `sign_off`, which writes `done`:
324
320
 
325
321
  ```text
326
- dispatch_comment({ issue: "LEGION-40", body: "<sign-off: scope, integration evidence, review, retro, merge, and the implementer's production report>" })
322
+ dispatch comment --issue LEGION-40 --body-file - <<'EOF'
323
+ <sign-off: scope, integration evidence, review, retro, merge, and the implementer's production report>
324
+ EOF
327
325
  legion({ op: "sign_off", issue: "LEGION-40" })
328
326
  ```
329
327
 
@@ -346,10 +344,10 @@ active phase worker.
346
344
  | `design-approved` | Its `version` is the approved one. A human approved the root spec document at its current version; the gate is open. Proceed to section 2. |
347
345
  | `design-changes-requested` | Its `version` and `reason`. A human asked for changes to the root spec at `version`, for `reason`. Revise the spec as the reason asks and request approval again as section 1 says; stay parked; the gate is closed. |
348
346
  | `phase-finished` | The daemon has already moved the issue to its next phase by its fixed table and started that phase's role; you start nothing. Read the committed handoff for the finishing phase; if it shows unresolved gaps, tell the role now working the issue (`envoy_publish` to its role topic). A `planner` notice that names a departure from the spec's design defers to section 1's full condition: when the approved Summary, Acceptance, scope and settled decisions still hold, the plan is the record; otherwise change the root spec and request approval again as section 1 says. A `reviewer` notice whose GitHub review is `CHANGES_REQUESTED` needs nothing from you: the daemon has returned the issue to `implementing` (Dispatch `in_progress`) and started the **implementer**, whose correction goes through the tester and the reviewer again, never straight to retro. A `reviewer` notice with an `APPROVED` review means the daemon has started retro (step 5). An `implementer` notice for `production_check` is its production report: read the record on the pull request and the issue, then run step 7. A `tester` notice with `verdict: "fail"` — its handoff carries `implementerProof.verdict: "rejected"`, or a failure naming the production-like proof — has gone back to the **implementer** by the daemon's table; never supply the proof from another role. A worker that reports no surface reaches the changed path sends that report instead of completing its phase, so the daemon starts nothing more on that issue and the worker stays idle in its session, not suspended: file a child issue in this tree to build the surface (infrastructure, tooling, or a skill), and when that child's `child-closed` arrives, tell the waiting worker to continue with `envoy_publish` to its role topic. That report is never a reason to advance the phase. |
349
- | `pr-blocked` | Its `reason` names the pull request and the `max_fix_attempts` it reached, or reads `Issue reached review_round_cap=<N>.` (the end of this row). The count is of heads pushed onto a red verdict that changed something outside `.legion/` — handoff-only pushes (`.legion/` paths only) never count, a push by the review App (a planner's, tester's, reviewer's or architect's) never counts, and the head after a red the tester's red tests earned (a review-App push that changed a path outside `.legion/`, however many handoff-only pushes follow it) does not count either — so after the tester's handoff-only push onto the implementer's red, the implementer's next push does count; a push the daemon cannot classify (a listener without `changed_paths`, a list the listener stopped at 100 paths or 32,768 runes of text, a push listing no commits) does. Published once per exhausted count, not on every later red verdict for that count. Read the failed CI evidence and recovery attempts. The notice moves nothing: the issue stays in its phase. In `implementing`, give the implementer the failing checks (`envoy_publish` to its role topic). In any later phase a worker holds, tell that worker (`envoy_publish` to its role topic) to move the issue back to `implementing` with `request_backward_move`, naming the failing checks, and the daemon starts the implementer; or file a corrective child. In `awaiting_merge`, where no worker holds a phase and a backward move is refused, open a `dispatch_ask` naming the failing checks for the human who merges, as section 6 does for a conflict there. Do not treat the blocked PR as final. A `review_round_cap` reason is the implementer's round count instead: every move back to an earlier phase counts one, and the notice comes again with each round past the cap. It moves nothing either, since the round that reached the cap has already started its worker. Tell the rounds apart by what announced each: a tester's fail by its `phase-finished` with `verdict: "fail"`; a request for changes by the reviewer's `phase-finished` over a `CHANGES_REQUESTED` review; the daemon's own move by a `checks-red` notice, whose reason names either a conflict (`the head conflicts with <base>`) or the required checks that failed (`CI is red at <head>`); a worker's `request_backward_move` by a `phase-finished` from the role that asked, whose reason reaches only the task of the worker the move started, so ask that worker. Judge the cap by the rounds that are not conflicts. Conflict returns need nothing from you beyond the next round. Tester fails, requests for changes and CI-red returns (a `checks-red` notice is raised for CI only when a required check fails on code the branch pushed) are the work going in circles: narrow it, re-plan it with the responsible worker, or ask a human with `dispatch_ask`. Judge a worker's backward move by the reason that worker gives. A move you ordered for a conflict found before `awaiting_merge` (section 6) is a conflict return. The merger's move back to `reviewing` because its task names no approved head is not the work going in circles either. A move that sends back code found wrong counts with the tester fails. |
347
+ | `pr-blocked` | Its `reason` names the pull request and the `max_fix_attempts` it reached, or reads `Issue reached review_round_cap=<N>.` (the end of this row). The count is of heads pushed onto a red verdict that changed something outside `.legion/` — handoff-only pushes (`.legion/` paths only) never count, a push by the review App (a planner's, tester's, reviewer's or architect's) never counts, and the head after a red the tester's red tests earned (a review-App push that changed a path outside `.legion/`, however many handoff-only pushes follow it) does not count either — so after the tester's handoff-only push onto the implementer's red, the implementer's next push does count; a push the daemon cannot classify (a listener without `changed_paths`, a list the listener stopped at 100 paths or 32,768 runes of text, a push listing no commits) does. Published once per exhausted count, not on every later red verdict for that count. Read the failed CI evidence and recovery attempts. The notice moves nothing: the issue stays in its phase. In `implementing`, give the implementer the failing checks (`envoy_publish` to its role topic). In any later phase a worker holds, tell that worker (`envoy_publish` to its role topic) to move the issue back to `implementing` with `request_backward_move`, naming the failing checks, and the daemon starts the implementer; or file a corrective child. In `awaiting_merge`, where no worker holds a phase and a backward move is refused, open a `dispatch ask` naming the failing checks for the human who merges, as section 6 does for a conflict there. Do not treat the blocked PR as final. A `review_round_cap` reason is the implementer's round count instead: every move back to an earlier phase counts one, and the notice comes again with each round past the cap. It moves nothing either, since the round that reached the cap has already started its worker. Tell the rounds apart by what announced each: a tester's fail by its `phase-finished` with `verdict: "fail"`; a request for changes by the reviewer's `phase-finished` over a `CHANGES_REQUESTED` review; the daemon's own move by a `checks-red` notice, whose reason names either a conflict (`the head conflicts with <base>`) or the required checks that failed (`CI is red at <head>`); a worker's `request_backward_move` by a `phase-finished` from the role that asked, whose reason reaches only the task of the worker the move started, so ask that worker. Judge the cap by the rounds that are not conflicts. Conflict returns need nothing from you beyond the next round. Tester fails, requests for changes and CI-red returns (a `checks-red` notice is raised for CI only when a required check fails on code the branch pushed) are the work going in circles: narrow it, re-plan it with the responsible worker, or ask a human with `dispatch ask`. Judge a worker's backward move by the reason that worker gives. A move you ordered for a conflict found before `awaiting_merge` (section 6) is a conflict return. The merger's move back to `reviewing` because its task names no approved head is not the work going in circles either. A move that sends back code found wrong counts with the tester fails. |
350
348
  | `pr-merged` | Its `reason` names the pull request. The PR merged, under the repository's rules, before the issue reached `awaiting_merge`. The workflow runs on and asks no one to merge it: the daemon starts the **implementer** on the production check once the issue gets there, and you start nothing. Its `phase-finished` for `production_check` is what brings you to step 7: verify the record on the pull request and this issue first, then sign off naming it with `sign_off`. A merge is not the close. |
351
349
  | `pr-closed-unmerged` | Decide from current scope whether the work is reopened, started over (`park_child` then `rerun_child`, for a child), or ended with a reason. Delegate the repository action to the responsible phase worker and keep ownership. |
352
- | A reply on an ask you follow | Interpret the reply in the issue's design context. Answer in its thread (`dispatch_comment` with `reply_to`; under an open ask whose next move is yours, such as the approval request you must revise or hand back, `reply_to_ask` with `turn: "agent"`, since a default-turn reply hands that request back to the human and a corrected `summary` is then refused), then adjust the plan or relay it via `envoy_publish` to the responsible worker's role token; scope and product decisions remain with you. |
350
+ | A reply on an ask you follow | Interpret the reply in the issue's design context. Answer in its thread (`dispatch comment --reply-to <comment id>`; under an open ask whose next move is yours, such as the approval request you must revise or hand back, `--reply-to-ask <ask id> --turn agent`, since a default-turn reply hands that request back to the human and a corrected `--summary` is then refused), then adjust the plan or relay it via `envoy_publish` to the responsible worker's role token; scope and product decisions remain with you. |
353
351
  | `worker-died` | Its `role` and `phase`. That role's claim failed: its launches or prompts ran out. For a phase worker the daemon holds the issue (phase `held`) and starts nothing more on it. Reassess the work, then decide with `retry_or_escalate`: `retry` when the failure looks agent-specific or transient, `escalate` to hand the held issue to the controller when it looks environmental. |
354
352
  | `held` | Its `role` and `phase` (the phase the issue left), or `phase` with `reason: "escalated"`. Without `reason`, that phase's worker ran out of launches or prompts: the issue is held (phase `held`), the `worker-died` that comes with it is yours to answer with `retry_or_escalate` as that row says, and the controller hears of the hold too. With `reason: "escalated"`, it records your own `escalate`: the controller has the issue now, and you start nothing for it. |
355
353
  | `ready-refused` | Its `version` and `reason`. The merger's READY was refused, and its packet is kept with the completion so no second READY is ever needed. With `version` greater than 0, `reason` reads `READY refused: approve design version <N> before requesting READY.`: the root's spec is already registered at that version, so get it approved (section 1), and the daemon advances the merge and posts the kept packet the moment it is. With `version` 0, `reason` reads `READY refused: no design version is approved; register and approve the tree's spec before requesting READY.`: the root has no spec registered yet, so register it and get it approved (section 1) exactly as above; the daemon releases the kept packet the same way — at registration itself when that opens the gate (`gates.design: off`, or Dispatch already shows the version approved), otherwise the moment a human approves the version you registered. Either way, nothing else is needed once the gate opens. A `reason` starting `READY_PACKET_MISSING` names a READY refused before the daemon kept packets: that READY is void and the issue stays in merging, so start it over (`park_child` then `rerun_child`, for a child) or end it. |
@@ -361,14 +359,14 @@ cross-tree conflict. Report those to the controller with `envoy_publish` to the
361
359
  your `Legion addressing` line names. Handle everything else in the
362
360
  tree. A product, scope, or design decision that needs the human, yours or one a worker escalated,
363
361
  is a decision block you write (section 1 says what one does to the root spec's gate). A standalone
364
- human to-do may use `dispatch_ask`; workers may reach the human directly with it the same way. Do not
362
+ human to-do may use `dispatch ask`; workers may reach the human directly with it the same way. Do not
365
363
  create a wait loop for any wake source.
366
364
 
367
365
  Never yield while waiting on a human. A human is waiting on you only where an open ask sits in
368
- their inbox, so open it before you stop: a decision block in the spec, `dispatch_ask` for a
369
- standalone human to-do, or `dispatch_request_approval` for the spec gate. Otherwise proceed:
366
+ their inbox, so open it before you stop: a decision block in the spec, `dispatch ask` for a
367
+ standalone human to-do, or `dispatch request-approval` for the spec gate. Otherwise proceed:
370
368
  proceeding is the default, and a stop that waits on nobody stalls the tree until someone notices.
371
369
 
372
370
  ## Architecture components
373
371
 
374
- Bootstrap on a root whose project has an architecture source: in the root's first PR (the implement worker pushes it), write `.dispatch/architecture/<id>.md` files for the planned components — front matter `title`, `parent`, `depends_on`, `external`; no `paths` yet. The importer reads the source branch's head, so the sync and the attach below work only once that PR has merged to the source branch: until then leave the root on `inherit` (the attach would answer `400 COMPONENTS_INPUT`, the component does not exist yet). After the merge, `dispatch_architecture_sync({ project })` and attach the root with `dispatch_issue_update({ issue, components: { mode: "explicit", ids: [...] } })`. Children inherit the root's attachment; give a child its own `components` only when it changes a narrower set, and `{ mode: "none", reason }` when it is not architectural work. Attach before decomposing, and require every implementer to change the component file beside the code it describes in the same review.
372
+ Bootstrap on a root whose project has an architecture source: in the root's first PR (the implement worker pushes it), write `.dispatch/architecture/<id>.md` files for the planned components — front matter `title`, `parent`, `depends_on`, `external`; no `paths` yet. The importer reads the source branch's head, so the sync and the attach below work only once that PR has merged to the source branch: until then leave the root on `inherit` (the attach would answer `400 COMPONENTS_INPUT`, the component does not exist yet). After the merge, run `dispatch architecture-sync --project <KEY>` and attach the root with `dispatch issue-update --issue <root> --components-json '{"mode":"explicit","ids":[…]}'`. Children inherit the root's attachment; give a child its own `--components-json` only when it changes a narrower set, and `{"mode":"none","reason":"…"}` when it is not architectural work. Attach before decomposing, and require every implementer to change the component file beside the code it describes in the same review.
@@ -108,9 +108,9 @@ else:
108
108
  as the matching wake below. A parked tree (root phase `done`: it lingers or is closed) needs
109
109
  nothing from you: a failed architect ignores the park and reads `failed` until the tree closes.
110
110
  2. List the project's triage issues handed to Legion with
111
- `dispatch_issues({project, status: "triage", label: "legion", limit: 250, offset: 0})`.
112
- When its first line ends `(showing 1-250 of N)`, read the next page with `offset: 250`, and so
113
- on until you have all N rows. The rows show no parent, so open each row with `dispatch_read`
111
+ `dispatch issues --project <PROJECT> --status triage --label legion --limit 250 --offset 0`.
112
+ When its first line ends `(showing 1-250 of N)`, read the next page with `--offset 250`, and so
113
+ on until you have all N rows. The rows show no parent, so open each row with `dispatch read`
114
114
  and follow `Links:` up through each `child_of` parent (a `child_of` under `Referenced by:` is a
115
115
  child of this issue, not its parent). Leave a child that has an ancestor in `admission.active`
116
116
  or `admission.waiting`: that tree's architect owns it. Triage every other row as a root,
@@ -137,8 +137,8 @@ only when you take it or file it for Legion, so the label means Legion has the i
137
137
  A person's label is their decision: never take it off. A child needs no label: it runs under its
138
138
  tree's architect once its root is admitted. `legion status <KEY> todo` admits a root, or a child
139
139
  outside a live tree (admitted as a root of its own), only while it carries the label, so an issue
140
- you hand over or file for Legion to run carries it first (`labels` in `dispatch_issue_update` or
141
- `dispatch_issue`). Taking the label off a waiting root drops it from the waiting line; taking it
140
+ you hand over or file for Legion to run carries it first (`--label` on `dispatch issue-update` or
141
+ `dispatch issue`). Taking the label off a waiting root drops it from the waiting line; taking it
142
142
  off an admitted tree does not stop it.
143
143
 
144
144
  ## Trees waiting on a root claim (Go daemon)
@@ -147,7 +147,7 @@ A Go root architect whose claim on its root issue was refused starts nothing and
147
147
  its slot, until the claim is free; nothing tells it when a session holder lets go without
148
148
  replying. So every turn rechecks them ([Turn discipline](#turn-discipline)), the daemon's `tick`
149
149
  included, which comes on its interval even with every slot taken: read each root in
150
- `admission.active` whose `issues.<KEY>.phase` is still `admitted` with `dispatch_read`. When its
150
+ `admission.active` whose `issues.<KEY>.phase` is still `admitted` with `dispatch read`. When its
151
151
  `Claimed by:` line is `nobody` or ends `· not running`, tell that tree's architect to claim again
152
152
  with `envoy_publish` to `notifications.role.` followed by its claim token,
153
153
  `issues.<KEY>.architect.locator.claim` in `legion state --json`. A claim that is its architect's
@@ -199,16 +199,16 @@ only labelled roots, so every root it ran since the daemon required the label ca
199
199
  labelled root in `todo` is the daemon's to admit or queue, one in `triage` is yours to triage
200
200
  (step 2 above), and one anywhere else was parked by Legion or by a person. A child you take becomes
201
201
  a root of its own: the daemon admits a labelled `todo` child whose tree Legion does not run as a
202
- new tree. The `dispatch_issues` rows show no labels and no children, so the listing below filters
202
+ new tree. The `dispatch issues` rows show no labels and no children, so the listing below filters
203
203
  neither: both are checked on each candidate (the table's first rows). List the `todo` issues one
204
- priority at a time, `priority: [0]` first, then `[1]`, `[2]`, `[3]`, and `[null]` (no priority)
204
+ priority at a time, `--priority 0` first, then `1`, `2`, `3`, and `none` (no priority)
205
205
  last, keeping the listing's order within each, which is the board's rank:
206
206
 
207
207
  ```text
208
- dispatch_issues({ project: "<PROJECT>", status: "todo", priority: [0], limit: 250, offset: 0 })
208
+ dispatch issues --project <PROJECT> --status todo --priority 0 --limit 250 --offset 0
209
209
  ```
210
210
 
211
- When the first line ends `(showing 1-250 of N)`, the next page is `offset: 250`, then `500`. Read
211
+ When the first line ends `(showing 1-250 of N)`, the next page is `--offset 250`, then `500`. Read
212
212
  pages only as far as you need: stop listing once the free slots are filled. `<PROJECT>` is the
213
213
  Dispatch project key, the prefix of this deployment's issue keys (`PROJ-12` → `PROJ`), which is
214
214
  also `daemon.project` in `legion state --json`: the project key exactly as `legion.yaml` writes it.
@@ -217,14 +217,14 @@ the row shows one, comes after the claim) is claimed, as the table below says: s
217
217
  reading it.
218
218
 
219
219
  **Walk.** Take the remaining rows in that order until the free slots are filled. Read each one with
220
- `dispatch_read({ issue: "<KEY>" })` and skip it when any of these holds. `dispatch_read` shows only
220
+ `dispatch read --issue <KEY>` and skip it when any of these holds. `dispatch read` shows only
221
221
  the issue's last 10 events, so the rows that read `Events:` are best-effort: the pull-request row
222
222
  leans on `External links:`, and the label row is the one that never depends on history.
223
223
 
224
224
  | Skip when | How you check it |
225
225
  |---|---|
226
226
  | It carries the `legion` label | `Labels:` lists `legion`, in any case. Legion has the issue or had it, as **Candidates** above says. |
227
- | It has any child | `dispatch_read({ ref: "dispatch://<KEY>/children" })` lists any child, open or `done`. It is an umbrella, and a finished umbrella is still no leaf. That also skips an issue whose only child is done, which is accepted. Its open children are candidates themselves, each in its own place in the order. |
227
+ | It has any child | `dispatch read --ref dispatch://<KEY>/children` lists any child, open or `done`. It is an umbrella, and a finished umbrella is still no leaf. That also skips an issue whose only child is done, which is accepted. Its open children are candidates themselves, each in its own place in the order. |
228
228
  | An ancestor is Legion's | Follow `Links:` up through each `child_of` parent, reading each one, and skip when any ancestor carries the `legion` label or is recorded under `issues` in `legion state --json`, whatever its status: a Legion tree, running or parked, owns its children. An ancestor's claim or route does not skip the issue: a coordinator holding an umbrella files `todo` leaves for others to pick up, and the claim on the issue itself is what keeps two sessions off the same work. A `child_of` under `Referenced by:` is a child of this issue, not its parent. |
229
229
  | Someone is designing it | `Open asks:` lists any ask, a `Spec approval: awaiting …` line shows the spec waits on a human, or `Events:` show an `artifact.version` or an `ask.opened` from the last seven days: a session or a person is shaping it even when nobody claims or routes it. |
230
230
  | Legion ran it without the label now on it | `legion state --json` records it under `issues`, whatever its status, or `Events:` show a status write by `session legion-daemon:<PROJECT>`, the daemon's actor on every `legion status` (yours included) and on its own `in_progress` at admission. That covers a root a person took the label off, and one that ran before the daemon required the label and never had it. Name each one you skip for this in your summary. The walk never sends a root Legion already ran back into Legion: a person does that with the label and `todo`, and you do it only when a wake below says to (`worker-died`). |
@@ -232,15 +232,15 @@ leans on `External links:`, and the label row is the one that never depends on h
232
232
  | Its route reaches a running session | `Route:` names a route with nothing after it, or with `(held by …)`. `(nobody holds it right now)` and `(that session is not running right now)` reach nobody; `(the Envoy listener did not answer, …)` counts as reaching someone. `Route: none` is free. |
233
233
  | A pull request is linked or named | `External links:` lists a pull request (kind `github_pr`, or a URL ending `/pull/<n>`), or a comment or message among `Events:` names one. You cannot read GitHub, so an open, merged, or closed pull request all count. A person who wants Legion on it anyway hands it over themselves: the label, then `todo`. |
234
234
  | Its assignee is working it | `Assignee:` names a person who holds the claim (the row above), or whose own comment or message among `Events:` says they are working on it. The assignee alone is who answers the issue's questions, not who works it. |
235
- | It is outside this deployment's scope | Read the scope the deployment instructions state against the title and, when the title does not settle it, the spec (`dispatch_doc_read({ issue: "<KEY>" })`). When in doubt, skip it. With no scope stated, every issue of the project is in scope. |
235
+ | It is outside this deployment's scope | Read the scope the deployment instructions state against the title and, when the title does not settle it, the spec (`dispatch doc-read --issue <KEY>`). When in doubt, skip it. With no scope stated, every issue of the project is in scope. |
236
236
 
237
237
  **Take.** For each candidate that passes, in order:
238
238
 
239
- 1. Add the label and keep the labels it has, which `Labels:` lists (`none` is no labels). `labels`
240
- replaces the whole set, so a label you leave out is removed.
239
+ 1. Add the label and keep the labels it has, which `Labels:` lists (`none` is no labels). The
240
+ `--label` flags replace the whole set, so a label you leave out is removed.
241
241
 
242
242
  ```text
243
- dispatch_issue_update({ issue: "<KEY>", labels: ["<each current label>", "legion"] })
243
+ dispatch issue-update --issue <KEY> --label <each current label> --label legion
244
244
  ```
245
245
 
246
246
  2. It is already in `todo`, so the label admits it: the daemon records it and gives it the free
@@ -250,7 +250,9 @@ leans on `External links:`, and the label row is the one that never depends on h
250
250
  step 4 gives (which follows the design gate policy):
251
251
 
252
252
  ```text
253
- dispatch_comment({ issue: "<KEY>", body: "Legion took this issue: it was the highest-priority open issue nobody else was working on. Assigned to <login>, who will get this tree's questions and its design approval. To stop Legion, move the issue to backlog. To keep Legion off it for good, also take the legion label off; taking the label off alone does not stop a tree that has started." })
253
+ dispatch comment --issue <KEY> --body-file - <<'EOF'
254
+ Legion took this issue: it was the highest-priority open issue nobody else was working on. Assigned to <login>, who will get this tree's questions and its design approval. To stop Legion, move the issue to backlog. To keep Legion off it for good, also take the legion label off; taking the label off alone does not stop a tree that has started.
255
+ EOF
254
256
  ```
255
257
 
256
258
  The daemon admits each root when Dispatch's event reaches it; the next `legion state --json`
@@ -264,28 +266,32 @@ change and why, and what is running.
264
266
 
265
267
  **Where.** On the issue the deployment instructions name for Legion's reports. With none named,
266
268
  on the project's `Legion daily report` issue:
267
- `dispatch_search({ query: "\"Legion daily report\"", project: "<PROJECT>" })` finds it. When it
269
+ `dispatch search --query '"Legion daily report"' --project <PROJECT>` finds it. When it
268
270
  does not exist, create it once and park it in `icebox`, so nobody takes it as work; it never
269
271
  carries the `legion` label:
270
272
 
271
273
  ```text
272
- dispatch_issue({ project: "<PROJECT>", title: "Legion daily report", spec: "## Summary\n\nLegion's controller posts one message here each day: what Legion finished, what it closed without a change and why, and what is running. This issue is not work, so it carries no `legion` label and stays in icebox." })
274
+ dispatch issue --project <PROJECT> --title 'Legion daily report' --spec-file - <<'EOF'
275
+ ## Summary
276
+
277
+ Legion's controller posts one message here each day: what Legion finished, what it closed without a change and why, and what is running. This issue is not work, so it carries no `legion` label and stays in icebox.
278
+ EOF
273
279
  legion status <report KEY> icebox
274
280
  ```
275
281
 
276
282
  **When.** On your first turn of each UTC day, whatever it is: your start, or a wake of any kind.
277
283
  While you are registered, the daemon's `tick` gives you a turn at least every
278
284
  `controller_wake_interval_seconds`. After the turn's own work, read the report issue with
279
- `dispatch_read`, and post when its `Events:` show no `message.created` from today.
285
+ `dispatch read`, and post when its `Events:` show no `message.created` from today.
280
286
 
281
- **What.** One `dispatch_message({ issue: "<report KEY>", body })` of at most 2,000 characters,
287
+ **What.** One `dispatch message --issue <report KEY> --body-file -` of at most 2,000 characters,
282
288
  written as `skill://dispatch`'s "Writing for the human" says: every issue by its key and title,
283
289
  every pull request by its URL.
284
290
 
285
- - **Finished.** `dispatch_issues({ project: "<PROJECT>", status: "done", updated_since: "<the
286
- previous report's time, or 24 hours ago>", limit: 250 })`, read every page, and keep the issues
291
+ - **Finished.** `dispatch issues --project <PROJECT> --status done --updated-since <the
292
+ previous report's time, or 24 hours ago> --limit 250`, read every page, and keep the issues
287
293
  `legion state --json` records under `issues` whose `issue.closed` event is after the previous
288
- report. For each, `dispatch_read` it: the pull request under `External links:` is the one that
294
+ report. For each, `dispatch read` it: the pull request under `External links:` is the one that
289
295
  merged.
290
296
  - **Closed without a change.** Those with no pull request, each with the reason its closing
291
297
  message gave (the `message.created` just before `issue.closed` among `Events:`).
@@ -326,7 +332,7 @@ priority first, then board rank ([Keeping the slots full](#keeping-the-slots-ful
326
332
  relevant Dispatch issue. A stale or duplicate wake may cost a read, never a wrong action.
327
333
  - **Controller state is disposable.** Do not reconstruct or preserve local controller
328
334
  bookkeeping between turns.
329
- - **Write for a human.** Every `dispatch_comment`, `dispatch_message`, and `dispatch_ask` you
335
+ - **Write for a human.** Every `dispatch comment`, `dispatch message`, and `dispatch ask` you
330
336
  post follows `skill://dispatch`'s "Writing for the human" rules: plain sentences, every
331
337
  identifier expanded on first use, no coined shorthand. A triage note that reads like a log
332
338
  line is not a triage note.
@@ -339,7 +345,7 @@ priority first, then board rank ([Keeping the slots full](#keeping-the-slots-ful
339
345
  | `slot-free on <KEY>` from the Go daemon (payload `{kind: "slot-free"}`) | the root whose slot the daemon released with no waiting root to take it | Verify a free slot in `legion state --json`, then fill it ([Keeping the slots full](#keeping-the-slots-full-go-daemon)) |
340
346
  | `todo on <KEY>` from the Go daemon (payload `{kind: "todo"}`) | an issue not handed to Legion that changed while in `todo` and a slot stood free, sent half a minute later | Verify a free slot, then walk the whole `todo` list ([Keeping the slots full](#keeping-the-slots-full-go-daemon)) |
341
347
  | `tick on <PROJECT>` from the Go daemon (payload `{kind: "tick", openCapabilities?: [<name>, …]}`) | the project key; the daemon's periodic wake, whatever the slots; `openCapabilities` names the deployment capabilities with no decision, each with its detail and `capabilities.decided.<name>` line under `capabilities` in `legion state --json` | Recheck the trees waiting on a claim, then walk if a slot is free — a gap never stops the walk; post the day's report if this is the day's first turn, naming each open capability in it (the operator closes or decides it) |
342
- | Architect escalation (controller-actionable only: re-file a child as a root issue, capacity, cross-tree conflicts) | request + context | Judge and act; the owning architect writes an issue-design decision as a decision block and opens `dispatch_ask` only for a human to-do |
348
+ | Architect escalation (controller-actionable only: re-file a child as a root issue, capacity, cross-tree conflicts) | request + context | Judge and act; the owning architect writes an issue-design decision as a decision block and opens `dispatch ask` only for a human to-do |
343
349
  | Mention | Slack/GitHub PR @mention text | Answer, or route to the owning issue's architect role |
344
350
  | `held on <KEY>` from the Go daemon (payload `{kind: "held", phase, role?, reason?}`) | the held issue, the phase it left, and the role whose claim failed, or `reason: "escalated"` | Verify the hold in `legion state` (the issue's phase is `held`). Without `reason`, a phase worker's launches or prompts ran out and the tree's architect decides retry or escalate: no action. With `reason: "escalated"` (on the record, `issues.<KEY>.holdReason` is `escalated`), the architect sent it to you: handle it as an architect escalation below. Parking the tree is `legion status <root> backlog`; setting the root back to `todo` later re-admits it as a new generation, which starts again from its architect |
345
351
  | `worker-died on <KEY>` from the Go daemon with `role: "architect"` | the tree root whose architect's claim failed, and the phase the root was in | The tree's architect ran out of launches or prompts and the daemon relaunches nothing; every other notice of the tree goes to that architect, so nobody inside the tree can act. Verify in `legion state` (`issues.<KEY>.architect.state` is `failed`); if the root's phase is `done`, the tree is already parked: no action. Otherwise re-admit the tree (`legion status <root> backlog`, then `todo`: a new generation, whose architect starts again with fresh budgets) or leave it parked and say why on the issue |
@@ -347,7 +353,7 @@ priority first, then board rank ([Keeping the slots full](#keeping-the-slots-ful
347
353
 
348
354
  ## New issue triage
349
355
 
350
- 1. Read `legion state --json`, then inspect the reported Dispatch issue with `dispatch_read`.
356
+ 1. Read `legion state --json`, then inspect the reported Dispatch issue with `dispatch read`.
351
357
  Verify the issue is in this project, is eligible for a root process, and whether it
352
358
  has pre-existing children. Dispatch and daemon state, not the wake text, decide triage.
353
359
  Note the `Assignee:` line: that human answers the tree's asks, and their Inbox opens on
@@ -371,7 +377,7 @@ priority first, then board rank ([Keeping the slots full](#keeping-the-slots-ful
371
377
  place. A root never waits for capacity in `backlog`: in `todo` the
372
378
  daemon's admission queue holds it (`admission.waiting`), so park only a root that should not
373
379
  run now. Do not triage a system-created child as a root issue.
374
- 4. When you post a triage note (a `dispatch_comment` on the issue saying what you decided and
380
+ 4. When you post a triage note (a `dispatch comment` on the issue saying what you decided and
375
381
  why), name who will be asked with the assignee sentence: `Assigned to <login>, who will get
376
382
  this tree's questions and its design approval`, or, when the `Assignee:` line says
377
383
  `unassigned`, `Unassigned — nobody's Inbox shows this tree's questions or its design approval
@@ -396,14 +402,14 @@ or when a wake tells you to re-admit it (`worker-died`).
396
402
 
397
403
  Only decide controller-actionable escalations: re-filing independent work, capacity, and
398
404
  cross-tree conflicts. The owning architect writes an issue-design decision as a decision block and
399
- uses `dispatch_ask` only for a human to-do, not the controller.
405
+ uses `dispatch ask` only for a human to-do, not the controller.
400
406
 
401
407
  For an independence judgment, verify the child and its parent against current daemon state
402
408
  and the Dispatch issue. If the work belongs in an independent root:
403
409
 
404
- 1. File a **fresh root issue** with `dispatch_issue({ project, title, spec })` (no `parent`;
405
- add `labels: ["legion"]`, without which it is never admitted).
406
- `project` is the issue key's prefix before `-<n>` (e.g. `LEGSMOKE-3` → `LEGSMOKE`): the
410
+ 1. File a **fresh root issue** with `dispatch issue --project <KEY> --title '<title>' --spec-file -`
411
+ (no `--parent`; add `--label legion`, without which it is never admitted).
412
+ `--project` is the issue key's prefix before `-<n>` (e.g. `LEGSMOKE-3` → `LEGSMOKE`): the
407
413
  project key exactly as `legion.yaml` writes it, which `legion state --json` shows as
408
414
  `daemon.project`. It is not the lowercase project token in role names such as
409
415
  `legion-<project>-controller`.
@@ -36,7 +36,7 @@ retrospective's durable output.
36
36
  and records it on the PR and the issue: the agent that developed it is responsible for testing
37
37
  in production. The architect's sign-off waits for that record.
38
38
  The record is the pull request's `Production:` line, one pull-request comment, and a
39
- `dispatch_message` on the issue, each naming what was driven, how, what was observed, and the
39
+ `dispatch message` on the issue, each naming what was driven, how, what was observed, and the
40
40
  merge commit. A defect the production check finds becomes a corrective child issue of the same tree,
41
41
  owned by the architect and implemented by the same implementer; the parent stays open until it lands.
42
42
 
@@ -157,16 +157,15 @@ Then push both commits with one `legion push`, so one CI run covers them. When t
157
157
  refused after step 4 wrote the body, write `<dir>/before.md` back the same way before you report
158
158
  the refusal to the architect, so the body matches the head GitHub has. After the push, post one
159
159
  Dispatch message on
160
- the issue — `issue` is your `LEGION_ISSUE`; Legion issues live on Dispatch, never on a GitHub
160
+ the issue — `--issue` is your `LEGION_ISSUE`; Legion issues live on Dispatch, never on a GitHub
161
161
  issue, and the `gh` shim refuses every GitHub-issue write — naming the documents, the
162
162
  one-to-three most useful takeaways, the two proofs you read, and the production check that
163
163
  follows the merge. The message must carry this revived implementer's structured
164
164
  attribution footer with `phase` set to `retro`; the body is capped at 2,000 characters:
165
165
 
166
- ```ts
167
- dispatch_message({
168
- issue: "<KEY>",
169
- body: `## Retro Complete
166
+ ```bash
167
+ dispatch message --issue <KEY> --body-file - <<'EOF'
168
+ ## Retro Complete
170
169
 
171
170
  **Learnings documented in:**
172
171
  - docs/solutions/<path>.md
@@ -178,8 +177,8 @@ dispatch_message({
178
177
 
179
178
  **Production check:** <what the implementer will drive after the merge, or the deploy/restart step a human will have to perform first>
180
179
 
181
- <!-- legion: {"session":"<session-id>","phase":"retro"} -->`,
182
- })
180
+ <!-- legion: {"session":"<session-id>","phase":"retro"} -->
181
+ EOF
183
182
  ```
184
183
 
185
184
  The `docs/solutions/` commit, the removal of `.legion/<issue>/`, the PR body lines those commits
@@ -59,14 +59,18 @@ only on this phase's artifact.
59
59
 
60
60
  You never start another Legion role: the daemon starts every phase worker itself, from its fixed
61
61
  workflow table. You may still use ordinary `task` subagents for your own phase work; none of them
62
- is a Legion role.
62
+ is a Legion role. A subagent runs in your pane as you, with every host tool but the `legion` tool
63
+ (it is never offered one), so a `legion` command from its bash, a completion included, is yours at
64
+ the daemon: give a subagent no completion step. Today such a command runs on the per-call grant
65
+ your own last credentialed call wrote, within its 60 seconds (*Four facts about `gh`*, below);
66
+ LEGION-631 (dispatch://LEGION-631) replaces that grant with the role's mounted token file.
63
67
  Escalate a product, scope, design, cross-phase, or lifecycle decision to the owning architect with
64
68
  `envoy_publish` to its role topic (`notifications.role.` followed by its encoded token, see
65
69
  above), carrying the verified facts and the decision needed. A `write` to `agent://` only reaches
66
70
  agents inside your own process, not the architect's separate one. Never write a decision block
67
71
  into a spec yourself: the architect decides whether the human must answer it and writes the block,
68
72
  since a new version of an approved root spec closes the tree's design gate. A standalone to-do
69
- only a human can do is a `dispatch_ask`, and its replies return to your own session.
73
+ only a human can do is a `dispatch ask`, and its replies return to your own session.
70
74
 
71
75
  Because the same agent works its role until the issue closes, you may receive more than one
72
76
  assignment across your lifetime: your session stays live after your phase ends, and when a later
@@ -228,7 +232,7 @@ subcommand's `comment`, `create`, `edit`, `close`, `reopen`, `delete`, `pin`, `u
228
232
  `lock`, `unlock`, and `develop`, and any raw `gh api` call to an `/issues` path whose method is not
229
233
  GET (an explicit `-X`, or the POST that `-f`/`-F`/`--input` imply; pull-request conversation
230
234
  comments live on that path too, so edit them with `gh pr comment`) — printing
231
- `Legion issues live on Dispatch; use dispatch_message or dispatch_comment on <your LEGION_ISSUE>`:
235
+ ``Legion issues live on Dispatch; use `dispatch message` or `dispatch comment` on <your LEGION_ISSUE>``:
232
236
  Legion never reads or writes a GitHub issue. `pr comment`, `pr review`,
233
237
  `api …/pulls/…`, `api graphql`, and issue reads are unaffected. The credential reaches `legion`
234
238
  through the file `$LEGION_GRANT_FILE` names, written by the extension before each of your bash
@@ -422,17 +426,18 @@ check write no `.legion/<issue>/<phase>.json`, commit no handoff, and report wit
422
426
 
423
427
  ## Completion: report to the architect, then stay
424
428
 
425
- Report completion to the architect: call the `legion` tool with `op: "handoff_complete"` and
426
- `summary`: two sentences for the architect. Report it through that tool call, never the shell
427
- command `legion handoff complete`: the tool call is what the extension's phase stall records, and
428
- a turn that ends with the phase still open gets one reminder. A completion run from the shell is
429
- recorded by the daemon all the same, but the stall does not see it and sends its one reminder; do
430
- not complete again on that reminder — the first completion stands, and a second is refused
431
- because the issue has already left your phase.
432
-
433
- This publishes your phase's completion to the architect's role and clears the daemon's
434
- record of this issue's active phase. Do not add pipeline labels, run a controller loop, or
435
- invent a different completion protocol — this is the whole contract.
429
+ Report completion to the architect with the `legion` tool's `handoff_complete` and a two-sentence
430
+ `summary`. This publishes your phase's completion to the architect's role and clears the daemon's
431
+ record of this issue's active phase. A shell `legion handoff complete`, should one run, completes
432
+ the phase at the daemon just as well and is not refused, but only the tool call tells the
433
+ extension's phase stall: after a shell completion the reminder arrives when the turn settles and
434
+ again after each later Envoy delivery's turn, until a tool `handoff_complete` next succeeds (in
435
+ practice the next phase's). A completion made on that reminder is refused and closes nothing —
436
+ `HANDOFF_NOT_CURRENT_PHASE` once the phase has moved, `HANDOFF_ALREADY_RECORDED` where a
437
+ completion moves nothing (the implementer before its pull request exists; the production check) —
438
+ so answer the reminder with a WAITING line saying the phase was completed from the shell. Do not
439
+ add pipeline labels, run a controller loop, or invent a different completion protocol — this is the
440
+ whole contract.
436
441
 
437
442
  A reviewer's phase ends with its completion, not with its review; the order of a review round
438
443
  (the handoff push; for an approval, CI settled green at that head; the review of that head; then
@@ -488,6 +493,6 @@ decision required.
488
493
 
489
494
  Never yield while blocked on a decision someone else owns. Before you stop, make the block visible
490
495
  where its owner will see it: a product, scope, design, lifecycle, or cross-phase decision goes to
491
- the owning architect as above, and a standalone human to-do goes in `dispatch_ask`. Otherwise
496
+ the owning architect as above, and a standalone human to-do goes in `dispatch ask`. Otherwise
492
497
  proceed: proceeding is the default, and a phase that stops silently holds its issue until someone
493
498
  notices.
@@ -96,7 +96,7 @@ review posted without a completion leaves the issue in reviewing until you finis
96
96
  `; ` on the one READY line, or `none` — quoted from the `## For the reviewer` block at that same
97
97
  head (or one line saying the body carries no brief — the packet still goes out), then the
98
98
  `--summary` output and the PR body's gate facts. The merger sends it as the `summary` of its
99
- `handoff_complete` with `ready: true`; the daemon posts it as a `dispatch_message` on the issue,
99
+ `handoff_complete` with `ready: true`; the daemon posts it as a `dispatch message` on the issue,
100
100
  publishes it to the project's merge queue role when one is set, and says on the issue when that
101
101
  role has no live holder. The READY packet names both the implementer's and tester's `E2E` lines;
102
102
  a missing one is reported to the architect instead of completing. Legion never merges.
@@ -111,10 +111,10 @@ review posted without a completion leaves the issue in reviewing until you finis
111
111
  this, since a staging gate does not run every resource production does. If the slot fails on
112
112
  the change, the implementer owns the fix and the next slot.
113
113
  The record has three places: the PR body's `Production:` line, one pull-request comment
114
- carrying the Legion footer, and a `dispatch_message` on the issue — the reviewer and merger
114
+ carrying the Legion footer, and a `dispatch message` on the issue — the reviewer and merger
115
115
  read GitHub, the architect reads the issue. When the deploy that carries the merge has not
116
116
  happened (a shared profile still holding the previous plugin release, a daemon still running
117
- the previous commit, a slot nobody has run), open a `dispatch_ask` that starts with the
117
+ the previous commit, a slot nobody has run), open a `dispatch ask` that starts with the
118
118
  production gap and why it matters, then names the required install or restart step, its risk,
119
119
  and outcome-named options. Keep the `Production:` line at `pending <what is missing>`, and
120
120
  complete the check once the human answers. Never record a staging pass as the production check,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-legion",
3
- "version": "8.4.2",
3
+ "version": "8.5.1",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "omp": {
@@ -12,7 +12,7 @@
12
12
  ]
13
13
  },
14
14
  "legion": {
15
- "daemonApiVersion": 16
15
+ "daemonApiVersion": 17
16
16
  },
17
17
  "repository": {
18
18
  "type": "git",
@@ -35,7 +35,7 @@
35
35
  "@legion/contracts": "0.10.0",
36
36
  "@legion/envoy-client": "0.1.0",
37
37
  "@legion/pi-shared": "0.1.0",
38
- "@oh-my-pi/pi-utils": "^18.1.15",
38
+ "@oh-my-pi/pi-utils": "^18.6.0",
39
39
  "@types/bun": "latest",
40
40
  "typescript": "^5.3.0",
41
41
  "zod": "4.3.6"