@sjawhar/opencode-legion-envoy 1.28.0 → 1.29.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/README.md CHANGED
@@ -16,6 +16,7 @@ This package exposes:
16
16
  - `dispatch_ask`
17
17
  - `dispatch_edit_ask`
18
18
  - `dispatch_resolve_ask`
19
+ - `dispatch_resolve_comment`
19
20
  - `dispatch_follow`
20
21
  - `dispatch_comment`
21
22
  - `dispatch_suggest`
@@ -28,7 +29,7 @@ This package exposes:
28
29
  - `dispatch_search`
29
30
  - `dispatch_open_asks`
30
31
 
31
- The fifteen native `dispatch_*` tools create and read Dispatch issues, asks, comments,
32
+ The sixteen native `dispatch_*` tools create and read Dispatch issues, asks, comments,
32
33
  documents, and artifacts, or search all of them. They are present when `dispatch.enabled`
33
34
  resolves a server URL and bearer token from envoy.json (`~/.config/opencode/envoy.json`, merged
34
35
  with `<repo>/.opencode/envoy.json`) or the `DISPATCH_URL` and `DISPATCH_TOKEN` environment
@@ -13849,6 +13849,14 @@ var dispatchToolSpecs = [
13849
13849
  reason: z.string({ min: 1 }).describe("Why the open ask no longer needs a human answer.")
13850
13850
  })
13851
13851
  },
