@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.
- package/dist/src/server.js +24 -8
- package/package.json +1 -1
- package/skills/dispatch/SKILL.md +17 -12
- package/skills/legion-architect/SKILL.md +21 -12
- package/skills/legion-worker/SKILL.md +6 -2
package/dist/src/server.js
CHANGED
|
@@ -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:// " +
|
|
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
|
-
|
|
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
|
|
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
package/skills/dispatch/SKILL.md
CHANGED
|
@@ -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
|
|
131
|
-
|
|
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
|
|
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
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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`;
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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.
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|