@sjawhar/pi-legion-envoy 7.1.1 → 7.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.
package/dist/envoy.js CHANGED
@@ -30007,6 +30007,7 @@ var ISSUE_REFERENCE = "An issue is a native KEY or external owner/repo#n referen
30007
30007
  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.";
30008
30008
  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.";
30009
30009
  var ASK_OPTIONS_CONTRACT = "Options carry the genuinely different approaches. Each option has a label, and its description " + "says what that approach costs.";
30010
+ var HUMAN_AGREED_TO_DOCUMENT = "the human has agreed to every point in the document";
30010
30011
  function documentOwnerValidation(requireArtifact, alwaysRequireArtifact = false) {
30011
30012
  return {
30012
30013
  check: (value) => {
@@ -30168,7 +30169,7 @@ var dispatchToolSpecs = [
30168
30169
  }
30169
30170
  ]
30170
30171
  },
30171
- description: "Open a durable, answerable decision on an issue or project document. Do not use it 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 you are blocked on a human for, including a credential or grant to renew, an " + "approval, or a decision, is an ask, never a message. " + "Anchor a document question, 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 ` + `and has at most 8 options. ${OWNER_REFERENCE}`,
30172
+ 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 ` + `and has at most 8 options. ${OWNER_REFERENCE}`,
30172
30173
  arguments: (z2) => ({
30173
30174
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
30174
30175
  project: z2.string().describe("Project key owning the document.").optional(),
@@ -30299,7 +30300,7 @@ var dispatchToolSpecs = [
30299
30300
  {
30300
30301
  name: "dispatch_message",
30301
30302
  example: { issue: "DSP-1", body: "Implementation started." },
30302
- description: "Post a note humans must read now: a reply to a human's message or a deliverable that landed. A blocker only a human can " + "clear is an ask (dispatch_ask), so it lands in their inbox. Never progress or status updates - Dispatch is a high-signal " + "record, not a log. Not a decision (dispatch_ask) 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. ${ISSUE_REFERENCE}`,
30303
+ 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. ${ISSUE_REFERENCE}`,
30303
30304
  arguments: (z2) => ({
30304
30305
  issue: z2.string().describe(`${ISSUE_REFERENCE} Omit it only when in_reply_to answers a human's direct message to this session.`).optional(),
30305
30306
  body: z2.string({ max: 2000 }).describe("Update text, at most 2,000 characters."),
@@ -30361,12 +30362,12 @@ var dispatchToolSpecs = [
30361
30362
  {
30362
30363
  name: "dispatch_request_approval",
30363
30364
  example: { issue: "DSP-1", summary: "A live sync replaces the nightly export." },
30364
- 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. A later version carries the same open " + "request forward and leaves it waiting on you; once the revision is complete and the human " + "has agreed to every point in it, call this again to hand that request 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. " + "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,
30365
+ 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,
30365
30366
  arguments: (z2) => ({
30366
30367
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
30367
30368
  project: z2.string().describe("Project key owning the document.").optional(),
30368
30369
  artifact: z2.string().describe("Project document artifact id, slug, or filename; primary document by default for an issue.").optional(),
30369
- 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. Request approval only once the human has agreed to every point in the document.")
30370
+ 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. Request approval only once ${HUMAN_AGREED_TO_DOCUMENT}.`)
30370
30371
  }),
30371
30372
  validation: documentOwnerValidation(true)
30372
30373
  },
@@ -35490,6 +35491,32 @@ function onEnvoyRoleRegained(listener) {
35490
35491
  legionRoleClaimBridge().regained = listener;
35491
35492
  }
35492
35493
 
35494
+ // src/opens-ask.ts
35495
+ function adviceCount(details, key) {
35496
+ const count = asObject(asObject(details)?.advice)?.[key];
35497
+ return typeof count === "number" ? count : 0;
35498
+ }
35499
+ function opensAsk({ toolName, details }) {
35500
+ switch (toolName) {
35501
+ case "dispatch_ask":
35502
+ case "dispatch_request_approval":
35503
+ return true;
35504
+ case "dispatch_issue":
35505
+ case "dispatch_artifact":
35506
+ return adviceCount(details, "decision_blocks") > 0;
35507
+ case "dispatch_doc_edit":
35508
+ return adviceCount(details, "decision_blocks_added") > 0;
35509
+ default:
35510
+ return false;
35511
+ }
35512
+ }
35513
+ function deviceTool({ toolName, details }) {
35514
+ if (toolName !== "write")
35515
+ return;
35516
+ const tool = asObject(asObject(details)?.xdev)?.tool;
35517
+ return typeof tool === "string" ? tool : undefined;
35518
+ }
35519
+
35493
35520
  // src/side-turn.ts
35494
35521
  function btwPrompt(question) {
35495
35522
  return [
@@ -35682,8 +35709,7 @@ var ECHOED_CHOICE = /\bPROCEEDING\b/;
35682
35709
  var isWaitingVerdict = (reply) => WAITING_VERDICT.test(reply) && !ECHOED_CHOICE.test(reply);
35683
35710
  var ASK_SELF_CHECK_TIMEOUT_MS = 60000;
35684
35711
  var ASK_CHECKS_PER_PERIOD = 5;
35685
- var UNASKED_WAIT_REMINDER = "You just said you are waiting on a human for something no open ask in Dispatch covers. Open an ask for it now with dispatch_ask, naming exactly what you need and from whom.";
35686
- var ASK_OPENING_TOOLS = ["dispatch_ask", "dispatch_request_approval"];
35712
+ var UNASKED_WAIT_REMINDER = "You just said you are waiting on a human for something no open ask in Dispatch covers. " + "Open it now: a decision block in the document it concerns (dispatch_doc_edit with an ask block), or " + "dispatch_ask for a to-do only a human can do, naming exactly what you need and from whom.";
35687
35713
  var DISPATCH_TOOL_PREFIX = "dispatch_";
35688
35714
  function isLegionManagedEntry(entry) {
35689
35715
  if (typeof entry !== "object" || entry === null)
@@ -36646,10 +36672,11 @@ function envoyExtension(pi) {
36646
36672
  if (event.isError)
36647
36673
  return;
36648
36674
  if (askAwareness.session_id === context.sessionManager.getSessionId() && askAwareness.period > 0) {
36649
- if (ASK_OPENING_TOOLS.includes(event.toolName)) {
36675
+ const tool = deviceTool(event) ?? event.toolName;
36676
+ if (opensAsk(event)) {
36650
36677
  askAwareness = { ...askAwareness, check_due: false };
36651
36678
  abortSelfCheck("the agent opened the ask itself");
36652
- } else if (!event.toolName.startsWith(DISPATCH_TOOL_PREFIX)) {
36679
+ } else if (!tool.startsWith(DISPATCH_TOOL_PREFIX)) {
36653
36680
  askAwareness = { ...askAwareness, check_due: true };
36654
36681
  }
36655
36682
  }
package/dist/legion.js CHANGED
@@ -14094,6 +14094,7 @@ var ISSUE_REFERENCE = "An issue is a native KEY or external owner/repo#n referen
14094
14094
  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.";
14095
14095
  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.";
14096
14096
  var ASK_OPTIONS_CONTRACT = "Options carry the genuinely different approaches. Each option has a label, and its description " + "says what that approach costs.";
14097
+ var HUMAN_AGREED_TO_DOCUMENT = "the human has agreed to every point in the document";
14097
14098
  function documentOwnerValidation(requireArtifact, alwaysRequireArtifact = false) {
14098
14099
  return {
14099
14100
  check: (value) => {
@@ -14255,7 +14256,7 @@ var dispatchToolSpecs = [
14255
14256
  }
14256
14257
  ]
14257
14258
  },
14258
- description: "Open a durable, answerable decision on an issue or project document. Do not use it 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 you are blocked on a human for, including a credential or grant to renew, an " + "approval, or a decision, is an ask, never a message. " + "Anchor a document question, 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 ` + `and has at most 8 options. ${OWNER_REFERENCE}`,
14259
+ 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 ` + `and has at most 8 options. ${OWNER_REFERENCE}`,
14259
14260
  arguments: (z2) => ({
14260
14261
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14261
14262
  project: z2.string().describe("Project key owning the document.").optional(),
@@ -14386,7 +14387,7 @@ var dispatchToolSpecs = [
14386
14387
  {
14387
14388
  name: "dispatch_message",
14388
14389
  example: { issue: "DSP-1", body: "Implementation started." },
14389
- description: "Post a note humans must read now: a reply to a human's message or a deliverable that landed. A blocker only a human can " + "clear is an ask (dispatch_ask), so it lands in their inbox. Never progress or status updates - Dispatch is a high-signal " + "record, not a log. Not a decision (dispatch_ask) 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. ${ISSUE_REFERENCE}`,
14390
+ 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. ${ISSUE_REFERENCE}`,
14390
14391
  arguments: (z2) => ({
14391
14392
  issue: z2.string().describe(`${ISSUE_REFERENCE} Omit it only when in_reply_to answers a human's direct message to this session.`).optional(),
14392
14393
  body: z2.string({ max: 2000 }).describe("Update text, at most 2,000 characters."),
@@ -14448,12 +14449,12 @@ var dispatchToolSpecs = [
14448
14449
  {
14449
14450
  name: "dispatch_request_approval",
14450
14451
  example: { issue: "DSP-1", summary: "A live sync replaces the nightly export." },
14451
- 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. A later version carries the same open " + "request forward and leaves it waiting on you; once the revision is complete and the human " + "has agreed to every point in it, call this again to hand that request 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. " + "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,
14452
+ 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,
14452
14453
  arguments: (z2) => ({
14453
14454
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14454
14455
  project: z2.string().describe("Project key owning the document.").optional(),
14455
14456
  artifact: z2.string().describe("Project document artifact id, slug, or filename; primary document by default for an issue.").optional(),
14456
- 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. Request approval only once the human has agreed to every point in the document.")
14457
+ 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. Request approval only once ${HUMAN_AGREED_TO_DOCUMENT}.`)
14457
14458
  }),
14458
14459
  validation: documentOwnerValidation(true)
14459
14460
  },
@@ -17417,7 +17418,7 @@ function endInjectedUserTurns(sessionID) {
17417
17418
  // package.json
17418
17419
  var package_default = {
17419
17420
  name: "@sjawhar/pi-legion-envoy",
17420
- version: "7.1.1",
17421
+ version: "7.2.0",
17421
17422
  license: "Apache-2.0",
17422
17423
  type: "module",
17423
17424
  omp: {
@@ -18838,8 +18839,9 @@ function paneRuleRefusal(toolCall, rules) {
18838
18839
  var LEGION_LOADED_MARKER = Symbol.for("legion.pi-envoy.legion-loaded");
18839
18840
  var CODE_MUTATION_TOOLS = ["edit", "write", "apply_patch"];
18840
18841
  var MERGER_BLOCKED_TOOLS = [...CODE_MUTATION_TOOLS, "task"];
18842
+ var TOOL_DEVICE_URL = /^xd:\/\//iu;
18841
18843
  function isToolDeviceInvocation(toolCall) {
18842
- return toolCall.toolName === "write" && typeof toolCall.input.path === "string" && toolCall.input.path.startsWith("xd://");
18844
+ return toolCall.toolName === "write" && typeof toolCall.input.path === "string" && TOOL_DEVICE_URL.test(toolCall.input.path);
18843
18845
  }
18844
18846
  function legionExtension(pi) {
18845
18847
  const instance = randomUUID2().slice(0, 8);
@@ -127,14 +127,8 @@ that points at the spec.
127
127
 
128
128
  The block is what reaches the human's Inbox. A question phrased as prose in the spec reaches
129
129
  nobody. A spec with no ask blocks is fine only when the issue genuinely needs no human decision.
130
- When `dispatch_issue` or `dispatch_artifact` answers `This spec holds no ask blocks …`, read it as a
131
- question: either no decision is needed and you say nothing, or you forgot to make it a block. When
132
- it answers `… typed-block openings in this document are text, not blocks`, the quoted openings are
133
- blocks stored as prose (inside a line, or a paste with something before every line): fix the markdown
134
- and upload again; a mention on purpose belongs in code. Neither answer sees a spec wrapped whole in a
135
- code fence (take the fence off), a malformed opening inside a line (`::ask{`, `:::ask {`: an ask opens
136
- only as `:::ask{…}` on a line of its own), or any `dispatch_doc_edit`: after an edit that writes an
137
- ask, `dispatch_doc_read` the section and check it renders as `:::ask{#<id> …}` on its own line.
130
+ What `dispatch_issue` and `dispatch_artifact` answer about a spec's blocks, and how to check an edit
131
+ wrote one, is in [Typed blocks](skill://dispatch/references/documents.md#typed-blocks).
138
132
 
139
133
  **Wrong:** a **Decisions needed** list at the top of the spec with three bullets.
140
134
  **Right:** each decision an `:::ask{#slug}` block at the end of the design section that discusses
@@ -223,6 +217,13 @@ field, and choosing your next issue are in [Working an issue](skill://dispatch/r
223
217
 
224
218
  ## Asking
225
219
 
220
+ Two kinds of question reach a human. A decision about the design a document records is a decision
221
+ block in that document ([Decision blocks](#decision-blocks)), whatever phase the work is in and
222
+ whether or not the document was approved: a block in an approved document makes the approval
223
+ stale, which is right. A Legion phase worker sends such a decision to its architect, which writes
224
+ the block (`skill://legion-worker`). A to-do or permission only a human can give, or a decision
225
+ with no document to live in, is a `dispatch_ask`. The gates below apply to both.
226
+
226
227
  ### Before you ask
227
228
 
228
229
  Every `dispatch_ask` passes four gates first:
@@ -266,14 +267,14 @@ Every `dispatch_ask` passes four gates first:
266
267
  is not what this forbids; changing something else is. An ask that turns his complaint about
267
268
  one control into a choice about another does not address what he asked, and changing that
268
269
  other control is a change he never asked for. What is still an ask the moment you know it,
269
- even before delivery, is anything "Anything you are blocked on a human for is an open ask"
270
- (further down) lists that the delivery waits on — including a conflict between what he asked
271
- for and another of his rules, which this gate would otherwise bury as settled.
270
+ even before delivery, is anything "Anything you are blocked on a human for is visible in
271
+ Dispatch" (further down) lists that the delivery waits on — including a conflict between what
272
+ he asked for and another of his rules, which this gate would otherwise bury as settled.
272
273
 
273
274
  Nobody audits or retracts another session's asks: passing every gate, and carrying the content
274
275
  instead of pointing at another message in prose (below), is the asking session's own check.
275
276
 
276
- Open a decision with:
277
+ Open a standalone ask with:
277
278
  ```ts
278
279
  dispatch_ask({
279
280
  issue?,
@@ -293,15 +294,14 @@ References belong in the question text; `ref` is sugar that appends its `dispatc
293
294
 
294
295
  An ask is read on a phone by someone who has not read the code. Write its question and options as
295
296
  [Writing for the human](#writing-for-the-human) says, and apply its phone test before posting.
296
- No file path, line number, sequence number, document version or role token leads the question; the
297
- evidence under it may cite one where the reader would check it, or anchor the ask to the passage.
298
- Anchor a document question with `anchor: { artifact, quote, occurrence? }`; `occurrence` is
299
- zero-based and selects a repeated quote. A quote anchor is pinned to its lowest complete
297
+ Never lead a question with a file path, line number, sequence number, document version or role token: its own text explains the problem.
298
+ Evidence below it may cite one where the reader checks it, or anchor the ask to the passage with `anchor: { artifact, quote, occurrence? }`; `occurrence` is zero-based and selects a repeated quote.
299
+ A quote anchor is pinned to its lowest complete
300
300
  containing block while retaining its quote as display text, so rewording the passage keeps it
301
301
  attached; a quote spanning top-level blocks, and existing anchors without a block, stay readable
302
302
  against their original document version if their quote disappears.
303
303
 
304
- An ask must be answerable from its own text and its anchor alone. Anchor a question about a document
304
+ An ask must be answerable from its own text and its anchor alone. Anchor a to-do about a document
305
305
  passage with `anchor`. Follow up on an ask or comment with `dispatch_comment`; cite anything else
306
306
  with a `dispatch://` reference (see [References](#references)). Never write "see above", "the
307
307
  message above", or "as attached".
@@ -329,19 +329,19 @@ they must read to decide belongs in the spec in the first place — see [Artifac
329
329
 
330
330
  Before saying you are waiting for human input, call `dispatch_open_asks`. With no arguments it lists this session's active asks across open issues and project documents, including whether the human or agent owes the next reply. With `dispatch_open_asks({ project })` it lists every open ask in that project — on its issues and on its documents, whoever authored them — which is how you see what a whole project is waiting on rather than just your own asks.
331
331
 
332
- **Unsettled product shape needs a decision before implementation.** When a page, navigation entry, table key, customer-scoping rule, or persisted sidecar would set product shape that Sami has not already settled, send a one-line ask before the first implementation commit. A lane's schema decision or a contract two lanes agree does not settle product shape. This does not turn a user-specified decision or routine implementation into an approval request. A control or behaviour the human asked for in words is settled by those words, together with every choice inside it that his words do not make (where it sits, its defaults, its options): build it without an ask, as gate 4 of [Before you ask](#before-you-ask) says. This rule covers only product shape outside what he asked for, and its ask comes before the commit that sets that shape.
332
+ **Unsettled product shape needs a decision before implementation.** When a page, navigation entry, table key, customer-scoping rule, or persisted sidecar would set product shape that Sami has not already settled, write a decision block in the document that records the work before the first implementation commit; in a Legion tree the architect writes it, and a phase worker sends the decision to its architect. A lane's schema decision or a contract two lanes agree does not settle product shape. This does not turn a user-specified decision or routine implementation into an approval request. A control or behaviour the human asked for in words is settled by those words, together with every choice inside it that his words do not make (where it sits, its defaults, its options): build it without a block, as gate 4 of [Before you ask](#before-you-ask) says. This rule covers only product shape outside what he asked for, and its block comes before the commit that sets that shape.
333
333
 
334
- **Anything you are blocked on a human for is an open ask.** An agent waits on a human only through
335
- an open ask. An approval, a credential or grant to renew, a setting only they can change, a review
336
- click, a decision, or a conflict between two of their own rules: open a `dispatch_ask` the moment
337
- you know. Start with the problem and why it matters, then the constraints, options and their
338
- costs, and your recommendation. For an action only the human can perform, state what it changes
339
- and risks; never make the action itself the question. Never write it into a spec, a comment reply,
340
- a message, or a pull-request body: nothing in those paths reaches the human's Inbox, and a human
341
- who is not reading your document does not know they are the blocker. Before asking, try to remove
342
- the step: a value already on the machine, a permission you already hold, an API that replaces the
343
- click. One ask per item, `urgency: "high"` when work is stopped on it; while it is open, keep
344
- working on everything that is not.
334
+ **Anything you are blocked on a human for is visible in Dispatch.** An agent waits on a human only
335
+ through an open ask. A to-do, permission, credential or grant renewal, setting only they can
336
+ change, review click, or conflict between two of their own rules is a `dispatch_ask` the moment you
337
+ know. Start with the problem and why it matters, then the constraints, options and their costs, and
338
+ your recommendation. For an action only the human can perform, state what it changes and risks;
339
+ never make the action itself the question (a design decision is a decision block, as above). Never
340
+ write a human to-do only into a spec, a comment reply, a message, or a pull-request body: nothing
341
+ in those paths reaches the human's Inbox, and a human who is not reading the document does not
342
+ know they are the blocker. Before asking, try to remove the step: a value already on the machine, a
343
+ permission you already hold, an API that replaces the click. One ask per item, `urgency: "high"`
344
+ when work is stopped on it; while it is open, keep working on everything that is not.
345
345
 
346
346
  Once an ask is open (who answers it, handing a human a to-do, editing, retracting or resolving it,
347
347
  answering a clarification, whose turn a reply gives), see
@@ -478,7 +478,7 @@ dispatch_message({ issue, body })
478
478
  ```
479
479
 
480
480
  It returns `details` `{ issue, message }`. `body` is capped at 2,000 characters. A message is not a decision
481
- (`dispatch_ask`) or document feedback (`dispatch_comment`), and it does not wake anyone unless the issue is routed.
481
+ (a decision block, or `dispatch_ask` for a human to-do) or document feedback (`dispatch_comment`), and it does not wake anyone unless the issue is routed.
482
482
 
483
483
  A BTW, Aside or Steer frame, or a message from the Agents page, is answered as
484
484
  [Targeted and direct messages](skill://dispatch/references/messages.md) says.
@@ -59,6 +59,15 @@ When a human answers a decision written as an ask block, the answer lives on tha
59
59
  human's answer; never rewrite the question into its answer or blank its options. An edit that leaves
60
60
  an ask block without a question or with a blank option is rejected with `INVALID_ASK_BLOCK`.
61
61
 
62
+ When `dispatch_issue` or `dispatch_artifact` answers `This spec holds no ask blocks …`, read it as a
63
+ question: either no decision is needed and you say nothing, or you forgot to make it a block. When
64
+ it answers `… typed-block openings in this document are text, not blocks`, the quoted openings are
65
+ blocks stored as prose (inside a line, or a paste with something before every line): fix the markdown
66
+ and upload again; a mention on purpose belongs in code. Neither answer sees a spec wrapped whole in a
67
+ code fence (take the fence off), a malformed opening inside a line (`::ask{`, `:::ask {`: an ask opens
68
+ only as `:::ask{…}` on a line of its own), or any `dispatch_doc_edit`: after an edit that writes an
69
+ ask, `dispatch_doc_read` the section and check it renders as `:::ask{#<id> …}` on its own line.
70
+
62
71
  ## Comments and suggestions
63
72
 
64
73
  Add feedback with:
@@ -1,12 +1,12 @@
1
- # Before and after: an ask, a reply, a message, and a draft
1
+ # Before and after: a decision block, a reply, a message, and a draft
2
2
 
3
- `skill://dispatch` sends you here for worked examples: a decision written as clickable options, a
4
- reply that names its mechanism, a message that should not be sent, and a draft placed where the
5
- human reads it.
3
+ `skill://dispatch` sends you here for worked examples: a design decision in its document, a reply
4
+ that names its mechanism, a message that should not be sent, and a draft placed where the human
5
+ reads it.
6
6
 
7
7
  ## Before / after
8
8
 
9
- Before — a wall of text hides the decision and makes the choices unclickable:
9
+ Before — a wall of text hides the decision and the human never receives it:
10
10
 
11
11
  ```text
12
12
  The deploy branch has the migration and dashboard changes. Staging is fine, but release notes are
@@ -14,19 +14,23 @@ not reviewed before tomorrow's customer demo. Review the notes, ship without the
14
14
  dashboard from the release. Waiting is safest, but the list above may be stale.
15
15
  ```
16
16
 
17
- After — state the problem and make each genuinely different option a button:
17
+ After — write the decision into the document section it concerns: state the problem, and make each
18
+ genuinely different option a button, so the answer stays with the release design:
18
19
 
19
20
  ```ts
20
- dispatch_ask({
21
+ dispatch_doc_edit({
21
22
  issue: "LEGION-815",
22
- question:
23
- "Release notes are unreviewed, and tomorrow's customer demo means the release cannot wait for a later review. How should we proceed? Recommendation: review the notes, then ship, to keep the release complete and reviewed.",
24
- options: [
25
- { label: "Review notes, then ship", description: "Delays release for review but keeps the release complete and reviewed." },
26
- { label: "Ship now", description: "Meets the demo deadline but leaves the release notes unreviewed." },
27
- ],
28
- urgency: "high",
29
- anchor: { artifact: "spec", quote: "Release requires reviewed operator instructions before deployment." },
23
+ artifact: "spec",
24
+ ops: [{
25
+ op: "insert",
26
+ after: "Release requires reviewed operator instructions before deployment.",
27
+ markdown: `:::ask{#release-gate urgency="high"}
28
+ Release notes are unreviewed, and tomorrow's customer demo means the release cannot wait for a later review. How should we proceed? Recommendation: review the notes, then ship, to keep the release complete and reviewed.
29
+
30
+ - Review notes, then ship: Delays release for review but keeps the release complete and reviewed.
31
+ - Ship now: Meets the demo deadline but leaves the release notes unreviewed.
32
+ :::`,
33
+ }],
30
34
  })
31
35
  ```
32
36
 
@@ -65,8 +69,9 @@ dispatch_ask({
65
69
  })
66
70
  ```
67
71
 
68
- After — the draft is a section of the spec, and the ask anchors there. If it really must be a
69
- file, the spec and the ask both link the slug from the upload result:
72
+ After — the draft is a section of the spec, and the ask anchors there. Sending it is a to-do only a
73
+ human can complete, so it stays a standalone ask. If it really must be a file, the spec and the ask
74
+ both link the slug from the upload result:
70
75
 
71
76
  ```ts
72
77
  dispatch_doc_edit({ issue: "OPS-52", artifact: "spec", ops: [
@@ -94,6 +99,7 @@ dispatch_ask({
94
99
  { label: "Send the reviewed update", description: "Delivers the linked update today but makes its text external." },
95
100
  { label: "Hold the update", description: "Avoids sending now but leaves customers without the update." },
96
101
  ],
102
+ ref: "dispatch://OPS-52/artifact/cu-update-2026-09-15-md",
97
103
  })
98
104
  ```
99
105
 
@@ -22,10 +22,11 @@ separate coordinator to finish necessary work.
22
22
  is the issue's active phase at a time, and calling `spawn_worker` for a different role
23
23
  while a phase is active supersedes that phase: the superseded worker's
24
24
  `handoff_complete` is then refused, so finish (or deliberately abandon) one role
25
- before assigning the next. Phase workers escalate lifecycle, scope, and
26
- cross-phase matters the same way: `envoy_publish` to your own encoded token. Any role
27
- may use `dispatch_ask` directly for a standalone human question; replies return to the
28
- asking session.
25
+ before assigning the next. Phase workers escalate lifecycle, product, scope, design, and
26
+ cross-phase decisions the same way: `envoy_publish` to your own encoded token. You decide whether
27
+ one needs the human and write its decision block yourself (section 1 says what one does to an
28
+ approved root spec); a worker never writes one. Any role may use `dispatch_ask` directly for a
29
+ standalone to-do only a human can complete, and replies return to the asking session.
29
30
  - The daemon spawns each role as its own process with the issue's context already in its
30
31
  environment. Never hand-format a role token: the daemon encodes one as
31
32
  `legion-<project>-<key>-<role>` with the issue key lower-cased; for example, project `acme`,
@@ -81,8 +82,8 @@ exercise a criterion end to end, building that path is a child issue of this tre
81
82
 
82
83
  Specifications written into Dispatch follow `skill://dispatch`'s [Writing a spec](../dispatch/SKILL.md#writing-a-spec).
83
84
  Wave releases, child closures, and your own status are visible from the issue tree and the
84
- handoffs; do not narrate them into the spec or a `dispatch_message`. A blocker only Sami can
85
- clear is a `dispatch_ask`.
85
+ handoffs; do not narrate them into the spec or a `dispatch_message`. A to-do only Sami can clear
86
+ is a `dispatch_ask`.
86
87
 
87
88
  The issue's primary document **is** the root specification. Extend it in place: a new version
88
89
  that adds only the evidence each decision needs and what the human decides, each as a
@@ -156,8 +157,9 @@ decision a human settled in one of its decision blocks changes. Such a change is
156
157
  has not agreed to: put the problem behind it to them as its own decision block, with its evidence,
157
158
  at the end of the section it changes, and request approval again as above once they have answered
158
159
  it. Release no new wave and spawn no new role until the next `design-approved` arrives — work
159
- already in flight continues. A settled decision is the human's. A plan that would overturn one goes
160
- back to the planner with the decision kept, which asks the human nothing, unless the planner brings
160
+ already in flight continues. Every merger's `READY` in the tree is refused until a human approves
161
+ the latest version. A settled decision is the human's. A plan that would overturn one goes back to
162
+ the planner with the decision kept, which asks the human nothing, unless the planner brings
161
163
  evidence the human did not weigh that would change the decision, such as a measurement showing the
162
164
  settled choice cannot meet the Acceptance; then that decision block names the decision and that
163
165
  evidence. A plan never overturns a settled decision on its own. A design change that leaves all
@@ -360,16 +362,17 @@ active phase worker.
360
362
 
361
363
  ## Escalation judgment
362
364
 
363
- Controller-actionable matters are exactly re-filing a genuinely independent child,
364
- capacity, and cross-tree conflict. Use the Legion escalation operation for those. Handle
365
- everything else in the tree, or use `dispatch_ask` for a human question; workers may reach
366
- Sami directly with `dispatch_ask` the same way. Do not create a wait loop for any wake
367
- source.
368
-
369
- Never yield while waiting on a human. A human is waiting on you only where an open ask sits
370
- in their inbox, so open it before you stop: `dispatch_ask`, a decision block in the spec, or
371
- `dispatch_request_approval` for the spec gate. Otherwise proceed: proceeding is the default, and
372
- a stop that waits on nobody stalls the tree until someone notices.
365
+ Controller-actionable matters are exactly re-filing a genuinely independent child, capacity, and
366
+ cross-tree conflict. Use the Legion escalation operation for those. Handle everything else in the
367
+ tree. A product, scope, or design decision that needs the human, yours or one a worker escalated,
368
+ is a decision block you write (section 1 says what one does to the root spec's gate). A standalone
369
+ human to-do may use `dispatch_ask`; workers may reach Sami directly with it the same way. Do not
370
+ create a wait loop for any wake source.
371
+
372
+ Never yield while waiting on a human. A human is waiting on you only where an open ask sits in
373
+ their inbox, so open it before you stop: a decision block in the spec, `dispatch_ask` for a
374
+ standalone human to-do, or `dispatch_request_approval` for the spec gate. Otherwise proceed:
375
+ proceeding is the default, and a stop that waits on nobody stalls the tree until someone notices.
373
376
 
374
377
  ## Architecture components
375
378
 
@@ -323,7 +323,7 @@ priority first, then board rank ([Keeping the slots full](#keeping-the-slots-ful
323
323
  | `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)) |
324
324
  | `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)) |
325
325
  | `tick on <PROJECT>` from the Go daemon (payload `{kind: "tick"}`) | the project key; the daemon's periodic wake, whatever the slots | Recheck the trees waiting on a claim, then walk if a slot is free; post the day's report if this is the day's first turn |
326
- | Architect escalation (controller-actionable only: re-file a child as a root issue, capacity, cross-tree conflicts) | request + context | Judge and act; issue-scoped human Q&A goes through `dispatch_ask` from the owning architect, not here |
326
+ | 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 |
327
327
  | Resync report | artifact-driven anomaly list (zero-owner trees, untriaged-open, launch-failed, admission-drift) | Verify against fresh state, then heal |
328
328
  | Resync report: `admission-drift` entry | issue key + whether the daemon added it to, or removed it from, its admission list (the detail says which) | No action: the daemon already repaired it in the same run. An issue that reappears in consecutive reports is a live leak — file a LEGION issue on Dispatch with both reports pasted as evidence (never a GitHub issue) |
329
329
  | `child-status` | child key + status transition | Not controller-actionable by default; if the daemon could not route it to the parent's architect role, verify the transition and forward it with `envoy_publish` |
@@ -390,8 +390,8 @@ controller decision, not a no-op.
390
390
  ## Architect escalation
391
391
 
392
392
  Only decide controller-actionable escalations: re-filing independent work, capacity, and
393
- cross-tree conflicts. Issue-scoped human Q&A goes through `dispatch_ask` from the owning
394
- architect, not the controller.
393
+ cross-tree conflicts. The owning architect writes an issue-design decision as a decision block and
394
+ uses `dispatch_ask` only for a human to-do, not the controller.
395
395
 
396
396
  For an independence judgment, verify the child and its parent against current daemon state
397
397
  and the Dispatch issue. If the work belongs in an independent root:
@@ -60,12 +60,13 @@ only on this phase's artifact.
60
60
  You never spawn another Legion role: spawning a worker
61
61
  (`legion({op: "spawn_worker", ... })`) is architect-only. You may still use ordinary `task`
62
62
  subagents for your own phase work; none of them is a Legion role.
63
- Escalate a product, scope, cross-phase, or lifecycle decision to the owning architect with
63
+ Escalate a product, scope, design, cross-phase, or lifecycle decision to the owning architect with
64
64
  `envoy_publish` to its role topic (`notifications.role.` followed by its encoded token, see
65
65
  above), carrying the verified facts and the decision needed. `hub` only reaches subagents
66
- inside your own process, not the architect's separate one. For a durable question that needs
67
- Sami directly, you may use `dispatch_ask` yourself; replies return to your own
68
- session.
66
+ inside your own process, not the architect's separate one. Never write a decision block into a
67
+ spec yourself: the architect decides whether the human must answer it and writes the block, since
68
+ a new version of an approved root spec closes the tree's design gate. A standalone to-do only a
69
+ human can do is a `dispatch_ask`, and its replies return to your own session.
69
70
 
70
71
  Because the same agent is always resumed for its phase, you may receive more than one
71
72
  assignment across your lifetime: after you complete and go idle, a later event (a review
@@ -165,7 +166,7 @@ other tree paused.
165
166
 
166
167
  ## Phase work
167
168
 
168
- Specifications written into Dispatch follow `skill://dispatch`'s [Writing a spec](../dispatch/SKILL.md#writing-a-spec).
169
+ Specifications written into Dispatch follow `skill://dispatch`'s [Writing a spec](../dispatch/SKILL.md#writing-a-spec), except that a phase worker writes no decision block: it sends an open product, scope or design decision to its architect, which writes the block.
169
170
 
170
171
  Follow the repository's normal engineering workflow and the assigned issue's acceptance
171
172
  criteria. Your phase's own charter and the predecessor handoffs you read define the phase
@@ -481,13 +482,12 @@ committed handoff as needed, without mutating anything (see Workspace and handof
481
482
  precedence above). You will also be the one resumed, with a new prompt in this same
482
483
  session, if this phase's work needs to run again.
483
484
 
484
- When blocked on lifecycle, scope, or cross-phase matters, `envoy_publish` the owning
485
- architect a concise message: issue, phase, verified observation, what you tried, and the
486
- decision required. Reach for `dispatch_ask` yourself only for a standalone human question
487
- outside that coordination.
485
+ When blocked on a product, scope, design, lifecycle, or cross-phase decision, `envoy_publish` the
486
+ owning architect a concise message: issue, phase, verified observation, what you tried, and the
487
+ decision required.
488
488
 
489
- Never yield while blocked on a decision someone else owns. Before you stop, make the block
490
- visible where its owner will see it: a lifecycle, scope, or cross-phase decision goes to the
491
- owning architect as above, and a standalone human question goes in `dispatch_ask`. Otherwise
492
- proceed: proceeding is the default, and a phase that stops silently holds its issue until
493
- someone notices.
489
+ Never yield while blocked on a decision someone else owns. Before you stop, make the block visible
490
+ 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
492
+ proceed: proceeding is the default, and a phase that stops silently holds its issue until someone
493
+ notices.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-legion-envoy",
3
- "version": "7.1.1",
3
+ "version": "7.2.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "omp": {