@sjawhar/opencode-legion-envoy 7.4.0 → 7.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/dispatch.js CHANGED
@@ -14243,7 +14243,7 @@ var dispatchToolSpecs = [
14243
14243
  ops: [{ op: "delete_column", block: "table-123", index: 1 }],
14244
14244
  precondition: { blocks: [{ id: "table-123", token: "sha256:current-table-token" }] }
14245
14245
  },
14246
- description: "Apply deterministic document edits: replace or delete quoted text, insert markdown at an anchor, retype an identified paragraph or typed block into a schema-declared typed block, delete or move a whole block by its id, or delete a table row or column in place. " + "Do not use it for review feedback or for reading; use dispatch comment, dispatch suggest, or dispatch doc-read instead. " + `For replace, delete, and quote anchors, find text as rendered: inline Markdown (**bold**, \`code\`) is tolerated and must be balanced; a leading '# ' matches a heading at any level. replace is inline: with is the new text of the matched span, so a marker of a different kind from the block's own stays literal text ('4. Design' written into a heading). A with that opens with a marker of the same kind as the matched block's own would write it twice and is INVALID_OP - including prose that merely looks like one ('1999. was a year' into an ordered item), which you write as text by escaping it ('1999\\. was a year'). The exception is a heading rename whose find carried a heading marker: replace(find="## Old", with="## New") gives '## New', and a different level applies only when find named the heading's actual level (find "## Old" with "### New" makes it an h3), since '# ' selects a heading without naming its level. A task item (\`- [ ]\` / \`- [x]\`) is ticked the same way: when find matches from the start of the item's text and with opens with '[x] ' or '[ ] ', the opening sets the item's box and the rest is its new text (replace(find="Write it", with="[x] Write it") ticks it); the same opening over a plain list item or a paragraph stays literal text. Any non-empty with that renders to no text - a line indented four spaces or a tab, which markdown reads as a code block, or whitespace alone - is INVALID_OP rather than a silent deletion; pass an empty with to delete the matched text on purpose - a list item, quote, typed block or footnote definition left holding only the emptied paragraph keeps it. ` + "with cannot open a new block: after a hard line break inside with (two trailing spaces, or a backslash, before the newline) a heading, bullet, '1.'/'1)' ordered, or '>' blockquote marker is INVALID_OP too, since that line would stay escaped text inside the matched block - use insert, plus delete for what it replaces, to add the block. A hard break in with is itself INVALID_OP when the matched text is in a heading or a table cell, which are written on one line. " + "A delete whose find is a block's entire text removes the block (a list emptied of its items goes too); delete with block removes any block by id, and move with block relocates one. A delete or retype that would take an ask block out of the document while its ask is open is refused, with nothing sent. delete_row and delete_column take a table block and a zero-based index, preserving the table block id and refusing to remove cells with open asks or unresolved comments. " + 'Insert and move anchors also accept "start", "end", "heading:<exact heading text>", and "block:<id>"; block ids and their tokens come from GET /api/v1/artifacts/{artifact UUID}/blocks (the route takes the artifact UUID, not its slug). ' + "Optionally require the state just read: precondition selects exactly one of a document token from dispatch doc-read, or block {id, token} values from /blocks. A block guard must include every block the batch changes; Dispatch resolves quote targets and rejects an uncovered batch rather than applying it. Use a document token for insert or move, which depend on document order. Prefer block tokens when the covered content blocks are independent sections. Tokens include inline marks, so a fresh human comment also makes a stale edit fail. PRECONDITION_FAILED means re-read; EDIT_QUEUE_FULL means back off before retrying. " + "The result carries the document token this edit produced, so a chain of guarded edits passes each result's token as the next edit's precondition with no dispatch doc-read between them. " + "A batch that leaves the document exactly as it was mints no version, named or not, and the result says nothing changed and names each operation that did nothing. " + "A change a browser removes while the edit is in flight is never reported as applied: EDIT_LOST_TO_CONCURRENT_CHANGE means the write was refused and nothing was written, so re-read the document and decide again, as with PRECONDITION_FAILED; lost_ops on a successful result names operations whose text the live document no longer has, because the deletion landed after the version was written. " + `The spec (or any document) holds requirements, design, and decisions - never progress, status, or timestamps. ${OWNER_REFERENCE} ${SPEC_WRITING_POINTER}`,
14246
+ 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}`,
14247
14247
  arguments: (z2) => ({
14248
14248
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14249
14249
  project: z2.string().describe("Project key owning the document.").optional(),
@@ -14257,7 +14257,7 @@ var dispatchToolSpecs = [
14257
14257
  markdown: z2.string().describe("Markdown to insert; a CR LF or a lone carriage return in it is written as a line feed.").optional(),
14258
14258
  after: z2.string().describe(`Insert or move after this anchor: a quote of the neighbouring block's text, or one of "start", "end", "heading:<exact heading text>", "block:<id>".`).optional(),
14259
14259
  before: z2.string().describe(`Insert or move before this anchor: a quote of the neighbouring block's text, or one of "start", "end", "heading:<exact heading text>", "block:<id>".`).optional(),
