@sjawhar/opencode-legion-envoy 3.2.8 → 3.2.10

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.
@@ -14117,7 +14117,7 @@ var dispatchToolSpecs = [
14117
14117
  artifact: z2.string().describe("Project document artifact id, slug, or filename containing the quoted text; optional when ref names it.").optional(),
14118
14118
  ref: z2.string().describe("Optional dispatch:// issue or document reference.").optional(),
14119
14119
  quote: z2.string().describe("Exact document text to replace."),
14120
- replace_with: z2.string().describe("Replacement text."),
14120
+ replace_with: z2.string().describe("Replacement text; a CR LF or a lone carriage return in it is written as a line feed."),
14121
14121
  body: z2.string({ max: 2000 }).describe("Optional rationale, at most 2,000 characters.").optional(),
14122
14122
  occurrence: z2.number({ int: true, min: 0 }).describe("Optional zero-based occurrence of quote.").optional()
14123
14123
  }),
@@ -14142,7 +14142,7 @@ var dispatchToolSpecs = [
14142
14142
  ops: [{ op: "delete_column", block: "table-123", index: 1 }],
14143
14143
  precondition: { blocks: [{ id: "table-123", token: "sha256:current-table-token" }] }
14144
14144
  },
14145
- 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. 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 - where the block holding it cannot be written without that paragraph, the replace is INVALID_OP and the refusal names the delete that removes it instead. ` + "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. 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. " + `The spec (or any document) holds requirements, design, and decisions - never progress, status, or timestamps. ${OWNER_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
14145
+ 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. 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. 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_GUIDANCE}`,
14146
14146
  arguments: (z2) => ({
14147
14147
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14148
14148
  project: z2.string().describe("Project key owning the document.").optional(),
@@ -14151,15 +14151,15 @@ var dispatchToolSpecs = [
14151
14151
  ops: z2.array(z2.object({
14152
14152
  op: z2.enum(DOC_EDIT_OPS).describe("Edit operation."),
14153
14153
  find: z2.string().describe("Text of the target as rendered, for replace or delete; inline markdown (**bold**, `code`) is tolerated and must be balanced; a leading '# ' matches a heading. A delete of a block's entire text removes the block.").optional(),
14154
- with: z2.string().describe("Replacement text for replace: inside a code block, the code's literal text as sent (line breaks at its end do not survive a read); text that would read as block syntax at a line start, such as '---' over a paragraph, is stored escaped and reads back as those characters, so a rule is added with insert beside the paragraph; elsewhere parsed as inline markdown within the matched block; a marker of a different kind from the block's own is literal text, one of the same kind is refused unless it is a heading rename (where a level named by find is what lets with change it), a backslash escape keeps prose that merely looks like a marker, a block marker after a hard line break is refused because replace cannot open a new block, and any non-empty value that renders to no text is refused - only an empty value deletes the match.").optional(),
14154
+ with: z2.string().describe("Replacement text for replace: inside a code block, the code's literal text as sent (line breaks at its end do not survive a read); text that would read as block syntax at a line start, such as '---' over a paragraph, is stored escaped and reads back as those characters, so a rule is added with insert beside the paragraph; elsewhere parsed as inline markdown within the matched block; a marker of a different kind from the block's own is literal text, one of the same kind is refused unless it is a heading rename (where a level named by find is what lets with change it), a backslash escape keeps prose that merely looks like a marker, a block marker after a hard line break is refused because replace cannot open a new block, and any non-empty value that renders to no text is refused - only an empty value deletes the match. A CR LF or a lone carriage return in it is written as a line feed.").optional(),
14155
14155
  occurrence: z2.number({ int: true, min: 0 }).describe("Optional zero-based match occurrence.").optional(),
14156
- markdown: z2.string().describe("Markdown to insert.").optional(),
14156
+ markdown: z2.string().describe("Markdown to insert; a CR LF or a lone carriage return in it is written as a line feed.").optional(),
14157
14157
  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(),
14158
14158
  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(),
14159
14159
  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(),
14160
14160
  index: z2.number({ int: true, min: 0 }).describe("Zero-based row or column index for delete_row or delete_column.").optional(),
14161
14161
  type: z2.string().describe("Typed block name for retype.").optional(),
14162
- attributes: z2.unknown().describe("Typed block attributes for retype.").optional()
14162
+ 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()
14163
14163
  }, { strict: true })).describe("Flat tagged edits; an operation takes only the keys below, a misspelled one is refused rather than ignored, and the server validates the fields its op requires."),
14164
14164
  precondition: z2.object({
14165
14165
  document: z2.string({ min: 1 }).describe("Token for the exact canonical document returned by dispatch_doc_read.").optional(),
@@ -14206,7 +14206,7 @@ var dispatchToolSpecs = [
14206
14206
  project: z2.string().describe("Project key for an unlinked document.").optional(),
14207
14207
  name: z2.string().describe("Artifact filename shown in Dispatch."),
14208
14208
  path: z2.string().describe("Local path to the file to upload.").optional(),
14209
- content: z2.string().describe("Inline text to store as a Markdown document.").optional(),
14209
+ content: z2.string().describe("Inline text to store as a Markdown document; a CR LF or a lone carriage return in it is stored as a line feed.").optional(),
14210
14210
  summary: z2.string().describe("Optional version summary.").optional()
14211
14211
  }),
14212
14212
  validation: {
@@ -16907,7 +16907,9 @@ ${followsAsk(askOwner)}`,
16907
16907
  const head = nothingChanged ? `Applied ${edited.applied} ops${retypedText}; nothing changed (${versionText})` : `Applied ${edited.applied} ops${retypedText} (${versionText})`;
