@sjawhar/opencode-legion-envoy 1.16.0 → 1.17.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 +75 -2
- package/package.json +1 -1
- package/skills/dispatch/SKILL.md +7 -4
- package/skills/legion-worker/SKILL.md +14 -7
package/dist/src/server.js
CHANGED
|
@@ -13647,6 +13647,16 @@ var MessageEventPayloadSchema = object({
|
|
|
13647
13647
|
reply_body: string2().optional(),
|
|
13648
13648
|
author: object({ kind: string2(), id: string2() }).optional()
|
|
13649
13649
|
});
|
|
13650
|
+
var DispatchTargetedMessagePayloadSchema = MessageEventPayloadSchema.extend({
|
|
13651
|
+
id: string2(),
|
|
13652
|
+
issue_key: string2(),
|
|
13653
|
+
author: object({ kind: string2(), id: string2() }),
|
|
13654
|
+
body: string2(),
|
|
13655
|
+
target: string2(),
|
|
13656
|
+
in_reply_to: string2().nullable(),
|
|
13657
|
+
deliveries: array(unknown()),
|
|
13658
|
+
created_at: string2()
|
|
13659
|
+
});
|
|
13650
13660
|
var MessageDeliveryEventPayloadSchema = object({
|
|
13651
13661
|
message_id: string2().optional(),
|
|
13652
13662
|
attempt: number2().int().positive().optional(),
|
|
@@ -13721,7 +13731,9 @@ function snippetText(snippet) {
|
|
|
13721
13731
|
// ../contracts/src/dispatch-tools.ts
|
|
13722
13732
|
function dispatchToolSchema(spec, z, opts) {
|
|
13723
13733
|
const shape = spec.arguments(z);
|
|
13724
|
-
|
|
13734
|
+
const strict = opts?.strict ?? spec.strict;
|
|
13735
|
+
const schemaOptions = strict === undefined ? undefined : { strict };
|
|
13736
|
+
return spec.validation === undefined ? z.object(shape, schemaOptions) : z.refineObject(shape, spec.validation.check, spec.validation.message, schemaOptions);
|
|
13725
13737
|
}
|
|
13726
13738
|
var ISSUE_REFERENCE = "An issue is a native KEY or external owner/repo#n reference; an external reference creates its native issue in the repository's dashboard-configured project or, failing that, the default project (DISPATCH_DEFAULT_PROJECT).";
|
|
13727
13739
|
var OWNER_REFERENCE = "Exactly one of issue and project is required. An issue is a native KEY or external owner/repo#n reference; a project is a project key such as CORE and addresses an unlinked project document named by artifact.";
|
|
@@ -13945,6 +13957,12 @@ var dispatchToolSpecs = [
|
|
|
13945
13957
|
project: z.string().describe("Optional project key to search within.").optional(),
|
|
13946
13958
|
limit: z.number({ int: true, min: 1, max: 50 }).describe("Maximum results, 1-50; default 20.").optional()
|
|
13947
13959
|
})
|
|
13960
|
+
},
|
|
13961
|
+
{
|
|
13962
|
+
name: "dispatch_open_asks",
|
|
13963
|
+
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.",
|
|
13964
|
+
arguments: () => ({}),
|
|
13965
|
+
strict: true
|
|
13948
13966
|
}
|
|
13949
13967
|
];
|
|
13950
13968
|
// ../contracts/src/envelope.ts
|
|
@@ -14682,6 +14700,12 @@ class DispatchClient {
|
|
|
14682
14700
|
async ask(issue, input) {
|
|
14683
14701
|
return this.#json("POST", ["api", "v1", "issues", await this.#resolveIssue(issue), "asks"], input);
|
|
14684
14702
|
}
|
|
14703
|
+
async openAsks(sessionID, since) {
|
|
14704
|
+
return this.#json("GET", ["api", "v1", "asks", "open"], undefined, {
|
|
14705
|
+
author_session: sessionID,
|
|
14706
|
+
...since === undefined ? {} : { since }
|
|
14707
|
+
});
|
|
14708
|
+
}
|
|
14685
14709
|
async resolveAsk(id, input) {
|
|
14686
14710
|
return this.#json("POST", ["api", "v1", "asks", id, "resolve"], input);
|
|
14687
14711
|
}
|
|
@@ -14934,7 +14958,8 @@ var issueFreeTools = {
|
|
|
14934
14958
|
dispatch_issue: true,
|
|
14935
14959
|
dispatch_edit_ask: true,
|
|
14936
14960
|
dispatch_resolve_ask: true,
|
|
14937
|
-
dispatch_search: true
|
|
14961
|
+
dispatch_search: true,
|
|
14962
|
+
dispatch_open_asks: true
|
|
14938
14963
|
};
|
|
14939
14964
|
function canonicalExternalIssueRef(value) {
|
|
14940
14965
|
const match = value.trim().match(externalIssueRefPattern);
|
|
@@ -15274,6 +15299,45 @@ function askSummary({ ask, replies }) {
|
|
|
15274
15299
|
].join(`
|
|
15275
15300
|
`);
|
|
15276
15301
|
}
|
|
15302
|
+
function openAskAge(ageSeconds) {
|
|
15303
|
+
const seconds = Math.max(0, Math.floor(ageSeconds));
|
|
15304
|
+
if (seconds < 60)
|
|
15305
|
+
return `${seconds}s`;
|
|
15306
|
+
const minutes = Math.floor(seconds / 60);
|
|
15307
|
+
if (minutes < 60)
|
|
15308
|
+
return `${minutes}m${seconds % 60 === 0 ? "" : ` ${seconds % 60}s`}`;
|
|
15309
|
+
const hours = Math.floor(minutes / 60);
|
|
15310
|
+
if (hours < 24)
|
|
15311
|
+
return `${hours}h${minutes % 60 === 0 ? "" : ` ${minutes % 60}m`}`;
|
|
15312
|
+
const days = Math.floor(hours / 24);
|
|
15313
|
+
return `${days}d${hours % 24 === 0 ? "" : ` ${hours % 24}h`}`;
|
|
15314
|
+
}
|
|
15315
|
+
function openAskOwner(ask) {
|
|
15316
|
+
return "issue" in ask.owner ? `${ask.owner.issue.key}: ${ask.owner.issue.title}` : `${ask.owner.document.project} / ${ask.owner.document.name}`;
|
|
15317
|
+
}
|
|
15318
|
+
function openAskLine(ask, baseUrl) {
|
|
15319
|
+
const priority = ask.priority === null ? "" : `P${ask.priority} \xB7 `;
|
|
15320
|
+
return `- ${openAskAge(ask.age_seconds)} \xB7 ${priority}${openAskOwner(ask)} \xB7 ${ask.question} \xB7 ${new URL(ask.ref, baseUrl).toString()}`;
|
|
15321
|
+
}
|
|
15322
|
+
function formatOpenAsksSummary(response, baseUrl) {
|
|
15323
|
+
const scope = "active open asks you authored on open issues and project documents";
|
|
15324
|
+
if (response.count === 0) {
|
|
15325
|
+
return ["There are no unanswered asks for this session.", `Scope: ${scope}.`].join(`
|
|
15326
|
+
`);
|
|
15327
|
+
}
|
|
15328
|
+
const waitingOnHuman = response.asks.filter((ask) => ask.waiting_on === "human");
|
|
15329
|
+
const waitingOnAgent = response.asks.filter((ask) => ask.waiting_on === "agent");
|
|
15330
|
+
return [
|
|
15331
|
+
`${response.count} unanswered ${response.count === 1 ? "ask" : "asks"} you authored on active issues and project documents.`,
|
|
15332
|
+
"",
|
|
15333
|
+
`Waiting on human (${waitingOnHuman.length}):`,
|
|
15334
|
+
...waitingOnHuman.map((ask) => openAskLine(ask, baseUrl)),
|
|
15335
|
+
"",
|
|
15336
|
+
`Waiting on agent (${waitingOnAgent.length}):`,
|
|
15337
|
+
...waitingOnAgent.map((ask) => openAskLine(ask, baseUrl))
|
|
15338
|
+
].join(`
|
|
15339
|
+
`);
|
|
15340
|
+
}
|
|
15277
15341
|
function commentSummary({ comment, replies }) {
|
|
15278
15342
|
const root = [
|
|
15279
15343
|
`${comment.id} \xB7 ${comment.author.kind} ${comment.author.id}`,
|
|
@@ -15322,6 +15386,15 @@ async function executeDispatchTool(input) {
|
|
|
15322
15386
|
if (!input.config.enabled || !configUrl || !configToken) {
|
|
15323
15387
|
throw new Error("Dispatch is disabled; resolve both DISPATCH_URL and DISPATCH_TOKEN");
|
|
15324
15388
|
}
|
|
15389
|
+
if (input.tool === "dispatch_open_asks") {
|
|
15390
|
+
const sessionId = input.sessionId?.trim();
|
|
15391
|
+
if (!sessionId)
|
|
15392
|
+
throw new Error("host session id is required for dispatch_open_asks");
|
|
15393
|
+
toolSchema(input.tool).parse(input.args);
|
|
15394
|
+
const client = new DispatchClient(configUrl, configToken, input.fetchImpl, input.signal);
|
|
15395
|
+
const response = await client.openAsks(sessionId);
|
|
15396
|
+
return { text: formatOpenAsksSummary(response, configUrl), details: { ...response } };
|
|
15397
|
+
}
|
|
15325
15398
|
const env = input.env ?? process.env;
|
|
15326
15399
|
const exec = input.exec ?? defaultExec;
|
|
15327
15400
|
const ownerArguments = await resolveOwnerArguments(input.tool, input.args, input.cwd, env, exec);
|
package/package.json
CHANGED
package/skills/dispatch/SKILL.md
CHANGED
|
@@ -50,7 +50,7 @@ rest. Use these headings in this order.
|
|
|
50
50
|
| Section | Required content | Form |
|
|
51
51
|
| --- | --- | --- |
|
|
52
52
|
| **Summary** | The problem, what changes for whom, and how we will know it worked — in plain words. | Three sentences at most. |
|
|
53
|
-
| **Decisions needed** | Only decisions that need human authority, taste, or risk appetite. Each is one plain question, two or three options with what each costs, and your recommendation with its reason — understandable without opening anything else.
|
|
53
|
+
| **Decisions needed** | Only decisions that need human authority, taste, or risk appetite. Each is one plain question, two or three options with what each costs, and your recommendation with its reason — understandable without opening anything else. Each is a `dispatch_ask`; anchor it only when it concerns a document passage. An answered item moves into Requirements with its provenance. If there is nothing to decide, write `None: this records what was agreed.` and do not ask for a review. | One decision per line. |
|
|
54
54
|
| **New since we talked** | Every design point the human did not settle in conversation, marked `inferred:` with the reasoning. Empty is fine. | One plain sentence per point. |
|
|
55
55
|
| **Acceptance** | Each outcome names what a user will observe and the check that proves it (browser scenario, API call, or command). An outcome without a check is not acceptance. | Numbered lines. |
|
|
56
56
|
| **Requirements** | What must hold, and where each came from: a quoted human sentence, or `inferred:` plus the reasoning. Readers treat inferred requirements as hypotheses. | `requirement \| where it comes from` table, or prose if the reader follows it more easily. |
|
|
@@ -132,9 +132,12 @@ containing block while retaining its quote as display text, so rewording the pas
|
|
|
132
132
|
attached; a quote spanning top-level blocks, and existing anchors without a block, stay readable
|
|
133
133
|
against their original document version if their quote disappears.
|
|
134
134
|
|
|
135
|
-
An ask must be answerable from its own text and its anchor alone. Anchor a question about a document
|
|
136
|
-
|
|
137
|
-
reference (see [References](#references)). Never write "see above", "the
|
|
135
|
+
An ask must be answerable from its own text and its anchor alone. Anchor a question about a document
|
|
136
|
+
passage with `anchor`. Follow up on an ask or comment with `dispatch_comment`; cite anything else
|
|
137
|
+
with a `dispatch://` reference (see [References](#references)). Never write "see above", "the
|
|
138
|
+
message above", or "as attached".
|
|
139
|
+
|
|
140
|
+
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.
|
|
138
141
|
|
|
139
142
|
**Anything that needs the human is an ask, or it does not exist.** An approval, a credential,
|
|
140
143
|
a setting only they can change, a review click, a conflict between two of their own rules - if
|
|
@@ -146,13 +146,20 @@ capability it needs; invoke GitHub through the credential helper:
|
|
|
146
146
|
legion gh -- <gh args…>
|
|
147
147
|
```
|
|
148
148
|
|
|
149
|
-
|
|
150
|
-
(`packages/
|
|
151
|
-
`legion gh -- …` are the same call, and each call
|
|
152
|
-
grant — identity is supplied per call, never stored.
|
|
153
|
-
`gh auth setup-git`; there is no login state to create. The shim
|
|
154
|
-
`gh api …/merge`): no worker role merges a pull request — the merge
|
|
155
|
-
authority.
|
|
149
|
+
Four facts about `gh` in a worker pane. The `gh` on your `PATH` is a shim
|
|
150
|
+
(`<state_dir>/worker-bin/gh`, installed by the daemon at startup — `packages/daemon/src/daemon/worker-bin.ts`)
|
|
151
|
+
that execs `legion gh -- "$@"`, so `gh …` and `legion gh -- …` are the same call, and each call
|
|
152
|
+
redeems a fresh token from your session's grant — identity is supplied per call, never stored.
|
|
153
|
+
Never run `gh auth login` or `gh auth setup-git`; there is no login state to create. The shim
|
|
154
|
+
refuses `pr merge` (and a raw `gh api …/merge`): no worker role merges a pull request — the merge
|
|
155
|
+
queue does, under its own authority. The credential reaches `legion` through the file
|
|
156
|
+
`$LEGION_GRANT_FILE` names, written before each of your bash commands by the extension; never
|
|
157
|
+
`cat`, `echo`, copy, or `export` it — `legion credential`, `legion gh`, `jj git push`, and
|
|
158
|
+
`legion handoff complete` read it themselves. The file is the pane's, not the command's: a `task`
|
|
159
|
+
subagent, an `eval` subprocess, or a background job in your pane reads the grant your last bash
|
|
160
|
+
command minted, so its `legion gh` or `jj git push` succeeds only within 60 seconds of that call
|
|
161
|
+
and 403s afterwards — a timing artifact, not a broken credential; run credentialed commands from
|
|
162
|
+
your own bash calls.
|
|
156
163
|
|
|
157
164
|
## GitHub PR comment attribution
|
|
158
165
|
|