13852
+ {
13853
+ name: "dispatch_resolve_comment",
13854
+ description: "Resolve a review comment thread once it has been addressed - typically your own comment " + "after the document was fixed. Any session or human may resolve any open comment on an " + "open issue or project document; reopening a resolved comment is human-only (the dashboard). " + "Not for asks: use dispatch_resolve_ask.",
13855
+ arguments: (z) => ({
13856
+ comment: z.string().describe("Comment id (uuid), or a dispatch://KEY/comment/<id> or " + "dispatch://PROJECT/artifact/<slug>/comment/<id> reference; a reference accepts an " + "8+ character id prefix that is unique on its owner.")
13857
+ }),
13858
+ strict: true
13859
+ },
13852
13860
  {
13853
13861
  name: "dispatch_follow",
13854
13862
  description: "Follow or unfollow an ask. Every session that opens or replies to an ask follows it: its answer, " + "edits, resolution, and replies reach that session directly. Unfollow to stop; follow to rejoin or " + "to hear an ask you never wrote to. Whole-issue subscription is separate: envoy_subscribe " + "notifications.dispatch.issue.<KEY>.>",
@@ -14902,6 +14910,9 @@ class DispatchClient {
14902
14910
  async getComment(id) {
14903
14911
  return this.#json("GET", ["api", "v1", "comments", id]);
14904
14912
  }
14913
+ async resolveComment(id, actor) {
14914
+ return this.#json("POST", ["api", "v1", "comments", id, "resolve"], { actor });
14915
+ }
14905
14916
  async getComments(issue, artifact) {
14906
14917
  return this.#json("GET", ["api", "v1", "issues", await this.#resolveIssue(issue), "comments"], undefined, artifact ? { artifact } : undefined);
14907
14918
  }
@@ -15186,6 +15197,19 @@ async function askOwnerDetails(client, ask, resolved) {
15186
15197
  const artifact = resolved?.artifact ?? await client.getArtifact(ask.artifact_id);
15187
15198
  return { ...documentResultDetails(artifact), ask: ask.id };
15188
15199
  }
15200
+ async function commentOwnerResult(client, comment, artifact) {
15201
+ if (comment.issue_key !== null) {
15202
+ return { label: comment.issue_key, details: { issue: comment.issue_key, comment: comment.id } };
15203
+ }
15204
+ if (comment.artifact_id === undefined || comment.artifact_id === null) {
15205
+ throw new Error("document comment is missing its artifact ID");
15206
+ }
15207
+ const owner = artifact ?? await client.getArtifact(comment.artifact_id);
15208
+ return {
15209
+ label: documentTopic(owner).label,
15210
+ details: { ...documentResultDetails(owner), comment: comment.id }
15211
+ };
15212
+ }
15189
15213
  async function followedAskDetails(client, ask, resolved) {
15190
15214
  return { ...await askOwnerDetails(client, ask, resolved), follows: { ask: ask.id } };
15191
15215
  }
@@ -15196,6 +15220,7 @@ var issueFreeTools = {
15196
15220
  dispatch_issue: true,
15197
15221
  dispatch_edit_ask: true,
15198
15222
  dispatch_resolve_ask: true,
15223
+ dispatch_resolve_comment: true,
15199
15224
  dispatch_follow: true,
15200
15225
  dispatch_search: true,
15201
15226
  dispatch_open_asks: true
@@ -15318,6 +15343,7 @@ function parseDispatchRef(ref) {
15318
15343
  }
15319
15344
  var uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
15320
15345
  var askIdProblem = "ask must be a bare ask id or a dispatch://.../ask/<id> reference";
15346
+ var commentIdProblem = "comment must be a bare comment id or a dispatch://.../comment/<id> reference";
15321
15347
  var messageIdProblem = "in_reply_to must be a full message id (uuid) or a dispatch://KEY/message/<id> reference";
15322
15348
  function askId(args) {
15323
15349
  const ask = stringArg(args, "ask");
@@ -15373,6 +15399,13 @@ function argumentProblems(tool, args) {
15373
15399
  }
15374
15400
  break;
15375
15401
  }
15402
+ case "dispatch_resolve_comment": {
15403
+ const comment = optionalString(args, "comment");
15404
+ if (comment?.startsWith("dispatch://") && parseDispatchRef(comment)?.kind !== "comment") {
15405
+ problems.push(commentIdProblem);
15406
+ }
15407
+ break;
15408
+ }
15376
15409
  case "dispatch_comment": {
15377
15410
  if (optionalString(args, "quote") !== undefined && optionalString(args, "artifact") === undefined) {
15378
15411
  problems.push("artifact is required when quote is supplied");
@@ -15952,6 +15985,23 @@ async function executeDispatchTool(input) {
15952
15985
  details: await askOwnerDetails(client, ask)
15953
15986
  };
15954
15987
  }
15988
+ case "dispatch_resolve_comment": {
15989
+ const reference = stringArg(args, "comment");
15990
+ const ref = reference.startsWith("dispatch://") ? parseDispatchRef(reference) : null;
15991
+ let id = reference;
15992
+ let document;
15993
+ if (ref?.owner.kind === "issue") {
15994
+ const issueKey = ref.owner.issue;
15995
+ id = await resolveIdPrefix(input.tool, "comment", ref, () => client.getComments(issueKey));
15996
+ } else if (ref !== null) {
15997
+ const artifact = (await resolveArtifact(client, ref.owner, ref.artifact)).artifact;
15998
+ document = artifact;
15999
+ id = await resolveIdPrefix(input.tool, "comment", ref, () => client.getArtifactComments(artifact.id));
16000
+ }
16001
+ const comment = await client.resolveComment(id, actor);
16002
+ const { label, details } = await commentOwnerResult(client, comment, document);
16003
+ return { text: `Resolved comment ${comment.id} on ${label}.`, details };
16004
+ }
15955
16005
  case "dispatch_ask": {
15956
16006
  const anchorArgs = asObject(args.anchor);
15957
16007
  const owner = documentOwner();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/opencode-legion-envoy",
3
- "version": "1.28.0",
3
+ "version": "1.29.1",
4
4
  "type": "module",
5
5
  "main": "dist/src/server.js",
6
6
  "exports": {
@@ -199,6 +199,8 @@ Use `retracted` when the question is obsolete and `resolved` when you found the
199
199
  in its Conversation card and reply thread. Resolution is not an answer: it never records a human decision, and an answered ask cannot be
200
200
  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
201
201
  `dispatch_comment` (mutually exclusive with `reply_to`).
202
+ A review comment you opened has its own closer, `dispatch_resolve_comment` — see
203
+ [Comments and suggestions](#comments-and-suggestions).
202
204
 
203
205
  A human answers or asks back from the same field; a question-shaped free-text answer is offered as a clarification first. A human
204
206
  reply while your ask is still open (the delivered `comment.created` carries `ask_state: open`) is a request for clarification, not
@@ -353,7 +355,21 @@ the human needs to act; see [Asking](#asking). Comments are edited only by their
353
355
  dashboard. A delivered `comment.created` event carries the comment `id`; reply to it with
354
356
  `dispatch_comment({ reply_to: <id> })`.
355
357
 
356
- Propose an exact replacement instead of describing it:
358
+ Resolve your own review comment once you have addressed it:
359
+
360
+ ```ts
361
+ dispatch_resolve_comment({ comment })
362
+ ```
363
+
364
+ `comment` is the comment id, or a `dispatch://KEY/comment/<id>` or `dispatch://PROJECT/artifact/<slug>/comment/<id>` reference (an
365
+ 8+ character id prefix is resolved against the owner's comments). It returns the owner details plus `comment`, and takes no reason —
366
+ say what you did in a `reply_to` first if the thread needs it. The server lets any session or human resolve any open comment, so
367
+ resolve only threads you opened or were asked to close; reopening a resolved thread is human-only (from the dashboard), though your
368
+ reply to it reopens it. Asks are closed with `dispatch_resolve_ask` instead.
369
+
370
+ An exact replacement for document text is a suggestion (`dispatch_suggest`), never a comment; a
371
+ comment is for a question or a note the human answers in words. A human accepts a suggestion with
372
+ one click and cannot accept a comment, so propose the replacement instead of describing it:
357
373
 
358
374
  ```ts
359
375
  dispatch_suggest({ issue?, project?, artifact, ref?, quote, replace_with, body?, occurrence? })
@@ -384,6 +400,11 @@ with your text. Never do that: the spec is edited in place with `dispatch_doc_ed
384
400
  artifact by the slug shown in the upload result or by its filename, and a project document by its artifact id, slug, or filename; the
385
401
  slug also arrives on `artifact.created` events.
386
402
 
403
+ Documents are CommonMark. A bare `<https://example.com|text>` is a CommonMark autolink and is normalised: the angle brackets are
404
+ dropped and the URL keeps `|text`. A backslash-escaped `\<https://example.com|text>` displays as `<https://example.com|text>` in the
405
+ document but comes back re-escaped (`\<`) from `dispatch_doc_read`. A Slack mrkdwn draft, or any other payload that is not Markdown,
406
+ still belongs inside a fenced code block, where it survives verbatim both ways.
407
+
387
408
  ## Structure over stream
388
409
 
389
410
  Dispatch is a structured workspace, never a message stream (Sami, 2026-09-13, verbatim: "strange