@sjawhar/opencode-legion-envoy 0.35.0 → 0.36.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/README.md +10 -8
- package/dist/src/server.js +190 -15
- package/package.json +1 -1
- package/skills/dispatch/SKILL.md +16 -3
package/README.md
CHANGED
|
@@ -22,14 +22,16 @@ This package exposes:
|
|
|
22
22
|
- `dispatch_doc_read`
|
|
23
23
|
- `dispatch_artifact`
|
|
24
24
|
- `dispatch_read`
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
25
|
+
- `dispatch_search`
|
|
26
|
+
|
|
27
|
+
The eleven native `dispatch_*` tools create and read Dispatch issues, asks, comments,
|
|
28
|
+
documents, and artifacts, or search all of them. They are present when `dispatch.enabled`
|
|
29
|
+
resolves a server URL and bearer token from envoy.json (`~/.config/opencode/envoy.json`, merged
|
|
30
|
+
with `<repo>/.opencode/envoy.json`) or the `DISPATCH_URL` and `DISPATCH_TOKEN` environment
|
|
31
|
+
variables; `dispatch.enabled: true` without `dispatch.serverUrl` targets `http://localhost:8766`.
|
|
32
|
+
Each issue-scoped call fills the target issue from the session working directory, stamps it with
|
|
33
|
+
the OpenCode session id and title, and stores a successful mutation's `details.topic` as tool
|
|
34
|
+
metadata so the host subscribes to that exact Dispatch topic.
|
|
33
35
|
|
|
34
36
|
`dispatch_artifact` accepts exactly one upload source: a local `path`, or inline `content`.
|
|
35
37
|
An architect can post a specification directly with
|
package/dist/src/server.js
CHANGED
|
@@ -13597,6 +13597,56 @@ var ChildStatusEventPayloadSchema = object({
|
|
|
13597
13597
|
from: string2().optional(),
|
|
13598
13598
|
to: string2().optional()
|
|
13599
13599
|
});
|
|
13600
|
+
// ../contracts/src/dispatch-snippet.ts
|
|
13601
|
+
var HTML_ENTITIES = [
|
|
13602
|
+
["<", "<"],
|
|
13603
|
+
[">", ">"],
|
|
13604
|
+
["'", "'"],
|
|
13605
|
+
[""", '"'],
|
|
13606
|
+
["&", "&"]
|
|
13607
|
+
];
|
|
13608
|
+
function decodeEntities(text) {
|
|
13609
|
+
let decoded = text;
|
|
13610
|
+
for (const [entity, character] of HTML_ENTITIES) {
|
|
13611
|
+
decoded = decoded.replaceAll(entity, character);
|
|
13612
|
+
}
|
|
13613
|
+
return decoded;
|
|
13614
|
+
}
|
|
13615
|
+
function hasOnlyBalancedMarkers(snippet) {
|
|
13616
|
+
let marked = false;
|
|
13617
|
+
for (const marker of snippet.matchAll(/<mark>|<\/mark>/gu)) {
|
|
13618
|
+
if (marker[0] === "<mark>") {
|
|
13619
|
+
if (marked)
|
|
13620
|
+
return false;
|
|
13621
|
+
marked = true;
|
|
13622
|
+
} else {
|
|
13623
|
+
if (!marked)
|
|
13624
|
+
return false;
|
|
13625
|
+
marked = false;
|
|
13626
|
+
}
|
|
13627
|
+
}
|
|
13628
|
+
return !marked;
|
|
13629
|
+
}
|
|
13630
|
+
function snippetSegments(snippet) {
|
|
13631
|
+
if (!hasOnlyBalancedMarkers(snippet)) {
|
|
13632
|
+
return snippet === "" ? [] : [{ text: decodeEntities(snippet), mark: false }];
|
|
13633
|
+
}
|
|
13634
|
+
let mark = false;
|
|
13635
|
+
const segments = [];
|
|
13636
|
+
for (const part of snippet.split(/(<mark>|<\/mark>)/u)) {
|
|
13637
|
+
if (part === "<mark>") {
|
|
13638
|
+
mark = true;
|
|
13639
|
+
} else if (part === "</mark>") {
|
|
13640
|
+
mark = false;
|
|
13641
|
+
} else if (part !== "") {
|
|
13642
|
+
segments.push({ text: decodeEntities(part), mark });
|
|
13643
|
+
}
|
|
13644
|
+
}
|
|
13645
|
+
return segments;
|
|
13646
|
+
}
|
|
13647
|
+
function snippetText(snippet) {
|
|
13648
|
+
return snippetSegments(snippet).map(({ text, mark }) => mark ? `**${text}**` : text).join("");
|
|
13649
|
+
}
|
|
13600
13650
|
// ../contracts/src/dispatch-tools.ts
|
|
13601
13651
|
function dispatchToolSchema(spec, z, opts) {
|
|
13602
13652
|
const shape = spec.arguments(z);
|
|
@@ -13618,12 +13668,13 @@ var DOC_EDIT_OPS = ["replace", "delete", "insert"];
|
|
|
13618
13668
|
var dispatchToolSpecs = [
|
|
13619
13669
|
{
|
|
13620
13670
|
name: "dispatch_issue",
|
|
13621
|
-
description: "Create a native Dispatch issue for newly tracked work. Do not use it when an existing issue
|
|
13671
|
+
description: "Create a native Dispatch issue for newly tracked work. Search first with dispatch_search; if potentially duplicate issues exist, this returns 409 POSSIBLE_DUPLICATE unless force is true after reading them. " + `Do not use it when an existing issue already covers the work; read or update that issue instead. ${ISSUE_REFERENCE}`,
|
|
13622
13672
|
arguments: (z) => ({
|
|
13623
13673
|
project: z.string().describe("Project key for the new issue."),
|
|
13624
13674
|
title: z.string().describe("Concise issue title."),
|
|
13625
13675
|
parent: z.string().describe("Optional parent issue.").optional(),
|
|
13626
13676
|
external: z.string().describe("Optional external issue reference.").optional(),
|
|
13677
|
+
force: z.boolean().describe("Create even though POSSIBLE_DUPLICATE listed similar issues; pass it only after reading them.").optional(),
|
|
13627
13678
|
spec: z.string().describe(`Optional initial primary-document markdown. ${SPEC_WRITING_GUIDANCE}`).optional()
|
|
13628
13679
|
})
|
|
13629
13680
|
},
|
|
@@ -13741,6 +13792,15 @@ var dispatchToolSpecs = [
|
|
|
13741
13792
|
issue: z.string().describe(ISSUE_REFERENCE).optional(),
|
|
13742
13793
|
ref: z.string().describe("Optional dispatch:// issue reference.").optional()
|
|
13743
13794
|
})
|
|
13795
|
+
},
|
|
13796
|
+
{
|
|
13797
|
+
name: "dispatch_search",
|
|
13798
|
+
description: "Search every issue, document, comment, ask, and message for a keyword or phrase and get deep links. " + "Use it before creating an issue or a design document, and to find where a word was written. " + 'Websearch syntax: "quoted phrase", -excluded, OR.',
|
|
13799
|
+
arguments: (z) => ({
|
|
13800
|
+
query: z.string({ min: 2 }).describe("Keyword, phrase, or websearch expression; at least 2 characters."),
|
|
13801
|
+
project: z.string().describe("Optional project key to search within.").optional(),
|
|
13802
|
+
limit: z.number({ int: true, min: 1, max: 50 }).describe("Maximum results, 1-50; default 20.").optional()
|
|
13803
|
+
})
|
|
13744
13804
|
}
|
|
13745
13805
|
];
|
|
13746
13806
|
// ../contracts/src/envelope.ts
|
|
@@ -13903,9 +13963,59 @@ var controllerIssue = strictObject({
|
|
|
13903
13963
|
secret: nonEmptyString,
|
|
13904
13964
|
issue: nonEmptyString
|
|
13905
13965
|
});
|
|
13966
|
+
var TREE_STATUSES = ["queued", "active", "lingering", "dead", "launch-failed", "closed"];
|
|
13967
|
+
var stateWindowLocator = strictObject({
|
|
13968
|
+
tmuxSession: nonEmptyString,
|
|
13969
|
+
tmuxWindowId: nonEmptyString,
|
|
13970
|
+
tmuxPaneId: nonEmptyString.optional()
|
|
13971
|
+
});
|
|
13972
|
+
var stateTreeLocator = stateWindowLocator.extend({ ompSessionFile: nonEmptyString.optional() });
|
|
13973
|
+
var stateIssue = strictObject({
|
|
13974
|
+
key: nonEmptyString,
|
|
13975
|
+
title: string2(),
|
|
13976
|
+
status: _enum2(LIFECYCLE_STATUSES).optional(),
|
|
13977
|
+
children: array(nonEmptyString),
|
|
13978
|
+
parent: nonEmptyString.optional(),
|
|
13979
|
+
lastAppliedSeq: number2().int().nonnegative().optional()
|
|
13980
|
+
});
|
|
13981
|
+
var stateTree = strictObject({
|
|
13982
|
+
status: _enum2(TREE_STATUSES),
|
|
13983
|
+
generation: number2().int().nonnegative(),
|
|
13984
|
+
launchFailures: number2().int().nonnegative(),
|
|
13985
|
+
readyConfirmedAt: number2().optional(),
|
|
13986
|
+
locator: stateTreeLocator.optional()
|
|
13987
|
+
});
|
|
13988
|
+
var stateGate = strictObject({
|
|
13989
|
+
designAskId: nonEmptyString.optional(),
|
|
13990
|
+
designApproved: nonEmptyString.optional()
|
|
13991
|
+
});
|
|
13992
|
+
var stateRole = strictObject({
|
|
13993
|
+
role: nonEmptyString,
|
|
13994
|
+
issue: nonEmptyString.optional(),
|
|
13995
|
+
generation: number2().int().nonnegative().optional(),
|
|
13996
|
+
sessionId: nonEmptyString.optional(),
|
|
13997
|
+
readyConfirmedAt: number2().optional(),
|
|
13998
|
+
launchFailures: number2().int().nonnegative().optional(),
|
|
13999
|
+
locator: stateWindowLocator.optional()
|
|
14000
|
+
});
|
|
13906
14001
|
var LegionDaemonApi = {
|
|
13907
14002
|
State: {
|
|
13908
|
-
response:
|
|
14003
|
+
response: strictObject({
|
|
14004
|
+
project: nonEmptyString,
|
|
14005
|
+
version: number2().int(),
|
|
14006
|
+
issues: record(string2(), stateIssue),
|
|
14007
|
+
trees: record(string2(), stateTree),
|
|
14008
|
+
admission: strictObject({
|
|
14009
|
+
cap: number2().int().nonnegative(),
|
|
14010
|
+
active: array(nonEmptyString),
|
|
14011
|
+
queue: array(nonEmptyString)
|
|
14012
|
+
}),
|
|
14013
|
+
gates: record(string2(), stateGate),
|
|
14014
|
+
controllerLocator: stateWindowLocator.optional(),
|
|
14015
|
+
roles: record(string2(), stateRole),
|
|
14016
|
+
controllerPendingNotices: number2().int().nonnegative(),
|
|
14017
|
+
pendingStatusWrites: array(nonEmptyString)
|
|
14018
|
+
})
|
|
13909
14019
|
},
|
|
13910
14020
|
ControllerReady: {
|
|
13911
14021
|
request: strictObject({ secret: nonEmptyString, sessionId: nonEmptyString }),
|
|
@@ -14331,6 +14441,9 @@ class DispatchClient {
|
|
|
14331
14441
|
async listIssues(options = {}) {
|
|
14332
14442
|
return this.#json("GET", ["api", "v1", "issues"], undefined, options);
|
|
14333
14443
|
}
|
|
14444
|
+
async search(query, options = {}) {
|
|
14445
|
+
return this.#json("GET", ["api", "v1", "search"], undefined, { q: query, ...options });
|
|
14446
|
+
}
|
|
14334
14447
|
async getIssue(issue) {
|
|
14335
14448
|
return this.#json("GET", ["api", "v1", "issues", await this.#resolveIssue(issue)]);
|
|
14336
14449
|
}
|
|
@@ -14505,6 +14618,7 @@ class DispatchClient {
|
|
|
14505
14618
|
var nativeIssueKeyPattern = /^[A-Z][A-Z0-9]{1,9}-[0-9]+$/;
|
|
14506
14619
|
var externalIssueRefPattern = /^([^/\s]+)\/([^/\s#]+)#([1-9][0-9]*)$/;
|
|
14507
14620
|
var bareIssueNumberPattern = /^[1-9][0-9]*$/;
|
|
14621
|
+
var issueFreeTools = new Set(["dispatch_issue", "dispatch_resolve_ask", "dispatch_search"]);
|
|
14508
14622
|
function canonicalExternalIssueRef(value) {
|
|
14509
14623
|
const match = value.trim().match(externalIssueRefPattern);
|
|
14510
14624
|
return match ? `${canonicalRepo(match[1] ?? "", match[2] ?? "")}#${match[3]}` : value;
|
|
@@ -14527,6 +14641,23 @@ function optionalNumber(args, name) {
|
|
|
14527
14641
|
const value = args[name];
|
|
14528
14642
|
return typeof value === "number" ? value : undefined;
|
|
14529
14643
|
}
|
|
14644
|
+
function isDuplicateCandidate(value) {
|
|
14645
|
+
if (typeof value !== "object" || value === null)
|
|
14646
|
+
return false;
|
|
14647
|
+
const candidate = value;
|
|
14648
|
+
return typeof candidate.key === "string" && typeof candidate.title === "string" && typeof candidate.status === "string" && typeof candidate.snippet === "string" && typeof candidate.shared_terms === "number" && typeof candidate.href === "string";
|
|
14649
|
+
}
|
|
14650
|
+
function duplicateCandidates(error) {
|
|
14651
|
+
if (error.candidates === undefined || !error.candidates.every(isDuplicateCandidate))
|
|
14652
|
+
throw error;
|
|
14653
|
+
return error.candidates;
|
|
14654
|
+
}
|
|
14655
|
+
function searchResultLine(result, baseUrl) {
|
|
14656
|
+
const artifactName = result.artifact ? ` ${result.artifact.name}` : "";
|
|
14657
|
+
const label = `${result.issue.key} [${result.issue.status}] ${result.issue.title} - ${result.kind}${artifactName}`;
|
|
14658
|
+
const href = new URL(result.href, baseUrl).toString();
|
|
14659
|
+
return `${label}: ${snippetText(result.snippet)} -> ${href}`;
|
|
14660
|
+
}
|
|
14530
14661
|
function askUrgency(args) {
|
|
14531
14662
|
const value = args.urgency;
|
|
14532
14663
|
return ASK_URGENCIES.find((urgency) => urgency === value);
|
|
@@ -14565,7 +14696,7 @@ function toolSchema(tool) {
|
|
|
14565
14696
|
return dispatchToolSchema(spec, zodSchemaApi(exports_external), { strict: true });
|
|
14566
14697
|
}
|
|
14567
14698
|
async function resolveIssueArguments(tool, args, cwd, env, exec) {
|
|
14568
|
-
if (tool
|
|
14699
|
+
if (issueFreeTools.has(tool))
|
|
14569
14700
|
return { args, ref: null };
|
|
14570
14701
|
const refArgument = args.ref;
|
|
14571
14702
|
const ref = typeof refArgument === "string" ? parseDispatchRef(refArgument) ?? (() => {
|
|
@@ -14713,7 +14844,9 @@ async function openArtifactMarks(client, resolved) {
|
|
|
14713
14844
|
];
|
|
14714
14845
|
}
|
|
14715
14846
|
async function executeDispatchTool(input) {
|
|
14716
|
-
|
|
14847
|
+
const configUrl = input.config.url;
|
|
14848
|
+
const configToken = input.config.token;
|
|
14849
|
+
if (!input.config.enabled || !configUrl || !configToken) {
|
|
14717
14850
|
throw new Error("Dispatch is disabled; resolve both DISPATCH_URL and DISPATCH_TOKEN");
|
|
14718
14851
|
}
|
|
14719
14852
|
const env = input.env ?? process.env;
|
|
@@ -14721,8 +14854,8 @@ async function executeDispatchTool(input) {
|
|
|
14721
14854
|
const issueArguments = await resolveIssueArguments(input.tool, input.args, input.cwd, env, exec);
|
|
14722
14855
|
const args = toolSchema(input.tool).parse(issueArguments.args);
|
|
14723
14856
|
const actor = toolActor(await resolveOrigin(env, exec, input.cwd), input);
|
|
14724
|
-
const client = new DispatchClient(
|
|
14725
|
-
const issueKey =
|
|
14857
|
+
const client = new DispatchClient(configUrl, configToken, input.fetchImpl);
|
|
14858
|
+
const issueKey = issueFreeTools.has(input.tool) ? null : await ensureIssue(client, stringArg(args, "issue"), actor);
|
|
14726
14859
|
const issue = () => {
|
|
14727
14860
|
if (issueKey === null)
|
|
14728
14861
|
throw new Error("issue is required");
|
|
@@ -14730,20 +14863,62 @@ async function executeDispatchTool(input) {
|
|
|
14730
14863
|
};
|
|
14731
14864
|
switch (input.tool) {
|
|
14732
14865
|
case "dispatch_issue": {
|
|
14866
|
+
const project = stringArg(args, "project");
|
|
14867
|
+
const title = stringArg(args, "title");
|
|
14733
14868
|
const parent = optionalString(args, "parent");
|
|
14734
14869
|
const external = optionalString(args, "external");
|
|
14870
|
+
const force = optionalBoolean(args, "force");
|
|
14735
14871
|
const spec = optionalString(args, "spec");
|
|
14736
|
-
|
|
14737
|
-
|
|
14738
|
-
|
|
14739
|
-
|
|
14740
|
-
|
|
14741
|
-
|
|
14742
|
-
|
|
14872
|
+
try {
|
|
14873
|
+
const created = await client.issue({
|
|
14874
|
+
project,
|
|
14875
|
+
title,
|
|
14876
|
+
...parent === undefined ? {} : { parent },
|
|
14877
|
+
...external === undefined ? {} : { external },
|
|
14878
|
+
...force === undefined ? {} : { force },
|
|
14879
|
+
...spec === undefined ? {} : { spec },
|
|
14880
|
+
actor
|
|
14881
|
+
});
|
|
14882
|
+
return {
|
|
14883
|
+
text: `Created ${created.key}: ${created.title}`,
|
|
14884
|
+
details: { issue: created.key, topic: dispatchIssueSubject(created.key, ">") }
|
|
14885
|
+
};
|
|
14886
|
+
} catch (error) {
|
|
14887
|
+
if (!(error instanceof DispatchServiceError) || error.code !== "POSSIBLE_DUPLICATE") {
|
|
14888
|
+
throw error;
|
|
14889
|
+
}
|
|
14890
|
+
const candidates = duplicateCandidates(error);
|
|
14891
|
+
return {
|
|
14892
|
+
text: [
|
|
14893
|
+
`Not created: "${title}" looks like a duplicate.`,
|
|
14894
|
+
...candidates.map((candidate) => {
|
|
14895
|
+
const href = new URL(candidate.href, configUrl).toString();
|
|
14896
|
+
return `${candidate.key} [${candidate.status}] ${candidate.title} \u2192 ${href}`;
|
|
14897
|
+
}),
|
|
14898
|
+
"Reference the existing issue, or call dispatch_issue again with force: true after reading it."
|
|
14899
|
+
].join(`
|
|
14900
|
+
`),
|
|
14901
|
+
details: { duplicates: candidates }
|
|
14902
|
+
};
|
|
14903
|
+
}
|
|
14904
|
+
}
|
|
14905
|
+
case "dispatch_search": {
|
|
14906
|
+
const query = stringArg(args, "query");
|
|
14907
|
+
const project = optionalString(args, "project");
|
|
14908
|
+
const limit = optionalNumber(args, "limit");
|
|
14909
|
+
const search = await client.search(query, {
|
|
14910
|
+
...project === undefined ? {} : { project },
|
|
14911
|
+
...limit === undefined ? {} : { limit }
|
|
14743
14912
|
});
|
|
14913
|
+
const results = search.results;
|
|
14914
|
+
const count = results.length;
|
|
14744
14915
|
return {
|
|
14745
|
-
text: `
|
|
14746
|
-
|
|
14916
|
+
text: count === 0 ? `No results for "${query}".` : [
|
|
14917
|
+
`${count} ${count === 1 ? "result" : "results"} for "${query}" (${search.took_ms} ms)`,
|
|
14918
|
+
...results.map((result) => searchResultLine(result, configUrl))
|
|
14919
|
+
].join(`
|
|
14920
|
+
`),
|
|
14921
|
+
details: { query, results }
|
|
14747
14922
|
};
|
|
14748
14923
|
}
|
|
14749
14924
|
case "dispatch_resolve_ask": {
|
package/package.json
CHANGED
package/skills/dispatch/SKILL.md
CHANGED
|
@@ -54,11 +54,24 @@ project configured for that repository in Dispatch Settings, then falls back to
|
|
|
54
54
|
|
|
55
55
|
Architects create newly tracked child work with:
|
|
56
56
|
```ts
|
|
57
|
-
dispatch_issue({ project, title, parent?, external?, spec? })
|
|
57
|
+
dispatch_issue({ project, title, parent?, external?, spec?, force? })
|
|
58
58
|
```
|
|
59
59
|
It returns `details` `{ issue, topic }`. Use `dispatch_issue` only to create an issue; never use
|
|
60
|
-
it to park a question.
|
|
61
|
-
|
|
60
|
+
it to park a question. When `spec` is supplied, follow [Writing a spec](#writing-a-spec).
|
|
61
|
+
|
|
62
|
+
## Search first
|
|
63
|
+
|
|
64
|
+
Before you create an issue or start a design document, search:
|
|
65
|
+
```ts
|
|
66
|
+
dispatch_search({ query, project?, limit? })
|
|
67
|
+
```
|
|
68
|
+
It returns every issue, document, comment, ask, and message that contains the words, with the
|
|
69
|
+
issue key and a link. Cite the hit you build on (`dispatch://KEY` or the document reference), or
|
|
70
|
+
state "no prior issue" in the spec. Websearch syntax applies: `"merge queue"`, `-daemon`, `OR`.
|
|
71
|
+
|
|
72
|
+
`dispatch_issue` refuses a title that near-duplicates an issue in the same project and returns
|
|
73
|
+
the candidates (`POSSIBLE_DUPLICATE`). Read them; reference the existing issue, or repeat the
|
|
74
|
+
call with `force: true` when it is genuinely new work.
|
|
62
75
|
|
|
63
76
|
## Asking
|
|
64
77
|
|