@sjawhar/opencode-legion-envoy 1.22.0 → 1.24.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/src/server.js +50 -8
- package/package.json +1 -1
- package/skills/dispatch/SKILL.md +27 -4
- package/skills/envoy/SKILL.md +10 -0
- package/skills/legion-architect/SKILL.md +1 -1
package/dist/src/server.js
CHANGED
|
@@ -13552,6 +13552,7 @@ function date4(params) {
|
|
|
13552
13552
|
// ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/classic/external.js
|
|
13553
13553
|
config(en_default());
|
|
13554
13554
|
// ../contracts/src/dispatch-api.ts
|
|
13555
|
+
var DELIVERY_CAPABILITIES = ["aside", "btw", "steer"];
|
|
13555
13556
|
function searchOwnerOf(result) {
|
|
13556
13557
|
return result.owner ?? { kind: "issue", key: result.issue.key };
|
|
13557
13558
|
}
|
|
@@ -13634,6 +13635,8 @@ var CommentEventPayloadSchema = object({
|
|
|
13634
13635
|
ask_id: string2().nullish(),
|
|
13635
13636
|
ask_question: string2().optional(),
|
|
13636
13637
|
ask_state: _enum2(["open", "answered", "resolved"]).optional(),
|
|
13638
|
+
ask_waiting_on: _enum2(["human", "agent"]).optional(),
|
|
13639
|
+
turn: _enum2(["human", "agent"]).nullish(),
|
|
13637
13640
|
anchor: object({ block_id: string2().nullable().optional(), quote: string2().optional() }).nullish(),
|
|
13638
13641
|
suggestion: object({ replace_with: string2().optional() }).nullish(),
|
|
13639
13642
|
author: object({ kind: string2(), id: string2() }).optional(),
|
|
@@ -13660,7 +13663,7 @@ var DispatchTargetedMessagePayloadSchema = MessageEventPayloadSchema.extend({
|
|
|
13660
13663
|
var MessageDeliveryEventPayloadSchema = object({
|
|
13661
13664
|
message_id: string2().optional(),
|
|
13662
13665
|
attempt: number2().int().positive().optional(),
|
|
13663
|
-
delivery: _enum2(
|
|
13666
|
+
delivery: _enum2(DELIVERY_CAPABILITIES).optional(),
|
|
13664
13667
|
session_id: string2().optional(),
|
|
13665
13668
|
target: string2().optional(),
|
|
13666
13669
|
title: string2().optional(),
|
|
@@ -13751,6 +13754,16 @@ function documentOwnerValidation(requireArtifact, alwaysRequireArtifact = false)
|
|
|
13751
13754
|
message: alwaysRequireArtifact ? "Exactly one of issue and project is required; artifact or ref must name the document." : "Exactly one of issue and project is required; with project, artifact names the document."
|
|
13752
13755
|
};
|
|
13753
13756
|
}
|
|
13757
|
+
var commentValidation = (() => {
|
|
13758
|
+
const owner = documentOwnerValidation(true);
|
|
13759
|
+
return {
|
|
13760
|
+
check: (value) => {
|
|
13761
|
+
const input = value;
|
|
13762
|
+
return owner.check(value) && (input.turn === undefined || typeof input.reply_to_ask === "string");
|
|
13763
|
+
},
|
|
13764
|
+
message: `${owner.message} turn requires reply_to_ask.`
|
|
13765
|
+
};
|
|
13766
|
+
})();
|
|
13754
13767
|
var SPEC_SECTIONS = [
|
|
13755
13768
|
"Summary",
|
|
13756
13769
|
"Decisions needed",
|
|
@@ -13846,9 +13859,10 @@ var dispatchToolSpecs = [
|
|
|
13846
13859
|
occurrence: z.number({ int: true, min: 0 }).describe("Optional zero-based occurrence of quote.").optional(),
|
|
13847
13860
|
body: z.string({ max: 2000 }).describe("Review comment, at most 2,000 characters."),
|
|
13848
13861
|
reply_to: z.string().describe("Full id of a comment to reply to; replying to any comment in a thread continues that " + "thread (an ask's clarification thread included).").optional(),
|
|
13849
|
-
reply_to_ask: z.string().describe("Optional ask id to reply to, threading this comment under that question. Mutually " + "exclusive with reply_to.").optional()
|
|
13862
|
+
reply_to_ask: z.string().describe("Optional ask id to reply to, threading this comment under that question. Mutually " + "exclusive with reply_to.").optional(),
|
|
13863
|
+
turn: z.enum(["agent", "human"]).describe("Only with reply_to_ask: who holds the turn after this reply. agent: a progress note - " + "you keep the turn and the ask stays 'Waiting on agents' for the human; human (default): " + "you need the human to act - the ask returns to 'Waiting on you'.").optional()
|
|
13850
13864
|
}),
|
|
13851
|
-
validation:
|
|
13865
|
+
validation: commentValidation
|
|
13852
13866
|
},
|
|
13853
13867
|
{
|
|
13854
13868
|
name: "dispatch_suggest",
|
|
@@ -14194,6 +14208,19 @@ var stateRole = strictObject({
|
|
|
14194
14208
|
launchFailures: number2().int().nonnegative().optional(),
|
|
14195
14209
|
locator: stateLocator.optional()
|
|
14196
14210
|
});
|
|
14211
|
+
var stateQueuedWorkerIdentity = {
|
|
14212
|
+
roleToken: nonEmptyString,
|
|
14213
|
+
issue: nonEmptyString,
|
|
14214
|
+
role: nonEmptyString
|
|
14215
|
+
};
|
|
14216
|
+
var stateQueuedWorker = union([
|
|
14217
|
+
strictObject(stateQueuedWorkerIdentity),
|
|
14218
|
+
strictObject({
|
|
14219
|
+
...stateQueuedWorkerIdentity,
|
|
14220
|
+
kind: _enum2(["assignment", "catchup"]),
|
|
14221
|
+
queuedAt: nonEmptyString
|
|
14222
|
+
})
|
|
14223
|
+
]);
|
|
14197
14224
|
var LegionDaemonApi = {
|
|
14198
14225
|
State: {
|
|
14199
14226
|
response: strictObject({
|
|
@@ -14210,7 +14237,8 @@ var LegionDaemonApi = {
|
|
|
14210
14237
|
controllerLocator: stateLocator.optional(),
|
|
14211
14238
|
roles: record(string2(), stateRole),
|
|
14212
14239
|
controllerPendingNotices: number2().int().nonnegative(),
|
|
14213
|
-
pendingStatusWrites: array(nonEmptyString)
|
|
14240
|
+
pendingStatusWrites: array(nonEmptyString),
|
|
14241
|
+
workerAdmission: strictObject({ queue: array(stateQueuedWorker) })
|
|
14214
14242
|
})
|
|
14215
14243
|
},
|
|
14216
14244
|
ControllerReady: {
|
|
@@ -14297,7 +14325,8 @@ var LegionDaemonApi = {
|
|
|
14297
14325
|
request: architectCapability.extend({
|
|
14298
14326
|
issue: nonEmptyString,
|
|
14299
14327
|
role: legionRole,
|
|
14300
|
-
task: nonEmptyString
|
|
14328
|
+
task: nonEmptyString,
|
|
14329
|
+
requestId: uuid2()
|
|
14301
14330
|
}),
|
|
14302
14331
|
response: object({
|
|
14303
14332
|
status: _enum2(["spawned", "resumed", "queued"]),
|
|
@@ -15598,21 +15627,34 @@ async function executeDispatchTool(input) {
|
|
|
15598
15627
|
if (replyTo !== undefined && replyToAsk !== undefined) {
|
|
15599
15628
|
throw new Error("reply_to and reply_to_ask cannot both be set");
|
|
15600
15629
|
}
|
|
15630
|
+
const requestedTurn = optionalString(args, "turn");
|
|
15631
|
+
if (requestedTurn !== undefined && replyToAsk === undefined) {
|
|
15632
|
+
throw new Error("turn requires reply_to_ask");
|
|
15633
|
+
}
|
|
15634
|
+
if (requestedTurn !== undefined && requestedTurn !== "agent" && requestedTurn !== "human") {
|
|
15635
|
+
throw new Error("turn must be agent or human");
|
|
15636
|
+
}
|
|
15601
15637
|
const commentInput = {
|
|
15602
15638
|
body: stringArg(args, "body"),
|
|
15603
15639
|
...anchored === undefined ? {} : { anchor: anchored },
|
|
15604
15640
|
...replyTo === undefined ? {} : { reply_to: replyTo },
|
|
15605
15641
|
...replyToAsk === undefined ? {} : { ask_id: replyToAsk },
|
|
15642
|
+
...requestedTurn === undefined ? {} : { turn: requestedTurn },
|
|
15606
15643
|
actor
|
|
15607
15644
|
};
|
|
15608
15645
|
const comment = resolved?.owner.kind === "project" ? await client.artifactComment(resolved.artifact.id, commentInput) : await client.comment(issue(), commentInput);
|
|
15646
|
+
const askState = comment.turn === null ? "" : ` (ask now waiting on ${comment.turn})`;
|
|
15609
15647
|
return {
|
|
15610
|
-
text: `Posted comment ${comment.id}`,
|
|
15648
|
+
text: `Posted comment ${comment.id}${askState}`,
|
|
15611
15649
|
details: resolved === undefined ? {
|
|
15612
15650
|
issue: comment.issue_key,
|
|
15613
15651
|
topic: dispatchIssueSubject(issue(), ">"),
|
|
15614
|
-
comment: comment.id
|
|
15615
|
-
|
|
15652
|
+
comment: comment.id,
|
|
15653
|
+
...comment.turn === null ? {} : { ask_waiting_on: comment.turn }
|
|
15654
|
+
} : writeResultDetails(resolved, {
|
|
15655
|
+
comment: comment.id,
|
|
15656
|
+
...comment.turn === null ? {} : { ask_waiting_on: comment.turn }
|
|
15657
|
+
})
|
|
15616
15658
|
};
|
|
15617
15659
|
}
|
|
15618
15660
|
case "dispatch_suggest": {
|
package/package.json
CHANGED
package/skills/dispatch/SKILL.md
CHANGED
|
@@ -42,6 +42,16 @@ agent's `envoy.json` to set the GitHub OAuth callback origin: the value must
|
|
|
42
42
|
be the exact URL humans type in their browser, and the GitHub App callback is
|
|
43
43
|
`<DISPATCH_SERVER_URL>/auth/callback`.
|
|
44
44
|
|
|
45
|
+
### Finding a route
|
|
46
|
+
|
|
47
|
+
The tools cover the everyday surface. For anything else, ask the server: `GET /api/v1` (no
|
|
48
|
+
credential) returns every route as `{method, path, auth, description}` sorted by path — `auth`
|
|
49
|
+
is `public`, `any` (a human or a bearer), `human` (a bearer gets `403 HUMAN_ONLY`), or `bearer`.
|
|
50
|
+
A path Dispatch does not serve under `/api` or `/v1` answers
|
|
51
|
+
`404 {"code":"NOT_FOUND","error":"no route for GET /v1/issues","hint":"GET /api/v1 lists every
|
|
52
|
+
route"}`; when you see that, you typed the path wrong — read the index rather than guessing. Every
|
|
53
|
+
`/api/v1` error body carries a `code`; branch on the code, never on the text.
|
|
54
|
+
|
|
45
55
|
## Writing a spec
|
|
46
56
|
|
|
47
57
|
A spec has two readers: the human who decides reads the top; the implementer who builds reads the
|
|
@@ -194,6 +204,14 @@ an answer: the human did not understand the question or needs more before choosi
|
|
|
194
204
|
in the same thread with `dispatch_comment({ reply_to_ask })`, or reword the question itself with `dispatch_edit_ask` when the wording
|
|
195
205
|
was the problem; either puts the ask back in front of them. Do not open a second ask.
|
|
196
206
|
|
|
207
|
+
Every reply to an open ask says whose turn it is next, and the Inbox files the ask by that, not by who spoke last. Your plain reply
|
|
208
|
+
(`turn` omitted, or `turn: "human"`) hands the turn to the human: the ask returns to their `Waiting on you`. When you are not done
|
|
209
|
+
yet — "dispatched two auditors, back with results", "checking the release branch, back shortly", any working-on-it note — reply with
|
|
210
|
+
`turn: "agent"`: the note lands in the thread, the ask stays under `Waiting on agents`, and the human is not told to act. Use
|
|
211
|
+
`turn: "agent"` for every progress note and `turn: "human"` (the default) only when you need them. A human's reply always hands the
|
|
212
|
+
turn to you. The result names the state (`ask now waiting on agent` / `human`), the delivered `comment.created` carries it as
|
|
213
|
+
`ask_waiting_on`, and every ask read carries it as `waiting_on`.
|
|
214
|
+
|
|
197
215
|
## Approval of a spec
|
|
198
216
|
|
|
199
217
|
Approval is a property of a document, not a question you phrase: a human approves a specific version, the way a pull-request
|
|
@@ -300,7 +318,7 @@ an ask block without a question or with a blank option is rejected with `INVALID
|
|
|
300
318
|
Add feedback with:
|
|
301
319
|
|
|
302
320
|
```ts
|
|
303
|
-
dispatch_comment({ issue?, project?, artifact?, ref?, quote?, occurrence?, body, reply_to?, reply_to_ask? })
|
|
321
|
+
dispatch_comment({ issue?, project?, artifact?, ref?, quote?, occurrence?, body, reply_to?, reply_to_ask?, turn? })
|
|
304
322
|
```
|
|
305
323
|
|
|
306
324
|
It returns issue or project-document owner details plus `comment` and, for writes, `topic`.
|
|
@@ -309,8 +327,10 @@ It returns issue or project-document owner details plus `comment` and, for write
|
|
|
309
327
|
for display. Omit both for a floating issue comment. A reply (`reply_to`/`reply_to_ask`) takes no
|
|
310
328
|
`quote`; it belongs to its parent's anchor. Reply to any comment in a thread; the server keeps
|
|
311
329
|
threads flat. A reply to a resolved thread reopens it. Use `reply_to_ask` to reply directly under a
|
|
312
|
-
question asked with `dispatch_ask
|
|
313
|
-
|
|
330
|
+
question asked with `dispatch_ask`; `turn` (only with `reply_to_ask`) says who holds the turn after
|
|
331
|
+
the reply — `agent` for a progress note that keeps the ask waiting on you, `human` (the default) when
|
|
332
|
+
the human needs to act; see [Asking](#asking). Comments are edited only by their author from the
|
|
333
|
+
dashboard. A delivered `comment.created` event carries the comment `id`; reply to it with
|
|
314
334
|
`dispatch_comment({ reply_to: <id> })`.
|
|
315
335
|
|
|
316
336
|
Propose an exact replacement instead of describing it:
|
|
@@ -395,7 +415,10 @@ dispatch_message({
|
|
|
395
415
|
delivery can post its answer automatically; use this call when the frame asks the primary agent to
|
|
396
416
|
reply. `dispatch_message` itself never carries `target` or `delivery`: agent-to-agent traffic goes
|
|
397
417
|
through Envoy or the hub. A bearer that targets over HTTP names its own session in `actor`
|
|
398
|
-
(`{kind: "session", id}`), and the card shows that session as the author.
|
|
418
|
+
(`{kind: "session", id}`), and the card shows that session as the author. `GET /api/v1/agents`
|
|
419
|
+
(any authenticated caller) lists live sessions with their capabilities (`aside`, `btw`, `steer`);
|
|
420
|
+
target only a session that advertises the mode you want. Sending to a session with no issue
|
|
421
|
+
(`POST /api/v1/agents/{session_id}/messages`) stays human-only.
|
|
399
422
|
|
|
400
423
|
## What comes back
|
|
401
424
|
|
package/skills/envoy/SKILL.md
CHANGED
|
@@ -90,6 +90,16 @@ envoy_send(
|
|
|
90
90
|
)
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
+
### Delivery capabilities
|
|
94
|
+
|
|
95
|
+
Each session row from `envoy_sessions` carries `capabilities`, the targeted-delivery modes that
|
|
96
|
+
session's host honours: `aside` (a message queued beside the model's work), `btw` (an ephemeral
|
|
97
|
+
question the host answers without disturbing the current turn), and `steer` (an interjection at
|
|
98
|
+
the next tool boundary). An OMP session advertises `aside`, `btw`, and `steer` (`aside` and
|
|
99
|
+
`steer` on a host without `askEphemeral`); a Claude Code session advertises `aside` only, because
|
|
100
|
+
its channel notifications queue for the next turn. Target a session only with a mode it
|
|
101
|
+
advertises.
|
|
102
|
+
|
|
93
103
|
## Waiting for CI or a merge
|
|
94
104
|
|
|
95
105
|
Subscribe to `notifications.github.example-org.example-repo.pr.42.>` and end the turn. The single
|
|
@@ -301,7 +301,7 @@ active phase worker.
|
|
|
301
301
|
| `design-approved` | Payload `{type:"design-approved"}`. A human approved the root spec document at its current version; the gate is open. Proceed to section 2. |
|
|
302
302
|
| `design-changes-requested` | Payload `{type:"design-changes-requested", version, reason, author?}`. A human asked for changes to the root spec at `version`, for `reason`. Revise the spec, call `dispatch_request_approval` again, and stay parked; the gate is closed. |
|
|
303
303
|
| `phase-complete` | Payload `{type:"phase-complete", issue, role, summary}`. May arrive live or via `catchup-overseer`'s `phaseCompletions`. Read the committed handoff for that phase, then spawn the next phase's owner, or `spawn_worker` on the same role again to resume it with corrections if the handoff shows unresolved gaps. A `reviewer` completion whose GitHub review is `CHANGES_REQUESTED` (the daemon returns the issue's Dispatch status to `in_progress` for this, on the reviewer's completion and again when you spawn the corrective implementer unless the daemon already knows the issue is `in_progress`) means `spawn_worker` the **implementer** again with the review findings — thread URLs and blocking items — as its task, then route back through tester and reviewer in order; never `spawn_worker` the reviewer directly off this wake and never proceed to retro on this verdict. A reviewer completion with an `APPROVED` review proceeds to retro (step 5). A `reviewer` completion after a conflict-forced rebase whose review body names an unchanged fingerprint is a confirmation, not a round: if retro already completed, `spawn_worker` the merger; otherwise resume the step you were on. An `implementer` completion that follows the merge is its production report: read the record on the pull request and the issue, then run step 7 — the issue is already at `retro`, the daemon writes no status for this completion, and you set `done` yourself. A `tester` completion whose handoff carries `implementerProof.verdict: "rejected"`, or a failure naming the production-like proof, goes back to the **implementer** with that finding — never forward to the reviewer, and never by supplying the proof from another role. A worker that reports no surface reaches the changed path gets a child issue in this tree (infrastructure, tooling, or a skill) and a resume once it lands; that report is never a reason to advance the phase. |
|
|
304
|
-
| `worker-queued` | Payload `{type:"worker-queued", issue, role}`. This role's task is queued for promotion — either the deployment's worker cap is full, or the live worker acknowledged the task without starting a turn and the daemon is retrying it (counted; the worker is replaced after three such failures, still with the same task). Do not respawn or retry — wait for `worker-started`. |
|
|
304
|
+
| `worker-queued` | Payload `{type:"worker-queued", issue, role}`. This role's task is queued for promotion — either the deployment's worker cap is full, or the live worker acknowledged the task without starting a turn and the daemon is retrying it (counted; the worker is replaced after three such failures, still with the same task). Do not respawn or retry — wait for `worker-started`. `legion state` shows the queue (`workerAdmission.queue`: role token, issue, role, kind, and the time the task was first queued — never the task text); read it before re-sending. A `spawn_worker` identical to the queued task changes nothing and is not announced again. Different text replaces the queued task silently in the same FIFO slot and retains its original queue time. A `spawn_worker` that fails with "got no response in 3 attempts" was already retried by the plugin under one request id and may still have reached the daemon: read the queue and the role's claim in `legion state` before sending it again. |
|
|
305
305
|
| `worker-started` | Payload `{type:"worker-started", issue, role}`. A previously queued role has been promoted and is now running. Treat it exactly as a normal spawn: resume tracking that role's live session. |
|
|
306
306
|
| `pr-ready` | Verify the live PR head, green status, and review state. Continue the review/retro/merger order only for that current head. |
|
|
307
307
|
| `pr-review` | Payload `{type:"pr-review", state, author, body}`. Delivered to whichever role is currently active for the issue, falling back to you when no worker phase is active. Follows the same verdict rule as a reviewer's `phase-complete`: `state: "changes_requested"` sends the implementer back in with the review findings, then tester, then reviewer — never the reviewer again and never retro; that `spawn_worker` returns the issue to `in_progress` on its own (the daemon writes it for a corrective implementer whenever the PR's latest recorded review is changes requested, a human's after approval included), so you set nothing by hand; `state: "approved"` proceeds toward retro (step 5) once the step 6 integration/merge-gate conditions are met. `state: "approved"` on a rebased head whose body names an unchanged fingerprint is that confirmation: proceed to retro if it has not run, otherwise to the merger — never to a second retro or test round. |
|