@sjawhar/opencode-legion-envoy 6.15.2 → 6.17.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/dist/dispatch.js CHANGED
@@ -17297,22 +17297,31 @@ async function refuseOpenDecisionBlocks(client, tool, resolved) {
17297
17297
  const asks = new Map(documentAsks.map((ask) => [ask.block_id, ask]));
17298
17298
  const lines = version2.markdown.split(`
17299
17299
  `);
17300
+ const stillOpen = (named, state) => {
17301
+ const next = state === "answered" ? "fold the answer into the text" : "write the decision into the text";
17302
+ return `${named}, ${state} but still open in version ${latest}: ${next} with dispatch doc-edit, which writes a version that carries it`;
17303
+ };
17300
17304
  const open = blocks.flatMap((block) => {
17301
17305
  const ask = asks.get(block.id);
17306
+ const openers = lines.filter((line) => line.includes(`ask{#${block.id} `) || line.includes(`ask{#${block.id}}`));
17307
+ const states = openers.map((line) => /\bstate="(\w+)"/.exec(line)?.[1]);
17302
17308
  const named = ask === undefined ? `block ${block.id}` : `${JSON.stringify(ask.question)} (block ${block.id}, ask ${ask.id})`;
17303
- const states = lines.filter((line) => line.includes(`ask{#${block.id} `) || line.includes(`ask{#${block.id}}`)).map((line) => /\bstate="(\w+)"/.exec(line)?.[1]);
17304
17309
  if (states.length === 0)
17305
17310
  return [`${named}, which version ${latest} does not hold yet`];
17306
17311
  if (!states.includes("open") && states.some((state) => state !== undefined))
17307
17312
  return [];
17308
- if (ask === undefined)
17313
+ if (ask !== undefined)
17314
+ return ask.state === "open" ? [named] : [stillOpen(named, ask.state)];
17315
+ const attribute = (name) => openers.map((line) => new RegExp(`\\b${name}="([^"]+)"`).exec(line)?.[1]).find((value) => value !== undefined);
17316
+ const sourceId = attribute("copied_from");
17317
+ const document = attribute("copied_from_document");
17318
+ if (sourceId === undefined || document === undefined) {
17309
17319
  return [`${named}, whose ask Dispatch has not opened yet`];
17310
- if (ask.state === "open")
17311
- return [named];
17312
- const next = ask.state === "answered" ? "fold the answer into the text" : "write the decision into the text";
17313
- return [
17314
- `${named}, ${ask.state} but still open in version ${latest}: ${next} with dispatch doc-edit, which writes a version that carries it`
17315
- ];
17320
+ }
17321
+ const source = parseDispatchRef(document);
17322
+ const where = source === null ? document : source.owner.kind === "project" ? documentLabel(source.owner.project, source.id) : `${source.owner.issue} ${source.id}`;
17323
+ const ref = source?.owner.kind === "issue" ? dispatchChildRef(dispatchIssueRef(source.owner.issue), "ask", sourceId) : dispatchChildRef(document, "ask", sourceId);
17324
+ return [`${named}, a copy of ask ${ref}, which is open on ${where}: answer it there`];
17316
17325
  });
17317
17326
  if (open.length === 0)
17318
17327
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/opencode-legion-envoy",
3
- "version": "6.15.2",
3
+ "version": "6.17.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "dist/src/server.js",
@@ -148,6 +148,20 @@ artifact by the slug the upload printed or by its filename, and a project docume
148
148
  slug also arrives on `artifact.created` events. Dispatch suffixes a slug two documents would share, so one document's
149
149
  filename can be another's slug (`plan v2` takes `plan-v2`, then a document named `plan-v2` takes `plan-v2-2`): a bare
150
150
  `--artifact` that names both is refused with each one's id, while a `dispatch://` reference's document part is always the slug.
151
+ A document you upload holding a copy of another document's `:::ask` block, under the same `#id` and with the same
152
+ question, options, `multiple` and urgency, on the same issue (or the same project for a project document), opens no
153
+ ask: the copy shows the state and answer of the ask it was copied from, names it and its document in `copied_from` and
154
+ `copied_from_document`, and cannot be answered there. Answer the original: its answer or resolution shows on the copy a
155
+ few seconds later. A copy may hold the original's earlier wording, from before someone reworded it. It stays a copy
156
+ while the original is open, and shows an answer or resolution given while the original still asked that wording; an
157
+ answer or resolution given after the rewording decides a question the copy does not ask, so the copy opens its own ask
158
+ instead. A copy whose text you change opens a new ask. Once the original's block leaves its document, its copies share
159
+ one new ask: the first copy to settle opens it and the others name it.
160
+
161
+ An answered or resolved ask's question is fixed. Editing its block's question, options, `multiple` or urgency in the
162
+ document, or uploading a version that changes them, does not change the ask: a few seconds later the block is written
163
+ back to the question the answer was given on, beside its answer. To ask something new, write a new ask block with its
164
+ own `#id`. An open ask's block is its text: edit it and the ask changes with it.
151
165
 
152
166
  A picture meant to be seen inline in a message, comment or ask is not uploaded with `dispatch artifact`: pass its local
153
167
  path with that command's `--image`, once per picture (PNG, JPEG, GIF or WebP by its bytes, at most 25 MiB each). Each is uploaded to the issue (a
@@ -193,12 +193,17 @@ unsent:
193
193
  `controller_wake_interval_seconds` (an hour by default), whatever the slots. An earlier walk
194
194
  that found nothing, a day with no event, and [a tree waiting on a root
195
195
  claim](#trees-waiting-on-a-root-claim-go-daemon) all get a turn from it. Its payload's
196
- `openCapabilities`, when present, names the deployment capabilities (the secrets broker, model
197
- fallback, every role's CPU and memory limits) the deployment's own configuration leaves open with
198
- no decision recorded. `read_state` lists every row under `capabilities`, each open one
199
- with its `detail` and the `configLine` to write into `legion.yaml`
200
- (`capabilities.decided.<name>: "<reason>"`); name them in the day's report. A gap is the
201
- operator's to close or to decide, never yours, and it never stops the walk.
196
+ `openCapabilities`, when present, names the open capability rows: the deployment capabilities
197
+ (the secrets broker, model fallback, every role's CPU and memory limits) the deployment's own
198
+ configuration leaves open with no decision recorded, and the live capabilities (`subagents`,
199
+ `web-search`, `mcp`, `repository-extensions`, `dispatch-envoy-tools`, `github`) a running
200
+ session's own check at its start failed. `read_state` lists every row under `capabilities`, each
201
+ open one with its `detail`: on a deployment row, with the `configLine` to write into
202
+ `legion.yaml` (`capabilities.decided.<name>: "<reason>"`); on a live row, naming each failing
203
+ session (its pod or pane, its role, its issue) and the fact its check found, with no
204
+ `configLine`, since nothing in `legion.yaml` decides a live row. Name them in the day's report.
205
+ A gap is the operator's to close or to decide — a live one the operator's or the issue's
206
+ architect's to fix in the session's environment — never yours, and it never stops the walk.
202
207
 
203
208
  **Scope first.** The scope the deployment instructions state decides which issues are candidates
204
209
  at all, before anything below. When they say you hand Legion no issue yourself, or that Legion
@@ -350,9 +355,12 @@ every pull request by its URL.
350
355
  when it made one: what it took, and how many candidates each row of the table skipped, so a
351
356
  reader can see why a free slot stays empty.
352
357
  - **Open capabilities.** Each row of `capabilities` in `read_state` whose `status` is
353
- `open` (the `tick` payload's `openCapabilities`): its name, its `detail`, and its `configLine`,
354
- the `legion.yaml` line that records a decision. The operator closes or decides it; you only
355
- report it, and a report with none says nothing of them.
358
+ `open` (the `tick` payload's `openCapabilities`): its name, its `detail`, and, on a deployment
359
+ row, its `configLine`, the `legion.yaml` line that records a decision. A live row a running
360
+ session failed has no `configLine`: its `detail` names the session (pod or pane), the role, the
361
+ issue and the fact, and nothing in `legion.yaml` decides it. The operator closes or decides a
362
+ deployment row, and the operator or the issue's architect fixes a live row's session
363
+ environment; you only report either, and a report with none says nothing of them.
356
364
 
357
365
  A day with nothing finished says so in one sentence. When the lists do not fit, keep the counts
358
366
  and the highest-priority issues.
@@ -394,7 +402,7 @@ priority first, then board rank ([Keeping the slots full](#keeping-the-slots-ful
394
402
  | New issue created in the Dispatch project, status `triage`: `triage on <KEY>` (payload `{kind: "triage"}`) on the controller topic, for an unrecorded root carrying the `legion` label only ("Issues handed to Legion" above); the start procedure above catches one sent while no controller ran | issue key + triage context (incl. pre-existing children) | Triage: `set_status` to `todo` to admit, or to `backlog`/`icebox` to park |
395
403
  | `slot-free on <KEY>` from the Go daemon (payload `{kind: "slot-free"}`) | the root whose slot the daemon released with no waiting root to take it | Verify a free slot in `read_state`, then fill it ([Keeping the slots full](#keeping-the-slots-full-go-daemon)) |
396
404
  | `todo on <KEY>` from the Go daemon (payload `{kind: "todo"}`) | an issue not handed to Legion that changed while in `todo` and a slot stood free, sent half a minute later | Verify a free slot, then walk the whole `todo` list ([Keeping the slots full](#keeping-the-slots-full-go-daemon)) |
397
- | `tick on <PROJECT>` from the Go daemon (payload `{kind: "tick", openCapabilities?: [<name>, …]}`) | the project key; the daemon's periodic wake, whatever the slots; `openCapabilities` names the deployment capabilities with no decision, each with its detail and `capabilities.decided.<name>` line under `capabilities` in `read_state` | Recheck the trees waiting on a claim, then walk if a slot is free — a gap never stops the walk; post the day's report if this is the day's first turn, naming each open capability in it (the operator closes or decides it) |
405
+ | `tick on <PROJECT>` from the Go daemon (payload `{kind: "tick", openCapabilities?: [<name>, …]}`) | the project key; the daemon's periodic wake, whatever the slots; `openCapabilities` names the deployment capabilities with no decision, each with its detail and `capabilities.decided.<name>` line under `capabilities` in `read_state`, and the live capabilities a running session's check failed, each with the detail naming the session (pod or pane), role, issue and fact, and no `configLine` | Recheck the trees waiting on a claim, then walk if a slot is free — a gap never stops the walk; post the day's report if this is the day's first turn, naming each open capability in it (the operator closes or decides a deployment row; the operator or the issue's architect fixes a live row's session; you take no other action) |
398
406
  | Architect escalation (controller-actionable only: re-file a child as a root issue, capacity, cross-tree conflicts) | request + context | Judge and act; the owning architect writes an issue-design decision as a decision block and opens `dispatch ask` only for a human to-do |
399
407
  | Mention | Slack/GitHub PR @mention text | Answer, or route to the owning issue's architect role |
400
408
  | `held on <KEY>` from the Go daemon (payload `{kind: "held", phase, role?, reason?}`) | the held issue, the phase it left, and the role whose claim failed, or `reason: "escalated"` | Verify the hold in `read_state` (the issue's phase is `held`). Without `reason`, a phase worker's launches or prompts ran out and the tree's architect decides retry or escalate: no action. With `reason: "escalated"` (on the record, `issues.<KEY>.holdReason` is `escalated`), the architect sent it to you: handle it as an architect escalation below. Parking the tree is `set_status` of the root to `backlog`; setting the root back to `todo` later re-admits it as a new generation, which starts again from its architect |