14260
- block: z2.string().describe("Block id for retype, delete, move, delete_row, or delete_column: the #id of a typed block, or an id from GET /api/v1/artifacts/{id}/blocks.").optional(),
14260
+ block: z2.string().describe("Block id for retype, delete, move, delete_row, or delete_column: the #id of a typed block, or an id dispatch doc-read --blocks lists.").optional(),
14261
14261
  index: z2.number({ int: true, min: 0 }).describe("Zero-based row or column index for delete_row or delete_column.").optional(),
14262
14262
  type: z2.string().describe("Typed block name for retype.").optional(),
14263
14263
  attributes: z2.unknown().describe("Typed block attributes for retype; a CR LF or a lone carriage return in a string value is written as a line feed.").optional()
@@ -14265,9 +14265,9 @@ var dispatchToolSpecs = [
14265
14265
  precondition: z2.object({
14266
14266
  document: z2.string({ min: 1 }).describe("Token for the exact canonical document returned by dispatch doc-read.").optional(),
14267
14267
  blocks: z2.array(z2.object({
14268
- id: z2.string({ min: 1 }).describe("Stable block id from GET /api/v1/artifacts/{id}/blocks."),
14268
+ id: z2.string({ min: 1 }).describe("Stable block id dispatch doc-read --blocks lists."),
14269
14269
  token: z2.string({ min: 1 }).describe("That block's full-state token, including inline marks.")
14270
- }), { min: 1 }).describe("Every content block this batch changes, each with the token returned by /blocks.").optional()
14270
+ }), { min: 1 }).describe("Every content block this batch changes, each with the token dispatch doc-read --blocks lists.").optional()
14271
14271
  }).describe("Optional optimistic-concurrency guard; select exactly one of document or blocks.").optional(),
14272
14272
  summary: z2.string().describe("Optional named-version summary.").optional()
14273
14273
  }),
@@ -14276,12 +14276,13 @@ var dispatchToolSpecs = [
14276
14276
  {
14277
14277
  name: "dispatch_doc_read",
14278
14278
  example: { issue: "DSP-1" },
14279
- description: "Read a live document or a named document version, or the text of an uploaded file at its latest or named version. " + "Do not use it for issue status, asks, or events; " + "use dispatch read instead. Supply ref, issue, or project plus artifact; issue plus an omitted artifact reads the primary document. " + "A live read returns its document token for an optional dispatch doc-edit precondition; use /blocks for per-block tokens. " + "A picture (an uploaded PNG, JPEG, GIF or WebP of at most 3,750,000 bytes, 5 MB once base64-encoded) comes back as an image you see, with its name, type, size and version, every time you ask, including one a read already showed this session; " + "any other file that is not UTF-8 text is described, with the route that serves its bytes. " + OWNER_REFERENCE,
14279
+ description: "Read a live document or a named document version, or the text of an uploaded file at its latest or named version. " + "Do not use it for issue status, asks, or events; " + "use dispatch read instead. Supply ref, issue, or project plus artifact; issue plus an omitted artifact reads the primary document. " + "A live read returns its document token for an optional dispatch doc-edit precondition. With blocks it lists the live document's blocks instead of its markdown, in document order: each block's id, type, token, anchored comments and asks, and opening words, and under a table each row by the index delete_row takes and each cell by its row and the column delete_column takes; block ids, block tokens and those indexes are what dispatch doc-edit takes. " + "A picture (an uploaded PNG, JPEG, GIF or WebP of at most 3,750,000 bytes, 5 MB once base64-encoded) comes back as an image you see, with its name, type, size and version, every time you ask, including one a read already showed this session; " + "any other file that is not UTF-8 text is described, with the route that serves its bytes. " + OWNER_REFERENCE,
14280
14280
  arguments: (z2) => ({
14281
14281
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14282
14282
  project: z2.string().describe("Project key owning the document.").optional(),
14283
14283
  artifact: z2.string().describe("Optional project document artifact id, slug, or filename; primary document by default for an issue.").optional(),
14284
14284
  version: z2.number({ int: true, min: 1 }).describe("Optional version number.").optional(),
14285
+ blocks: z2.boolean().describe("List the live document's blocks, with each table's rows and cells, instead of its markdown; refused with a version, or for an uploaded file.").optional(),
14285
14286
  ref: z2.string().describe("Optional dispatch:// document reference; a picture in a conversation on the Agents page is dispatch://agent/<session id>/artifact/<slug>@vN.").optional()
14286
14287
  }),
14287
14288
  validation: documentOwnerValidation(true)
@@ -16637,6 +16638,12 @@ function argumentProblems(tool, args) {
16637
16638
  }
16638
16639
  break;
16639
16640
  }
16641
+ case "dispatch_doc_read": {
16642
+ if (optionalBoolean(args, "blocks") === true && optionalNumber(args, "version") !== undefined) {
16643
+ problems.push("blocks lists the live document, the one dispatch doc-edit changes; name no version");
16644
+ }
16645
+ break;
16646
+ }
16640
16647
  }
16641
16648
  return problems;
16642
16649
  }
