@sjawhar/opencode-legion-envoy 1.13.1 → 1.14.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.
@@ -13594,6 +13594,11 @@ var askEventPayloadFields = {
13594
13594
  question: string2().optional(),
13595
13595
  options: array(object({ label: string2().optional() })).nullish(),
13596
13596
  answer: object({ selected: array(string2()).nullish(), text: string2().nullish() }).nullish(),
13597
+ anchor: object({
13598
+ quote: string2().optional(),
13599
+ mark_id: string2().optional(),
13600
+ block_id: string2().nullable().optional()
13601
+ }).passthrough().nullish(),
13597
13602
  resolution: object({
13598
13603
  kind: _enum2(["retracted", "resolved"]).optional(),
13599
13604
  reason: string2().optional(),
@@ -13628,7 +13633,7 @@ var CommentEventPayloadSchema = object({
13628
13633
  ask_id: string2().nullish(),
13629
13634
  ask_question: string2().optional(),
13630
13635
  ask_state: _enum2(["open", "answered", "resolved"]).optional(),
13631
- anchor: object({ quote: string2().optional() }).nullish(),
13636
+ anchor: object({ block_id: string2().nullable().optional(), quote: string2().optional() }).nullish(),
13632
13637
  suggestion: object({ replace_with: string2().optional() }).nullish(),
13633
13638
  author: object({ kind: string2(), id: string2() }).optional(),
13634
13639
  created_at: string2().optional()
@@ -13658,7 +13663,9 @@ var ChildStatusEventPayloadSchema = object({
13658
13663
  var SubscriptionRemovedEventPayloadSchema = object({
13659
13664
  session_id: string2().optional(),
13660
13665
  by: object({ kind: string2(), id: string2().optional() }).passthrough().optional(),
13661
- topics: array(string2()).optional()
13666
+ topics: array(string2()).optional(),
13667
+ pending: boolean2().optional(),
13668
+ request_event_id: number2().int().positive().optional()
13662
13669
  });
13663
13670
  // ../contracts/src/dispatch-snippet.ts
13664
13671
  var HTML_ENTITIES = [
@@ -13760,7 +13767,7 @@ var dispatchToolSpecs = [
13760
13767
  },
13761
13768
  {
13762
13769
  name: "dispatch_ask",
13763
- description: "Open a durable, answerable decision or human to-do on an issue or project document. Do not use it for a status update or discussion; " + "use dispatch_message instead. Use kind: action for a to-do a human must complete; it has fixed Done / Can't answers. " + "Anchor a document question, thread reply_to/reply_to_ask, or cite a dispatch:// " + `reference \u2014 it must be answerable from its own text and anchor alone, never "see above". Question is at most 800 ` + `characters and has at most 8 options. ${OWNER_REFERENCE}`,
13770
+ description: "Open a durable, answerable decision or human to-do on an issue or project document. Do not use it for a status update or discussion; " + "use dispatch_message instead. Use kind: action for a to-do a human must complete; it has fixed Done / Can't answers. " + "Anchor a document question, thread reply_to/reply_to_ask, or cite a dispatch:// " + 'reference \u2014 it must be answerable from its own text and anchor alone, never "see above". A quote anchor is pinned to its block. Question is at most 800 ' + `characters and has at most 8 options. ${OWNER_REFERENCE}`,
13764
13771
  arguments: (z) => ({
13765
13772
  issue: z.string().describe(ISSUE_REFERENCE).optional(),
13766
13773
  project: z.string().describe("Project key owning the document.").optional(),
@@ -13814,7 +13821,7 @@ var dispatchToolSpecs = [
13814
13821
  },
13815
13822
  {
13816
13823
  name: "dispatch_comment",
13817
- description: "Add review feedback to an issue or project document quote, or reply to a question asked with dispatch_ask. " + "Do not use it for an exact replacement; use " + `dispatch_suggest instead. Body is at most 2,000 characters. ${OWNER_REFERENCE}`,
13824
+ description: "Add review feedback to an issue or project document quote, or reply to a question asked with dispatch_ask. " + "Do not use it for an exact replacement; use " + `dispatch_suggest instead. A quote anchor is pinned to its block. Body is at most 2,000 characters. ${OWNER_REFERENCE}`,
13818
13825
  arguments: (z) => ({
13819
13826
  issue: z.string().describe(ISSUE_REFERENCE).optional(),
13820
13827
  project: z.string().describe("Project key owning the document.").optional(),
@@ -14611,6 +14618,11 @@ function asErrorShape(value) {
14611
14618
  function isJson(response) {
14612
14619
  return response.headers.get("content-type")?.includes("application/json") ?? false;
14613
14620
  }
14621
+ var DISPATCH_TOOL_DEADLINE_MS = 60000;
14622
+ function requestSignal(signal) {
14623
+ const deadline = AbortSignal.timeout(DISPATCH_TOOL_DEADLINE_MS);
14624
+ return signal === undefined ? deadline : AbortSignal.any([signal, deadline]);
14625
+ }
14614
14626
 
14615
14627
  class DispatchClient {
14616
14628
  token;
@@ -14618,10 +14630,12 @@ class DispatchClient {
14618
14630
  #baseUrl;
14619
14631
  #resolvedIssues = new Map;
14620
14632
  #creatingIssues = new Map;
14621
- constructor(baseUrl, token, fetchImpl = fetch) {
14633
+ #signal;
14634
+ constructor(baseUrl, token, fetchImpl = fetch, signal) {
14622
14635
  this.token = token;
14623
14636
  this.fetchImpl = fetchImpl;
14624
14637
  this.#baseUrl = baseUrl.replace(/\/+$/, "");
14638
+ this.#signal = requestSignal(signal);
14625
14639
  }
14626
14640
  async issue(input) {
14627
14641
  return this.#json("POST", ["api", "v1", "issues"], input);
@@ -14833,6 +14847,7 @@ class DispatchClient {
14833
14847
  const response = await this.fetchImpl(this.#url(path, query), {
14834
14848
  method,
14835
14849
  headers,
14850
+ signal: this.#signal,
14836
14851
  ...body === undefined ? {} : { body: JSON.stringify(body) }
14837
14852
  });
14838
14853
  return this.#response(response);
@@ -14841,7 +14856,8 @@ class DispatchClient {
14841
14856
  const response = await this.fetchImpl(this.#url(path), {
14842
14857
  method,
14843
14858
  headers: { Accept: "application/json", Authorization: `Bearer ${this.token}` },
14844
- body
14859
+ body,
14860
+ signal: this.#signal
14845
14861
  });
14846
14862
  return this.#response(response);
14847
14863
  }
@@ -15306,7 +15322,7 @@ async function executeDispatchTool(input) {
15306
15322
  const ownerArguments = await resolveOwnerArguments(input.tool, input.args, input.cwd, env, exec);
15307
15323
  const args = toolSchema(input.tool).parse(ownerArguments.args);
15308
15324
  const actor = toolActor(await resolveOrigin(env, exec, input.cwd), input);
15309
- const client = new DispatchClient(configUrl, configToken, input.fetchImpl);
15325
+ const client = new DispatchClient(configUrl, configToken, input.fetchImpl, input.signal);
15310
15326
  const owner = ownerArguments.owner?.kind === "issue" ? {
15311
15327
  kind: "issue",
15312
15328
  issue: await ensureIssue(client, ownerArguments.owner.issue, actor)
@@ -15889,7 +15905,7 @@ class EnvoyApiError extends Error {
15889
15905
  function createEnvoyClient(config) {
15890
15906
  const baseUrl = normalizeEnvoyUrl(config.baseUrl);
15891
15907
  const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
15892
- const apiToken = process.env["ENVOY_TOKEN"];
15908
+ const { ENVOY_TOKEN: apiToken } = process.env;
15893
15909
  const request = async (path, init) => {
15894
15910
  const url = `${baseUrl}${path}`;
15895
15911
  for (let attempt = 0;attempt < 2; attempt += 1) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/opencode-legion-envoy",
3
- "version": "1.13.1",
3
+ "version": "1.14.1",
4
4
  "type": "module",
5
5
  "main": "dist/src/server.js",
6
6
  "exports": {
@@ -127,8 +127,10 @@ recommendation and its reason last, in `question`. Never put file paths, line nu
127
127
  numbers, document versions, or role tokens in the question; if the human needs that detail, anchor
128
128
  the ask to the document passage instead. Apply the phone test from "Writing for the human" before
129
129
  posting. Anchor a document question with `anchor: { artifact, quote, occurrence? }`; `occurrence`
130
- is zero-based and selects a repeated quote, and an anchor whose quote later disappears becomes
131
- orphaned but stays readable against its original document version.
130
+ is zero-based and selects a repeated quote. A quote anchor is pinned to its lowest complete
131
+ containing block while retaining its quote as display text, so rewording the passage keeps it
132
+ attached; a quote spanning top-level blocks, and existing anchors without a block, stay readable
133
+ against their original document version if their quote disappears.
132
134
 
133
135
  An ask must be answerable from its own text and its anchor alone. Anchor a question about a document passage with `anchor`; thread one
134
136
  about a comment with `reply_to`; thread a follow-up on your own ask with `reply_to_ask`; cite anything else with a `dispatch://`
@@ -163,7 +165,8 @@ dispatch_edit_ask({
163
165
  })
164
166
  ```
165
167
  At least one field besides `ask` is required. Use this only while the same decision remains open: it keeps the prior text in the event
166
- log. An answered or resolved ask cannot be edited. If the decision is moot or superseded, retract the old ask and open a new one.
168
+ log and invalidates any answer draft against the prior `edited_at` revision, so the human sees the new wording and explicitly reconfirms.
169
+ An answered or resolved ask cannot be edited. If the decision is moot or superseded, retract the old ask and open a new one.
167
170
 
168
171
  An ask stays open until a human answers, unless its question no longer needs that answer. Retract a moot or superseded question, or
169
172
  self-resolve one after finding the answer:
@@ -179,10 +182,11 @@ in its Conversation card and reply thread. Resolution is not an answer: it never
179
182
  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
180
183
  `dispatch_comment` (mutually exclusive with `reply_to`).
181
184
 
182
- A human reply while your ask is still open (the delivered `comment.created` carries `ask_state: open`) is a request for
183
- clarification, not an answer: the human did not understand the question or needs more before choosing. The ask now waits on you
184
- in their Inbox. Answer in the same thread with `dispatch_comment({ reply_to_ask })`, or reword the question itself with
185
- `dispatch_edit_ask` when the wording was the problem; either puts the ask back in front of them. Do not open a second ask.
185
+ A human answers or asks back from the same field; a question-shaped free-text answer is offered as a clarification first. A human
186
+ reply while your ask is still open (the delivered `comment.created` carries `ask_state: open`) is a request for clarification, not
187
+ an answer: the human did not understand the question or needs more before choosing. The ask now waits on you in their Inbox. Answer
188
+ in the same thread with `dispatch_comment({ reply_to_ask })`, or reword the question itself with `dispatch_edit_ask` when the wording
189
+ was the problem; either puts the ask back in front of them. Do not open a second ask.
186
190
 
187
191
  ## Approval of a spec
188
192
 
@@ -289,11 +293,12 @@ dispatch_comment({ issue?, project?, artifact?, ref?, quote?, occurrence?, body,
289
293
 
290
294
  It returns issue or project-document owner details plus `comment` and, for writes, `topic`.
291
295
  `ref` names the owner (an issue or project-document reference) in place of `issue`/`project`.
292
- `quote` requires `artifact`; omit both for a floating issue comment. A reply (`reply_to`/`reply_to_ask`)
293
- takes no `quote`; it belongs to its parent's anchor. Reply to any comment in a thread; the server
294
- keeps threads flat. A reply to a resolved thread reopens it. Use `reply_to_ask` to reply directly
295
- under a question asked with `dispatch_ask`. Comments are edited only by their author from the
296
- dashboard. A delivered `comment.created` event carries the comment `id`; reply to it with
296
+ `quote` requires `artifact`; its anchor is pinned to the containing block while retaining the quote
297
+ for display. Omit both for a floating issue comment. A reply (`reply_to`/`reply_to_ask`) takes no
298
+ `quote`; it belongs to its parent's anchor. Reply to any comment in a thread; the server keeps
299
+ threads flat. A reply to a resolved thread reopens it. Use `reply_to_ask` to reply directly under a
300
+ question asked with `dispatch_ask`. Comments are edited only by their author from the dashboard. A
301
+ delivered `comment.created` event carries the comment `id`; reply to it with
297
302
  `dispatch_comment({ reply_to: <id> })`.
298
303
 
299
304
  Propose an exact replacement instead of describing it:
@@ -127,10 +127,12 @@ legion({
127
127
 
128
128
  The daemon spawns that sub-architect as its own process with the child's context already
129
129
  in its environment; a resume of an existing role continues the same process instead of
130
- starting a fresh one. Keep the returned session identifiers, because retro and adjustment
131
- use those live sessions. Park while children are in flight. On each child closure,
132
- re-scope open work, close obsolete work with a reason, and release the next wave only
133
- when it now makes sense. There is no inter-child dependency mechanism to encode.
130
+ starting a fresh one. Keep the returned session identifiers; retro and adjustment resume
131
+ those same sessions through `spawn_worker` (a finished worker is retired after
132
+ `worker_idle_retire_seconds` and comes back from its session file). Park while children are
133
+ in flight. On each child closure, re-scope open work, close obsolete work with a reason, and
134
+ release the next wave only when it now makes sense. There is no inter-child dependency
135
+ mechanism to encode.
134
136
 
135
137
  ## 3. Children complete
136
138
 
@@ -160,18 +162,24 @@ review and the merge-gate sequence.
160
162
 
161
163
  ## 5. Retro
162
164
 
163
- Retro is mandatory for every issue that passed review, before merge. Message the
164
- implementer's live session (idle since it completed its phase; the daemon never tears
165
- it down) with `envoy_publish` to its role token, naming the skill:
165
+ Retro is mandatory for every issue that passed review, before merge. Send the implementer
166
+ back in through the daemon `spawn_worker` on the implementer carrying the retro task. This
167
+ resumes the same agent whether its pane is still live or the daemon has already retired it
168
+ idle (a finished worker is retired after `worker_idle_retire_seconds`, default 600 s, and
169
+ resumed from its session file on its next assignment). Never `envoy_publish` to a finished
170
+ worker's role topic for this: a retired role has no live holder and the publish is rejected
171
+ with 404.
166
172
 
167
173
  ```text
168
- envoy_publish({
169
- topic: "notifications.role.<implementer's encoded token>",
170
- message: "Run the legion-retro skill now. Capture durable learnings and post the issue comment; do not create a .legion handoff file."
174
+ legion({
175
+ op: "spawn_worker",
176
+ issue: "LEGION-40",
177
+ role: "implementer",
178
+ task: "Run the legion-retro skill now. Capture durable learnings and post the issue comment; do not create a .legion handoff file."
171
179
  })
172
180
  ```
173
181
 
174
- Wait for the messaged implementer to report its durable retro result. Retro output is
182
+ Wait for the implementer to report its durable retro result. Retro output is
175
183
  `docs/solutions/` plus an issue comment; it must not create a `.legion` file or change
176
184
  the reviewer-approved head after cleanup.
177
185
 
@@ -189,7 +197,8 @@ Preserve this order exactly:
189
197
  head. The deletion must land before that approval, which is head-pinned. An implementer
190
198
  completion always writes the issue's status as `testing`; this one is not a test round,
191
199
  so on its `phase-complete` wake call `legion({ op: "set_status", issue, status: "retro" })`
192
- before messaging the reviewer to approve;
200
+ before `spawn_worker` on the reviewer to approve that head (a finished reviewer may already
201
+ be retired; `spawn_worker` resumes it);
193
202
  3. retro completes without dirtying the branch beyond `docs/solutions/`;
194
203
  4. the merger verifies the current head is the reviewer-approved head plus only the retro
195
204
  commits and publishes `READY #<n> at <sha>` to `notifications.role.pr-queue`; it never
@@ -61,7 +61,9 @@ Reach any live role on this issue the same way you reach the architect: `envoy_p
61
61
  `notifications.role.` followed by that role's encoded token. Use it when you need context an
62
62
  earlier phase has that its handoff doesn't cover — ask the planner why a constraint was
63
63
  scoped that way, ask the implementer what a commit actually did. A role that finished its
64
- phase is still alive and idle in its pane; it answers.
64
+ phase stays idle in its pane for the daemon's idle-retire window and answers; once retired (no
65
+ live holder, a publish is rejected 404), read its committed handoff or ask the architect to
66
+ `spawn_worker` it.
65
67
 
66
68
  ## Workspace and handoff precedence
67
69
 
@@ -316,7 +318,9 @@ record of this issue's active phase. Do not add pipeline labels, run a controlle
316
318
  invent a different completion protocol — this is the whole contract.
317
319
 
318
320
  **Stay in this session afterward.** Your process does not exit when your phase completes;
319
- it goes idle in its pane. Other roles on this issue may reach you through Envoy with
321
+ it goes idle in its pane, and after `worker_idle_retire_seconds` (default 600 s) idle with no
322
+ active phase the daemon retires it — your next assignment resumes this same session from its
323
+ session file, so it is still you. Other roles on this issue may reach you through Envoy with
320
324
  questions about the work you did — answer them, reading `$LEGION_WORKSPACE` and your own
321
325
  committed handoff as needed, without mutating anything (see Workspace and handoff
322
326
  precedence above). You will also be the one resumed, with a new prompt in this same