@sjawhar/opencode-legion-envoy 1.15.0 → 1.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/src/server.js
CHANGED
|
@@ -13721,7 +13721,9 @@ function snippetText(snippet) {
|
|
|
13721
13721
|
// ../contracts/src/dispatch-tools.ts
|
|
13722
13722
|
function dispatchToolSchema(spec, z, opts) {
|
|
13723
13723
|
const shape = spec.arguments(z);
|
|
13724
|
-
|
|
13724
|
+
const strict = opts?.strict ?? spec.strict;
|
|
13725
|
+
const schemaOptions = strict === undefined ? undefined : { strict };
|
|
13726
|
+
return spec.validation === undefined ? z.object(shape, schemaOptions) : z.refineObject(shape, spec.validation.check, spec.validation.message, schemaOptions);
|
|
13725
13727
|
}
|
|
13726
13728
|
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
13729
|
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 +13947,12 @@ var dispatchToolSpecs = [
|
|
|
13945
13947
|
project: z.string().describe("Optional project key to search within.").optional(),
|
|
13946
13948
|
limit: z.number({ int: true, min: 1, max: 50 }).describe("Maximum results, 1-50; default 20.").optional()
|
|
13947
13949
|
})
|
|
13950
|
+
},
|
|
13951
|
+
{
|
|
13952
|
+
name: "dispatch_open_asks",
|
|
13953
|
+
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.",
|
|
13954
|
+
arguments: () => ({}),
|
|
13955
|
+
strict: true
|
|
13948
13956
|
}
|
|
13949
13957
|
];
|
|
13950
13958
|
// ../contracts/src/envelope.ts
|
|
@@ -14682,6 +14690,12 @@ class DispatchClient {
|
|
|
14682
14690
|
async ask(issue, input) {
|
|
14683
14691
|
return this.#json("POST", ["api", "v1", "issues", await this.#resolveIssue(issue), "asks"], input);
|
|
14684
14692
|
}
|
|
14693
|
+
async openAsks(sessionID, since) {
|
|
14694
|
+
return this.#json("GET", ["api", "v1", "asks", "open"], undefined, {
|
|
14695
|
+
author_session: sessionID,
|
|
14696
|
+
...since === undefined ? {} : { since }
|
|
14697
|
+
});
|
|
14698
|
+
}
|
|
14685
14699
|
async resolveAsk(id, input) {
|
|
14686
14700
|
return this.#json("POST", ["api", "v1", "asks", id, "resolve"], input);
|
|
14687
14701
|
}
|
|
@@ -14934,7 +14948,8 @@ var issueFreeTools = {
|
|
|
14934
14948
|
dispatch_issue: true,
|
|
14935
14949
|
dispatch_edit_ask: true,
|
|
14936
14950
|
dispatch_resolve_ask: true,
|
|
14937
|
-
dispatch_search: true
|
|
14951
|
+
dispatch_search: true,
|
|
14952
|
+
dispatch_open_asks: true
|
|
14938
14953
|
};
|
|
14939
14954
|
function canonicalExternalIssueRef(value) {
|
|
14940
14955
|
const match = value.trim().match(externalIssueRefPattern);
|
|
@@ -15274,6 +15289,45 @@ function askSummary({ ask, replies }) {
|
|
|
15274
15289
|
].join(`
|
|
15275
15290
|
`);
|
|
15276
15291
|
}
|
|
15292
|
+
function openAskAge(ageSeconds) {
|
|
15293
|
+
const seconds = Math.max(0, Math.floor(ageSeconds));
|
|
15294
|
+
if (seconds < 60)
|
|
15295
|
+
return `${seconds}s`;
|
|
15296
|
+
const minutes = Math.floor(seconds / 60);
|
|
15297
|
+
if (minutes < 60)
|
|
15298
|
+
return `${minutes}m${seconds % 60 === 0 ? "" : ` ${seconds % 60}s`}`;
|
|
15299
|
+
const hours = Math.floor(minutes / 60);
|
|
15300
|
+
if (hours < 24)
|
|
15301
|
+
return `${hours}h${minutes % 60 === 0 ? "" : ` ${minutes % 60}m`}`;
|
|
15302
|
+
const days = Math.floor(hours / 24);
|
|
15303
|
+
return `${days}d${hours % 24 === 0 ? "" : ` ${hours % 24}h`}`;
|
|
15304
|
+
}
|
|
15305
|
+
function openAskOwner(ask) {
|
|
15306
|
+
return "issue" in ask.owner ? `${ask.owner.issue.key}: ${ask.owner.issue.title}` : `${ask.owner.document.project} / ${ask.owner.document.name}`;
|
|
15307
|
+
}
|
|
15308
|
+
function openAskLine(ask, baseUrl) {
|
|
15309
|
+
const priority = ask.priority === null ? "" : `P${ask.priority} \xB7 `;
|
|
15310
|
+
return `- ${openAskAge(ask.age_seconds)} \xB7 ${priority}${openAskOwner(ask)} \xB7 ${ask.question} \xB7 ${new URL(ask.ref, baseUrl).toString()}`;
|
|
15311
|
+
}
|
|
15312
|
+
function formatOpenAsksSummary(response, baseUrl) {
|
|
15313
|
+
const scope = "active open asks you authored on open issues and project documents";
|
|
15314
|
+
if (response.count === 0) {
|
|
15315
|
+
return ["There are no unanswered asks for this session.", `Scope: ${scope}.`].join(`
|
|
15316
|
+
`);
|
|
15317
|
+
}
|
|
15318
|
+
const waitingOnHuman = response.asks.filter((ask) => ask.waiting_on === "human");
|
|
15319
|
+
const waitingOnAgent = response.asks.filter((ask) => ask.waiting_on === "agent");
|
|
15320
|
+
return [
|
|
15321
|
+
`${response.count} unanswered ${response.count === 1 ? "ask" : "asks"} you authored on active issues and project documents.`,
|
|
15322
|
+
"",
|
|
15323
|
+
`Waiting on human (${waitingOnHuman.length}):`,
|
|
15324
|
+
...waitingOnHuman.map((ask) => openAskLine(ask, baseUrl)),
|
|
15325
|
+
"",
|
|
15326
|
+
`Waiting on agent (${waitingOnAgent.length}):`,
|
|
15327
|
+
...waitingOnAgent.map((ask) => openAskLine(ask, baseUrl))
|
|
15328
|
+
].join(`
|
|
15329
|
+
`);
|
|
15330
|
+
}
|
|
15277
15331
|
function commentSummary({ comment, replies }) {
|
|
15278
15332
|
const root = [
|
|
15279
15333
|
`${comment.id} \xB7 ${comment.author.kind} ${comment.author.id}`,
|
|
@@ -15322,6 +15376,15 @@ async function executeDispatchTool(input) {
|
|
|
15322
15376
|
if (!input.config.enabled || !configUrl || !configToken) {
|
|
15323
15377
|
throw new Error("Dispatch is disabled; resolve both DISPATCH_URL and DISPATCH_TOKEN");
|
|
15324
15378
|
}
|
|
15379
|
+
if (input.tool === "dispatch_open_asks") {
|
|
15380
|
+
const sessionId = input.sessionId?.trim();
|
|
15381
|
+
if (!sessionId)
|
|
15382
|
+
throw new Error("host session id is required for dispatch_open_asks");
|
|
15383
|
+
toolSchema(input.tool).parse(input.args);
|
|
15384
|
+
const client = new DispatchClient(configUrl, configToken, input.fetchImpl, input.signal);
|
|
15385
|
+
const response = await client.openAsks(sessionId);
|
|
15386
|
+
return { text: formatOpenAsksSummary(response, configUrl), details: { ...response } };
|
|
15387
|
+
}
|
|
15325
15388
|
const env = input.env ?? process.env;
|
|
15326
15389
|
const exec = input.exec ?? defaultExec;
|
|
15327
15390
|
const ownerArguments = await resolveOwnerArguments(input.tool, input.args, input.cwd, env, exec);
|
package/package.json
CHANGED
package/skills/dispatch/SKILL.md
CHANGED
|
@@ -136,6 +136,8 @@ An ask must be answerable from its own text and its anchor alone. Anchor a quest
|
|
|
136
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://`
|
|
137
137
|
reference (see [References](#references)). Never write "see above", "the message above", or "as attached".
|
|
138
138
|
|
|
139
|
+
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.
|
|
140
|
+
|
|
139
141
|
**Anything that needs the human is an ask, or it does not exist.** An approval, a credential,
|
|
140
142
|
a setting only they can change, a review click, a conflict between two of their own rules - if
|
|
141
143
|
your work waits on it, open a `dispatch_ask` with `kind: "action"` the moment you know, the
|
|
@@ -244,7 +244,7 @@ corresponding lifecycle procedure.
|
|
|
244
244
|
| `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. |
|
|
245
245
|
| `pr-ready` | Verify the live PR head, green status, and review state. Continue the review/retro/merger order only for that current head. |
|
|
246
246
|
| `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; `state: "approved"` proceeds toward retro (step 5) once the step 6 integration/merge-gate conditions are met. |
|
|
247
|
-
| `pr-blocked` | Read the failed CI evidence and recovery attempts. Assign a focused implementer or corrective child, then return it through testing and review; do not treat the blocked PR as final. |
|
|
247
|
+
| `pr-blocked` | Payload `{type:"pr-blocked", pr, attempts}`. `attempts` counts heads pushed onto a red verdict that changed something outside `.legion/` — handoff-only pushes (`.legion/` paths only) never count; a push the daemon cannot classify (a listener without `changed_paths`, a list capped at 100, a push listing no commits) does. Published once per exhausted count, not on every later red verdict for that count. Read the failed CI evidence and recovery attempts. Assign a focused implementer or corrective child, then return it through testing and review; do not treat the blocked PR as final. |
|
|
248
248
|
| `pr-merged` | Payload `{type:"pr-merged", pr, mergeCommitSha}`. The merge queue landed the PR. This is your cue for step 7: post the sign-off comment naming that merge commit and set the issue `done`. Nothing else follows a merge. |
|
|
249
249
|
| `pr-closed-unmerged` | Decide from current scope whether to reopen the work, send a fresh implementer, or cancel it with a reason. Delegate the repository action to the responsible phase worker and keep ownership. |
|
|
250
250
|
| `issue-comment` | Interpret the comment in the issue's design context. Answer it, adjust the plan, or relay it via `envoy_publish` to the responsible worker's role token; scope and product decisions remain with you. |
|
|
@@ -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
|
|