@@ -17315,6 +17322,47 @@ ${text}`,
17315
17322
  details
17316
17323
  };
17317
17324
  }
17325
+ var BLOCK_OPENING_CHARS = 60;
17326
+ async function listDocumentBlocks(client, artifact) {
17327
+ const before = await client.docRead(artifact.id);
17328
+ const blocks = await client.artifactBlocks(artifact.id);
17329
+ const after = await client.docRead(artifact.id);
17330
+ if (before.token !== after.token || before.markdown !== after.markdown) {
17331
+ throw new Error(`${artifact.name} changed while dispatch doc-read --blocks read it, so the opening words could name the wrong text; nothing is listed. Run it again.`);
17332
+ }
17333
+ const bytes = new TextEncoder().encode(before.markdown);
17334
+ const decoder = new TextDecoder;
17335
+ const lines = [
17336
+ `Blocks of ${artifact.name}, in document order, each indented under the block holding it: id, type, (token; anchored comments and asks), opening words. A table lists its rows by the index delete_row takes and its cells by the row and the column delete_column takes.`
17337
+ ];
17338
+ const holders = [];
17339
+ for (const block of blocks) {
17340
+ while (holders.length > 0 && block.from >= (holders.at(-1)?.to ?? 0))
17341
+ holders.pop();
17342
+ const indent = " ".repeat(holders.length);
17343
+ const flat = decoder.decode(bytes.subarray(block.from, block.to)).replace(/\s+/g, " ").trim();
17344
+ const points = Array.from(flat);
17345
+ const opening = points.length > BLOCK_OPENING_CHARS ? `${points.slice(0, BLOCK_OPENING_CHARS).join("")}\u2026` : flat;
17346
+ const { comments, asks } = block.references;
17347
+ lines.push(`${indent}${block.id} ${block.type} (${block.token ?? "no token"}; ${comments} comment${comments === 1 ? "" : "s"}, ${asks} ask${asks === 1 ? "" : "s"}): ${opening}`);
17348
+ if (block.type === "table") {
17349
+ if (block.table === undefined) {
17350
+ lines.push(`${indent} This Dispatch does not report table rows yet, so no row or column index is listed.`);
17351
+ } else {
17352
+ for (const row of block.table.rows) {
17353
+ lines.push(`${indent} row ${row.index}, ${row.id === "" ? "no id yet" : `id ${row.id}`}`);
17354
+ for (const cell of row.cells) {
17355
+ const under = cell.header === "" ? "" : `, under ${JSON.stringify(cell.header)}`;
17356
+ lines.push(`${indent} row ${cell.row} column ${cell.column}, ${cell.id === "" ? "no id yet" : `id ${cell.id}`}${under}: ${cell.text === "" ? "(empty)" : cell.text}`);
17357
+ }
17358
+ }
17359
+ }
17360
+ }
17361
+ holders.push(block);
17362
+ }
17363
+ return { text: lines.join(`
17364
+ `), token: before.token };
17365
+ }
17318
17366
  async function refuseOpenDecisionBlocks(client, tool, resolved) {
17319
17367
  const artifact = resolved.artifact;
17320
17368
  const latest = artifact.approval?.latest_version;
@@ -18089,11 +18137,29 @@ ${followsAsk(askOwner)}`,
18089
18137
  const { agentArtifact } = ownerArguments;
18090
18138
  if (agentArtifact !== undefined) {
18091
18139
  const artifact = await client.getAgentArtifact(agentArtifact.session, agentArtifact.slug);
18140
+ if (optionalBoolean(args, "blocks") === true) {
18141
+ throw new Error(`${artifact.name} is an uploaded ${artifact.kind}, not a document, so it has no blocks. Read it with dispatch doc-read without --blocks.`);
18142
+ }
18092
18143
  return readUploadedFile(client, artifact, { session: agentArtifact.session, artifact: artifact.slug }, optionalNumber(args, "version") ?? agentArtifact.version, `agent/${agentArtifact.session}`, shown);
18093
18144
  }
18094
18145
  const artifactReference = optionalString(args, "artifact") ?? (ownerArguments.ref?.kind === "spec" || ownerArguments.ref?.kind === "artifact" ? ownerArguments.ref.id : undefined);
18095
18146
  const resolved = await resolveDocument(documentOwner(), artifactReference);
18096
18147
  const version2 = optionalNumber(args, "version") ?? ownerArguments.ref?.version;
