@sjawhar/opencode-legion-envoy 1.35.0 → 1.37.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.
package/README.md CHANGED
@@ -28,11 +28,13 @@ This package exposes:
28
28
  - `dispatch_artifact`
29
29
  - `dispatch_read`
30
30
  - `dispatch_search`
31
+ - `dispatch_issues`
32
+ - `dispatch_architecture_sync`
31
33
  - `dispatch_open_asks`
32
34
  - `dispatch_whoami`
33
35
 
34
- The eighteen native `dispatch_*` tools create and read Dispatch issues, asks, comments,
35
- documents, and artifacts, or search all of them. They are present when `dispatch.enabled`
36
+ The twenty native `dispatch_*` tools create and read Dispatch issues, asks, comments,
37
+ documents, and artifacts, list a project's issues, sync a project's architecture model, or search all of them. They are present when `dispatch.enabled`
36
38
  resolves a server URL and bearer token from envoy.json (`~/.config/opencode/envoy.json`, merged
37
39
  with `<repo>/.opencode/envoy.json`) or the `DISPATCH_URL` and `DISPATCH_TOKEN` environment
38
40
  variables; `dispatch.enabled: true` without `dispatch.serverUrl` targets `http://localhost:8766`.
@@ -14024,10 +14024,32 @@ var dispatchToolSpecs = [
14024
14024
  limit: z.number({ int: true, min: 1, max: 50 }).describe("Maximum results, 1-50; default 20.").optional()
14025
14025
  })
14026
14026
  },
14027
+ {
14028
+ name: "dispatch_issues",
14029
+ description: "List a project's issues for a roadmap or backlog pass: every issue in one project, each carrying " + "its status, priority, parent, labels, and open-ask count, so you can see backlog shape without " + "opening every issue. Optionally filter by status, parent, label, or how recently it changed. Do " + "not use it to search by keyword or phrase; dispatch_search remains the keyword surface. Rows are " + "capped at limit (default 50, max 250), applied to the response here, not by the server.",
14030
+ arguments: (z) => ({
14031
+ project: z.string().describe("Project key to list issues from."),
14032
+ status: z.enum(ISSUE_STATUSES).describe("Optional lifecycle status filter.").optional(),
14033
+ parent: z.string().describe("Optional parent issue key filter.").optional(),
14034
+ label: z.string().describe("Optional label filter.").optional(),
14035
+ updated_since: z.string().describe("Optional RFC3339 timestamp; only issues updated at or after it.").optional(),
14036
+ limit: z.number({ int: true, min: 1, max: 250 }).describe("Maximum rows, 1-250; default 50.").optional()
14037
+ })
14038
+ },
14039
+ {
14040
+ name: "dispatch_architecture_sync",
14041
+ description: "Import a project's architecture model from its configured source repository now, instead of " + "waiting for the server's five-minute schedule. Returns the imported commit and component " + "count, or the recorded error when the model was rejected (the previous model stays up). " + "The source itself is configured by a human in Settings; 404 SOURCE_NOT_FOUND without one.",
14042
+ arguments: (z) => ({
14043
+ project: z.string().describe("Project key whose architecture source to sync, such as CORE.")
14044
+ }),
14045
+ strict: true
14046
+ },
14027
14047
  {
14028
14048
  name: "dispatch_open_asks",
14029
- description: "List this session's active unanswered asks across issues and project documents, including age and whose reply is needed. Call before saying you are waiting for human input.",
14030
- arguments: () => ({}),
14049
+ description: "List active unanswered asks, oldest first, with age and whose reply is needed. Omit project to " + "see only this session's own authored asks (call before saying you are waiting for human input); " + "supply project to see every open ask across that project's issues and documents, whoever authored " + "them.",
14050
+ arguments: (z) => ({
14051
+ project: z.string().describe("Project key; when supplied, lists every open ask in the project instead of only this session's own.").optional()
14052
+ }),
14031
14053
  strict: true
14032
14054
  },
14033
14055
  {
@@ -14856,9 +14878,15 @@ class DispatchClient {
14856
14878
  ...since === undefined ? {} : { since }
14857
14879
  });
14858
14880
  }
14881
+ async openAsksForProject(project) {
14882
+ return this.#json("GET", ["api", "v1", "asks", "open"], undefined, { project });
14883
+ }
14859
14884
  async whoami() {
14860
14885
  return this.#json("GET", ["api", "v1", "whoami"]);
14861
14886
  }
14887
+ async syncArchitectureSource(project) {
14888
+ return this.#json("POST", ["api", "v1", "projects", project, "architecture-source", "sync"]);
14889
+ }
14862
14890
  async resolveAsk(id, input) {
14863
14891
  return this.#json("POST", ["api", "v1", "asks", id, "resolve"], input);
14864
14892
  }