16908
16908
  const unchangedOps = edited.unchanged_ops ?? [];
16909
16909
  const unchangedText = unchangedOps.length === 0 ? "" : `; ${unchangedOps.length === 1 ? "operation" : "operations"} ${unchangedOps.join(", ")} changed nothing`;
16910
- const applied = `${head}${unchangedText}`;
16910
+ const lostOps = edited.lost_ops;
16911
+ const lostText = lostOps === undefined || lostOps !== null && lostOps.length === 0 ? "" : lostOps === null ? "; could not confirm this edit survived, because the live document is being reloaded \u2014 re-read it" : `; ${versionText} carries text the live document no longer has: a concurrent change removed what ${lostOps.length === 1 ? "operation" : "operations"} ${lostOps.join(", ")} wrote \u2014 re-read the document`;
16912
+ const applied = `${head}${unchangedText}${lostText}`;
16911
16913
  const adviceLines = renderAdvice(input.tool, resolvedTopic(resolved).label, edited.advice, {});
16912
16914
  const tokenTrailer = edited.token === undefined ? [] : [`Document token: ${edited.token}`];
16913
16915
  return {
@@ -16921,6 +16923,7 @@ ${followsAsk(askOwner)}`,
16921
16923
  applied: edited.applied,
16922
16924
  ...edited.version === null ? {} : { version: edited.version.number },
16923
16925
  ...nothingChanged ? { changed: false } : {},
16926
+ ...lostOps === undefined ? {} : { lost_ops: lostOps },
16924
16927
  ...edited.token === undefined ? {} : { token: edited.token },
16925
16928
  ...edited.advice === undefined ? {} : { advice: edited.advice }
16926
16929
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/opencode-legion-envoy",
3
- "version": "3.2.8",
3
+ "version": "3.2.10",
4
4
  "type": "module",
5
5
  "main": "dist/src/server.js",
6
6
  "exports": {
@@ -571,6 +571,10 @@ nothing and the document is intact. Nothing retries it for you: the Dispatch cli
571
571
  Wait a few seconds and make the same call again. A second refusal in a row is worth telling your human about,
572
572
  with the document's reference.
573
573
 
574
+ `dispatch_doc_read` can answer it too, though it writes nothing: a read never opens a live room, and waits out
575
+ a room that is reloading, so it is refused only when the document's durable copy cannot be read or decoded.
576
+ Retry it the same way.
577
+
574
578
  ## Typed blocks
575
579
 
576
580
  The server declares typed document blocks at `GET /api/v1/schema/blocks`. Write one only with the
@@ -9,8 +9,8 @@ reject a stale edit.
9
9
  dispatch_doc_edit({ issue?, project?, artifact, ops, precondition?, summary? })
10
10
  ```
11
11
  It returns issue or project-document owner details plus `applied`, optional `version`, `changed`,
12
- `unchanged_ops`, and the document token this edit produced, rendered as a `Document token: <token>`
13
- line. `ops` is an array of this exact `EditOp` shape:
12
+ `unchanged_ops`, `lost_ops`, and the document token this edit produced, rendered as a
13
+ `Document token: <token>` line. `ops` is an array of this exact `EditOp` shape:
14
14
 
15
15
  ```ts
16
16
  type EditOp = {
@@ -66,15 +66,17 @@ heading's actual level — `find="## Old"`, `with="### New"` retitles and makes
66
66
  so `find="# Old"` renames the text and keeps whatever level it selected. `with` that forms more than one
67
67
  paragraph is rejected (`INVALID_OP` on `with`) — see the recipe for a multi-paragraph rewrite below; so is any non-empty `with` that
68
68
  renders to no text, which a line indented four spaces or a tab does (markdown reads that as a code block), as does whitespace
69
- alone. An empty `with` deletes the matched text on purpose; where the block holding it cannot be written without that
70
- paragraph, the replace is `INVALID_OP`, and the refusal names the `delete` that removes it instead. Inside a code
69
+ alone. An empty `with` deletes the matched text on purpose; a list item, quote, typed block or footnote definition left
70
+ holding only the emptied paragraph keeps it, and reads back holding it. Emptying a task item's first paragraph while
71
+ another block follows it in the item is `INVALID_OP`, since the browser editor reads no such item as a task. Inside a code
71
72
  block none of this applies: `with` is the code's literal text, written as sent, whitespace, markdown syntax and
72
73
  references included, except that line breaks at the end of the code's text, and a line holding only whitespace in a
73
74
  list item's code, do not survive the next read. A line of colons in code inside a typed block is kept: Dispatch writes
74
75
  that typed block's fence longer than any such line the browser editor would end it at. Text a `replace` writes that
75
76
  would read as block syntax at a line start is stored escaped and reads back as the characters you sent: `---` over a
76
77
  paragraph is stored `\---`, not a rule, so to add a rule, `insert` it beside the paragraph (`insert` with markdown
77
- `***`). Use zero-based
78
+ `***`). A CR LF or a lone carriage return in any text you write - an insert's markdown, a `with` in text or in code,
79
+ a suggestion, an upload or a spec - is stored as a line feed, as the browser editor reads both. Use zero-based
78
80
  `occurrence` for a
79
81
  repeated target; re-read a missing or ambiguous target before retrying. Pass `summary` to name the version when recording a decision.
80
82
 
@@ -115,7 +117,7 @@ block before or after it. The delete is what costs the id (below); a typed block
115
117
  carries it, which works only in that order, because an insert carrying an id the document still holds is refused.
116
118
  Then read the document back with
117
119
  `dispatch_doc_read` and read the passage and its neighbours, not a grep for the words you added: an empty
118
- `with` deletes the matched text on purpose where the block allows it, so a `replace` whose `with` you meant to fill
120
+ `with` deletes the matched text on purpose, so a `replace` whose `with` you meant to fill
119
121
  empties that paragraph — the block and its id stay, holding nothing — and only a read shows what the document now says.
120
122
 
121
123
  A batch that leaves the document's semantic identity unchanged — including its inline anchor marks, so an edit that only orphans a
@@ -158,6 +160,17 @@ token; Dispatch applies no part of that batch. It is the hashline `#TAG` propert
158
160
  not line numbers: canonical Markdown lines shift under concurrent edits and rendering changes, while block ids
159
161
  survive moves and retyping.
160
162
 
163
+ A browser editing the same document while your edit is in flight never makes your edit a silent
164
+ no-op. The two changes merge, and a human's deletion of the paragraph you are rewriting wins — but
165
+ you are told. `409 EDIT_LOST_TO_CONCURRENT_CHANGE` means the whole batch was refused and nothing
166
+ was written: re-read the document and decide again, as with `PRECONDITION_FAILED`. A success whose
167
+ `lost_ops` names operations means the version was written and the live document already lacks what
168
+ those operations wrote, because the deletion landed after the version: re-read before building on
169
+ it. `lost_ops: []` is the ordinary outcome, and a result that says it could not confirm the edit
170
+ survived means the room is reloading — re-read. An operation that only removes text (`delete`,
171
+ `delete_row`, `delete_column`, a `replace` that shortens) is never reported lost: a concurrent
172
+ deletion cannot undo a removal.
173
+
161
174
  `retype` turns the paragraph or typed block with `block` into the named typed `type` in place. It keeps the
162
175
  block id, keeps a typed block's body, and uses `attributes` for client-owned typed attributes. Use it when
163
176
  an existing paragraph is the question that should become a decision.
@@ -307,18 +307,46 @@ this proof.
307
307
  reviewer answers each thread it opened with exactly one of `Accepted: fixed in <commit> — <one line>`,
308
308
  `Accepted: not a defect — <reason>`, or `Still open: <what remains>`; nothing else is an
309
309
  acceptance, and nobody replies after an `Accepted:` (any later reply that is not itself an
310
- `Accepted:` — the opener's own follow-up included — leaves the thread open, since the command
311
- reads only the newest comment). The review App can reply on a thread but cannot resolve it:
310
+ `Accepted:` — the opener's own follow-up included — leaves the thread open, because resolution
311
+ considers only the newest comment). The review App can reply on a thread but cannot resolve it:
312
312
  GitHub grants resolving a review thread to the pull request's author, and the implementer opens