18148
+ if (optionalBoolean(args, "blocks") === true) {
18149
+ if (resolved.artifact.kind === "file" || resolved.artifact.kind === "image") {
18150
+ throw new Error(`${resolved.artifact.name} is an uploaded ${resolved.artifact.kind}, not a document, so it has no blocks. Read it with dispatch doc-read without --blocks.`);
18151
+ }
18152
+ const listed = await listDocumentBlocks(client, resolved.artifact);
18153
+ return {
18154
+ text: listed.token === undefined ? listed.text : `${listed.text}
18155
+
18156
+ Document token: ${listed.token}`,
18157
+ details: resolved.owner.kind === "project" ? {
18158
+ project: resolved.artifact.project,
18159
+ document: documentLabel(resolved.artifact.project, resolved.artifact.slug)
18160
+ } : { issue: resolved.issue?.key }
18161
+ };
18162
+ }
18097
18163
  if (resolved.artifact.kind === "file" || resolved.artifact.kind === "image") {
18098
18164
  const { artifact } = resolved;
18099
18165
  return readUploadedFile(client, artifact, resolved.owner.kind === "project" ? {
@@ -14100,7 +14100,7 @@ var dispatchToolSpecs = [
14100
14100
  ops: [{ op: "delete_column", block: "table-123", index: 1 }],
14101
14101
  precondition: { blocks: [{ id: "table-123", token: "sha256:current-table-token" }] }
14102
14102
  },
14103
- description: "Apply deterministic document edits: replace or delete quoted text, insert markdown at an anchor, retype an identified paragraph or typed block into a schema-declared typed block, delete or move a whole block by its id, or delete a table row or column in place. " + "Do not use it for review feedback or for reading; use dispatch comment, dispatch suggest, or dispatch doc-read instead. " + `For replace, delete, and quote anchors, find text as rendered: inline Markdown (**bold**, \`code\`) is tolerated and must be balanced; a leading '# ' matches a heading at any level. replace is inline: with is the new text of the matched span, so a marker of a different kind from the block's own stays literal text ('4. Design' written into a heading). A with that opens with a marker of the same kind as the matched block's own would write it twice and is INVALID_OP - including prose that merely looks like one ('1999. was a year' into an ordered item), which you write as text by escaping it ('1999\\. was a year'). The exception is a heading rename whose find carried a heading marker: replace(find="## Old", with="## New") gives '## New', and a different level applies only when find named the heading's actual level (find "## Old" with "### New" makes it an h3), since '# ' selects a heading without naming its level. A task item (\`- [ ]\` / \`- [x]\`) is ticked the same way: when find matches from the start of the item's text and with opens with '[x] ' or '[ ] ', the opening sets the item's box and the rest is its new text (replace(find="Write it", with="[x] Write it") ticks it); the same opening over a plain list item or a paragraph stays literal text. Any non-empty with that renders to no text - a line indented four spaces or a tab, which markdown reads as a code block, or whitespace alone - is INVALID_OP rather than a silent deletion; pass an empty with to delete the matched text on purpose - a list item, quote, typed block or footnote definition left holding only the emptied paragraph keeps it. ` + "with cannot open a new block: after a hard line break inside with (two trailing spaces, or a backslash, before the newline) a heading, bullet, '1.'/'1)' ordered, or '>' blockquote marker is INVALID_OP too, since that line would stay escaped text inside the matched block - use insert, plus delete for what it replaces, to add the block. A hard break in with is itself INVALID_OP when the matched text is in a heading or a table cell, which are written on one line. " + "A delete whose find is a block's entire text removes the block (a list emptied of its items goes too); delete with block removes any block by id, and move with block relocates one. A delete or retype that would take an ask block out of the document while its ask is open is refused, with nothing sent. delete_row and delete_column take a table block and a zero-based index, preserving the table block id and refusing to remove cells with open asks or unresolved comments. " + 'Insert and move anchors also accept "start", "end", "heading:<exact heading text>", and "block:<id>"; block ids and their tokens come from GET /api/v1/artifacts/{artifact UUID}/blocks (the route takes the artifact UUID, not its slug). ' + "Optionally require the state just read: precondition selects exactly one of a document token from dispatch doc-read, or block {id, token} values from /blocks. A block guard must include every block the batch changes; Dispatch resolves quote targets and rejects an uncovered batch rather than applying it. Use a document token for insert or move, which depend on document order. Prefer block tokens when the covered content blocks are independent sections. Tokens include inline marks, so a fresh human comment also makes a stale edit fail. PRECONDITION_FAILED means re-read; EDIT_QUEUE_FULL means back off before retrying. " + "The result carries the document token this edit produced, so a chain of guarded edits passes each result's token as the next edit's precondition with no dispatch doc-read between them. " + "A batch that leaves the document exactly as it was mints no version, named or not, and the result says nothing changed and names each operation that did nothing. " + "A change a browser removes while the edit is in flight is never reported as applied: EDIT_LOST_TO_CONCURRENT_CHANGE means the write was refused and nothing was written, so re-read the document and decide again, as with PRECONDITION_FAILED; lost_ops on a successful result names operations whose text the live document no longer has, because the deletion landed after the version was written. " + `The spec (or any document) holds requirements, design, and decisions - never progress, status, or timestamps. ${OWNER_REFERENCE} ${SPEC_WRITING_POINTER}`,
14103
+ 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}`,
14104
14104
  arguments: (z2) => ({
14105
14105
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14106
14106
  project: z2.string().describe("Project key owning the document.").optional(),
@@ -14114,7 +14114,7 @@ var dispatchToolSpecs = [
14114
14114
  markdown: z2.string().describe("Markdown to insert; a CR LF or a lone carriage return in it is written as a line feed.").optional(),
14115
14115
  after: z2.string().describe(`Insert or move after this anchor: a quote of the neighbouring block's text, or one of "start", "end", "heading:<exact heading text>", "block:<id>".`).optional(),
14116
14116
  before: z2.string().describe(`Insert or move before this anchor: a quote of the neighbouring block's text, or one of "start", "end", "heading:<exact heading text>", "block:<id>".`).optional(),
14117
- block: z2.string().describe("Block id for retype, delete, move, delete_row, or delete_column: the #id of a typed block, or an id from GET /api/v1/artifacts/{id}/blocks.").optional(),
14117
+ block: z2.string().describe("Block id for retype, delete, move, delete_row, or delete_column: the #id of a typed block, or an id dispatch doc-read --blocks lists.").optional(),
14118
14118
  index: z2.number({ int: true, min: 0 }).describe("Zero-based row or column index for delete_row or delete_column.").optional(),
14119
14119
  type: z2.string().describe("Typed block name for retype.").optional(),
14120
14120
  attributes: z2.unknown().describe("Typed block attributes for retype; a CR LF or a lone carriage return in a string value is written as a line feed.").optional()
@@ -14122,9 +14122,9 @@ var dispatchToolSpecs = [
14122
14122
  precondition: z2.object({
14123
14123
  document: z2.string({ min: 1 }).describe("Token for the exact canonical document returned by dispatch doc-read.").optional(),
14124
14124
  blocks: z2.array(z2.object({
14125
- id: z2.string({ min: 1 }).describe("Stable block id from GET /api/v1/artifacts/{id}/blocks."),
14125
+ id: z2.string({ min: 1 }).describe("Stable block id dispatch doc-read --blocks lists."),
14126
14126
  token: z2.string({ min: 1 }).describe("That block's full-state token, including inline marks.")
14127
- }), { min: 1 }).describe("Every content block this batch changes, each with the token returned by /blocks.").optional()
14127
+ }), { min: 1 }).describe("Every content block this batch changes, each with the token dispatch doc-read --blocks lists.").optional()
14128
14128
  }).describe("Optional optimistic-concurrency guard; select exactly one of document or blocks.").optional(),
