@sjawhar/opencode-legion-envoy 1.36.0 → 1.38.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 +3 -2
- package/dist/src/server.js +89 -7
- package/package.json +1 -1
- package/skills/dispatch/SKILL.md +17 -0
- package/skills/legion-architect/SKILL.md +4 -0
package/README.md
CHANGED
|
@@ -29,11 +29,12 @@ This package exposes:
|
|
|
29
29
|
- `dispatch_read`
|
|
30
30
|
- `dispatch_search`
|
|
31
31
|
- `dispatch_issues`
|
|
32
|
+
- `dispatch_architecture_sync`
|
|
32
33
|
- `dispatch_open_asks`
|
|
33
34
|
- `dispatch_whoami`
|
|
34
35
|
|
|
35
|
-
The
|
|
36
|
-
documents, and artifacts, list a project's issues, or search all of them. They are present when `dispatch.enabled`
|
|
36
|
+
The twenty native `dispatch_*` tools create and read Dispatch issues, asks, comments,
|
|
37
|
+
documents, and artifacts, list a project's issues, sync a project's architecture model, or search all of them. They are present when `dispatch.enabled`
|
|
37
38
|
resolves a server URL and bearer token from envoy.json (`~/.config/opencode/envoy.json`, merged
|
|
38
39
|
with `<repo>/.opencode/envoy.json`) or the `DISPATCH_URL` and `DISPATCH_TOKEN` environment
|
|
39
40
|
variables; `dispatch.enabled: true` without `dispatch.serverUrl` targets `http://localhost:8766`.
|
package/dist/src/server.js
CHANGED
|
@@ -13766,6 +13766,14 @@ var commentValidation = (() => {
|
|
|
13766
13766
|
message: `${owner.message} turn requires reply_to_ask.`
|
|
13767
13767
|
};
|
|
13768
13768
|
})();
|
|
13769
|
+
var ISSUE_COMPONENTS_MODES = ["inherit", "explicit", "none"];
|
|
13770
|
+
function componentsArgument(z) {
|
|
13771
|
+
return z.object({
|
|
13772
|
+
mode: z.enum(ISSUE_COMPONENTS_MODES).describe("inherit: take the parent chain's attachment (the default; deletes this issue's own). explicit: attach to ids. none: not architectural work, with reason."),
|
|
13773
|
+
ids: z.array(z.string({ min: 1 }), { min: 1, max: 50 }).describe("For mode explicit: bare component ids from the project's architecture model (web, dispatch-server), not external ones.").optional(),
|
|
13774
|
+
reason: z.string({ min: 1 }).describe("For mode none: why this issue is not architectural (process, hiring, ops).").optional()
|
|
13775
|
+
}).describe("Attach the issue to architecture components. Attach the root before decomposing it; children inherit unless they choose.");
|
|
13776
|
+
}
|
|
13769
13777
|
var SPEC_SECTIONS = [
|
|
13770
13778
|
"Summary",
|
|
13771
13779
|
"Decisions needed",
|
|
@@ -13804,12 +13812,13 @@ var dispatchToolSpecs = [
|
|
|
13804
13812
|
spec: z.string().describe(`Optional initial primary-document markdown. ${SPEC_WRITING_GUIDANCE}`).optional(),
|
|
13805
13813
|
labels: z.array(z.string({ min: 1, max: 40 }), { max: 20 }).describe("Optional initial labels, at most 20 labels of up to 40 characters.").optional(),
|
|
13806
13814
|
priority: z.number({ int: true, min: 0, max: 3 }).describe("Optional coarse priority: P0 is highest and P3 is lowest.").optional(),
|
|
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()
|
|
13815
|
+
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(),
|
|
13816
|
+
components: componentsArgument(z).optional()
|
|
13808
13817
|
})
|
|
13809
13818
|
},
|
|
13810
13819
|
{
|
|
13811
13820
|
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), set its route,
|
|
13821
|
+
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), set its route, set or clear its parent, " + "or attach it to architecture components. 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. components replaces the issue's own attachment and is allowed on a " + "closed issue. Priority is the human's and is not settable here. At least one " + `field besides issue is required. ${ISSUE_REFERENCE}`,
|
|
13813
13822
|
arguments: (z) => ({
|
|
13814
13823
|
issue: z.string().describe(ISSUE_REFERENCE),
|
|
13815
13824
|
status: z.enum(ISSUE_STATUSES).describe("New lifecycle status.").optional(),
|
|
@@ -13817,14 +13826,15 @@ var dispatchToolSpecs = [
|
|
|
13817
13826
|
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
13827
|
external_links: z.array(z.string({ min: 1 })).describe("URLs to link; merged into the issue's existing external links by URL.").optional(),
|
|
13819
13828
|
route: z.string().describe("Route the issue to role:<name> or session:<id>; an empty string clears it.").optional(),
|
|
13820
|
-
parent: z.string().describe("Parent issue key in the same project; an empty string clears the parent.").optional()
|
|
13829
|
+
parent: z.string().describe("Parent issue key in the same project; an empty string clears the parent.").optional(),
|
|
13830
|
+
components: componentsArgument(z).optional()
|
|
13821
13831
|
}),
|
|
13822
13832
|
validation: {
|
|
13823
13833
|
check: (value) => {
|
|
13824
13834
|
const input = value;
|
|
13825
|
-
return typeof input.status === "string" || typeof input.title === "string" || Array.isArray(input.labels) || Array.isArray(input.external_links) || typeof input.route === "string" || typeof input.parent === "string";
|
|
13835
|
+
return typeof input.status === "string" || typeof input.title === "string" || Array.isArray(input.labels) || Array.isArray(input.external_links) || typeof input.route === "string" || typeof input.parent === "string" || typeof input.components === "object" && input.components !== null;
|
|
13826
13836
|
},
|
|
13827
|
-
message: "Issue update requires at least one field besides issue: status, title, labels, external_links, route, or
|
|
13837
|
+
message: "Issue update requires at least one field besides issue: status, title, labels, external_links, route, parent, or components."
|
|
13828
13838
|
},
|
|
13829
13839
|
strict: true
|
|
13830
13840
|
},
|
|
@@ -14036,6 +14046,14 @@ var dispatchToolSpecs = [
|
|
|
14036
14046
|
limit: z.number({ int: true, min: 1, max: 250 }).describe("Maximum rows, 1-250; default 50.").optional()
|
|
14037
14047
|
})
|
|
14038
14048
|
},
|
|
14049
|
+
{
|
|
14050
|
+
name: "dispatch_architecture_sync",
|
|
14051
|
+
description: "Import a project's architecture model from its configured source repository now, instead of " + "waiting for the server's five-minute schedule. Returns the imported commit and component " + "count, or the recorded error when the model was rejected (the previous model stays up). " + "The source itself is configured by a human in Settings; 404 SOURCE_NOT_FOUND without one.",
|
|
14052
|
+
arguments: (z) => ({
|
|
14053
|
+
project: z.string().describe("Project key whose architecture source to sync, such as CORE.")
|
|
14054
|
+
}),
|
|
14055
|
+
strict: true
|
|
14056
|
+
},
|
|
14039
14057
|
{
|
|
14040
14058
|
name: "dispatch_open_asks",
|
|
14041
14059
|
description: "List active unanswered asks, oldest first, with age and whose reply is needed. Omit project to " + "see only this session's own authored asks (call before saying you are waiting for human input); " + "supply project to see every open ask across that project's issues and documents, whoever authored " + "them.",
|
|
@@ -14876,6 +14894,9 @@ class DispatchClient {
|
|
|
14876
14894
|
async whoami() {
|
|
14877
14895
|
return this.#json("GET", ["api", "v1", "whoami"]);
|
|
14878
14896
|
}
|
|
14897
|
+
async syncArchitectureSource(project) {
|
|
14898
|
+
return this.#json("POST", ["api", "v1", "projects", project, "architecture-source", "sync"]);
|
|
14899
|
+
}
|
|
14879
14900
|
async resolveAsk(id, input) {
|
|
14880
14901
|
return this.#json("POST", ["api", "v1", "asks", id, "resolve"], input);
|
|
14881
14902
|
}
|
|
@@ -15291,7 +15312,8 @@ var issueFreeTools = {
|
|
|
15291
15312
|
dispatch_search: true,
|
|
15292
15313
|
dispatch_issues: true,
|
|
15293
15314
|
dispatch_open_asks: true,
|
|
15294
|
-
dispatch_whoami: true
|
|
15315
|
+
dispatch_whoami: true,
|
|
15316
|
+
dispatch_architecture_sync: true
|
|
15295
15317
|
};
|
|
15296
15318
|
function canonicalExternalIssueRef(value) {
|
|
15297
15319
|
const match = value.trim().match(externalIssueRefPattern);
|
|
@@ -15315,6 +15337,27 @@ function optionalNumber(args, name) {
|
|
|
15315
15337
|
const value = args[name];
|
|
15316
15338
|
return typeof value === "number" ? value : undefined;
|
|
15317
15339
|
}
|
|
15340
|
+
function optionalComponents(args, name) {
|
|
15341
|
+
const value = args[name];
|
|
15342
|
+
if (typeof value !== "object" || value === null)
|
|
15343
|
+
return;
|
|
15344
|
+
const input = value;
|
|
15345
|
+
return {
|
|
15346
|
+
mode: input.mode,
|
|
15347
|
+
...input.ids === undefined ? {} : { ids: input.ids },
|
|
15348
|
+
...input.reason === undefined ? {} : { reason: input.reason }
|
|
15349
|
+
};
|
|
15350
|
+
}
|
|
15351
|
+
function componentsChange(input, after) {
|
|
15352
|
+
switch (input.mode) {
|
|
15353
|
+
case "explicit":
|
|
15354
|
+
return `components -> explicit [${after.ids.join(", ")}]`;
|
|
15355
|
+
case "none":
|
|
15356
|
+
return `components -> none (${after.reason ?? input.reason ?? ""})`;
|
|
15357
|
+
case "inherit":
|
|
15358
|
+
return "components -> inherit";
|
|
15359
|
+
}
|
|
15360
|
+
}
|
|
15318
15361
|
function isDuplicateCandidate(value) {
|
|
15319
15362
|
if (typeof value !== "object" || value === null)
|
|
15320
15363
|
return false;
|
|
@@ -15698,6 +15741,18 @@ function approvalLine(artifact) {
|
|
|
15698
15741
|
return `Approval: changes requested on v${approval.version} by ${approval.by?.id ?? "unknown"}: ${approval.reason ?? ""}`;
|
|
15699
15742
|
}
|
|
15700
15743
|
}
|
|
15744
|
+
function componentsLine(components) {
|
|
15745
|
+
const inherited = components.inherited_from === null ? "" : ` (inherited from ${components.inherited_from})`;
|
|
15746
|
+
const retired = components.unknown.length === 0 ? "" : ` (retired: ${components.unknown.join(", ")})`;
|
|
15747
|
+
switch (components.mode) {
|
|
15748
|
+
case "explicit":
|
|
15749
|
+
return `Components: ${components.ids.length === 0 ? "none live" : components.ids.join(", ")}${inherited}${retired}`;
|
|
15750
|
+
case "none":
|
|
15751
|
+
return `Components: none \u2014 ${components.reason ?? ""}${inherited}`;
|
|
15752
|
+
case "inherit":
|
|
15753
|
+
return "Components: unassigned";
|
|
15754
|
+
}
|
|
15755
|
+
}
|
|
15701
15756
|
function issueSummary(issue, events, references, graph) {
|
|
15702
15757
|
const asks = issue.open_asks;
|
|
15703
15758
|
const spec = issue.artifacts?.find((artifact) => artifact.primary);
|
|
@@ -15706,6 +15761,8 @@ function issueSummary(issue, events, references, graph) {
|
|
|
15706
15761
|
throw new Error("Dispatch issue is missing priority");
|
|
15707
15762
|
if (issue.assignee === undefined)
|
|
15708
15763
|
throw new Error("Dispatch issue is missing assignee");
|
|
15764
|
+
if (issue.components === undefined)
|
|
15765
|
+
throw new Error("Dispatch issue is missing components");
|
|
15709
15766
|
return [
|
|
15710
15767
|
`Title: ${issue.title}`,
|
|
15711
15768
|
`Key: ${issue.key}`,
|
|
@@ -15713,6 +15770,7 @@ function issueSummary(issue, events, references, graph) {
|
|
|
15713
15770
|
`Assignee: ${issue.assignee ?? "unassigned"}`,
|
|
15714
15771
|
...issue.priority === null ? [] : [`Priority: P${issue.priority}`],
|
|
15715
15772
|
`Labels: ${issue.labels.length === 0 ? "none" : issue.labels.join(", ")}`,
|
|
15773
|
+
componentsLine(issue.components),
|
|
15716
15774
|
`Route: ${issue.route ?? "none"}`,
|
|
15717
15775
|
...specApproval === undefined ? [] : [`Spec ${specApproval.replace(/^Approval/, "approval")}`],
|
|
15718
15776
|
"Open asks:",
|
|
@@ -16002,6 +16060,7 @@ async function executeDispatchTool(input) {
|
|
|
16002
16060
|
const spec = optionalString(args, "spec");
|
|
16003
16061
|
const priority = optionalNumber(args, "priority");
|
|
16004
16062
|
const assignee = optionalString(args, "assignee");
|
|
16063
|
+
const components = optionalComponents(args, "components");
|
|
16005
16064
|
const labels = args.labels;
|
|
16006
16065
|
try {
|
|
16007
16066
|
const created = await client.issue({
|
|
@@ -16013,6 +16072,7 @@ async function executeDispatchTool(input) {
|
|
|
16013
16072
|
...spec === undefined ? {} : { spec },
|
|
16014
16073
|
...priority === undefined ? {} : { priority },
|
|
16015
16074
|
...assignee === undefined ? {} : { assignee },
|
|
16075
|
+
...components === undefined ? {} : { components },
|
|
16016
16076
|
...Array.isArray(labels) ? { labels } : {},
|
|
16017
16077
|
actor
|
|
16018
16078
|
});
|
|
@@ -16045,6 +16105,7 @@ async function executeDispatchTool(input) {
|
|
|
16045
16105
|
const title = optionalString(args, "title");
|
|
16046
16106
|
const route = optionalString(args, "route");
|
|
16047
16107
|
const parent = optionalString(args, "parent");
|
|
16108
|
+
const components = optionalComponents(args, "components");
|
|
16048
16109
|
const labels = Array.isArray(args.labels) ? args.labels : undefined;
|
|
16049
16110
|
const requestedLinks = Array.isArray(args.external_links) ? [...new Set(args.external_links)] : undefined;
|
|
16050
16111
|
let newLinks = [];
|
|
@@ -16058,6 +16119,7 @@ async function executeDispatchTool(input) {
|
|
|
16058
16119
|
...labels === undefined ? {} : { labels },
|
|
16059
16120
|
...route === undefined ? {} : { route },
|
|
16060
16121
|
...parent === undefined ? {} : { parent: parent === "" ? null : parent },
|
|
16122
|
+
...components === undefined ? {} : { components },
|
|
16061
16123
|
...requestedLinks === undefined ? {} : { external_links: [...before.external_links, ...newLinks.map((url) => ({ url }))] },
|
|
16062
16124
|
actor
|
|
16063
16125
|
});
|
|
@@ -16070,7 +16132,8 @@ async function executeDispatchTool(input) {
|
|
|
16070
16132
|
newLinks.length === 0 ? `already linked ${requestedLinks.join(", ")} ${linkCount}` : `linked ${newLinks.join(", ")} ${linkCount}`
|
|
16071
16133
|
],
|
|
16072
16134
|
...route === undefined ? [] : [after.route === null ? "route cleared" : `route ${after.route}`],
|
|
16073
|
-
...parent === undefined ? [] : [after.parent === null ? "parent cleared" : `parent -> ${after.parent}`]
|
|
16135
|
+
...parent === undefined ? [] : [after.parent === null ? "parent cleared" : `parent -> ${after.parent}`],
|
|
16136
|
+
...components === undefined ? [] : [componentsChange(components, after.components)]
|
|
16074
16137
|
];
|
|
16075
16138
|
return {
|
|
16076
16139
|
text: `${after.key}: ${changes.join("; ")} ${notSubscribed(issueTopic(after.key))}`,
|
|
@@ -16139,6 +16202,25 @@ async function executeDispatchTool(input) {
|
|
|
16139
16202
|
details: { issues: rows }
|
|
16140
16203
|
};
|
|
16141
16204
|
}
|
|
16205
|
+
case "dispatch_architecture_sync": {
|
|
16206
|
+
const project = stringArg(args, "project");
|
|
16207
|
+
const source = await client.syncArchitectureSource(project);
|
|
16208
|
+
const at = source.last_sync_at ?? "unknown time";
|
|
16209
|
+
return {
|
|
16210
|
+
text: source.last_error === null ? `Synced ${project} architecture from ${source.repo}@${source.branch}: commit ${source.last_commit ?? "unknown"} (${at}).` : [
|
|
16211
|
+
`Sync failed for ${project} (${source.repo}@${source.branch}): ${source.last_error}`,
|
|
16212
|
+
source.last_commit === null ? "No model has ever imported for this project." : `The previous model stays up (commit ${source.last_commit}).`
|
|
16213
|
+
].join(`
|
|
16214
|
+
`),
|
|
16215
|
+
details: {
|
|
16216
|
+
project,
|
|
16217
|
+
repo: source.repo,
|
|
16218
|
+
branch: source.branch,
|
|
16219
|
+
commit: source.last_commit,
|
|
16220
|
+
error: source.last_error
|
|
16221
|
+
}
|
|
16222
|
+
};
|
|
16223
|
+
}
|
|
16142
16224
|
case "dispatch_resolve_ask": {
|
|
16143
16225
|
const kind = stringArg(args, "kind");
|
|
16144
16226
|
const ask = await client.resolveAsk(stringArg(args, "ask"), {
|
package/package.json
CHANGED
package/skills/dispatch/SKILL.md
CHANGED
|
@@ -248,6 +248,23 @@ that follow from it:
|
|
|
248
248
|
the question carries `dispatch://KEY/artifact/<slug>` (or `ref`), never just its filename. Text
|
|
249
249
|
they must read to decide belongs in the spec in the first place — see [Artifacts](#artifacts).
|
|
250
250
|
|
|
251
|
+
Attach an issue to the architecture components it changes, before decomposing it — children inherit the parent's attachment unless they choose their own, so attaching the root once classifies the whole tree:
|
|
252
|
+
|
|
253
|
+
```
|
|
254
|
+
dispatch_issue({ project: "CORE", title: "...", components: { mode: "explicit", ids: ["dispatch-server", "web"] } })
|
|
255
|
+
dispatch_issue_update({ issue: "CORE-12", components: { mode: "explicit", ids: ["web"] } }) // this issue's own set, replacing what it inherited
|
|
256
|
+
dispatch_issue_update({ issue: "CORE-13", components: { mode: "none", reason: "hiring, not code" } })
|
|
257
|
+
dispatch_issue_update({ issue: "CORE-14", components: { mode: "inherit" } }) // back to the parent chain's attachment
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Component ids are the file names under the repository's `.dispatch/architecture/` (`web.md` → `web`); an id the model lacks, or an `external` component, is refused with `COMPONENTS_INPUT`. A closed issue can be classified without reopening. Two rules: declare and attach before decomposing work, and change code and its architecture description (`.dispatch/architecture/<id>.md`) in the same review — the tree at `GET /api/v1/projects/{key}/architecture` counts every issue whose effective set names a component or anything it contains.
|
|
261
|
+
|
|
262
|
+
Import a project's architecture model from its configured source repository now (a human configures the source in Settings):
|
|
263
|
+
```ts
|
|
264
|
+
dispatch_architecture_sync({ project: "CORE" })
|
|
265
|
+
```
|
|
266
|
+
It returns the imported commit, or the recorded error when the model was rejected — the previous model stays up. Without a configured source it answers 404 `SOURCE_NOT_FOUND`.
|
|
267
|
+
|
|
251
268
|
Before saying you are waiting for human input, call `dispatch_open_asks`. With no arguments it lists this session's active asks across open issues and project documents, including whether the human or agent owes the next reply. With `dispatch_open_asks({ project })` it lists every open ask in that project — on its issues and on its documents, whoever authored them — which is how you audit what a whole project is waiting on rather than just your own asks.
|
|
252
269
|
|
|
253
270
|
**Anything that needs the human is an ask, or it does not exist.** An approval, a credential,
|
|
@@ -331,3 +331,7 @@ capacity, and cross-tree conflict. Use the Legion escalation operation for those
|
|
|
331
331
|
everything else in the tree, or use `dispatch_ask` for a human question; workers may reach
|
|
332
332
|
Sami directly with `dispatch_ask` the same way. Do not create a wait loop for any wake
|
|
333
333
|
source.
|
|
334
|
+
|
|
335
|
+
## Architecture components
|
|
336
|
+
|
|
337
|
+
Bootstrap on a root whose project has an architecture source: in the root's first PR (the implement worker pushes it), write `.dispatch/architecture/<id>.md` files for the planned components — front matter `title`, `parent`, `depends_on`, `external`; no `paths` yet. The importer reads the source branch's head, so the sync and the attach below work only once that PR has merged to the source branch: until then leave the root on `inherit` (the attach would answer `400 COMPONENTS_INPUT`, the component does not exist yet). After the merge, `dispatch_architecture_sync({ project })` and attach the root with `dispatch_issue_update({ issue, components: { mode: "explicit", ids: [...] } })`. Children inherit the root's attachment; give a child its own `components` only when it changes a narrower set, and `{ mode: "none", reason }` when it is not architectural work. Attach before decomposing, and require every implementer to change the component file beside the code it describes in the same review.
|