@sjawhar/pi-legion 11.0.0 → 11.2.0

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.
Files changed (32) hide show
  1. package/README.md +5 -4
  2. package/agents/plan-gap-analyst.md +24 -16
  3. package/agents/plan-reviewer.md +59 -16
  4. package/dist/legion.js +179 -61
  5. package/dist/skills/leading-a-work-stream/SKILL.md +96 -0
  6. package/dist/skills/leading-a-work-stream/references/daily-plan.md +8 -0
  7. package/dist/skills/leading-a-work-stream/references/decision-rights.md +6 -0
  8. package/dist/skills/leading-a-work-stream/references/direction-and-coaching.md +13 -0
  9. package/dist/skills/leading-a-work-stream/references/dispatch-upkeep.md +7 -0
  10. package/dist/skills/leading-a-work-stream/references/sources.md +9 -0
  11. package/dist/skills/leading-a-work-stream/references/speed-tick.md +44 -0
  12. package/dist/skills/leading-a-work-stream/references/user-quotes.md +7 -0
  13. package/dist/skills/legion-architect/SKILL.md +11 -8
  14. package/dist/skills/legion-architect/references/acceptance.md +18 -5
  15. package/dist/skills/legion-architect/references/decomposition.md +3 -2
  16. package/dist/skills/legion-architect/references/design-gate.md +42 -19
  17. package/dist/skills/legion-architect/references/wake-routing.md +4 -4
  18. package/dist/skills/legion-controller/SKILL.md +31 -17
  19. package/dist/skills/legion-controller/references/admission.md +5 -3
  20. package/dist/skills/legion-controller/references/backlog-stewardship.md +17 -2
  21. package/dist/skills/legion-controller/references/daily-report.md +4 -4
  22. package/dist/skills/legion-controller/references/finding-asks.md +111 -14
  23. package/dist/skills/legion-controller/references/slack-mentions.md +2 -2
  24. package/dist/skills/legion-controller/references/triage.md +9 -5
  25. package/dist/skills/legion-controller/references/wakes.md +22 -8
  26. package/dist/skills/legion-worker/SKILL.md +7 -6
  27. package/dist/skills/legion-worker/references/conflicts-and-rewrites.md +23 -16
  28. package/dist/skills/legion-worker/references/findings.md +126 -31
  29. package/dist/skills/legion-worker/references/merge-gate.md +57 -25
  30. package/dist/skills/legion-worker/references/pr-body.md +9 -5
  31. package/dist/skills/legion-worker/references/skip-checks-trailer.md +1 -1
  32. package/package.json +2 -2
package/dist/legion.js CHANGED
@@ -14179,6 +14179,7 @@ var askEventPayloadFields = {
14179
14179
  text: exports_external.string().nullish(),
14180
14180
  at: exports_external.string().optional()
14181
14181
  }).nullish(),
14182
+ restored: exports_external.boolean().optional(),
14182
14183
  anchor: exports_external.object({
14183
14184
  quote: exports_external.string().optional(),
14184
14185
  mark_id: exports_external.string().optional(),
@@ -14345,6 +14346,8 @@ var AGENT_STREAM_LIMITS = {
14345
14346
  commandNameChars: 100,
14346
14347
  commandDescriptionChars: 200
14347
14348
  };
14349
+ // ../contracts/src/dispatch-block-ref.ts
14350
+ var encoder = new TextEncoder;
14348
14351
  // ../contracts/src/dispatch-text-rules.ts
14349
14352
  var SESSION_ID_PATTERN = "[^/?#\\s\\p{Z}\\p{Cc}<>\"'`\\[\\]|]+";
14350
14353
  var sessionIdOnly = new RegExp(`^${SESSION_ID_PATTERN}$`, "u");
@@ -14433,7 +14436,7 @@ function componentsArgument(z2) {
14433
14436
  reason: z2.string({ min: 1 }).describe("For mode none: why this issue is not architectural (process, hiring, ops).").optional()
14434
14437
  }).describe("Attach the issue to architecture components. Attach the root before decomposing it; children inherit unless they choose.");
14435
14438
  }
14436
- var SPEC_WRITING_POINTER = 'Write a spec as the "Writing a spec" section of skill://dispatch says.';
14439
+ var SPEC_WRITING_POINTER = "Write a spec as skill://dispatch-writing-specs says.";
14437
14440
  function imagesArgument(z2, owner, cap) {
14438
14441
  return z2.array(z2.string({ min: 1 }), { min: 1 }).describe(`Optional local picture files to show inline: PNG, JPEG, GIF or WebP (by their bytes, not their name), each at most 25 MiB. Each is uploaded to ${owner} and appended after a blank line as one ![<file name>](dispatch://\u2026@vN) line, in this order; those lines count toward ${cap}.`).optional();
14439
14442
  }