313
- every Legion pull request (`packages/daemon/src/daemon/AGENTS.md`, GitHub Apps). So the
314
- **implementer** runs `legion threads resolve --pr <number> --repo <owner>/<repo>` before every
315
- push that answers a review (the corrective push and the final `.legion/` deletion push) and
316
- pastes its output into the `Threads` section. The command resolves each unresolved thread
317
- whose newest comment is the opener's own `Accepted:` reply, one `resolveReviewThread` per
318
- thread, prints `resolved <url>` or `left open <url> — newest reply by <login> is not an acceptance`,
319
- and exits 1 naming the thread's URL and GitHub's message when GitHub refuses one; report that
320
- exit to the architect, which opens an ask for a human to resolve the thread by hand —
321
- never skip it silently. The merger runs the same command once more before publishing READY
313
+ every Legion pull request (`packages/daemon/src/daemon/AGENTS.md`, GitHub Apps).
314
+ When `LEGION_GRANT_FILE` or `LEGION_GRANT` is set, use `legion threads resolve --pr <number> --repo <owner>/<repo>`; when neither is set, use `gh api graphql`
315
+ with the session's GitHub credential and the fallback below.
316
+ In a Legion pane, the **implementer** runs the command before every push that answers a review
317
+ (the corrective push and the final `.legion/` deletion push) and pastes its output into the
318
+ `Threads` section. The command resolves each unresolved thread whose newest submitted comment is
319
+ the opener's own `Accepted:` reply, one `resolveReviewThread` per thread, prints `resolved <url>`
320
+ or `left open <url> — newest reply by <login> is not an acceptance`, and exits 1 naming the
321
+ thread's URL and GitHub's message when GitHub refuses one.
322
+
323
+ Without a grant, page through `reviewThreads`, skip `isResolved: true`, and compare the opener
324
+ with the newest comment. Query shape, inside `repository { pullRequest { … } }`:
325
+
326
+ ```graphql
327
+ reviewThreads(first: 100, after: $after) {
328
+ pageInfo { hasNextPage endCursor }
329
+ nodes {
330
+ id isResolved
331
+ opener: comments(first: 1) { nodes { author { login } } }
332
+ newest: comments(last: 1) { nodes { author { login } body state } }
333
+ }
334
+ }
335
+ ```
336
+
337
+ Resolve only when the newest comment is submitted, its `author { login }` equals the opener's,
338
+ and its `body`, after removing leading spaces, tabs, CR, and LF, begins `Accepted:`. For each
339
+ such thread:
340
+
341
+ ```graphql
342
+ mutation($threadId: ID!) {
343
+ resolveReviewThread(input: { threadId: $threadId }) { thread { isResolved } }
344
+ }
345
+ ```
346
+
347
+ Re-read `reviewThreads` and confirm that thread's `isResolved` is true. In either route, report
348
+ a refused resolution to the architect, which opens an ask for a human to resolve the thread by
349
+ hand — never skip it silently. The merger runs the command once more before publishing READY
322
350
  and does not publish while any `left open` line remains.
323
351
  - **Correctness fixes land in this PR; cleanup is one named fast-follow.** A finding that
324
352
  changes behavior, hides an error, or breaks a gate is fixed here — never deferred.