@sjawhar/opencode-legion-envoy 1.30.0 → 1.31.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/README.md +2 -1
- package/dist/src/server.js +81 -13
- package/package.json +1 -1
- package/skills/dispatch/SKILL.md +23 -0
package/README.md
CHANGED
|
@@ -13,6 +13,7 @@ This package exposes:
|
|
|
13
13
|
- `envoy_whoami`
|
|
14
14
|
- `envoy_sessions`
|
|
15
15
|
- `dispatch_issue`
|
|
16
|
+
- `dispatch_issue_update`
|
|
16
17
|
- `dispatch_ask`
|
|
17
18
|
- `dispatch_edit_ask`
|
|
18
19
|
- `dispatch_resolve_ask`
|
|
@@ -30,7 +31,7 @@ This package exposes:
|
|
|
30
31
|
- `dispatch_open_asks`
|
|
31
32
|
- `dispatch_whoami`
|
|
32
33
|
|
|
33
|
-
The
|
|
34
|
+
The eighteen native `dispatch_*` tools create and read Dispatch issues, asks, comments,
|
|
34
35
|
documents, and artifacts, or search all of them. They are present when `dispatch.enabled`
|
|
35
36
|
resolves a server URL and bearer token from envoy.json (`~/.config/opencode/envoy.json`, merged
|
|
36
37
|
with `<repo>/.opencode/envoy.json`) or the `DISPATCH_URL` and `DISPATCH_TOKEN` environment
|
package/dist/src/server.js
CHANGED
|
@@ -13779,6 +13779,17 @@ var SPEC_SECTIONS = [
|
|
|
13779
13779
|
];
|
|
13780
13780
|
var SPEC_WRITING_GUIDANCE = `When writing a spec, use these sections in order: ${SPEC_SECTIONS.join(", ")}. ` + "Write for a reader who has not seen the code: plain sentences, every identifier expanded on " + "first use, no coined shorthand; see skills/dispatch Writing for the human and Writing a spec.";
|
|
13781
13781
|
var ASK_URGENCIES = ["low", "med", "high", "blocking"];
|
|
13782
|
+
var ISSUE_STATUSES = [
|
|
13783
|
+
"triage",
|
|
13784
|
+
"icebox",
|
|
13785
|
+
"backlog",
|
|
13786
|
+
"todo",
|
|
13787
|
+
"in_progress",
|
|
13788
|
+
"testing",
|
|
13789
|
+
"needs_review",
|
|
13790
|
+
"retro",
|
|
13791
|
+
"done"
|
|
13792
|
+
];
|
|
13782
13793
|
var DOC_EDIT_OPS = ["replace", "delete", "insert", "retype", "move"];
|
|
13783
13794
|
var dispatchToolSpecs = [
|
|
13784
13795
|
{
|
|
@@ -13796,6 +13807,26 @@ var dispatchToolSpecs = [
|
|
|
13796
13807
|
assignee: z.string().describe("GitHub login of the human who answers this issue's asks; defaults to your owner when you act for a person, else the parent's assignee, else unassigned.").optional()
|
|
13797
13808
|
})
|
|
13798
13809
|
},
|
|
13810
|
+
{
|
|
13811
|
+
name: "dispatch_issue_update",
|
|
13812
|
+
description: "Update an existing issue: move its lifecycle status, retitle it, replace its labels, link a URL " + "(the pull request that delivers it, a run, a document), or set its route. Status is one of " + `${ISSUE_STATUSES.join(", ")}; outside Legion, move it yourself as the work advances; inside ` + "Legion the daemon moves it. external_links are " + "merged into the issue's existing links by URL, so linking the pull request you just opened " + "keeps every earlier link. Priority is the human's and is not settable here. At least one " + `field besides issue is required. ${ISSUE_REFERENCE}`,
|
|
13813
|
+
arguments: (z) => ({
|
|
13814
|
+
issue: z.string().describe(ISSUE_REFERENCE),
|
|
13815
|
+
status: z.enum(ISSUE_STATUSES).describe("New lifecycle status.").optional(),
|
|
13816
|
+
title: z.string({ min: 1 }).describe("Replacement title.").optional(),
|
|
13817
|
+
labels: z.array(z.string({ min: 1, max: 40 }), { max: 20 }).describe("Replacement label set, at most 20 labels of up to 40 characters; replaces every existing label.").optional(),
|
|
13818
|
+
external_links: z.array(z.string({ min: 1 })).describe("URLs to link; merged into the issue's existing external links by URL.").optional(),
|
|
13819
|
+
route: z.string().describe("Route the issue to role:<name> or session:<id>; an empty string clears it.").optional()
|
|
13820
|
+
}),
|
|
13821
|
+
validation: {
|
|
13822
|
+
check: (value) => {
|
|
13823
|
+
const input = value;
|
|
13824
|
+
return typeof input.status === "string" || typeof input.title === "string" || Array.isArray(input.labels) || Array.isArray(input.external_links) || typeof input.route === "string";
|
|
13825
|
+
},
|
|
13826
|
+
message: "Issue update requires at least one field besides issue: status, title, labels, external_links, or route."
|
|
13827
|
+
},
|
|
13828
|
+
strict: true
|
|
13829
|
+
},
|
|
13799
13830
|
{
|
|
13800
13831
|
name: "dispatch_ask",
|
|
13801
13832
|
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}`,
|
|
@@ -14166,17 +14197,6 @@ var legionRole = _enum2(LEGION_ROLES);
|
|
|
14166
14197
|
var requiredUnknown = unknown().refine((value) => value !== undefined, {
|
|
14167
14198
|
message: "Required"
|
|
14168
14199
|
});
|
|
14169
|
-
var LIFECYCLE_STATUSES = [
|
|
14170
|
-
"triage",
|
|
14171
|
-
"icebox",
|
|
14172
|
-
"backlog",
|
|
14173
|
-
"todo",
|
|
14174
|
-
"in_progress",
|
|
14175
|
-
"testing",
|
|
14176
|
-
"needs_review",
|
|
14177
|
-
"retro",
|
|
14178
|
-
"done"
|
|
14179
|
-
];
|
|
14180
14200
|
var architectCapability = strictObject({
|
|
14181
14201
|
tree: nonEmptyString,
|
|
14182
14202
|
sessionId: nonEmptyString,
|
|
@@ -14208,7 +14228,7 @@ var stateTreeLocator = discriminatedUnion("runtime", [
|
|
|
14208
14228
|
var stateIssue = strictObject({
|
|
14209
14229
|
key: nonEmptyString,
|
|
14210
14230
|
title: string2(),
|
|
14211
|
-
status: _enum2(
|
|
14231
|
+
status: _enum2(ISSUE_STATUSES).optional(),
|
|
14212
14232
|
children: array(nonEmptyString),
|
|
14213
14233
|
parent: nonEmptyString.optional(),
|
|
14214
14234
|
lastAppliedSeq: number2().int().nonnegative().optional()
|
|
@@ -14377,7 +14397,7 @@ var LegionDaemonApi = {
|
|
|
14377
14397
|
},
|
|
14378
14398
|
IssueStatus: {
|
|
14379
14399
|
request: controllerIssue.extend({
|
|
14380
|
-
status: _enum2(
|
|
14400
|
+
status: _enum2(ISSUE_STATUSES),
|
|
14381
14401
|
tree: nonEmptyString.optional(),
|
|
14382
14402
|
sessionId: nonEmptyString.optional()
|
|
14383
14403
|
}),
|
|
@@ -14799,6 +14819,9 @@ class DispatchClient {
|
|
|
14799
14819
|
async getIssue(issue) {
|
|
14800
14820
|
return this.#json("GET", ["api", "v1", "issues", await this.#resolveIssue(issue)]);
|
|
14801
14821
|
}
|
|
14822
|
+
async updateIssue(issue, input) {
|
|
14823
|
+
return this.#json("PATCH", ["api", "v1", "issues", await this.#resolveIssue(issue)], input);
|
|
14824
|
+
}
|
|
14802
14825
|
async getIssueEvents(issue, after = 0, limit = 200) {
|
|
14803
14826
|
return this.#json("GET", ["api", "v1", "issues", await this.#resolveIssue(issue), "events"], undefined, { after, limit });
|
|
14804
14827
|
}
|
|
@@ -15980,6 +16003,51 @@ async function executeDispatchTool(input) {
|
|
|
15980
16003
|
};
|
|
15981
16004
|
}
|
|
15982
16005
|
}
|
|
16006
|
+
case "dispatch_issue_update": {
|
|
16007
|
+
const issueKey = issue();
|
|
16008
|
+
const status = optionalString(args, "status");
|
|
16009
|
+
const title = optionalString(args, "title");
|
|
16010
|
+
const route = optionalString(args, "route");
|
|
16011
|
+
const labels = Array.isArray(args.labels) ? args.labels : undefined;
|
|
16012
|
+
const requestedLinks = Array.isArray(args.external_links) ? [...new Set(args.external_links)] : undefined;
|
|
16013
|
+
let newLinks = [];
|
|
16014
|
+
try {
|
|
16015
|
+
const before = await client.getIssue(issueKey);
|
|
16016
|
+
const linked = before.external_links.map((link) => link.url);
|
|
16017
|
+
newLinks = requestedLinks?.filter((url) => !linked.includes(url)) ?? [];
|
|
16018
|
+
const after = await client.updateIssue(issueKey, {
|
|
16019
|
+
...status === undefined ? {} : { status },
|
|
16020
|
+
...title === undefined ? {} : { title },
|
|
16021
|
+
...labels === undefined ? {} : { labels },
|
|
16022
|
+
...route === undefined ? {} : { route },
|
|
16023
|
+
...requestedLinks === undefined ? {} : { external_links: [...before.external_links, ...newLinks.map((url) => ({ url }))] },
|
|
16024
|
+
actor
|
|
16025
|
+
});
|
|
16026
|
+
const linkCount = `(${after.external_links.length} ${after.external_links.length === 1 ? "link" : "links"})`;
|
|
16027
|
+
const changes = [
|
|
16028
|
+
...status === undefined ? [] : [`status ${before.status} -> ${after.status}`],
|
|
16029
|
+
...title === undefined ? [] : [`title "${after.title}"`],
|
|
16030
|
+
...labels === undefined ? [] : [after.labels.length === 0 ? "labels cleared" : `labels ${after.labels.join(", ")}`],
|
|
16031
|
+
...requestedLinks === undefined ? [] : [
|
|
16032
|
+
newLinks.length === 0 ? `already linked ${requestedLinks.join(", ")} ${linkCount}` : `linked ${newLinks.join(", ")} ${linkCount}`
|
|
16033
|
+
],
|
|
16034
|
+
...route === undefined ? [] : [after.route === null ? "route cleared" : `route ${after.route}`]
|
|
16035
|
+
];
|
|
16036
|
+
return {
|
|
16037
|
+
text: `${after.key}: ${changes.join("; ")} ${notSubscribed(issueTopic(after.key))}`,
|
|
16038
|
+
details: {
|
|
16039
|
+
issue: after.key,
|
|
16040
|
+
status: after.status,
|
|
16041
|
+
external_links: after.external_links.map((link) => link.url)
|
|
16042
|
+
}
|
|
16043
|
+
};
|
|
16044
|
+
} catch (error) {
|
|
16045
|
+
if (!(error instanceof DispatchServiceError))
|
|
16046
|
+
throw error;
|
|
16047
|
+
const taken = error.status === 500 && newLinks.length > 0 ? `; one of ${newLinks.join(", ")} may already be linked from another issue (a URL links exactly one issue)` : "";
|
|
16048
|
+
throw new DispatchServiceError(error.code, error.status, `${error.code}: ${error.message}${taken}`, error.candidates);
|
|
16049
|
+
}
|
|
16050
|
+
}
|
|
15983
16051
|
case "dispatch_search": {
|
|
15984
16052
|
const query = stringArg(args, "query");
|
|
15985
16053
|
const project = optionalString(args, "project");
|
package/package.json
CHANGED
package/skills/dispatch/SKILL.md
CHANGED
|
@@ -108,6 +108,29 @@ dispatch_issue({ project, title, parent?, external?, spec?, force?, labels?: str
|
|
|
108
108
|
`details` `{ issue }`; creating an issue does not subscribe you to it (see [Following](#following)). Use `dispatch_issue` only to create an issue; never use it to park a question. When `spec` is supplied,
|
|
109
109
|
follow [Writing a spec](#writing-a-spec).
|
|
110
110
|
|
|
111
|
+
## Issue status is yours to move
|
|
112
|
+
|
|
113
|
+
The issue's status is how a human sees delivery without asking a session. Outside Legion (where
|
|
114
|
+
the daemon writes it), the session doing the work moves it, the way a person moves a card:
|
|
115
|
+
`in_progress` when implementation starts, `testing` when the change is being proven on a
|
|
116
|
+
production-like surface, `needs_review` when its pull request is open and waiting on the merge
|
|
117
|
+
queue, `done` when the change has been driven in production (a merge is not `done`). Move child
|
|
118
|
+
issues you own as well as the root. An issue left at `triage` while work is underway is a defect:
|
|
119
|
+
Sami, 2026-09-15, on the roadmap he could not read — "I'm not even sure what their development
|
|
120
|
+
status is." Waiting for the deploy lane is not a status and is never announced. Priority stays the
|
|
121
|
+
human's: set it on creation only when their intent is clear, and change it only on their word.
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
// PATCH /api/v1/issues/{key} — status, title, labels, external_links (merged by URL), route
|
|
125
|
+
dispatch_issue_update({ issue: "AGENTC-175", status: "testing" })
|
|
126
|
+
dispatch_issue_update({ issue: "AGENTC-175", external_links: ["https://github.com/owner/repo/pull/7"] })
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Link the pull request that delivers the issue in `external_links` when you open it; the issue page
|
|
130
|
+
renders its state and checks from that link. The call is authenticated with the same bearer as every
|
|
131
|
+
other `dispatch_*` tool: a Legion pane reads it from the `DISPATCH_TOKEN_FILE` path the daemon sets on
|
|
132
|
+
the pane; an OMP session outside Legion reads `dispatch.token` from `~/.config/opencode/envoy.json`.
|
|
133
|
+
|
|
111
134
|
## Search first
|
|
112
135
|
|
|
113
136
|
Before you create an issue or start a design document, search:
|