@sjawhar/opencode-legion-envoy 2.0.0 → 2.0.2

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.
@@ -14022,7 +14022,7 @@ var dispatchToolSpecs = [
14022
14022
  ask: "01234567-0000-4000-8000-000000000001",
14023
14023
  question: "Ship the revised plan?"
14024
14024
  },
14025
- description: "Edit an open question in place. Use it to correct or refine the same decision; retract the " + "old ask and open a new one when the decision itself changes. Previous text remains in the " + "event log. Only the asking session can edit it; answered or resolved asks cannot be edited.",
14025
+ description: "Edit an open question in place. Use it to correct or refine the same decision; retract the " + "old ask and open a new one when the decision itself changes. Previous text remains in the " + "event log. Only the asking session can edit it; answered or resolved asks cannot be edited. " + "An ask that lives as an `ask` block in a document is written in the document too, changing " + "only the fields you name - pass urgency alone and the question's wording, formatting, links " + "and comment anchors are untouched - so the edit writes a document version and closes a " + "spec's design gate until that version is " + 'approved; an option label containing ": " and a question with a line beginning ":::" are ' + "refused, naming the field, because the block cannot carry them unchanged.",
14026
14026
  arguments: (z2) => ({
14027
14027
  ask: z2.string().describe("Ask id (uuid); an 8+ hex prefix unique among this session's own open asks works too."),
14028
14028
  question: z2.string({ max: ASK_QUESTION_MAX }).describe(`Replacement decision question, at most ${ASK_QUESTION_MAX} characters.`).optional(),
@@ -14048,7 +14048,7 @@ var dispatchToolSpecs = [
14048
14048
  kind: "retracted",
14049
14049
  reason: "A newer question supersedes this one."
14050
14050
  },
14051
- description: "Retract an open question that is moot or resolve one after finding the answer. This closes the question without answering it.",
14051
+ description: "Retract an open question that is moot or resolve one after finding the answer. This closes " + "the question without answering it. An ask that lives as an `ask` block in a document is " + 'closed in the document too. A reason beginning "removed from the document in version" is ' + "refused: that marks a retraction the document's own settlement wrote.",
14052
14052
  arguments: (z2) => ({
14053
14053
  ask: z2.string().describe("Ask id (uuid) to close; an 8+ hex prefix unique among this session's own open asks " + "works too."),
14054
14054
  kind: z2.enum(["retracted", "resolved"]).describe("Whether the ask is retracted or self-resolved."),
@@ -14131,7 +14131,7 @@ var dispatchToolSpecs = [
14131
14131
  ops: [{ op: "delete_column", block: "table-123", index: 1 }],
14132
14132
  precondition: { blocks: [{ id: "table-123", token: "sha256:current-table-token" }] }
14133
14133
  },
14134
- 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; a leading '# ' matches a heading. replace is inline: with is the new text of the matched span, so a leading list or heading marker stays literal text. " + "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 spec (or any document) holds requirements, design, and decisions - never progress, status, or timestamps. ${OWNER_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
14134
+ 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 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. " + "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}`,
14135
14135
  arguments: (z2) => ({
14136
14136
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14137
14137
  project: z2.string().describe("Project key owning the document.").optional(),
@@ -14139,8 +14139,8 @@ var dispatchToolSpecs = [
14139
14139
  ref: z2.string().describe("Optional dispatch:// issue or document reference.").optional(),
14140
14140
  ops: z2.array(z2.object({
14141
14141
  op: z2.enum(DOC_EDIT_OPS).describe("Edit operation."),
14142
- find: z2.string().describe("Text of the target as rendered, for replace or delete; inline markdown (**bold**, `code`) is tolerated; a leading '# ' matches a heading. A delete of a block's entire text removes the block.").optional(),
14143
- with: z2.string().describe("Replacement text for replace, parsed as inline markdown within the matched block; a leading list or heading marker is literal text.").optional(),
14142
+ 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(),
14143
+ with: z2.string().describe("Replacement text for replace, 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, and any non-empty value that renders to no text is refused - only an empty value deletes the match.").optional(),
14144
14144
  occurrence: z2.number({ int: true, min: 0 }).describe("Optional zero-based match occurrence.").optional(),
14145
14145
  markdown: z2.string().describe("Markdown to insert.").optional(),
14146
14146
  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(),
@@ -16902,7 +16902,12 @@ ${followsAsk(askOwner)}`,
16902
16902
  });
16903
16903
  const retyped = ops.filter((operation) => operation.op === "retype").length;
16904
16904
  const versionText = edited.version === null ? "no new version" : `version ${edited.version.number}`;
16905
- const applied = retyped === 0 ? `Applied ${edited.applied} ops (${versionText})` : `Applied ${edited.applied} ops; retyped ${retyped} block${retyped === 1 ? "" : "s"} (${versionText})`;
16905
+ const retypedText = retyped === 0 ? "" : `; retyped ${retyped} block${retyped === 1 ? "" : "s"}`;
16906
+ const nothingChanged = edited.changed === false;
16907
+ const head = nothingChanged ? `Applied ${edited.applied} ops${retypedText}; nothing changed (${versionText})` : `Applied ${edited.applied} ops${retypedText} (${versionText})`;
16908
+ const unchangedOps = edited.unchanged_ops ?? [];
16909
+ const unchangedText = unchangedOps.length === 0 ? "" : `; ${unchangedOps.length === 1 ? "operation" : "operations"} ${unchangedOps.join(", ")} changed nothing`;
16910
+ const applied = `${head}${unchangedText}`;
16906
16911
  const adviceLines = renderAdvice(input.tool, resolvedTopic(resolved).label, edited.advice, {});
16907
16912
  return {
16908
16913
  text: [`${applied} ${notSubscribed(resolvedTopic(resolved))}`, ...adviceLines].join(`
@@ -16910,6 +16915,7 @@ ${followsAsk(askOwner)}`,
16910
16915
  details: writeResultDetails(resolved, {
16911
16916
  applied: edited.applied,
16912
16917
  ...edited.version === null ? {} : { version: edited.version.number },
16918
+ ...nothingChanged ? { changed: false } : {},
16913
16919
  ...edited.advice === undefined ? {} : { advice: edited.advice }
16914
16920
  })
16915
16921
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/opencode-legion-envoy",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "type": "module",
5
5
  "main": "dist/src/server.js",
6
6
  "exports": {
@@ -417,10 +417,16 @@ At least one field besides `ask` is required. Use this only while the same decis
417
417
  log and invalidates any answer draft against the prior `edited_at` revision, so the human sees the new wording and explicitly reconfirms.
418
418
  An answered or resolved ask cannot be edited. If the decision is moot or superseded, retract the old ask and open a new one.
419
419
 
420
- An ask that lives as an `ask` block in a document takes its question and options from the document, so edit those with
421
- `dispatch_doc_edit` (`replace` on the block's text, or `delete`/`insert` on its option items), never with `dispatch_edit_ask`: the
422
- next document save reasserts the block's text over whatever `dispatch_edit_ask` wrote, and that reversal is logged as an edit by
423
- the document's saver. `dispatch_edit_ask` is for asks opened with `dispatch_ask` that have no block.
420
+ An ask that lives as an `ask` block in a document keeps its question and options in the block, and `dispatch_edit_ask` writes the
421
+ block along with the row, so the edit stands and the document reads the same. It changes only the fields you name: pass `urgency`
422
+ alone and the question's own wording, formatting, links and comment anchors are untouched. Pass `question` or `options` and that part
423
+ is rewritten, so anchors inside the text you replaced move as they would for any document edit. Either way it is a document edit: it
424
+ writes a new version, which on a spec awaiting approval closes the design gate until the new version is approved. Editing the block
425
+ with `dispatch_doc_edit` works too and is the way to change anything else about it, including adding formatting to a question.
426
+ Re-sending a field unchanged rewrites nothing, so retrying the whole ask is safe.
427
+ Two shapes the block cannot carry are refused outright, naming the field and writing nothing: an option label containing `": "`,
428
+ which is what separates a label from its description, and a question with a line beginning `:::`. Blank lines separate paragraphs;
429
+ a single newline is kept as a line break.
424
430
 
425
431
  An ask stays open until a human answers, unless its question no longer needs that answer. Retract a moot or superseded question, or
426
432
  self-resolve one after finding the answer:
@@ -432,7 +438,10 @@ dispatch_resolve_ask({
432
438
  })
433
439
  ```
434
440
  Use `retracted` when the question is obsolete and `resolved` when you found the answer. Include the reason because the question remains
435
- in its Conversation card and reply thread. Resolution is not an answer: it never records a human decision, and an answered ask cannot be
441
+ in its Conversation card and reply thread; a reason beginning `removed from the document in version` is refused, because that is how a
442
+ retraction the document's own settlement wrote is recognised. Resolving a block ask records it in the block too, so it stays resolved
443
+ however the document moves afterwards — while deleting the block from the document is the other way to close one, and putting the block
444
+ back reopens it. Resolution is not an answer: it never records a human decision, and an answered ask cannot be
436
445
  resolved. A human may reply to an open or answered ask; so may you, e.g. after finding the answer — use `reply_to_ask` on
437
446
  `dispatch_comment` (mutually exclusive with `reply_to`).
438
447
  A review comment you opened has its own closer, `dispatch_resolve_comment` — see
@@ -507,8 +516,8 @@ omitted `artifact` reads the issue specification; a project needs `artifact`; an
507
516
  ```ts
508
517
  dispatch_doc_edit({ issue?, project?, artifact, ops, precondition?, summary? })
509
518
  ```
510
- It returns issue or project-document owner details plus `applied` and optional `version`. `ops` is an array of this
511
- exact `EditOp` shape:
519
+ It returns issue or project-document owner details plus `applied`, optional `version`, `changed`, and
520
+ `unchanged_ops`. `ops` is an array of this exact `EditOp` shape:
512
521
 
513
522
  ```ts
514
523
  type EditOp = {
@@ -545,12 +554,28 @@ canonicalizes short ragged rows by padding missing cells, so column deletion pre
545
554
  `GET /api/v1/artifacts/<artifact UUID>/blocks` reports a table's own references plus its descendant cell anchors. A row or column
546
555
  deletion that would remove an open ask or unresolved comment anchor is `INVALID_OP` on `index`, naming the axis and anchor ids;
547
556
  answered asks and resolved comments are history and do not block it. A `find` or quote anchor tolerates inline Markdown
548
- (`**bold**`, `` `code` ``) and a leading `# ` selects a heading by its text; a miss names the three nearest blocks so the next quote
549
- lands. `replace` is inline: `with` is the new text of the matched span inside its block, so a leading list or heading
550
- marker (`4. Design`, `# Title`) stays literal text and never turns the block into a list or heading; `with` that forms more than one
551
- paragraph is rejected (`INVALID_OP` on `with`) — delete the block and insert new blocks instead. Use zero-based `occurrence` for a
557
+ (`**bold**`, `` `code` ``) and a leading `# ` selects a heading by its text; a miss names the quote and the three nearest blocks so
558
+ the next quote lands, and a `find` cut before a closing `**` or `` ` `` is refused as an unbalanced inline mark rather than reported
559
+ as a miss. A `heading:` anchor matches the whole heading text exactly — a prefix of a longer heading is a miss, naming the anchor and
560
+ the nearest headings. `replace` is inline: `with` is the new text of the matched span inside its block, so a marker of a *different*
561
+ kind from the block's own (`4. Design` written into a heading, `# Title` into a paragraph) stays literal text and never turns the
562
+ block into a list or heading. A `with` that opens with a marker of the *same* kind as the matched block's own would write it twice and
563
+ is rejected (`INVALID_OP` on `with`) — including prose that merely looks like a marker (`1999. was a year` into an ordered item),
564
+ which is written as text with a backslash escape (`1999\. was a year`) — omit the marker to replace the block's text, or use `insert`
565
+ plus `delete` to change the block's kind, level or number. The one exception is a heading rename whose `find` carried a heading
566
+ marker: `replace(find="## Old", with="## New")` gives `## New`. A different level in `with` applies only when `find` named the
567
+ heading's actual level — `find="## Old"`, `with="### New"` retitles and makes it an h3 — because `# ` is the level-blind selector,
568
+ so `find="# Old"` renames the text and keeps whatever level it selected. `with` that forms more than one
569
+ paragraph is rejected (`INVALID_OP` on `with`) — delete the block and insert new blocks instead; so is any non-empty `with` that
570
+ renders to no text, which a line indented four spaces or a tab does (markdown reads that as a code block), as does whitespace
571
+ alone. An empty `with` is the one that deletes the matched text on purpose. Use zero-based `occurrence` for a
552
572
  repeated target; re-read a missing or ambiguous target before retrying. Pass `summary` to name the version when recording a decision.
553
573
 
574
+ A batch that leaves the document's semantic identity unchanged — including its inline anchor marks, so an edit that only orphans a
575
+ comment or ask anchor still mints its version — mints no version, named or not: the response carries
576
+ `changed: false` with `unchanged_ops` naming each operation that did nothing, and the tool result says nothing changed. A `summary`
577
+ does not force a version for such a batch; `POST /api/v1/artifacts/<id>/versions`, which names the current state on purpose, still does.
578
+
554
579
  A `delete` whose `find` is a block's entire text removes the block itself — the bullet, paragraph, or heading, not just its words — and
555
580
  a list emptied of every item disappears with it; a partial match keeps the block with its remaining text. Deleting the text of a bullet
556
581
  that holds a nested list hoists that list's items into the bullet's place (as an outliner does); a bullet with any other content