@sjawhar/opencode-legion-envoy 1.23.0 → 1.24.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.
@@ -13552,6 +13552,7 @@ function date4(params) {
13552
13552
  // ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/classic/external.js
13553
13553
  config(en_default());
13554
13554
  // ../contracts/src/dispatch-api.ts
13555
+ var DELIVERY_CAPABILITIES = ["aside", "btw", "steer"];
13555
13556
  function searchOwnerOf(result) {
13556
13557
  return result.owner ?? { kind: "issue", key: result.issue.key };
13557
13558
  }
@@ -13634,6 +13635,8 @@ var CommentEventPayloadSchema = object({
13634
13635
  ask_id: string2().nullish(),
13635
13636
  ask_question: string2().optional(),
13636
13637
  ask_state: _enum2(["open", "answered", "resolved"]).optional(),
13638
+ ask_waiting_on: _enum2(["human", "agent"]).optional(),
13639
+ turn: _enum2(["human", "agent"]).nullish(),
13637
13640
  anchor: object({ block_id: string2().nullable().optional(), quote: string2().optional() }).nullish(),
13638
13641
  suggestion: object({ replace_with: string2().optional() }).nullish(),
13639
13642
  author: object({ kind: string2(), id: string2() }).optional(),
@@ -13660,7 +13663,7 @@ var DispatchTargetedMessagePayloadSchema = MessageEventPayloadSchema.extend({
13660
13663
  var MessageDeliveryEventPayloadSchema = object({
13661
13664
  message_id: string2().optional(),
13662
13665
  attempt: number2().int().positive().optional(),
13663
- delivery: _enum2(["btw", "aside", "steer"]).optional(),
13666
+ delivery: _enum2(DELIVERY_CAPABILITIES).optional(),
13664
13667
  session_id: string2().optional(),
13665
13668
  target: string2().optional(),
13666
13669
  title: string2().optional(),
@@ -13751,6 +13754,16 @@ function documentOwnerValidation(requireArtifact, alwaysRequireArtifact = false)
13751
13754
  message: alwaysRequireArtifact ? "Exactly one of issue and project is required; artifact or ref must name the document." : "Exactly one of issue and project is required; with project, artifact names the document."
13752
13755
  };
13753
13756
  }
13757
+ var commentValidation = (() => {
13758
+ const owner = documentOwnerValidation(true);
13759
+ return {
13760
+ check: (value) => {
13761
+ const input = value;
13762
+ return owner.check(value) && (input.turn === undefined || typeof input.reply_to_ask === "string");
13763
+ },
13764
+ message: `${owner.message} turn requires reply_to_ask.`
13765
+ };
13766
+ })();
13754
13767
  var SPEC_SECTIONS = [
13755
13768
  "Summary",
13756
13769
  "Decisions needed",
@@ -13846,9 +13859,10 @@ var dispatchToolSpecs = [
13846
13859
  occurrence: z.number({ int: true, min: 0 }).describe("Optional zero-based occurrence of quote.").optional(),
13847
13860
  body: z.string({ max: 2000 }).describe("Review comment, at most 2,000 characters."),
13848
13861
  reply_to: z.string().describe("Full id of a comment to reply to; replying to any comment in a thread continues that " + "thread (an ask's clarification thread included).").optional(),
13849
- reply_to_ask: z.string().describe("Optional ask id to reply to, threading this comment under that question. Mutually " + "exclusive with reply_to.").optional()
13862
+ reply_to_ask: z.string().describe("Optional ask id to reply to, threading this comment under that question. Mutually " + "exclusive with reply_to.").optional(),
13863
+ turn: z.enum(["agent", "human"]).describe("Only with reply_to_ask: who holds the turn after this reply. agent: a progress note - " + "you keep the turn and the ask stays 'Waiting on agents' for the human; human (default): " + "you need the human to act - the ask returns to 'Waiting on you'.").optional()
13850
13864
  }),
13851
- validation: documentOwnerValidation(true)
13865
+ validation: commentValidation
13852
13866
  },
13853
13867
  {
13854
13868
  name: "dispatch_suggest",
@@ -15613,21 +15627,34 @@ async function executeDispatchTool(input) {
15613
15627
  if (replyTo !== undefined && replyToAsk !== undefined) {
15614
15628
  throw new Error("reply_to and reply_to_ask cannot both be set");
15615
15629
  }
15630
+ const requestedTurn = optionalString(args, "turn");
15631
+ if (requestedTurn !== undefined && replyToAsk === undefined) {
15632
+ throw new Error("turn requires reply_to_ask");
15633
+ }
15634
+ if (requestedTurn !== undefined && requestedTurn !== "agent" && requestedTurn !== "human") {
15635
+ throw new Error("turn must be agent or human");
15636
+ }
15616
15637
  const commentInput = {
15617
15638
  body: stringArg(args, "body"),
15618
15639
  ...anchored === undefined ? {} : { anchor: anchored },
15619
15640
  ...replyTo === undefined ? {} : { reply_to: replyTo },
15620
15641
  ...replyToAsk === undefined ? {} : { ask_id: replyToAsk },
15642
+ ...requestedTurn === undefined ? {} : { turn: requestedTurn },
15621
15643
  actor
15622
15644
  };
15623
15645
  const comment = resolved?.owner.kind === "project" ? await client.artifactComment(resolved.artifact.id, commentInput) : await client.comment(issue(), commentInput);
15646
+ const askState = comment.turn === null ? "" : ` (ask now waiting on ${comment.turn})`;
15624
15647
  return {
15625
- text: `Posted comment ${comment.id}`,
15648
+ text: `Posted comment ${comment.id}${askState}`,
15626
15649
  details: resolved === undefined ? {
15627
15650
  issue: comment.issue_key,
15628
15651
  topic: dispatchIssueSubject(issue(), ">"),
15629
- comment: comment.id
15630
- } : writeResultDetails(resolved, { comment: comment.id })
15652
+ comment: comment.id,
15653
+ ...comment.turn === null ? {} : { ask_waiting_on: comment.turn }
15654
+ } : writeResultDetails(resolved, {
15655
+ comment: comment.id,
15656
+ ...comment.turn === null ? {} : { ask_waiting_on: comment.turn }
15657
+ })
15631
15658
  };
15632
15659
  }
15633
15660
  case "dispatch_suggest": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/opencode-legion-envoy",
3
- "version": "1.23.0",
3
+ "version": "1.24.0",
4
4
  "type": "module",
5
5
  "main": "dist/src/server.js",
6
6
  "exports": {
@@ -42,6 +42,16 @@ agent's `envoy.json` to set the GitHub OAuth callback origin: the value must
42
42
  be the exact URL humans type in their browser, and the GitHub App callback is
43
43
  `<DISPATCH_SERVER_URL>/auth/callback`.
44
44
 
45
+ ### Finding a route
46
+
47
+ The tools cover the everyday surface. For anything else, ask the server: `GET /api/v1` (no
48
+ credential) returns every route as `{method, path, auth, description}` sorted by path — `auth`
49
+ is `public`, `any` (a human or a bearer), `human` (a bearer gets `403 HUMAN_ONLY`), or `bearer`.
50
+ A path Dispatch does not serve under `/api` or `/v1` answers
51
+ `404 {"code":"NOT_FOUND","error":"no route for GET /v1/issues","hint":"GET /api/v1 lists every
52
+ route"}`; when you see that, you typed the path wrong — read the index rather than guessing. Every
53
+ `/api/v1` error body carries a `code`; branch on the code, never on the text.
54
+
45
55
  ## Writing a spec
46
56
 
47
57
  A spec has two readers: the human who decides reads the top; the implementer who builds reads the
@@ -194,6 +204,14 @@ an answer: the human did not understand the question or needs more before choosi
194
204
  in the same thread with `dispatch_comment({ reply_to_ask })`, or reword the question itself with `dispatch_edit_ask` when the wording
195
205
  was the problem; either puts the ask back in front of them. Do not open a second ask.
196
206
 
207
+ Every reply to an open ask says whose turn it is next, and the Inbox files the ask by that, not by who spoke last. Your plain reply
208
+ (`turn` omitted, or `turn: "human"`) hands the turn to the human: the ask returns to their `Waiting on you`. When you are not done
209
+ yet — "dispatched two auditors, back with results", "checking the release branch, back shortly", any working-on-it note — reply with
210
+ `turn: "agent"`: the note lands in the thread, the ask stays under `Waiting on agents`, and the human is not told to act. Use
211
+ `turn: "agent"` for every progress note and `turn: "human"` (the default) only when you need them. A human's reply always hands the
212
+ turn to you. The result names the state (`ask now waiting on agent` / `human`), the delivered `comment.created` carries it as
213
+ `ask_waiting_on`, and every ask read carries it as `waiting_on`.
214
+
197
215
  ## Approval of a spec
198
216
 
199
217
  Approval is a property of a document, not a question you phrase: a human approves a specific version, the way a pull-request
@@ -300,7 +318,7 @@ an ask block without a question or with a blank option is rejected with `INVALID
300
318
  Add feedback with:
301
319
 
302
320
  ```ts
303
- dispatch_comment({ issue?, project?, artifact?, ref?, quote?, occurrence?, body, reply_to?, reply_to_ask? })
321
+ dispatch_comment({ issue?, project?, artifact?, ref?, quote?, occurrence?, body, reply_to?, reply_to_ask?, turn? })
304
322
  ```
305
323
 
306
324
  It returns issue or project-document owner details plus `comment` and, for writes, `topic`.
@@ -309,8 +327,10 @@ It returns issue or project-document owner details plus `comment` and, for write
309
327
  for display. Omit both for a floating issue comment. A reply (`reply_to`/`reply_to_ask`) takes no
310
328
  `quote`; it belongs to its parent's anchor. Reply to any comment in a thread; the server keeps
311
329
  threads flat. A reply to a resolved thread reopens it. Use `reply_to_ask` to reply directly under a
312
- question asked with `dispatch_ask`. Comments are edited only by their author from the dashboard. A
313
- delivered `comment.created` event carries the comment `id`; reply to it with
330
+ question asked with `dispatch_ask`; `turn` (only with `reply_to_ask`) says who holds the turn after
331
+ the reply — `agent` for a progress note that keeps the ask waiting on you, `human` (the default) when
332
+ the human needs to act; see [Asking](#asking). Comments are edited only by their author from the
333
+ dashboard. A delivered `comment.created` event carries the comment `id`; reply to it with
314
334
  `dispatch_comment({ reply_to: <id> })`.
315
335
 
316
336
  Propose an exact replacement instead of describing it:
@@ -395,7 +415,10 @@ dispatch_message({
395
415
  delivery can post its answer automatically; use this call when the frame asks the primary agent to
396
416
  reply. `dispatch_message` itself never carries `target` or `delivery`: agent-to-agent traffic goes
397
417
  through Envoy or the hub. A bearer that targets over HTTP names its own session in `actor`
398
- (`{kind: "session", id}`), and the card shows that session as the author.
418
+ (`{kind: "session", id}`), and the card shows that session as the author. `GET /api/v1/agents`
419
+ (any authenticated caller) lists live sessions with their capabilities (`aside`, `btw`, `steer`);
420
+ target only a session that advertises the mode you want. Sending to a session with no issue
421
+ (`POST /api/v1/agents/{session_id}/messages`) stays human-only.
399
422
 
400
423
  ## What comes back
401
424
 
@@ -90,6 +90,16 @@ envoy_send(
90
90
  )
91
91
  ```
92
92
 
93
+ ### Delivery capabilities
94
+
95
+ Each session row from `envoy_sessions` carries `capabilities`, the targeted-delivery modes that
96
+ session's host honours: `aside` (a message queued beside the model's work), `btw` (an ephemeral
97
+ question the host answers without disturbing the current turn), and `steer` (an interjection at
98
+ the next tool boundary). An OMP session advertises `aside`, `btw`, and `steer` (`aside` and
99
+ `steer` on a host without `askEphemeral`); a Claude Code session advertises `aside` only, because
100
+ its channel notifications queue for the next turn. Target a session only with a mode it
101
+ advertises.
102
+
93
103
  ## Waiting for CI or a merge
94
104
 
95
105
  Subscribe to `notifications.github.example-org.example-repo.pr.42.>` and end the turn. The single