@sjawhar/opencode-legion-envoy 3.2.7 → 3.2.9

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. " + `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, and a line of three or more colons in code inside a typed block, indented less than four columns from where the typed block's lines start, is refused, since the browser editor ends the typed block there: indent it four or more spaces, or move the code block out); 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: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/opencode-legion-envoy",
3
- "version": "3.2.7",
3
+ "version": "3.2.9",
4
4
  "type": "module",
5
5
  "main": "dist/src/server.js",
6
6
  "exports": {
@@ -575,7 +575,9 @@ with the document's reference.
575
575
 
576
576
  The server declares typed document blocks at `GET /api/v1/schema/blocks`. Write one only with the
577
577
  container-directive form `:::name{#block-id key="value"}` on its own line, ordinary block children,
578
- and a closing `:::` at the same nesting. An unclosed typed block at document level is rejected. For
578
+ and a closing line of as many colons at the same nesting. A typed block directly inside another needs the outer
579
+ one's fence a colon longer (`::::callout{…}` around a `:::callout{…}`), and so does one whose code holds a `:::` line;
580
+ Dispatch writes its fences that way. An unclosed typed block at document level is rejected. For
579
581
  a new typed block, omit `#block-id`; Dispatch mints it. When editing an existing typed block, retain
580
582
  its id and every rendered attribute. Never copy an existing block's id into new markdown: an id
581
583
  names one block, so an insert, upload or suggestion whose markdown names an id the document holds
@@ -66,17 +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
- list item's code, do not survive the next read; and a line of three or more colons in code inside a typed block,
74
- indented less than four columns from where the typed block's lines start, is `INVALID_OP`, since the browser editor
75
- ends the typed block there - indent it four or more spaces (a tab reaches only the next tab stop, which inside a list
76
- item or a blockquote can be two columns away), or move the code block out of the typed block. Text a `replace` writes
77
- that would read as block syntax at a line start is stored escaped and reads back as the characters you sent: `---` over
78
- a paragraph is stored `\---`, not a rule, so to add a rule, `insert` it beside the paragraph (`insert` with markdown
79
- `***`). Use zero-based
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
75
+ that typed block's fence longer than any such line the browser editor would end it at. Text a `replace` writes that
76
+ would read as block syntax at a line start is stored escaped and reads back as the characters you sent: `---` over a
77
+ paragraph is stored `\---`, not a rule, so to add a rule, `insert` it beside the paragraph (`insert` with markdown
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
80
80
  `occurrence` for a
81
81
  repeated target; re-read a missing or ambiguous target before retrying. Pass `summary` to name the version when recording a decision.
82
82
 
@@ -117,7 +117,7 @@ block before or after it. The delete is what costs the id (below); a typed block
117
117
  carries it, which works only in that order, because an insert carrying an id the document still holds is refused.
118
118
  Then read the document back with
119
119
  `dispatch_doc_read` and read the passage and its neighbours, not a grep for the words you added: an empty
120
- `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
121
121
  empties that paragraph — the block and its id stay, holding nothing — and only a read shows what the document now says.
122
122
 
123
123
  A batch that leaves the document's semantic identity unchanged — including its inline anchor marks, so an edit that only orphans a
@@ -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.