@@ -14471,7 +14474,7 @@ var dispatchToolSpecs = [
14471
14474
  {
14472
14475
  name: "dispatch_issue",
14473
14476
  example: { project: "DSP", title: "Native workspace" },
14474
- 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. " + "When passing a spec, before calling, have a fresh read-only reader check it against the code and the human's words, then resolve every finding; a Legion architect uses `plan-gap-analyst`. " + "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}`,
14477
+ 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. " + "When passing a spec, before calling, have a fresh read-only reader check it against the code and the human's words, then resolve every finding; a Legion architect uses `plan-gap-analyst`. " + "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. " + "Create a project only when a person asks for one; work that spans projects goes in the project that owns most of it. " + `Do not use it when an existing issue already covers the work; read or update that issue instead. ${ISSUE_REFERENCE}`,
14475
14478
  arguments: (z2) => ({
14476
14479
  project: z2.string().describe("Project key for the new issue."),
14477
14480
  title: z2.string({ max: ISSUE_TITLE_MAX }).describe("Concise issue title, at most 1,000 characters."),
@@ -14482,7 +14485,7 @@ var dispatchToolSpecs = [
14482
14485
  spec: z2.string().describe(`Optional initial primary-document markdown. ${SPEC_WRITING_POINTER}`).optional(),
14483
14486
  labels: z2.array(z2.string({ min: 1, max: 40 }), { max: 20 }).describe("Optional initial labels, at most 20 labels of up to 40 characters.").optional(),
14484
14487
  priority: z2.number({ int: true, min: 0, max: 3 }).describe("Optional coarse priority, a number from 0 to 3: 0 is highest and 3 is lowest.").optional(),
14485
- assignee: z2.string().describe("Email of the person who answers this issue's asks (someone who has signed in to Dispatch); defaults to your owner when you act for a person, else the parent's assignee, else the project's default assignee when it still names a person who has signed in, else unassigned.").optional(),
14488
+ assignee: z2.string().describe("Email of the person who answers this issue's asks (someone who has signed in to Dispatch); defaults to your owner when you act for a person, else the parent's assignee, else the project's default assignee when it still names a person who has signed in, else unassigned. Set it only when a person said who owns the issue.").optional(),
14486
14489
  components: componentsArgument(z2).optional(),
14487
14490
  repo: z2.string().describe("The repository the issue targets, owner/name, mapped to the issue's project in Settings; a child inherits its parent's when unset, and an issue with none on its parent chain takes the project's default.").optional()
14488
14491
  })
@@ -14494,7 +14497,7 @@ var dispatchToolSpecs = [
14494
14497
  priority: 1,
14495
14498
  before: "DSP-2"
14496
14499
  },
14497
- description: "Update an existing issue: move its lifecycle status, retitle it, replace its labels, set " + "its priority, link a URL (the pull request that delivers it, a run, a document), set its " + "route, set or clear its parent, replace the issues it is blocked by, attach it to " + "architecture components, set the repository it targets, or move it on its project's " + "board. Status is one of " + `${ISSUE_STATUSES.join(", ")}; outside Legion, move it yourself as the work advances; inside ` + "Legion the daemon moves it. A non-empty external_links is " + "merged into the issue's existing links by URL, so linking the pull request you just opened " + "keeps every earlier link; an empty external_links removes every link instead. components " + "replaces the issue's own attachment. Closing an issue " + "(status done) requires reason, the note that says why: it is posted on the issue as a " + "message, then the issue closes, because a closed issue refuses messages, comments, and " + "artifacts; reason goes only with status done. A closed issue takes only a board move " + "(before, after), components, " + "a reopening status (any status but done), and an external_links write that adds no new " + "link (so a link can move to the issue that replaced it); everything else, priority " + "included, waits for the reopen. " + "priority is yours to set and a human overrides it. before and after move the issue on " + "its project's board: before puts it above that issue, after below, and both together " + "land it between them. A project's board order is one agent's to keep; in a Legion " + "deployment that is the controller. At least one " + `field besides issue is required. ${ISSUE_REFERENCE}`,
14500
+ description: "Update an existing issue: move its lifecycle status, retitle it, replace its labels, set " + "its priority, link a URL (the pull request that delivers it, a run, a document), set its " + "route, set or clear its parent, replace the issues it is blocked by, attach it to " + "architecture components, set the repository it targets, or move it on its project's " + "board. Status is one of " + `${ISSUE_STATUSES.join(", ")}; outside Legion, move it yourself as the work advances; inside ` + "Legion the daemon moves it. A non-empty external_links is " + "merged into the issue's existing links by URL, so linking the pull request you just opened " + "keeps every earlier link; an empty external_links removes every link instead. components " + "replaces the issue's own attachment. Closing an issue " + "(status done) requires reason, the note that says why: it is posted on the issue as a " + "message, then the issue closes, because a closed issue refuses messages, comments, and " + "artifacts; reason goes only with status done. A closed issue takes only a board move " + "(before, after), components, " + "a reopening status (any status but done), and an external_links write that adds no new " + "link (so a link can move to the issue that replaced it); everything else, priority " + "included, waits for the reopen. " + "priority is yours to set and a human overrides it. before and after move the issue on " + "its project's board: before puts it above that issue, after below, and both together " + "land it between them. A project's board order is one agent's to keep; in a Legion " + "deployment that is the controller. Never reassign an issue a person holds to get a " + "faster answer: who holds it is their call. At least one " + `field besides issue is required. ${ISSUE_REFERENCE}`,
14498
14501
  arguments: (z2) => ({
14499
14502
  issue: z2.string().describe(ISSUE_REFERENCE),
14500
14503
  status: z2.enum(ISSUE_STATUSES).describe("New lifecycle status.").optional(),
@@ -14544,12 +14547,12 @@ var dispatchToolSpecs = [
14544
14547
  }
14545
14548
  ]
14546
14549
  },
14547
- 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}`,
14550
+ 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. Never create an issue to park a " + "question: the question is an ask on the issue it concerns. " + 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. Measure what you can first, and put the measurement and the size of the " + "population it covers in the question; drop an option that cannot fix most of that " + "population, never offer it cut down, and keep an option not to act (Hold). Set urgency high " + "when your work halts on the answer, and keep working on everything else. " + "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}`,
14548
14551
  arguments: (z2) => ({
14549
14552
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14550
14553
  project: z2.string().describe("Project key owning the document.").optional(),
14551
14554
  artifact: z2.string().describe("Project document artifact id, slug, or filename.").optional(),
14552
- ref: z2.string().describe("Optional dispatch:// reference (issue, document, message, or ask); appended to the question and rendered as a link.").optional(),
14555
+ ref: z2.string().describe("Optional dispatch:// reference (issue, document, block, message, or ask); appended to the question and rendered as a link.").optional(),
14553
14556
  question: z2.string({ max: ASK_QUESTION_MAX }).describe(`${ASK_QUESTION_CONTRACT} At most ${ASK_QUESTION_MAX} characters.`),
14554
14557
  options: z2.array(z2.object({
14555
14558
  label: z2.string().describe("Selectable option label."),
@@ -14608,7 +14611,7 @@ var dispatchToolSpecs = [
14608
14611
  kind: "retracted",
14609
14612
  reason: "A newer question supersedes this one."
14610
14613
  },
14611
- description: "Retract an open question that is moot or resolve one after finding the answer. This closes " + "the question without answering it. An ask that lives as an `ask` block in a document is " + 'closed in the document too. A reason beginning "removed from the document in version" is ' + "refused: that marks a retraction the document's own settlement wrote.",
14614
+ description: "Retract an open question that is moot or resolve one after finding the answer. This closes " + "the question without answering it. An ask that lives as an `ask` block in a document is " + 'closed in the document too. A reason beginning "removed from the document in version" is ' + "refused: that marks a retraction the document's own settlement wrote. Use retracted for an ask " + "a newer one replaces or the design made moot, resolved for one whose answer you found; an " + "answered ask cannot be resolved, and resolving records no decision by a person.",
14612
14615
  arguments: (z2) => ({
14613
14616
  ask: z2.string().describe("Ask id (uuid) to close; an 8+ hex prefix unique among this session's own open asks " + "works too."),
14614
14617
  kind: z2.enum(["retracted", "resolved"]).describe("Whether the ask is retracted or self-resolved."),
@@ -14637,7 +14640,7 @@ var dispatchToolSpecs = [
14637
14640
  {
14638
14641
  name: "dispatch_comment",
14639
14642
  example: { issue: "DSP-1", body: "Looks good." },
14640
- 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 ${DISPATCH_BODY_MAX} characters, the lines images appends included. ${OWNER_REFERENCE}`,
14643
+ 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. A reply under an open ask asks for clarification, not an answer: answer it with reply_to_ask, or reword the ask with dispatch edit-ask, rather than opening a second ask. Body is at most ${DISPATCH_BODY_MAX} characters, the lines images appends included. ${OWNER_REFERENCE}`,
14641
14644
  arguments: (z2) => ({
14642
14645
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14643
14646
  project: z2.string().describe("Project key owning the document.").optional(),
@@ -14677,7 +14680,7 @@ var dispatchToolSpecs = [
14677
14680
  {
14678
14681
  name: "dispatch_message",
14679
14682
  example: { issue: "DSP-1", body: "Implementation started." },
14680
- 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 ${DISPATCH_BODY_MAX} characters, the lines images appends included. ${ISSUE_REFERENCE}`,
14683
+ 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; keep --issue whenever the frame you answer gave one. A BTW, Aside or Steer " + "frame carries a reply_with command: answer with it, on that issue. On Oh My Pi an Agents-page Send or Aside " + "arrives as a user message: answer in the conversation, with no dispatch message. " + `Body is at most ${DISPATCH_BODY_MAX} characters, the lines images appends included. ${ISSUE_REFERENCE}`,
14681
14684
  arguments: (z2) => ({
14682
14685
  issue: z2.string().describe(`${ISSUE_REFERENCE} Omit it only when in_reply_to answers a human's direct message to this session.`).optional(),
14683
14686
  body: z2.string({ max: DISPATCH_BODY_MAX }).describe(`Update text, at most ${DISPATCH_BODY_MAX} characters.`),
@@ -14694,7 +14697,7 @@ var dispatchToolSpecs = [
14694
14697
  ops: [{ op: "delete_column", block: "table-123", index: 1 }],
14695
14698
  precondition: { blocks: [{ id: "table-123", token: "sha256:current-table-token" }] }
14696
14699
  },
14697
- 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>"; dispatch doc-read --blocks lists every block's id, type, opening words and token, and each table's rows and cells with the indexes delete_row and delete_column take (GET /api/v1/artifacts/{artifact UUID}/blocks serves the same list). ` + "Optionally require the state just read: precondition selects exactly one of a document token from dispatch doc-read, or block {id, token} values from dispatch doc-read --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}`,
14700
+ 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>"; dispatch doc-read --blocks lists every block's id, type, opening words and token, and each table's rows and cells with the indexes delete_row and delete_column take (GET /api/v1/artifacts/{artifact UUID}/blocks serves the same list). ` + "Optionally require the state just read: precondition selects exactly one of a document token from dispatch doc-read, or block {id, token} values from dispatch doc-read --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. " + "One replace per paragraph: more new paragraphs go in one insert after the last one you rewrote, since two inserts at one anchor come out reversed; read the passage back with dispatch doc-read afterwards. " + "A block id written as {#id} longer than 256 bytes is stored, but its block is no graph node, so no reference reaches it. " + `The spec holds requirements, design, and decisions - never progress, status, or timestamps; a record of open items or progress is a document of its own. ${OWNER_REFERENCE} ${SPEC_WRITING_POINTER}`,
14698
14701
  arguments: (z2) => ({
14699
14702
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14700
14703
  project: z2.string().describe("Project key owning the document.").optional(),
@@ -14741,12 +14744,12 @@ var dispatchToolSpecs = [
14741
14744
  {
14742
14745
  name: "dispatch_request_approval",
14743
14746
  example: { issue: "DSP-1", summary: "A live sync replaces the nightly export." },
14744
- description: "Ask a human to approve a document at its current version. Before calling, have a fresh read-only reader check the spec against the code and the human's words, then resolve every finding; a Legion architect uses `plan-gap-analyst`. 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 outside its decision blocks after it: " + "request approval again once that revision is complete and " + HUMAN_AGREED_TO_DOCUMENT + ", with a one-sentence summary naming what changed since the earlier approval: the card " + "names that approval (version, date, reviewer) and its Show diff control carries the detail. " + "A human's own answer to a decision block carries the approval to the version it writes when " + "that answer is the whole change since the approved version, so nothing is requested for " + "it. A new version of a Legion root spec closes its armed design gate until a human " + "approves it; the carried approval reopens it too. " + "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,
14747
+ description: "Ask a human to approve a document at its current version. Before calling, have a fresh read-only reader check the spec against the code and the human's words, then resolve every finding; a Legion architect runs `plan-gap-analyst` and `plan-reviewer`. 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 outside its decision blocks after it: " + "request approval again once that revision is complete and " + HUMAN_AGREED_TO_DOCUMENT + ", with a one-sentence summary naming what changed since the earlier approval: the card " + "names that approval (version, date, reviewer) and its Show diff control carries the detail. " + "A human's own answer to a decision block carries the approval to the version it writes when " + "that answer is the whole change since the approved version, so nothing is requested for " + "it. A new version of a Legion root spec closes its armed design gate until a human " + "approves it; the carried approval reopens it too. " + "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,
14745
14748
  arguments: (z2) => ({
14746
14749
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14747
14750
  project: z2.string().describe("Project key owning the document.").optional(),
14748
14751
  artifact: z2.string().describe("Project document artifact id, slug, or filename; primary document by default for an issue.").optional(),
14749
- summary: z2.string({ min: 1 }).describe(`What the human is approving, in one to three sentences, and nothing else: no commentary on itself or the conversation, and no question; after an earlier approval, one sentence naming what changed since it, since the card shows that approval and a diff. Request approval only once ${HUMAN_AGREED_TO_DOCUMENT}.`)
14752
+ summary: z2.string({ min: 1 }).describe(`What the human is approving, in one to three sentences, and nothing else: no commentary on itself or the conversation, and no question; after an earlier approval, one sentence naming what changed since it, since the card shows that approval and a diff. A Legion architect adds one last sentence with its two spec reviews' results. Request approval only once ${HUMAN_AGREED_TO_DOCUMENT}.`)
14750
14753
  }),
14751
14754
  validation: documentOwnerValidation(true)
14752
14755
  },
@@ -14754,7 +14757,7 @@ var dispatchToolSpecs = [
14754
14757
  name: "dispatch_artifact",
14755
14758
  example: { issue: "DSP-1", name: "design.md", content: `# Design
14756
14759
  ` },
14757
- 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}`,
14760
+ 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. Never upload a document's own name to change it (spec.md replaces the spec whole); edit it with dispatch doc-edit. Uploading the same name again makes its next version. 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}`,
14758
14761
  arguments: (z2) => ({
14759
14762
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14760
14763
  project: z2.string().describe("Project key for an unlinked document.").optional(),
@@ -14774,12 +14777,12 @@ var dispatchToolSpecs = [
14774
14777
  {
14775
14778
  name: "dispatch_read",
14776
14779
  example: { issue: "DSP-1" },
14777
- description: "Read an issue or 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, " + "issue plus artifact (one of its documents), 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). A document summary names its issue, reference, versions and approval. " + "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). " + `An issue read, and its dispatch://KEY/log, lists the issue's last ${DEFAULT_ISSUE_READ_EVENTS} events, oldest first; ` + "--events N lists the last N, and --all-events every one. " + "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,
14780
+ description: "Read an issue or 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, " + "issue plus artifact (one of its documents), 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). A document summary names its issue, reference, versions and approval. " + "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). " + `An issue read, and its dispatch://KEY/log, lists the issue's last ${DEFAULT_ISSUE_READ_EVENTS} events, oldest first; ` + "--events N lists the last N, and --all-events every one. " + "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. " + "A block read prints `Block:`, `Document:`, `Changed in version:` and `Text:`, then those two sections; a pinned one adds " + "the block's text at that version and whether it is unchanged, changed or removed since, and an absent block, unpinned, " + "reads `No block <id> in <document>.` " + "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,
14778
14781
  arguments: (z2) => ({
14779
14782
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14780
14783
  project: z2.string().describe("Project key owning the document.").optional(),
14781
14784
  artifact: z2.string().describe("Document artifact id, slug, or filename: one of the issue's, or a project document.").optional(),
14782
- ref: z2.string().describe("Optional dispatch:// issue or document reference.").optional(),
14785
+ ref: z2.string().describe("Optional dispatch:// reference: dispatch://KEY, dispatch://KEY/spec, " + "dispatch://KEY/artifact/<slug>[@vN], dispatch://KEY/ask/<id>, dispatch://KEY/comment/<id>, " + "dispatch://KEY/message/<id>, dispatch://PROJECT/artifact/<slug>[@vN], and its " + "/ask/<id> and /comment/<id>; or the dashboard URL of one. A block: " + "dispatch://KEY/spec#b-<block id>[@vN], dispatch://KEY/artifact/<slug>#b-<block id>[@vN] or " + "dispatch://PROJECT/artifact/<slug>#b-<block id>[@vN], the id percent-encoded except ASCII " + "letters, digits and -.").optional(),
14783
14786
  message: z2.string().describe("A message id (uuid): reads the conversation it belongs to, the root message and every " + "reply. Name nothing else with it.").optional(),
14784
14787
  events: z2.number({ int: true, min: 1 }).describe(`How many of the issue's newest events an issue read or its dispatch://KEY/log lists, oldest first; default ${DEFAULT_ISSUE_READ_EVENTS}. Name no --all-events with it.`).optional(),
14785
14788
  all_events: z2.boolean().describe(`List every one of the issue's events, oldest first, on an issue read or its dispatch://KEY/log, in place of the last ${DEFAULT_ISSUE_READ_EVENTS}. Name no --events with it.`).optional()
@@ -14811,7 +14814,7 @@ var dispatchToolSpecs = [
14811
14814
  {
14812
14815
  name: "dispatch_issues",
14813
14816
  example: { project: "PROJ", limit: 250, offset: 250 },
14814
- 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. " + "all takes the whole filtered list in one read instead, for a caller that must not miss or " + "repeat a row while other writers change the list, and especially for one whose own writes " + "move issues out of what it filtered on.",
14817
+ 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, as of the server's copy of the listener's session list, at " + "most 5 s old, 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. " + "all takes the whole filtered list in one read instead, for a caller that must not miss or " + "repeat a row while other writers change the list, and especially for one whose own writes " + "move issues out of what it filtered on.",
14815
14818
  arguments: (z2) => ({
14816
14819
  project: z2.string().describe("Project key to list issues from."),
14817
14820
  status: z2.enum(ISSUE_STATUSES).describe("Optional lifecycle status filter.").optional(),
@@ -14845,7 +14848,7 @@ var dispatchToolSpecs = [
14845
14848
  {
14846
14849
  name: "dispatch_architecture_sync",
14847
14850
  example: { project: "CORE", repo: "acme/infra" },
14848
- description: "Import a project's architecture model from its source repositories now, instead of waiting " + "for the server's five-minute schedule. A project may have several sources, one per " + "repository, and its model is every source's components together. Without repo every " + "source imports and each answers on its own line; with repo only that source does. Each " + "line names the imported commit, or the recorded error when that source's model was " + "rejected (its previous components stay up, and every other source is untouched). Sources " + "are saved in Settings or with PUT /api/v1/projects/{key}/architecture-sources/{owner}/{repo}; " + "404 SOURCE_NOT_FOUND when the project has none, or none for that repository.",
14851
+ description: "Import a project's architecture model from its source repositories now, instead of waiting " + "for the server's five-minute schedule. A project may have several sources, one per " + "repository, and its model is every source's components together. Without repo every " + "source imports and each answers on its own line; with repo only that source does. Each " + "line names the imported commit, or the recorded error when that source's model was " + "rejected (its previous components stay up, and every other source is untouched). A failed " + "line also says what of that source still shows (its commit, its component count when it " + "was saved again on another branch and has not imported from it yet, or that it never " + "imported), and a credential or branch problem is that source's failed line, or with repo " + "the error 409 SOURCE_ACCESS. Sources " + "are saved in Settings or with PUT /api/v1/projects/{key}/architecture-sources/{owner}/{repo}; " + "404 SOURCE_NOT_FOUND when the project has none, or none for that repository.",
14849
14852
  arguments: (z2) => ({
14850
14853
  project: z2.string().describe("Project key whose architecture sources to sync, such as CORE."),
14851
14854
  repo: z2.string().describe("One source repository of the project to sync alone, owner/name; omit it to sync every source.").optional()
@@ -15606,7 +15609,7 @@ var picturesShown = new Map;
15606
15609
 
15607
15610
  // ../envoy-client/src/dispatch-execute.ts
15608
15611
  var triageAdviceShown = new Set;
15609
- 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";
15612
+ 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), " + "dispatch://KEY-1/spec#b-<block id>, dispatch://KEY-1/artifact/<slug>#b-<block id> or " + "dispatch://PROJECT/artifact/<slug>#b-<block id> (one block of a document, its id " + "percent-encoded, optionally pinned to a version of the document with @v<N> after it; for " + "dispatch read), 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";
15610
15613
  var toolSchemas = new Map;
15611
15614
  function artifactByReference(artifacts, artifactReference, owner, canonical) {
15612
15615
  const bySlug = artifacts.find((candidate) => candidate.slug === artifactReference);
@@ -15747,6 +15750,7 @@ var issueKey = exports_external.string().regex(DISPATCH_KEY_PATTERN);
15747
15750
  var timestamp = exports_external.iso.datetime({ offset: true });
15748
15751
  var LEGION_WORKFLOW_PHASES = [
15749
15752
  "admitted",
15753
+ "awaiting_person",
15750
15754
  "planning",
15751
15755
  "implementing",
15752
15756
  "testing",
@@ -15859,6 +15863,32 @@ var legionCheckView = exports_external.strictObject({
15859
15863
  nextWakeAt: timestamp,
15860
15864
  last: exports_external.boolean()
15861
15865
  });
15866
+ var LEGION_WAIT_ANSWERS = [
15867
+ "ask",
15868
+ "retracted",
15869
+ "closed",
15870
+ "reply",
15871
+ "turn",
15872
+ "approved",
15873
+ "changes_requested",
15874
+ "version"
15875
+ ];
15876
+ var legionWaitTimes = {
15877
+ since: timestamp.optional(),
15878
+ answeredAt: timestamp.optional(),
15879
+ answeredBy: exports_external.enum(LEGION_WAIT_ANSWERS).optional()
15880
+ };
15881
+ var legionWaitingOn = exports_external.discriminatedUnion("kind", [
15882
+ exports_external.strictObject({ kind: exports_external.literal("ask"), ask: nonEmptyString, ...legionWaitTimes }),
15883
+ exports_external.strictObject({
15884
+ kind: exports_external.literal("gate"),
15885
+ gateVersion: exports_external.number().int().positive(),
15886
+ artifactId: nonEmptyString,
15887
+ ...legionWaitTimes
15888
+ })
15889
+ ]).refine((wait) => wait.answeredAt === undefined === (wait.answeredBy === undefined), {
15890
+ message: "answeredAt and answeredBy appear together"
15891
+ });
15862
15892
  var legionPendingStatusWrite = exports_external.strictObject({
15863
15893
  issue: nonEmptyString,
15864
15894
  payload: exports_external.record(exports_external.string(), exports_external.unknown()),
@@ -15891,6 +15921,7 @@ var legionIssue = exports_external.strictObject({
15891
15921
  pullRequest: legionPullRequestView.optional(),
15892
15922
  designGate: legionGateView.optional(),
15893
15923
  slot: legionSlotView.optional(),
15924
+ waitingOn: legionWaitingOn.optional(),
15894
15925
  check: legionCheckView.optional()
15895
15926
  });
15896
15927
  var daemonInfo = exports_external.strictObject({
@@ -16182,6 +16213,71 @@ async function callReadyWithRetry(label, call) {
16182
16213
  }
16183
16214
  }
16184
16215
  }
16216
+ var DAEMON_RESTART_BUDGET_MS = 10 * 60000;
16217
+ var DAEMON_RESTART_FIRST_DELAY_MS = 500;
16218
+ var DAEMON_RESTART_MAX_DELAY_MS = 30000;
16219
+ function daemonUnreachable(error48) {
16220
+ if (error48 instanceof LegionDaemonApiError) {
16221
+ return (error48.status === 502 || error48.status === 503) && error48.code === undefined ? "dropped" : undefined;
16222
+ }
16223
+ if (!(error48 instanceof Error))
16224
+ return;
16225
+ const code = "code" in error48 && typeof error48.code === "string" ? error48.code : undefined;
16226
+ if (code === "ConnectionRefused" || code === "ECONNREFUSED")
16227
+ return "refused";
16228
+ if (code === "ConnectionClosed" || code === "ECONNRESET" || code === "EPIPE")
16229
+ return "dropped";
16230
+ return error48 instanceof TypeError && (error48.message === "Failed to fetch" || error48.message === "fetch failed") ? "dropped" : undefined;
16231
+ }
16232
+ async function retryWhileDaemonRestarts(label, call, options = {}) {
16233
+ const { signal } = options;
16234
+ const now = options.now ?? Date.now;
16235
+ const sleep = options.sleep ?? ((ms, abort) => {
16236
+ const wait = Promise.withResolvers();
16237
+ const timer = setTimeout(wait.resolve, ms);
16238
+ abort?.addEventListener("abort", () => {
16239
+ clearTimeout(timer);
16240
+ wait.resolve();
16241
+ }, { once: true });
16242
+ return wait.promise;
16243
+ });
16244
+ const deadline = now() + DAEMON_RESTART_BUDGET_MS;
16245
+ let delay = DAEMON_RESTART_FIRST_DELAY_MS;
16246
+ let told = false;
16247
+ let dropped = false;
16248
+ let lastFailure;
16249
+ for (;; ) {
16250
+ if (signal?.aborted && lastFailure !== undefined) {
16251
+ throw new Error(`${label}: the Legion daemon was unreachable (${messageFor(lastFailure)}) and the call was aborted${dropped ? "; an attempt lost its connection before the daemon answered and may have been applied, so read the record before calling again" : "; call it again once the daemon is back"}`);
16252
+ }
16253
+ try {
16254
+ return await call();
16255
+ } catch (error48) {
16256
+ const unreachable = daemonUnreachable(error48);
16257
+ if (unreachable === undefined) {
16258
+ if (dropped && error48 instanceof LegionDaemonApiError) {
16259
+ error48.message += ` (an earlier attempt of ${label} lost its connection before the daemon answered, and may have been applied: read the record before calling again)`;
16260
+ }
16261
+ throw error48;
16262
+ }
16263
+ lastFailure = error48;
16264
+ dropped ||= unreachable === "dropped";
16265
+ const remaining = deadline - now();
16266
+ if (remaining <= 0) {
16267
+ throw new Error(`${label}: the Legion daemon was unreachable (${messageFor(error48)}) and stayed so for ${DAEMON_RESTART_BUDGET_MS / 60000} minutes${dropped ? "; an attempt lost its connection before the daemon answered and may have been applied, so read the record before calling again" : "; call it again once the daemon is back"}`);
16268
+ }
16269
+ if (!told && !signal?.aborted) {
16270
+ told = true;
16271
+ const message = `The Legion daemon is unreachable, most likely restarting (${messageFor(error48)}); ${label} is being retried for up to ${DAEMON_RESTART_BUDGET_MS / 60000} minutes.`;
16272
+ console.error(`[legion] ${message}`);
16273
+ options.onRetrying?.(message);
16274
+ }
16275
+ if (!signal?.aborted)
16276
+ await sleep(Math.min(delay, remaining), signal);
16277
+ delay = Math.min(delay * 2, DAEMON_RESTART_MAX_DELAY_MS);
16278
+ }
16279
+ }
16280
+ }
16185
16281
  function parseStrictly(schema, body) {
16186
16282
  let json2;
16187
16283
  try {
@@ -17046,7 +17142,7 @@ async function bootLegionClaim(host2) {
17046
17142
  // package.json
17047
17143
  var package_default = {
17048
17144
  name: "@sjawhar/pi-legion",
17049
- version: "11.0.0",
17145
+ version: "11.2.0",
17050
17146
  license: "Apache-2.0",
17051
17147
  type: "module",
17052
17148
  omp: {
@@ -17058,7 +17154,7 @@ var package_default = {
17058
17154
  ]
17059
17155
  },
17060
17156
  legion: {
17061
- daemonApiVersion: 39
17157
+ daemonApiVersion: 40
17062
17158
  },
17063
17159
  repository: {
17064
17160
  type: "git",
@@ -17560,36 +17656,8 @@ async function executePhaseOp(runtime, input) {
17560
17656
  switch (args.op) {
17561
17657
  case "read_record":
17562
17658
  return { record: recordFrom(await daemon.state(), args.issue) };
17563
- case "handoff_complete": {
17564
- const record2 = recordFrom(await daemon.state(), claim.issue);
17565
- if (claim.role === "implementer" && record2.phase === "done") {
17566
- if (record2.check !== undefined) {
17567
- if (args.verdict === undefined) {
17568
- throw new Error("handoff_complete on a production check names a verdict of pass, pending or fail");
17569
- }
17570
- if (args.ready === true) {
17571
- throw new Error("handoff_complete on a production check is no READY: leave ready out");
17572
- }
17573
- }
17574
- return daemon.handoffComplete({
17575
- grantId: await mintClaimGrant(daemon, claim),
17576
- summary: args.summary,
17577
- verdict: args.verdict ?? "",
17578
- ready: false
17579
- });
17580
- }
17581
- if (args.verdict === "pending") {
17582
- throw new Error("handoff_complete's verdict is pass or fail on a phase completion; pending is a production check's");
17583
- }
17584
- const commit = await runtime.handoffCommit(record2.phase, claim.role);
17585
- return daemon.handoffComplete({
17586
- grantId: await mintClaimGrant(daemon, claim),
17587
- summary: args.summary,
17588
- verdict: args.verdict ?? "",
17589
- ready: args.ready ?? false,
17590
- commit
17591
- });
17592
- }
17659
+ case "handoff_complete":
17660
+ return retryWhileDaemonRestarts("handoff_complete", () => completeHandoff(runtime, args), runtime.restart);
17593
17661
  case "request_backward_move": {
17594
17662
  let issue2;
17595
17663
  if (claim.role === "architect") {
@@ -17610,6 +17678,37 @@ async function executePhaseOp(runtime, input) {
17610
17678
  }
17611
17679
  }
17612
17680
  }
17681
+ async function completeHandoff(runtime, args) {
17682
+ const { daemon, claim } = runtime;
17683
+ const record2 = recordFrom(await daemon.state(), claim.issue);
17684
+ if (claim.role === "implementer" && record2.phase === "done") {
17685
+ if (record2.check !== undefined) {
17686
+ if (args.verdict === undefined) {
17687
+ throw new Error("handoff_complete on a production check names a verdict of pass, pending or fail");
17688
+ }
17689
+ if (args.ready === true) {
17690
+ throw new Error("handoff_complete on a production check is no READY: leave ready out");
17691
+ }
17692
+ }
17693
+ return daemon.handoffComplete({
17694
+ grantId: await mintClaimGrant(daemon, claim),
17695
+ summary: args.summary,
17696
+ verdict: args.verdict ?? "",
17697
+ ready: false
17698
+ });
17699
+ }
17700
+ if (args.verdict === "pending") {
17701
+ throw new Error("handoff_complete's verdict is pass or fail on a phase completion; pending is a production check's");
17702
+ }
17703
+ const commit = await runtime.handoffCommit(record2.phase, claim.role);
17704
+ return daemon.handoffComplete({
17705
+ grantId: await mintClaimGrant(daemon, claim),
17706
+ summary: args.summary,
17707
+ verdict: args.verdict ?? "",
17708
+ ready: args.ready ?? false,
17709
+ commit
17710
+ });
17711
+ }
17613
17712
 
17614
17713
  // ../pi-shared/src/tool-result.ts
17615
17714
  function toolSuccess(text, details = {}, images = []) {
@@ -17684,17 +17783,35 @@ function assertOperationInput(parameters, operation2) {
17684
17783
  }
17685
17784
  }
17686
17785
  function createLegionTool(deps) {
17687
- const { pi, daemon, session, controllerGrant, onPhaseCompleted, handoffCommit, resolveDocument } = deps;
17786
+ const {
17787
+ pi,
17788
+ daemon,
17789
+ session,
17790
+ controllerGrant,
17791
+ onPhaseCompleted,
17792
+ handoffCommit,
17793
+ resolveDocument,
17794
+ restartClock
17795
+ } = deps;
17688
17796
  return {
17689
17797
  name: "legion",
17690
17798
  label: "legion",
17691
17799
  description: legionToolDescription({
17692
17800
  envoyPublish: "envoy_publish",
17693
- after: PHASE_OP_SENTENCES.requestBackwardMove + "sign_off is the architect's acceptance of an issue in accepting, moving it to retro; the daemon refuses it in every " + "other phase (SIGNOFF_OUTSIDE_ACCEPTING) and refuses it SIGNOFF_CHILD_IN_WORKFLOW while any issue beneath it is still in " + "the workflow, naming each of them: accept once the last of them is done, or take one out with park_child. " + "The controller's read_state returns the daemon's whole state and set_status moves an issue to todo, backlog or icebox. "
17801
+ after: PHASE_OP_SENTENCES.requestBackwardMove + "sign_off is the architect's acceptance of an issue in accepting, moving it to retro; the daemon refuses it in every " + "other phase (SIGNOFF_OUTSIDE_ACCEPTING), and issues still running beneath it do not hold it: a root's close waits " + "for them instead. " + "The controller's read_state returns the daemon's whole state and set_status moves an issue to todo, backlog or icebox. "
17694
17802
  }),
17695
17803
  defaultInactive: true,
17696
17804
  parameters: toolSchema(pi),
17697
- execute: async (_id, parameters, _signal, _onUpdate, context) => {
17805
+ execute: async (_id, parameters, signal, onUpdate, context) => {
17806
+ const restart = {
17807
+ ...restartClock,
17808
+ ...signal === undefined ? {} : { signal },
17809
+ onRetrying: (message) => {
17810
+ if (typeof onUpdate === "function") {
17811
+ onUpdate({ content: [{ type: "text", text: message }], details: {} });
17812
+ }
17813
+ }
17814
+ };
17698
17815
  try {
17699
17816
  const active = session(context);
17700
17817
  const operation2 = requiredString2(parameters, "legion", "op");
@@ -17702,7 +17819,7 @@ function createLegionTool(deps) {
17702
17819
  throw new Error(`${operation2} is not available to a ${active.kind} session`);
17703
17820
  }
17704
17821
  if (active.kind !== "controller" && isPhaseOp(operation2)) {
17705
- const answer = await executePhaseOp({ daemon: daemon(), claim: active, handoffCommit }, parameters);
17822
+ const answer = await executePhaseOp({ daemon: daemon(), claim: active, handoffCommit, restart }, parameters);
17706
17823
  if (operation2 === "handoff_complete")
17707
17824
  onPhaseCompleted(context);
17708
17825
  return jsonSuccess(answer);
@@ -17737,8 +17854,12 @@ function createLegionTool(deps) {
17737
17854
  const reference = requiredString2(parameters, operation2, "artifactId");
17738
17855
  const isId = LegionGateRegisterRequest.shape.artifactId.safeParse(reference).success;
17739
17856
  const artifactId = isId ? reference : await resolveDocument(issue2, reference);
17740
- const grantId = await mintClaimGrant(client, active);
17741
- await client.gateRegister({ grantId, issue: issue2, artifactId, version: version2 });
17857
+ await retryWhileDaemonRestarts("register_gate", async () => client.gateRegister({
17858
+ grantId: await mintClaimGrant(client, active),
17859
+ issue: issue2,
17860
+ artifactId,
17861
+ version: version2
17862
+ }), restart);
17742
17863
  return jsonSuccess({});
17743
17864
  }
17744
17865
  case "release_children": {
@@ -17763,11 +17884,8 @@ function createLegionTool(deps) {
17763
17884
  return jsonSuccess({});
17764
17885
  }
17765
17886
  case "sign_off": {
17766
- const grantId = await mintClaimGrant(client, active);
17767
- await client.signOff({
17768
- grantId,
17769
- issue: requiredString2(parameters, operation2, "issue")
17770
- });
17887
+ const issue2 = requiredString2(parameters, operation2, "issue");
17888
+ await retryWhileDaemonRestarts("sign_off", async () => client.signOff({ grantId: await mintClaimGrant(client, active), issue: issue2 }), restart);
17771
17889
  return jsonSuccess({});
17772
17890
  }
17773
17891
  case "close_root": {