@@ -15272,8 +15300,10 @@ var issueFreeTools = {
15272
15300
  dispatch_resolve_comment: true,
15273
15301
  dispatch_follow: true,
15274
15302
  dispatch_search: true,
15303
+ dispatch_issues: true,
15275
15304
  dispatch_open_asks: true,
15276
- dispatch_whoami: true
15305
+ dispatch_whoami: true,
15306
+ dispatch_architecture_sync: true
15277
15307
  };
15278
15308
  function canonicalExternalIssueRef(value) {
15279
15309
  const match = value.trim().match(externalIssueRefPattern);
@@ -15933,10 +15963,15 @@ async function executeDispatchTool(input) {
15933
15963
  if (problems.length > 0)
15934
15964
  throw new ToolInputError(input.tool, problems);
15935
15965
  if (input.tool === "dispatch_open_asks") {
15966
+ const client = new DispatchClient(configUrl, configToken, fetchImpl, input.signal);
15967
+ const project = optionalString(ownerArguments.args, "project");
15968
+ if (project !== undefined) {
15969
+ const response = await client.openAsksForProject(project);
15970
+ return { text: formatOpenAsksSummary(response, configUrl), details: { ...response } };
15971
+ }
15936
15972
  const sessionId = input.sessionId?.trim();
15937
15973
  if (!sessionId)
15938
15974
  throw new Error("host session id is required for dispatch_open_asks");
15939
- const client = new DispatchClient(configUrl, configToken, fetchImpl, input.signal);
15940
15975
  const response = await client.openAsks(sessionId);
15941
15976
  return { text: formatOpenAsksSummary(response, configUrl), details: { ...response } };
15942
15977
  }
@@ -16083,6 +16118,58 @@ async function executeDispatchTool(input) {
16083
16118
  details: { query, results }
16084
16119
  };
16085
16120
  }
16121
+ case "dispatch_issues": {
16122
+ const project = stringArg(args, "project");
16123
+ const status = optionalString(args, "status");
16124
+ const parent = optionalString(args, "parent");
16125
+ const label = optionalString(args, "label");
16126
+ const updatedSince = optionalString(args, "updated_since");
16127
+ const limit = Math.min(Math.max(optionalNumber(args, "limit") ?? 50, 1), 250);
16128
+ const issues = await client.listIssues({
16129
+ project,
16130
+ ...status === undefined ? {} : { status },
16131
+ ...parent === undefined ? {} : { parent },
16132
+ ...label === undefined ? {} : { label },
16133
+ ...updatedSince === undefined ? {} : { updated_since: updatedSince }
16134
+ });
16135
+ const rows = issues.slice(0, limit).map((row) => ({
16136
+ key: row.key,
16137
+ title: row.title,
16138
+ status: row.status,
16139
+ priority: row.priority,
16140
+ parent: row.parent,
16141
+ labels: row.labels ?? [],
16142
+ open_asks: row.open_asks,
16143
+ updated_at: row.updated_at
16144
+ }));
16145
+ return {
16146
+ text: rows.length === 0 ? `No issues in ${project}.` : [
16147
+ `${rows.length} ${rows.length === 1 ? "issue" : "issues"} in ${project}` + (issues.length > rows.length ? ` (showing ${rows.length} of ${issues.length})` : ""),
16148
+ ...rows.map((row) => `${row.key} [${row.status}]${row.priority === null ? "" : ` P${row.priority}`} ${row.title}` + (row.open_asks === 0 ? "" : ` \xB7 ${row.open_asks} open ${row.open_asks === 1 ? "ask" : "asks"}`))
16149
+ ].join(`
16150
+ `),
16151
+ details: { issues: rows }
16152
+ };
16153
+ }
16154
+ case "dispatch_architecture_sync": {
16155
+ const project = stringArg(args, "project");
16156
+ const source = await client.syncArchitectureSource(project);
16157
+ const at = source.last_sync_at ?? "unknown time";
16158
+ return {
16159
+ text: source.last_error === null ? `Synced ${project} architecture from ${source.repo}@${source.branch}: commit ${source.last_commit ?? "unknown"} (${at}).` : [
16160
+ `Sync failed for ${project} (${source.repo}@${source.branch}): ${source.last_error}`,
16161
+ source.last_commit === null ? "No model has ever imported for this project." : `The previous model stays up (commit ${source.last_commit}).`
16162
+ ].join(`
16163
+ `),
16164
+ details: {
16165
+ project,
16166
+ repo: source.repo,
16167
+ branch: source.branch,
16168
+ commit: source.last_commit,
16169
+ error: source.last_error
16170
+ }
16171
+ };
16172
+ }
16086
16173
  case "dispatch_resolve_ask": {
16087
16174
  const kind = stringArg(args, "kind");
16088
16175
  const ask = await client.resolveAsk(stringArg(args, "ask"), {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/opencode-legion-envoy",
3
- "version": "1.35.0",
3
+ "version": "1.37.0",
4
4
  "type": "module",
5
5
  "main": "dist/src/server.js",
6
6
  "exports": {
@@ -148,6 +148,20 @@ start with the issue key; standalone project-document hit lines start with
148
148
  `dispatch_issue` refuses a title that near-duplicates an issue in the same project and returns the candidates (`POSSIBLE_DUPLICATE`).
149
149
  Read them; reference the existing issue, or repeat the call with `force: true` when it is genuinely new work.
150
150
 
151
+ ## Reading a project's backlog
152
+
153
+ To see the shape of a project rather than find a phrase, list its issues:
154
+ ```ts
155
+ dispatch_issues({ project, status?, parent?, label?, updated_since?, limit? })
156
+ ```
157
+ Each row carries the issue key, title, status, priority, parent, labels, its open-ask count, and
158
+ when it last changed — a roadmap or backlog pass without opening every issue. Filter with `status`
159
+ (a lifecycle status), `parent` (one issue's children), `label`, or `updated_since` (an RFC3339
160
+ timestamp, for "what moved this week"). `limit` caps the rows at 50 by default and 250 at most.
161
+
162
+ This is not search: it matches no text. Use `dispatch_search` for a keyword or phrase, and
163
+ `dispatch_issues` when you want every issue in a project and its current state.
164
+
151
165
  ## Asking
152
166
 
153
167
  ### Before you ask
@@ -171,8 +185,8 @@ production import). Every `dispatch_ask` passes three gates first:
171
185
  would have to define. This is the phone test in [Writing for the human](#writing-for-the-human).
172
186
  If you cannot write it that way, you do not understand it well enough to ask.
173
187
 
174
- The platform PO audits open asks. One that fails a gate is retracted, with the PO's answer as the
175
- record.
188
+ The platform PO audits open asks. One that fails a gate — or that points at another message in
189
+ prose instead of carrying its content (below) — is retracted, with the PO's answer as the record.
176
190
 
177
191
  Open a decision with:
178
192
  ```ts
@@ -209,11 +223,38 @@ passage with `anchor`. Follow up on an ask or comment with `dispatch_comment`; c
209
223
  with a `dispatch://` reference (see [References](#references)). Never write "see above", "the
210
224
  message above", or "as attached".
211
225
 
226
+ **Pointing at another message is a defect, not a shortcut.** Sami, 2026-09-17, verbatim, on an
227
+ ask that read "the settings listed in my comment just above" after a long procedure had been posted
228
+ as a comment: "you just dump information into messages and then add a new ask that references a
229
+ previous message in prose with no link or no context whatsoever and uses compressed shorthand
230
+ jargon." The ask view does not show the issue's comments, so that ask was unanswerable; "Cloud
231
+ Identity licence check / 2SV override / 1-day grace" was shorthand he had never used. The rules
232
+ that follow from it:
233
+
234
+ - An ask that names another message in prose — "my comment above", "the procedure I posted",
235
+ "see the earlier message" — is retracted by the PO as failing the gates. Put the content IN the
236
+ ask. If it does not fit the 800-character budget, the step is too big: split the step, never
237
+ point elsewhere. The only pointers an ask may carry are a `dispatch://` reference or a document
238
+ `anchor`, and they cite — the ask still says in one line what the reader will find there and can
239
+ be answered without following them.
240
+ - Expand every term the reader has not used first. A product name, an internal setting, an
241
+ acronym, a value you coined this session — write what it is in the ask, in his words.
242
+ - A runbook the human must execute is one ask per step, each self-contained: what to do, where,
243
+ what result proves it, `Done` / `Can't` options. Each later step opens only after the previous is
244
+ answered and states that step's verified result in one line ("Step 1 done: the licence shows
245
+ Cloud Identity Free on the admin console.") — never a pointer to the earlier ask.
246
+
212
247
  **A decision about an uploaded artifact links it.** If the human must read an artifact to answer,
213
248
  the question carries `dispatch://KEY/artifact/<slug>` (or `ref`), never just its filename. Text
214
249
  they must read to decide belongs in the spec in the first place — see [Artifacts](#artifacts).
215
250
 
216
- Before saying you are waiting for human input, call `dispatch_open_asks`. It lists this session's active asks across open issues and project documents, including whether the human or agent owes the next reply.
251
+ Import a project's architecture model from its configured source repository now (a human configures the source in Settings):
252
+ ```ts
253
+ dispatch_architecture_sync({ project: "CORE" })
254
+ ```
255
+ It returns the imported commit, or the recorded error when the model was rejected — the previous model stays up. Without a configured source it answers 404 `SOURCE_NOT_FOUND`.
256
+
257
+ Before saying you are waiting for human input, call `dispatch_open_asks`. With no arguments it lists this session's active asks across open issues and project documents, including whether the human or agent owes the next reply. With `dispatch_open_asks({ project })` it lists every open ask in that project — on its issues and on its documents, whoever authored them — which is how you audit what a whole project is waiting on rather than just your own asks.
217
258
 
218
259
  **Anything that needs the human is an ask, or it does not exist.** An approval, a credential,
219
260
  a setting only they can change, a review click, a conflict between two of their own rules - if