@sjawhar/opencode-legion-envoy 0.12.0 → 0.12.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/opencode-legion-envoy",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "type": "module",
5
5
  "main": "dist/src/server.js",
6
6
  "exports": {
@@ -158,6 +158,15 @@ dispatch({
158
158
  (`#N is not a dispatch thread`, `#N is closed; open a new thread`).
159
159
  - Same decision → same thread. A genuinely new decision → a new thread, even if it came
160
160
  up in the reply.
161
+ - **A follow-up supersedes the thread's earlier unanswered questions.** The dashboard
162
+ offers an answer only for the latest turn; earlier open questions read "superseded by a
163
+ later follow-up". If an earlier question still stands, restate it in the follow-up's
164
+ `ask` list. A follow-up with no `ask` list is one free-text question — the text under
165
+ its `## Question` — and the human answers it in prose.
166
+ - **Ask once.** An unanswered question is not a reason to post again. New facts that do
167
+ not change the decision go in a plain `gh issue comment` on the thread, not a new
168
+ `dispatch` call; a follow-up is for a changed question. A thread that fills with
169
+ restated asks is noise for the human and hides the decision it is waiting on.
161
170
  - **Read a challenge as an answer.** "Why not the bot?" is not a request for more prose;
162
171
  it is the human declining your framing. Say what you now know, then ask the narrowed
163
172
  question with new options — unless the challenge itself decides ("Why not the bot? Just
@@ -99,7 +99,7 @@ Settlement waits for the head to be quiet for a few seconds, every reported chec
99
99
  and every recorded GitHub check suite to be `completed`. It covers those reported checks and suites
100
100
  for the head, not GitHub's required-checks set; until then, a silent subscription is normal.
101
101
 
102
- Check settlement is at-least-once: a settlement can be followed by a `superseded_settlement: "true"` payload. Every settlement carries its attempt set `check_runs` — the latest GitHub check-run id per check name, sorted by name — plus the listener's `generation` (the record's state version) and `snapshot` (the record's hash). Consumers order same-head settlements by the attempt set, compared per shared name: no id lower and some id higher (or a new name) is newer; every shared id equal and no new name is the same set; no id higher and some lower is older; anything else is a mixed view and is dropped as a conflict (names only in the stored set are ignored — a check can vanish from GitHub's view, and a record recreated after the seven-day KV TTL starts sparse). Within one producer record per-name ids never decrease, and a consumer's fence is the per-name maximum over every view it has accepted — an accepted set merges into the fence, nothing is pruned — so the fence never decreases either: a newer attempt is newer whatever its completion time, no timestamps take part in ordering, and a name an incomplete view omitted cannot later reappear as new. At the same set the listener's `generation` orders its own settlements: lower is stale; equal is a duplicate when the `snapshot` matches and otherwise a conflict (an equal pair with a different snapshot cannot occur within one record's lifetime; a recreated record may reuse one and is dropped). A live settlement is a possibly incomplete view of the head (a missed webhook, a record recreated after the KV TTL): it decides the outcome of every name it reports — at any id the ordering accepted, including the same run observed in place — and says nothing about the rest, whose last known outcome stands; the head is red while any failure remains. A consumer that reconciles a verdict from GitHub's rollup compares the rollup's attempt set the same way, but GitHub's read is complete: its failing check runs and failing commit statuses replace the stored ones wholesale. Statuses have no check run and the listener never sees them, so a consumer keeps them apart from check-run failures: a check run that shares a status's name cannot retire it — only GitHub does (likewise a deleted check's failure). A newer rollup set merges into the fence and takes the identity (no listener generation); the same set applies GitHub's verdict and keeps the listener identity for duplicate detection; an older, mixed, or empty-over-fenced set is ignored. A terminal read (green or red) then holds the tie at that set: a live settlement at the same set is accepted only if its effective outcome — the check-run failures it reports plus the stored ones it omits and the stored commit-status failures — agrees with the reconciled verdict, refreshing the listener identity without releasing GitHub's authority; a disagreeing one is stale whatever its generation until the set advances; a pending or cancelled-only read uncertifies a green head, leaves a red one untouched, and holds nothing — it releases any authority held at that set — so the terminal live settlement that follows applies at once, subject to the ordinary generation and duplicate rules (a replay or a lower generation still does not apply). Pending is therefore not a commutative join: a pending read after a live green uncertifies it until the next terminal view. Two remainders. An in-place conclusion change on an existing run id: GitHub's view stands and the listener's is recovered by the next successful, non-skipped read at that set — the dropped delivery is not replayed. A check whose highest run is deleted on GitHub: the fence keeps that id, so a rollup reporting a lower run under the same name is older until a newer run appears. A head publishes only when at least one check has a positive run id; legacy checks without one remain in the status groups and failing names but not in `check_runs`. A legacy in-progress check whose completion is never observed holds the head unsettled until it reruns; rerun the affected check to release it.
102
+ Check settlement is at-least-once: a settlement can be followed by a `superseded_settlement: "true"` payload. Every settlement carries its attempt set `check_runs` — the latest GitHub check-run id per check name, sorted by name — plus the listener's `generation` (the record's state version) and `snapshot` (the record's hash). Consumers order same-head settlements by the attempt set, compared per shared name: no id lower and some id higher (or a new name — a new name counts as higher) is newer; every shared id equal and no new name is the same set; no id higher, no new name, and some id lower is older; anything else (a higher or new alongside a lower) is a mixed view and is dropped as a conflict (names only in the stored set are ignored — a check can vanish from GitHub's view, and a record recreated after the seven-day KV TTL starts sparse). Within one producer record per-name ids never decrease, and a consumer's fence is the per-name maximum over every view it has accepted — an accepted set merges into the fence, nothing is pruned — so the fence never decreases either: a newer attempt is newer whatever its completion time, no timestamps take part in ordering, and a name an incomplete view omitted cannot later reappear as new. At the same set the listener's `generation` orders its own settlements: lower is stale; equal is a duplicate when the `snapshot` matches and otherwise a conflict (an equal pair with a different snapshot cannot occur within one record's lifetime; a recreated record may reuse one and is dropped). A live settlement is a possibly incomplete view of the head (a missed webhook, a record recreated after the KV TTL): it decides the outcome of every name it reports — at any id the ordering accepted, including the same run observed in place — and says nothing about the rest: a known failure among them stands (the consumer keeps failure names, not a per-name status map), and the head is red while any failure remains. A consumer that reconciles a verdict from GitHub's rollup compares the rollup's attempt set the same way, but GitHub's read is complete: its failing check runs and failing commit statuses replace the stored ones wholesale. Statuses have no check run and the listener never sees them, so a consumer keeps them apart from check-run failures: a check run that shares a status's name cannot retire it — only GitHub does (likewise a deleted check's failure). A newer rollup set merges into the fence and takes the identity (no listener generation); the same set applies GitHub's verdict and keeps the listener identity for duplicate detection; an older, mixed, or empty-over-fenced set is ignored. A terminal read (green or red) then holds the tie at that set: a live settlement at the same set is accepted only if its effective outcome — the check-run failures it reports plus the stored ones it omits and the stored commit-status failures — agrees with the reconciled verdict, refreshing the listener identity without releasing GitHub's authority; a disagreeing one is stale whatever its generation until the set advances; a pending or cancelled-only read uncertifies a green head, leaves a red one untouched, and holds nothing — it releases any authority held at that set — so the terminal live settlement that follows applies at once, subject to the ordinary generation and duplicate rules (a replay or a lower generation still does not apply). Pending is therefore not a commutative join: a pending read after a live green uncertifies it until the next terminal view. Two remainders. An in-place conclusion change on an existing run id: GitHub's view stands and the listener's is recovered by the next successful, non-skipped read at that set — the dropped delivery is not replayed. A check whose highest run is deleted on GitHub: the fence keeps that id, so a rollup reporting a lower run under the same name is older until a newer run appears. A consumer that orders head changes by the PR's `updated_at` (GitHub's second resolution) accepts a read of a different head at an equal clock — a stale read returning the previous head within the same second as its replacement rewinds that consumer until its next accurate, non-skipped read. A head publishes only when at least one check has a positive run id; legacy checks without one remain in the status groups and failing names but not in `check_runs`. A legacy in-progress check whose completion is never observed holds the head unsettled until it reruns; rerun the affected check to release it.
103
103
 
104
104
  ## When a subscription is silent
105
105
 
package/src/server.ts CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  } from "@legion/envoy-client/dispatch-contract";
15
15
  import { dispatchSubscriptionTopic } from "@legion/envoy-client/dispatch-subscribe";
