@sjawhar/opencode-legion-envoy 0.41.2 → 0.42.0

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.
@@ -13619,6 +13619,8 @@ var CommentEventPayloadSchema = object({
13619
13619
  var MessageEventPayloadSchema = object({
13620
13620
  id: string2().optional(),
13621
13621
  body: string2().optional(),
13622
+ reply_to: string2().nullish(),
13623
+ reply_body: string2().optional(),
13622
13624
  author: object({ kind: string2(), id: string2() }).optional()
13623
13625
  });
13624
13626
  var ChildStatusEventPayloadSchema = object({
@@ -13802,15 +13804,16 @@ var dispatchToolSpecs = [
13802
13804
  },
13803
13805
  {
13804
13806
  name: "dispatch_message",
13805
- description: "Post a note to the issue's Conversation for humans: a status they should see now, or a reply to a human's message. " + "Not a progress ledger (the issue's progress.md artifact), a decision (dispatch_ask), or document feedback " + `(dispatch_comment). Body is at most 2,000 characters. ${ISSUE_REFERENCE}`,
13807
+ description: "Post a note humans must read now: a reply to a human's message, a deliverable that landed, or a blocker only " + "they can clear. Never progress or status updates - Dispatch is a high-signal record, not a log. Not a decision " + `(dispatch_ask) or document feedback (dispatch_comment). Body is at most 2,000 characters. ${ISSUE_REFERENCE}`,
13806
13808
  arguments: (z) => ({
13807
13809
  issue: z.string().describe(ISSUE_REFERENCE),
13808
- body: z.string({ max: 2000 }).describe("Update text, at most 2,000 characters.")
13810
+ body: z.string({ max: 2000 }).describe("Update text, at most 2,000 characters."),
13811
+ reply_to: z.string().describe("Optional message id or dispatch://KEY/message/<id> reference to reply to, threading " + "this message under it so the reply stays with the original in the Conversation.").optional()
13809
13812
  })
13810
13813
  },
13811
13814
  {
13812
13815
  name: "dispatch_doc_edit",
13813
- description: "Apply deterministic text edits to an issue or project document. Do not use it for review feedback or for reading; use " + "dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. The spec (or any document) holds requirements, " + `design, and decisions \u2014 record progress in the issue's progress.md artifact instead. ${OWNER_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
13816
+ description: "Apply deterministic text edits to an issue or project document. Do not use it for review feedback or for reading; use " + "dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. The spec (or any document) holds requirements, " + `design, and decisions - never progress, status, or timestamps. ${OWNER_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
13814
13817
  arguments: (z) => ({
13815
13818
  issue: z.string().describe(ISSUE_REFERENCE).optional(),
13816
13819
  project: z.string().describe("Project key owning the document.").optional(),
@@ -14598,6 +14601,16 @@ class DispatchClient {
14598
14601
  async message(issue, input) {
14599
14602
  return this.#json("POST", ["api", "v1", "issues", await this.#resolveIssue(issue), "messages"], input);
14600
14603
  }
14604
+ async getMessage(issue, id) {
14605
+ return this.#json("GET", [
14606
+ "api",
14607
+ "v1",
14608
+ "issues",
14609
+ await this.#resolveIssue(issue),
14610
+ "messages",
14611
+ id
14612
+ ]);
14613
+ }
14601
14614
  async artifact(issue, input) {
14602
14615
  const artifactPath = ["api", "v1", "issues", await this.#resolveIssue(issue), "artifacts"];
14603
14616
  if ("content" in input)
@@ -14862,10 +14875,10 @@ function parseDispatchRef(ref) {
14862
14875
  id: targetID
14863
14876
  };
14864
14877
  }
14865
- const issueReference = ref.match(/^dispatch:\/\/([A-Z][A-Z0-9]{1,9}-[1-9][0-9]*)(?:\/(spec)|\/(log)|\/(children)|\/artifact\/([^/@]+)(?:@v(\d+))?|\/ask\/([^/]+)|\/comment\/([^/]+))?$/);
14878
+ const issueReference = ref.match(/^dispatch:\/\/([A-Z][A-Z0-9]{1,9}-[1-9][0-9]*)(?:\/(spec)|\/(log)|\/(children)|\/artifact\/([^/@]+)(?:@v(\d+))?|\/ask\/([^/]+)|\/comment\/([^/]+)|\/message\/([^/]+))?$/);
14866
14879
  if (!issueReference)
14867
14880
  return null;
14868
- const [, issue, spec, log, children, artifact, version, ask, comment] = issueReference;
14881
+ const [, issue, spec, log, children, artifact, version, ask, comment, message] = issueReference;
14869
14882
  if (!issue || version !== undefined && Number(version) < 1)
14870
14883
  return null;
14871
14884
  const owner = { kind: "issue", issue };
@@ -14887,6 +14900,8 @@ function parseDispatchRef(ref) {
14887
14900
  return { owner, kind: "ask", id: ask };
14888
14901
  if (comment)
14889
14902
  return { owner, kind: "comment", id: comment };
14903
+ if (message)
14904
+ return { owner, kind: "message", id: message };
14890
14905
  return { owner, kind: "issue", id: issue };
14891
14906
  }
14892
14907
  function askId(args) {
@@ -14899,6 +14914,16 @@ function askId(args) {
14899
14914
  }
14900
14915
  return reference.id;
14901
14916
  }
14917
+ function messageReplyTo(args) {
14918
+ const replyTo = optionalString(args, "reply_to");
14919
+ if (replyTo === undefined || !replyTo.startsWith("dispatch://"))
14920
+ return replyTo;
14921
+ const reference = parseDispatchRef(replyTo);
14922
+ if (reference?.kind !== "message") {
14923
+ throw new Error("reply_to must be a bare message id or a dispatch://.../message/<id> reference");
14924
+ }
14925
+ return reference.id;
14926
+ }
14902
14927
  function toolSchema(tool) {
14903
14928
  const spec = dispatchToolSpecs.find((candidate) => candidate.name === tool);
14904
14929
  if (!spec)
@@ -14910,7 +14935,7 @@ async function resolveOwnerArguments(tool, args, cwd, env, exec) {
14910
14935
  return { args, ref: null, owner: null };
14911
14936
  const refArgument = args.ref;
14912
14937
  const ref = typeof refArgument === "string" ? parseDispatchRef(refArgument) ?? (() => {
14913
- throw new Error("ref must be a valid dispatch:// reference such as dispatch://KEY-1, " + "dispatch://KEY-1/ask/<uuid>, dispatch://KEY-1/comment/<uuid>, " + "dispatch://KEY-1/artifact/<slug>, or dispatch://PROJECT/artifact/<slug>");
14938
+ throw new Error("ref must be a valid dispatch:// reference such as dispatch://KEY-1, " + "dispatch://KEY-1/ask/<uuid>, dispatch://KEY-1/comment/<uuid>, " + "dispatch://KEY-1/message/<uuid>, dispatch://KEY-1/artifact/<slug>, or " + "dispatch://PROJECT/artifact/<slug>");
14914
14939
  })() : null;
14915
14940
  const issueArgument = args.issue;
14916
14941
  const projectArgument = args.project;
@@ -15101,6 +15126,18 @@ function commentSummary({ comment, replies }) {
15101
15126
  return ["Comment:", ...root, "Reply chain:", ...chain.length === 0 ? ["- none"] : chain].join(`
15102
15127
  `);
15103
15128
  }
15129
+ function messageSummary({ message, replies }) {
15130
+ const root = [
15131
+ `${message.id} \xB7 ${message.author.kind} ${message.author.id}`,
15132
+ `Body: ${message.body}`
15133
+ ];
15134
+ const chain = replies.flatMap((reply) => [
15135
+ `${reply.id} \xB7 ${reply.author.kind} ${reply.author.id}`,
15136
+ `Body: ${reply.body}`
15137
+ ]);
15138
+ return ["Message:", ...root, "Reply chain:", ...chain.length === 0 ? ["- none"] : chain].join(`
15139
+ `);
15140
+ }
15104
15141
  async function openArtifactMarks(client, resolved) {
15105
15142
  const asks = resolved.owner.kind === "project" ? await client.getArtifactAsks(resolved.artifact.id) : resolved.issue?.open_asks ?? [];
15106
15143
  const marks = asks.filter((ask) => ask.state === "open" && ask.anchor?.artifact_id === resolved.artifact.id).map((ask) => `ask ${ask.id}`);
@@ -15308,9 +15345,15 @@ async function executeDispatchTool(input) {
15308
15345
  };
15309
15346
  }
15310
15347
  case "dispatch_message": {
15311
- const message = await client.message(issue(), { body: stringArg(args, "body"), actor });
15348
+ const replyTo = messageReplyTo(args);
15349
+ const message = await client.message(issue(), {
15350
+ body: stringArg(args, "body"),
15351
+ ...replyTo === undefined ? {} : { reply_to: replyTo },
15352
+ actor
15353
+ });
15354
+ const messageRef = `dispatch://${message.issue_key}/message/${message.id}`;
15312
15355
  return {
15313
- text: `Posted message ${message.id}`,
15356
+ text: `Posted message ${message.id} (${messageRef})`,
15314
15357
  details: {
15315
15358
  issue: message.issue_key,
15316
15359
  topic: dispatchIssueSubject(message.issue_key, ">"),
@@ -15397,6 +15440,16 @@ Open anchored asks/comments: ${marks.join(", ")}`,
15397
15440
  details: ownerArguments.ref.owner.kind === "project" ? { project: ownerArguments.ref.owner.project } : { issue: comment.comment.issue_key }
15398
15441
  };
15399
15442
  }
15443
+ if (ownerArguments.ref?.kind === "message") {
15444
+ if (ownerArguments.ref.owner.kind !== "issue") {
15445
+ throw new Error("message references are issue-scoped");
15446
+ }
15447
+ const messageRead = await client.getMessage(ownerArguments.ref.owner.issue, ownerArguments.ref.id);
15448
+ return {
15449
+ text: messageSummary(messageRead),
15450
+ details: { issue: messageRead.message.issue_key }
15451
+ };
15452
+ }
15400
15453
  if (documentOwner().kind === "project") {
15401
15454
  const resolved = await resolveArtifact(client, documentOwner(), stringArg(args, "artifact"));
15402
15455
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/opencode-legion-envoy",
3
- "version": "0.41.2",
3
+ "version": "0.42.0",
4
4
  "type": "module",
5
5
  "main": "dist/src/server.js",
6
6
  "exports": {
@@ -5,8 +5,9 @@ description: "Use when asking Sami a question, updating the spec, commenting on
5
5
 
6
6
  # Dispatch
7
7
 
8
- Dispatch is your issue's or project document's living spec, asks, comments, and artifacts. The transcript is your scratch pad. Anything
9
- meant for a human goes through a `dispatch_*` tool.
8
+ Dispatch is your issue's or project document's living spec, asks, comments, and artifacts a high-signal record for the humans who
9
+ decide, never a log of your work. The transcript is your scratch pad; progress and status stay there. Anything meant for a human
10
+ goes through a `dispatch_*` tool.
10
11
 
11
12
  The server enforces high signal: an ask question is at most 800 characters with at most eight options; comment and message bodies are at
12
13
  most 2,000 characters; an artifact is at most 25 MiB. It refuses over-limit input; it never truncates it. GitHub threads and markers no
@@ -121,8 +122,8 @@ resolved. A human may reply to an open or answered ask; so may you, e.g. after f
121
122
 
122
123
  The spec holds requirements, design, acceptance, decisions, and rejected alternatives, structured per [Writing a spec](#writing-a-spec).
123
124
  It changes only when a decision or requirement changes, and every version that records one is named with `summary`. Never write
124
- progress, status, timestamps, an "Update HH:MMZ" section, a PR list, or handoff notes into the spec that belongs in
125
- [Progress](#progress) instead.
125
+ progress, status, timestamps, an "Update HH:MMZ" section, a PR list, or handoff notes into the spec. Progress is not a
126
+ Dispatch object at all: it lives in your transcript and your pull request (see [Messages](#messages)).
126
127
 
127
128
  Read the current document before changing it:
128
129
 
@@ -163,28 +164,6 @@ and deleting a cell's quoted text removes only that text.
163
164
  Use `replace` for inline continuation. Use zero-based `occurrence` for a repeated target; re-read a missing or ambiguous target before
164
165
  retrying. Pass `summary` to name the version when recording a decision.
165
166
 
166
- ## Progress
167
-
168
- Every issue you work has one progress artifact, `progress.md` — for humans reading later and for your own successor after compaction. It
169
- is never a wake signal.
170
-
171
- Create it once, on first use:
172
- ```ts
173
- dispatch_artifact({ issue, name: "progress.md", content: "### 2026-09-11 15:00Z - Started\n..." })
174
- ```
175
- The server slugs `progress.md` to `progress-md`; address every later edit with that slug. Append — never edit or remove an earlier
176
- entry:
177
- ```ts
178
- dispatch_doc_edit({
179
- issue,
180
- artifact: "progress-md",
181
- ops: [{ op: "insert", after: "end", markdown: "### 2026-09-11 16:10Z - Blocked\n..." }],
182
- })
183
- ```
184
- Newest entry last. Each entry is `### <UTC time> - <headline>` followed by 1-5 lines: what changed (cite `dispatch://` refs or PR
185
- links), what is blocked and on whom, and what is next. `.legion/<phase>.json` is the durable machine handoff between phases;
186
- `progress.md` is the human-readable narrative for the same work — keep both, never conflate one for the other.
187
-
188
167
  ## Comments and suggestions
189
168
 
190
169
  Add feedback with:
@@ -230,15 +209,17 @@ or by its filename; the slug also arrives on `artifact.created` events.
230
209
 
231
210
  ## Messages
232
211
 
233
- Post a note to the issue's Conversation for humans: a short status they should see now (a deploy landed, a blocker appeared), or a reply
234
- to a human's message.
212
+ Dispatch is a high-signal record for humans, not a log of what you are doing. A message is a reply to a human's message, or a
213
+ change a human must know about now: a deliverable landed, a blocker only they can clear. Nothing else — no progress updates, no
214
+ "starting X", no "still working", no restating the spec, no status on a timer. Your transcript is where work is narrated; the
215
+ pull request is where it is summarised. One message that a human reads beats ten that train them to skip you.
235
216
 
236
217
  ```ts
237
218
  dispatch_message({ issue, body })
238
219
  ```
239
220
 
240
- It returns `details` `{ issue, topic, message }`. `body` is capped at 2,000 characters. It is not a progress ledger (`progress.md`), a
241
- decision (`dispatch_ask`), or document feedback (`dispatch_comment`). Your message does not wake anyone unless the issue is routed.
221
+ It returns `details` `{ issue, topic, message }`. `body` is capped at 2,000 characters. A message is not a decision
222
+ (`dispatch_ask`) or document feedback (`dispatch_comment`), and it does not wake anyone unless the issue is routed.
242
223
 
243
224
  ## What comes back
244
225
 
@@ -252,8 +233,8 @@ dispatch_read({ issue?, project?, artifact?, ref? })
252
233
 
253
234
  With an issue ref, it returns the issue summary, open asks, references, and recent events with `details` `{ issue }`. With a project
254
235
  document owner or ref, it returns a document summary with `details` `{ project, document }`. With an ask ref, it returns that ask's
255
- question, options, state, answer, and its reply thread. With a comment ref, it returns that comment and its quoted reply chain. Reads do
256
- not subscribe; use `dispatch_doc_read` for document contents.
236
+ question, options, state, answer, and its reply thread. With a comment ref, it returns that comment and its quoted reply chain. With a
237
+ message ref, it returns that message and its reply chain. Reads do not subscribe; use `dispatch_doc_read` for document contents.
257
238
 
258
239
  ## References
259
240
 
@@ -265,6 +246,7 @@ dispatch://KEY/spec
265
246
  dispatch://KEY/artifact/<slug>[@vN]
266
247
  dispatch://KEY/ask/<id>
267
248
  dispatch://KEY/comment/<id>
249
+ dispatch://KEY/message/<id>
268
250
  dispatch://PROJECT/artifact/<slug>[@vN]
269
251
  dispatch://PROJECT/artifact/<slug>/ask/<id>
270
252
  dispatch://PROJECT/artifact/<slug>/comment/<id>
@@ -299,27 +281,18 @@ dispatch_ask({
299
281
  })
300
282
  ```
301
283
 
302
- Before — progress typed once into a message, gone once compaction drops it from context:
284
+ Before — a progress note that nobody needs, posted where humans look for decisions:
303
285
 
304
286
  ```ts
305
287
  dispatch_message({ issue: "LEGION-815", body: "Merged the release PR, moving to docs next." })
306
288
  ```
307
289
 
308
- After — append it to the progress artifact, where the record survives:
290
+ After — nothing. The merge is visible on the pull request; the docs work shows up as its own deliverable. Post a message only when
291
+ a human must act or a deliverable is theirs to use:
309
292
 
310
293
  ```ts
311
- dispatch_doc_edit({
294
+ dispatch_message({
312
295
  issue: "LEGION-815",
313
- artifact: "progress-md",
314
- ops: [
315
- {
316
- op: "insert",
317
- after: "end",
318
- markdown:
319
- "### 2026-09-11 15:40Z - Release PR merged\n" +
320
- "- dispatch://LEGION-815/artifact/spec stays unchanged; this is progress, not a decision.\n" +
321
- "- Next: docs review.",
322
- },
323
- ],
296
+ body: "Release 1.4 is live on the devbox (dispatch://LEGION-815/artifact/release-notes). Nothing needed from you.",
324
297
  })
325
298
  ```
@@ -71,9 +71,9 @@ exercise a criterion end to end, building that path is a child issue of this tre
71
71
  inert until released.
72
72
 
73
73
  Specifications written into Dispatch follow [`skills/dispatch`'s Writing a spec](../dispatch/SKILL.md#writing-a-spec).
74
- Record ongoing status — wave releases, child closures, blockers in the issue's `progress.md`
75
- artifact (see [`skills/dispatch`'s Progress](../dispatch/SKILL.md#progress)), never in the spec
76
- or a `dispatch_message`.
74
+ Wave releases, child closures, and your own status are visible from the issue tree and the
75
+ handoffs; do not narrate them into the spec or a `dispatch_message`. A blocker only Sami can
76
+ clear is a `dispatch_ask`.
77
77
 
78
78
  Write one root specification containing the accepted scope, adoption/decomposition,
79
79
  waves, acceptance criteria, and integration test. When the config-armed root design gate
@@ -97,9 +97,7 @@ committed predecessor handoffs in lifecycle order from `$LEGION_WORKSPACE/.legio
97
97
  Read only files that precede the assigned phase. There is no handoff schema (rejected
98
98
  design — no schema validation runs anywhere in this pipeline): write the phase-specific
99
99
  fields the next phase and the architect need, consistent with what predecessor phases
100
- already wrote. The durable copy lives in `$LEGION_WORKSPACE/.legion/<phase>.json` the machine
101
- handoff between phases, not a human-readable status; post that to the issue's `progress.md`
102
- artifact instead (see [`skills/dispatch`'s Progress](../dispatch/SKILL.md#progress)). If a
100
+ already wrote. The durable copy lives in `$LEGION_WORKSPACE/.legion/<phase>.json`. If a
103
101
  committed handoff conflicts with memory or a prior transcript, the committed file wins: it
104
102
  is the copy that survived.
105
103