14129
14129
  summary: z2.string().describe("Optional named-version summary.").optional()
14130
14130
  }),
@@ -14133,12 +14133,13 @@ var dispatchToolSpecs = [
14133
14133
  {
14134
14134
  name: "dispatch_doc_read",
14135
14135
  example: { issue: "DSP-1" },
14136
- description: "Read a live document or a named document version, or the text of an uploaded file at its latest or named version. " + "Do not use it for issue status, asks, or events; " + "use dispatch read instead. Supply ref, issue, or project plus artifact; issue plus an omitted artifact reads the primary document. " + "A live read returns its document token for an optional dispatch doc-edit precondition; use /blocks for per-block tokens. " + "A picture (an uploaded PNG, JPEG, GIF or WebP of at most 3,750,000 bytes, 5 MB once base64-encoded) comes back as an image you see, with its name, type, size and version, every time you ask, including one a read already showed this session; " + "any other file that is not UTF-8 text is described, with the route that serves its bytes. " + OWNER_REFERENCE,
14136
+ description: "Read a live document or a named document version, or the text of an uploaded file at its latest or named version. " + "Do not use it for issue status, asks, or events; " + "use dispatch read instead. Supply ref, issue, or project plus artifact; issue plus an omitted artifact reads the primary document. " + "A live read returns its document token for an optional dispatch doc-edit precondition. With blocks it lists the live document's blocks instead of its markdown, in document order: each block's id, type, token, anchored comments and asks, and opening words, and under a table each row by the index delete_row takes and each cell by its row and the column delete_column takes; block ids, block tokens and those indexes are what dispatch doc-edit takes. " + "A picture (an uploaded PNG, JPEG, GIF or WebP of at most 3,750,000 bytes, 5 MB once base64-encoded) comes back as an image you see, with its name, type, size and version, every time you ask, including one a read already showed this session; " + "any other file that is not UTF-8 text is described, with the route that serves its bytes. " + OWNER_REFERENCE,
14137
14137
  arguments: (z2) => ({
14138
14138
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14139
14139
  project: z2.string().describe("Project key owning the document.").optional(),
14140
14140
  artifact: z2.string().describe("Optional project document artifact id, slug, or filename; primary document by default for an issue.").optional(),
14141
14141
  version: z2.number({ int: true, min: 1 }).describe("Optional version number.").optional(),
14142
+ blocks: z2.boolean().describe("List the live document's blocks, with each table's rows and cells, instead of its markdown; refused with a version, or for an uploaded file.").optional(),
14142
14143
  ref: z2.string().describe("Optional dispatch:// document reference; a picture in a conversation on the Agents page is dispatch://agent/<session id>/artifact/<slug>@vN.").optional()
14143
14144
  }),
14144
14145
  validation: documentOwnerValidation(true)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/opencode-legion-envoy",
3
- "version": "7.4.0",
3
+ "version": "7.5.1",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "dist/src/server.js",
@@ -406,7 +406,9 @@ every version that records a decision is named with `dispatch doc-edit --summary
406
406
 
407
407
  Read the current document before changing it with `dispatch doc-read` (`--issue`, or `--project`
408
408
  and `--artifact`, an optional `--version`, or `--ref`).
409
- It prints live or versioned markdown with open marks. A live read ends with a document token; `--issue` with no
409
+ It prints live or versioned markdown with open marks. A live read ends with a document token; `--blocks` lists the live
410
+ document's block ids, tokens and table rows instead, which `dispatch doc-edit` takes to address a block that is not a typed
411
+ block. `--issue` with no
410
412
  `--artifact` reads the issue specification; a project needs `--artifact`; and a `dispatch://PROJECT/artifact/<document-ref>`
411
413
  ref supplies both, where `document-ref` is the slug (an id or a filename resolves when no document has that slug). A file
412
414
  `dispatch artifact` uploaded reads its text at the latest or named version, or a description when it is not UTF-8 text.
@@ -60,10 +60,15 @@ table's block id. A row index includes the header: row `0` is the header and its
60
60
  row and any row's last column cannot be deleted. An index is required. A missing, non-integer, negative, or out-of-range index is
61
61
  `INVALID_OP` on `index`, naming the supplied value and the table's actual dimensions before making any change. Markdown parsing
62
62
  canonicalizes short ragged rows by padding missing cells, so column deletion preserves every non-selected cell in the canonical table.
63
- `GET /api/v1/artifacts/<artifact UUID>/blocks` reports a table's own references plus its descendant cell anchors.
64
- `GET /api/v1/artifacts/<artifact UUID>/blocks/<block id>` places one block: its path from the top-level block down, and for a cell
65
- the row index (0 is the header, what `delete_row` takes), the column index (what `delete_column` takes), the text of the header cell
66
- drawn above it (with colspans or rowspans, the column the cell is drawn in) and the row's cells. A row or column
63
+ `dispatch doc-read --issue <KEY> --blocks` (or `--project` and `--artifact`) lists every block of the live document in place of its
64
+ markdown, one line each, in document order and indented under the block holding it: its id, its type, its token, how many comments
65
+ and asks anchor to it (a table counts its cells' anchors) and its opening words. Under a table it lists each row by its index, what
66
+ `delete_row` takes (0 is the header), and each cell by its row and its column, what `delete_column` takes, with the opening words
67
+ of the header drawn above it. A cell spanning columns or rows counts once in its row, so its column is that index and not where
68
+ the markdown writes it. A Dispatch that does not list table rows yet says so instead of an index. It takes no `--version`, since
69
+ only the live document's ids and tokens address an edit, and an uploaded file has no blocks.
70
+ `GET /api/v1/artifacts/<artifact UUID>/blocks` and `GET /api/v1/artifacts/<artifact UUID>/blocks/<block id>` answer the same
71
+ facts for a reader outside the command. A row or column
67
72
  deletion that would remove an open ask or unresolved comment anchor is `INVALID_OP` on `index`, naming the axis and anchor ids;
68
73
  answered asks and resolved comments are history and do not block it. A `find` or quote anchor tolerates inline Markdown
69
74
  (`**bold**`, `` `code` ``) and a leading `# ` selects a heading by its text; a miss names the quote and the three nearest blocks so
@@ -160,14 +165,12 @@ blockquote or list item removes that emptied container, the list too when no ite
160
165
  held nothing else (`> - Only.` loses the blockquote as well as the item and the list). The moved block itself keeps its id,
161
166
  as do `replace` (an accepted empty `with` and a heading-level change included), `insert` and `retype`; `retype` carries the
162
167
  paragraph's id onto the typed block it becomes. Block ids are the `#id` a typed block renders
163
- (`:::ask{#5467e5ce-…}`) and, for every block including untyped ones, the `id` rows from
164
- `GET /api/v1/artifacts/<artifact UUID>/blocks` (or `/api/v1/issues/{key}/artifacts/{slug}/blocks`), each with its `type` and byte range
165
- in canonical markdown; the UUID route does not accept a slug. A later operation in the same atomic batch that names a block removed by
168
+ (`:::ask{#5467e5ce-…}`) and, for every block including untyped ones, the ids `dispatch doc-read --blocks` lists. A later operation in the same atomic batch that names a block removed by
166
169
  an earlier `delete {block}` fails as `INVALID_OP` naming the earlier operation and the parent block that cascaded the removal. A move
167
170
  whose anchor lies inside the moved block, or a delete that would leave a typed block without the body its content rule requires, is
168
171
  `INVALID_OP` naming the field and the rule.
169
172
 
170
- `GET /api/v1/artifacts/<artifact UUID>/blocks` includes a full-state `token` on every block, including
173
+ `dispatch doc-read --blocks` lists a full-state `token` on every block, including
171
174
  inline marks. To reject a stale edit, pass `--precondition-json` with exactly one of
172
175
  `{"document": "<document token>"}` or
173
176
  `{"blocks": [{"id": "<block id>", "token": "<block token>"}]}`. A document token comes from
@@ -234,7 +234,22 @@ child has closed, and sign off the root only after it closes. This holds until t
234
234
  integrating phase ships (dispatch://LEGION-223). When that check's tester fails, the daemon sends
235
235
  the child back to its implementer; a failure whose fix belongs in other work becomes a new
236
236
  corrective child wave you create and release, and the tree returns to children-in-flight. Do not
237
- downgrade the parent criterion or silently carry the failure forward.
237
+ downgrade the parent criterion or silently carry the failure forward. If the root stays `done`
238
+ through its linger with a child still in the workflow, the daemon hands that child to Legion as a
239
+ root of its own (`todo` with the `legion` label). It carries that child's phase, gate, phase records
240
+ and pull request into a new generation and starts the role of that phase; that root's architect
241
+ gets a catch-up that says `handed_over` and `carried` and resumes from the phase, never
242
+ registering its spec or asking for approval again. A child whose old root had no gate carries
243
+ none, and its catch-up says `handed_over` without `carried`. Its workers already run their phases,
244
+ so its architect decomposes nothing. It reads the root's phase from the catch-up (for a root
245
+ `held`, the phase its `held` notice names). Before `awaiting_merge` it registers its spec, the gate
246
+ its READY waits for, and starts nothing on that gate's `design-approved`; in `awaiting_merge` or
247
+ `production_check` the READY is already through, so it registers no spec, asks no human to approve
248
+ one, and signs off after the production check. `awaiting_merge` can move back: a red or
249
+ conflicting head returns the root to `implementing` and its next READY reads the gate again, so if
250
+ a later `ready-refused` arrives because the head was sent back, it registers then, as that row
251
+ says. The child stays its own tree after hand-over; reopening the old root later does not take it
252
+ back. Park a child first when its work should stop with the root.
238
253
 
239
254
  ## 4. Integration verification
240
255
 
@@ -361,9 +376,11 @@ active phase worker.
361
376
 
362
377
  | Wake | Procedure |
363
378
  | --- | --- |
379
+ | `carried-gate-opened` | Only a root whose catch-up said `carried` gets it. Its `version` and `reason`: a human approved the old root's spec document at `version`, which opened the gate this root carried; the daemon has released any READY it refused and started any child waiting on it. It is not `design-approved`: do not proceed to section 2 or release a wave because of it, and start nothing. |
364
380
  | `child-status` | A child of your tree left the workflow or re-entered it; the notice's reason names the child and its new status. `todo` (a human's move, your `release_children`, or your `rerun_child`) means the child runs again under your tree from planning, and the daemon starts it; you start nothing. `backlog`, `icebox` or `triage` (a human's move, or your `park_child`) means the daemon has suspended the child's workers, and it advances no further until it is set back to `todo`. What the rest of the tree does is your decision. |
365
381
  | `child-closed` | Read the child completion and remaining open children. Re-scope open work, park obsolete work with `park_child`, and release an appropriate next wave with `release_children`. When a worker waits on this child for a missing surface (see `phase-finished`), tell it to continue with `envoy_publish` to its role topic. The last child's close is not the end-game (section 3). |
366
- | `design-approved` | Its `version` is the approved one. A human approved the root spec document at its current version; the gate is open. Proceed to section 2. |
382
+ | `child-release-stalled` | Its `reason` names a child your `release_children` released while Dispatch already showed it `todo`, and that child's parent, whose release has not reached the daemon after three tries. Check the parent's record in `legion state`; run `release_children` for the child again once the parent is recorded in your tree, and tell the controller if it never is. |
383
+ | `design-approved` | Its `version` is the approved one. A human approved the root spec document at its current version; the gate is open. Proceed to section 2 — except on a root whose catch-up said `handed_over`: its workers already run their phases, so start nothing; the approval releases any READY the gate refused. |
367
384
  | `design-changes-requested` | Its `version` and `reason`. A human asked for changes to the root spec at `version`, for `reason`. Revise the spec as the reason asks and request approval again as section 1 says; stay parked; the gate is closed. |
368
385
  | `phase-finished` | The daemon has already moved the issue to its next phase by its fixed table and started that phase's role; you start nothing. Read the committed handoff for the finishing phase; if it shows unresolved gaps, tell the role now working the issue (`envoy_publish` to its role topic). A `planner` notice that names a departure from the spec's design defers to section 1's full condition: when the approved Summary, Acceptance, scope and settled decisions still hold, the plan is the record; otherwise change the root spec and request approval again as section 1 says. A `reviewer` notice whose GitHub review is `CHANGES_REQUESTED` needs nothing from you: the daemon has returned the issue to `implementing` (Dispatch `in_progress`) and started the **implementer**, whose correction goes through the tester and the reviewer again, never straight to retro. A `reviewer` notice with an `APPROVED` review means the daemon has started retro (step 5). An `implementer` notice for `production_check` is its production report: read the record on the pull request and the issue, then run step 7. A `tester` notice with `verdict: "fail"` — its handoff carries `implementerProof.verdict: "rejected"`, or a failure naming the production-like proof — has gone back to the **implementer** by the daemon's table; never supply the proof from another role. A worker that reports no surface reaches the changed path sends that report instead of completing its phase, so the daemon starts nothing more on that issue and the worker stays idle in its session, not suspended: file a child issue in this tree to build the surface (infrastructure, tooling, or a skill), and when that child's `child-closed` arrives, tell the waiting worker to continue with `envoy_publish` to its role topic. That report is never a reason to advance the phase. |
369
386
  | `pr-blocked` | Its `reason` names the pull request and the `max_fix_attempts` it reached, or reads `Issue reached review_round_cap=<N>.` (the end of this row). The count is of heads pushed onto a red verdict that changed something outside `.legion/` — handoff-only pushes (`.legion/` paths only) never count, a push by the review App (a planner's, tester's, reviewer's or architect's) never counts, and the head after a red the tester's red tests earned (a review-App push that changed a path outside `.legion/`, however many handoff-only pushes follow it) does not count either — so after the tester's handoff-only push onto the implementer's red, the implementer's next push does count; a push the daemon cannot classify (a listener without `changed_paths`, a list the listener stopped at 100 paths or 32,768 runes of text, a push listing no commits) does. Published once per exhausted count, not on every later red verdict for that count. Read the failed CI evidence and recovery attempts. The notice moves nothing, and no later CI red within the attempt that ran the count out (the counted head, and every head pushed on top of it until a push lands on a head CI judged green, the tester's red tests and handoff onto the held red included) sends the work back from `testing` or `reviewing` either: the issue stays in its phase until you move it, or a worker's own decision does (a tester's `fail`, a reviewer's request for changes). A head pushed onto green CI leaves that attempt, so its red still comes as a `checks-red`. In `implementing`, give the implementer the failing checks (`envoy_publish` to its role topic). In any later phase a worker holds, tell that worker (`envoy_publish` to its role topic) to move the issue back to `implementing` with `request_backward_move`, naming the failing checks, and the daemon starts the implementer; or file a corrective child. In `awaiting_merge`, where no worker holds a phase and a backward move is refused, open a `dispatch ask` naming the failing checks for the human who merges, as section 6 does for a conflict there. Do not treat the blocked PR as final. A `review_round_cap` reason is the implementer's round count instead: every move back to an earlier phase counts one, and the notice comes again with each round past the cap. It moves nothing either, since the round that reached the cap has already started its worker. Tell the rounds apart by what announced each: a tester's fail by its `phase-finished` with `verdict: "fail"`; a request for changes by the reviewer's `phase-finished` over a `CHANGES_REQUESTED` review; the daemon's own move by a `checks-red` notice, whose reason names either a conflict (`the head conflicts with <base>`) or the required checks that failed (`CI is red at <head>`); a worker's `request_backward_move` by a `phase-finished` from the role that asked, whose reason reaches only the task of the worker the move started, so ask that worker. Judge the cap by the rounds that are not conflicts. Conflict returns need nothing from you beyond the next round. Tester fails, requests for changes and CI-red returns (a `checks-red` notice is raised for CI only when a required check fails on code the branch pushed) are the work going in circles: narrow it, re-plan it with the responsible worker, or ask a human with `dispatch ask`. Judge a worker's backward move by the reason that worker gives. A move you ordered for a conflict found before `awaiting_merge` (section 6) is a conflict return. The merger's move back to `reviewing` because its task names no approved head is not the work going in circles either. A move that sends back code found wrong counts with the tester fails. |
@@ -372,7 +389,7 @@ active phase worker.
372
389
  | A reply on an ask you follow | Interpret the reply in the issue's design context. Answer in its thread (`dispatch comment --reply-to <comment id>`; under an open ask whose next move is yours, such as the approval request you must revise or hand back, `--reply-to-ask <ask id> --turn agent`, since a default-turn reply hands that request back to the human and a corrected `--summary` is then refused), then adjust the plan or relay it via `envoy_publish` to the responsible worker's role token; scope and product decisions remain with you. |
373
390
  | `worker-died` | Its `role` and `phase`. That role's claim failed: its launches or prompts ran out. For a phase worker the daemon holds the issue (phase `held`) and starts nothing more on it. Reassess the work, then decide with `retry_or_escalate`: `retry` when the failure looks agent-specific or transient, `escalate` to hand the held issue to the controller when it looks environmental. |
374
391
  | `held` | Its `role` and `phase` (the phase the issue left), or `phase` with `reason: "escalated"`. Without `reason`, that phase's worker ran out of launches or prompts: the issue is held (phase `held`), the `worker-died` that comes with it is yours to answer with `retry_or_escalate` as that row says, and the controller hears of the hold too. With `reason: "escalated"`, it records your own `escalate`: the controller has the issue now, and you start nothing for it. |
375
- | `ready-refused` | Its `version` and `reason`. The merger's READY was refused, and its packet is kept with the completion so no second READY is ever needed. With `version` greater than 0, `reason` reads `READY refused: approve design version <N> before requesting READY.`: the root's spec is already registered at that version, so get it approved (section 1), and the daemon advances the merge and posts the kept packet the moment it is. With `version` 0, `reason` reads `READY refused: no design version is approved; register and approve the tree's spec before requesting READY.`: the root has no spec registered yet, so register it and get it approved (section 1) exactly as above; the daemon releases the kept packet the same way — at registration itself when that opens the gate (`gates.design: off`, or Dispatch already shows the version approved), otherwise the moment a human approves the version you registered. Either way, nothing else is needed once the gate opens. A `reason` starting `READY_PACKET_MISSING` names a READY refused before the daemon kept packets: that READY is void and the issue stays in merging, so start it over (`park_child` then `rerun_child`, for a child) or end it. |
392
+ | `ready-refused` | Its `version` and `reason`. The merger's READY was refused, and its packet is kept with the completion so no second READY is ever needed. With `version` greater than 0, `reason` reads `READY refused: approve design version <N> before requesting READY.`: the root's spec is already registered at that version, so get it approved (section 1), and the daemon advances the merge and posts the kept packet the moment it is. With `version` 0, `reason` reads `READY refused: no design version is approved; register and approve the tree's spec before requesting READY.`: the root has no spec registered yet, so register it and get it approved (section 1) exactly as above; the daemon releases the kept packet the same way — at registration itself when that opens the gate (`gates.design: off`, or Dispatch already shows the version approved), otherwise the moment a human approves the version you registered. Either way, nothing else is needed once the gate opens. On a root whose catch-up said `carried`, the gate is a copy on the old root's spec document: run nothing of section 1; tell the controller the READY waits on a human's approval of that document, and `carried-gate-opened` follows the approval. A `reason` starting `READY_PACKET_MISSING` names a READY refused before the daemon kept packets: that READY is void and the issue stays in merging, so start it over (`park_child` then `rerun_child`, for a child) or end it. |
376
393
 
377
394
  ## Escalation judgment
378
395