16
16
  import { machineID } from "@legion/envoy-client/machine";
17
- import { envoyToolSpecs } from "@legion/envoy-client/tool-contract";
17
+ import { envoyToolSpecs, type ToolSpec } from "@legion/envoy-client/tool-contract";
18
18
  import { createEnvoyClient } from "@legion/envoy-client/transport";
19
19
  import { tool } from "@opencode-ai/plugin/tool";
20
20
  import { logger } from "./log";
@@ -32,16 +32,20 @@ const skillsDirectory = [
32
32
  path.resolve(moduleDirectory, "../../../skills"),
33
33
  ].find((dir) => existsSync(dir));
34
34
 
35
- const [
36
- subscribeSpec,
37
- unsubscribeSpec,
38
- listSpec,
39
- sendSpec,
40
- publishSpec,
41
- roleSetSpec,
42
- whoamiSpec,
43
- sessionsSpec,
44
- ] = envoyToolSpecs;
35
+ function toolSpec(name: string): ToolSpec {
36
+ const spec = envoyToolSpecs.find((candidate) => candidate.name === name);
37
+ if (spec === undefined) throw new Error(`envoy tool contract has no ${name}`);
38
+ return spec;
39
+ }
40
+
41
+ const subscribeSpec = toolSpec("envoy_subscribe");
42
+ const unsubscribeSpec = toolSpec("envoy_unsubscribe");
43
+ const listSpec = toolSpec("envoy_list");
44
+ const sendSpec = toolSpec("envoy_send");
45
+ const publishSpec = toolSpec("envoy_publish");
46
+ const roleSetSpec = toolSpec("envoy_role_set");
47
+ const whoamiSpec = toolSpec("envoy_whoami");
48
+ const sessionsSpec = toolSpec("envoy_sessions");
45
49
 
46
50
  // The dispatch tool's LLM-facing schema, built with OpenCode's own zod
47
51
  // (`tool.schema`): OpenCode converts plugin schemas with that instance, and a