@sjawhar/pi-legion-envoy 0.34.0 → 0.35.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 +10 -10
- package/dist/envoy.js +139 -14
- package/dist/legion.js +139 -14
- package/dist/skills/dispatch/SKILL.md +16 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -79,10 +79,10 @@ extension files it does not contain.
|
|
|
79
79
|
|
|
80
80
|
## Native Dispatch tools
|
|
81
81
|
|
|
82
|
-
The extension registers `dispatch_issue`, `dispatch_ask`,
|
|
83
|
-
`dispatch_comment`, `dispatch_suggest`, `dispatch_message`,
|
|
84
|
-
`dispatch_doc_read`, `dispatch_artifact`,
|
|
85
|
-
configuration resolves both a base URL and bearer token.
|
|
82
|
+
The extension registers eleven native Dispatch tools: `dispatch_issue`, `dispatch_ask`,
|
|
83
|
+
`dispatch_resolve_ask`, `dispatch_comment`, `dispatch_suggest`, `dispatch_message`,
|
|
84
|
+
`dispatch_doc_edit`, `dispatch_doc_read`, `dispatch_artifact`, `dispatch_read`, and
|
|
85
|
+
`dispatch_search`, when Dispatch configuration resolves both a base URL and bearer token.
|
|
86
86
|
|
|
87
87
|
Configure the shared `envoy.json` with:
|
|
88
88
|
|
|
@@ -101,14 +101,14 @@ The user file is `~/.config/opencode/envoy.json`; a
|
|
|
101
101
|
`DISPATCH_TOKEN` override the file values for one process. Omitting
|
|
102
102
|
`dispatch.serverUrl` while `dispatch.enabled` is true targets
|
|
103
103
|
`http://localhost:8766`, the Go server's listen address. Invalid configuration,
|
|
104
|
-
an invalid URL, or an empty token leaves the
|
|
104
|
+
an invalid URL, or an empty token leaves the eleven tools unavailable and reports
|
|
105
105
|
the source of the error.
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
`owner/repo#n` reference. A Legion session may omit `issue` when
|
|
109
|
-
`LEGION_ISSUE` identifies its root issue and its working directory resolves to a
|
|
110
|
-
|
|
111
|
-
`
|
|
107
|
+
Every native tool except `dispatch_search` operates on a Dispatch issue: a native
|
|
108
|
+
`KEY` or an external `owner/repo#n` reference. A Legion session may omit `issue` when
|
|
109
|
+
`LEGION_ISSUE` identifies its root issue and its working directory resolves to a repository.
|
|
110
|
+
`dispatch_doc_read` and `dispatch_read` also accept `dispatch://` references;
|
|
111
|
+
`dispatch_search` needs only its query and returns no subscription topic.
|
|
112
112
|
|
|
113
113
|
Every mutation result carries `details.topic` as
|
|
114
114
|
`notifications.dispatch.issue.<KEY>.>`. The extension's `tool_result` hook
|
package/dist/envoy.js
CHANGED
|
@@ -29694,6 +29694,56 @@ var ChildStatusEventPayloadSchema = object({
|
|
|
29694
29694
|
from: string2().optional(),
|
|
29695
29695
|
to: string2().optional()
|
|
29696
29696
|
});
|
|
29697
|
+
// ../contracts/src/dispatch-snippet.ts
|
|
29698
|
+
var HTML_ENTITIES = [
|
|
29699
|
+
["<", "<"],
|
|
29700
|
+
[">", ">"],
|
|
29701
|
+
["'", "'"],
|
|
29702
|
+
[""", '"'],
|
|
29703
|
+
["&", "&"]
|
|
29704
|
+
];
|
|
29705
|
+
function decodeEntities(text) {
|
|
29706
|
+
let decoded = text;
|
|
29707
|
+
for (const [entity, character] of HTML_ENTITIES) {
|
|
29708
|
+
decoded = decoded.replaceAll(entity, character);
|
|
29709
|
+
}
|
|
29710
|
+
return decoded;
|
|
29711
|
+
}
|
|
29712
|
+
function hasOnlyBalancedMarkers(snippet) {
|
|
29713
|
+
let marked = false;
|
|
29714
|
+
for (const marker of snippet.matchAll(/<mark>|<\/mark>/gu)) {
|
|
29715
|
+
if (marker[0] === "<mark>") {
|
|
29716
|
+
if (marked)
|
|
29717
|
+
return false;
|
|
29718
|
+
marked = true;
|
|
29719
|
+
} else {
|
|
29720
|
+
if (!marked)
|
|
29721
|
+
return false;
|
|
29722
|
+
marked = false;
|
|
29723
|
+
}
|
|
29724
|
+
}
|
|
29725
|
+
return !marked;
|
|
29726
|
+
}
|
|
29727
|
+
function snippetSegments(snippet) {
|
|
29728
|
+
if (!hasOnlyBalancedMarkers(snippet)) {
|
|
29729
|
+
return snippet === "" ? [] : [{ text: decodeEntities(snippet), mark: false }];
|
|
29730
|
+
}
|
|
29731
|
+
let mark = false;
|
|
29732
|
+
const segments = [];
|
|
29733
|
+
for (const part of snippet.split(/(<mark>|<\/mark>)/u)) {
|
|
29734
|
+
if (part === "<mark>") {
|
|
29735
|
+
mark = true;
|
|
29736
|
+
} else if (part === "</mark>") {
|
|
29737
|
+
mark = false;
|
|
29738
|
+
} else if (part !== "") {
|
|
29739
|
+
segments.push({ text: decodeEntities(part), mark });
|
|
29740
|
+
}
|
|
29741
|
+
}
|
|
29742
|
+
return segments;
|
|
29743
|
+
}
|
|
29744
|
+
function snippetText(snippet) {
|
|
29745
|
+
return snippetSegments(snippet).map(({ text, mark }) => mark ? `**${text}**` : text).join("");
|
|
29746
|
+
}
|
|
29697
29747
|
// ../contracts/src/dispatch-tools.ts
|
|
29698
29748
|
function dispatchToolSchema(spec, z, opts) {
|
|
29699
29749
|
const shape = spec.arguments(z);
|
|
@@ -29715,12 +29765,13 @@ var DOC_EDIT_OPS = ["replace", "delete", "insert"];
|
|
|
29715
29765
|
var dispatchToolSpecs = [
|
|
29716
29766
|
{
|
|
29717
29767
|
name: "dispatch_issue",
|
|
29718
|
-
description: "Create a native Dispatch issue for newly tracked work. Do not use it when an existing issue
|
|
29768
|
+
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}`,
|
|
29719
29769
|
arguments: (z) => ({
|
|
29720
29770
|
project: z.string().describe("Project key for the new issue."),
|
|
29721
29771
|
title: z.string().describe("Concise issue title."),
|
|
29722
29772
|
parent: z.string().describe("Optional parent issue.").optional(),
|
|
29723
29773
|
external: z.string().describe("Optional external issue reference.").optional(),
|
|
29774
|
+
force: z.boolean().describe("Create even though POSSIBLE_DUPLICATE listed similar issues; pass it only after reading them.").optional(),
|
|
29724
29775
|
spec: z.string().describe(`Optional initial primary-document markdown. ${SPEC_WRITING_GUIDANCE}`).optional()
|
|
29725
29776
|
})
|
|
29726
29777
|
},
|
|
@@ -29838,6 +29889,15 @@ var dispatchToolSpecs = [
|
|
|
29838
29889
|
issue: z.string().describe(ISSUE_REFERENCE).optional(),
|
|
29839
29890
|
ref: z.string().describe("Optional dispatch:// issue reference.").optional()
|
|
29840
29891
|
})
|
|
29892
|
+
},
|
|
29893
|
+
{
|
|
29894
|
+
name: "dispatch_search",
|
|
29895
|
+
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.',
|
|
29896
|
+
arguments: (z) => ({
|
|
29897
|
+
query: z.string({ min: 2 }).describe("Keyword, phrase, or websearch expression; at least 2 characters."),
|
|
29898
|
+
project: z.string().describe("Optional project key to search within.").optional(),
|
|
29899
|
+
limit: z.number({ int: true, min: 1, max: 50 }).describe("Maximum results, 1-50; default 20.").optional()
|
|
29900
|
+
})
|
|
29841
29901
|
}
|
|
29842
29902
|
];
|
|
29843
29903
|
// ../contracts/src/envelope.ts
|
|
@@ -31169,6 +31229,9 @@ class DispatchClient {
|
|
|
31169
31229
|
async listIssues(options = {}) {
|
|
31170
31230
|
return this.#json("GET", ["api", "v1", "issues"], undefined, options);
|
|
31171
31231
|
}
|
|
31232
|
+
async search(query, options = {}) {
|
|
31233
|
+
return this.#json("GET", ["api", "v1", "search"], undefined, { q: query, ...options });
|
|
31234
|
+
}
|
|
31172
31235
|
async getIssue(issue) {
|
|
31173
31236
|
return this.#json("GET", ["api", "v1", "issues", await this.#resolveIssue(issue)]);
|
|
31174
31237
|
}
|
|
@@ -31343,6 +31406,7 @@ class DispatchClient {
|
|
|
31343
31406
|
var nativeIssueKeyPattern = /^[A-Z][A-Z0-9]{1,9}-[0-9]+$/;
|
|
31344
31407
|
var externalIssueRefPattern = /^([^/\s]+)\/([^/\s#]+)#([1-9][0-9]*)$/;
|
|
31345
31408
|
var bareIssueNumberPattern = /^[1-9][0-9]*$/;
|
|
31409
|
+
var issueFreeTools = new Set(["dispatch_issue", "dispatch_resolve_ask", "dispatch_search"]);
|
|
31346
31410
|
function canonicalExternalIssueRef(value) {
|
|
31347
31411
|
const match = value.trim().match(externalIssueRefPattern);
|
|
31348
31412
|
return match ? `${canonicalRepo(match[1] ?? "", match[2] ?? "")}#${match[3]}` : value;
|
|
@@ -31365,6 +31429,23 @@ function optionalNumber(args, name) {
|
|
|
31365
31429
|
const value = args[name];
|
|
31366
31430
|
return typeof value === "number" ? value : undefined;
|
|
31367
31431
|
}
|
|
31432
|
+
function isDuplicateCandidate(value) {
|
|
31433
|
+
if (typeof value !== "object" || value === null)
|
|
31434
|
+
return false;
|
|
31435
|
+
const candidate = value;
|
|
31436
|
+
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";
|
|
31437
|
+
}
|
|
31438
|
+
function duplicateCandidates(error) {
|
|
31439
|
+
if (error.candidates === undefined || !error.candidates.every(isDuplicateCandidate))
|
|
31440
|
+
throw error;
|
|
31441
|
+
return error.candidates;
|
|
31442
|
+
}
|
|
31443
|
+
function searchResultLine(result, baseUrl) {
|
|
31444
|
+
const artifactName = result.artifact ? ` ${result.artifact.name}` : "";
|
|
31445
|
+
const label = `${result.issue.key} [${result.issue.status}] ${result.issue.title} - ${result.kind}${artifactName}`;
|
|
31446
|
+
const href = new URL(result.href, baseUrl).toString();
|
|
31447
|
+
return `${label}: ${snippetText(result.snippet)} -> ${href}`;
|
|
31448
|
+
}
|
|
31368
31449
|
function askUrgency(args) {
|
|
31369
31450
|
const value = args.urgency;
|
|
31370
31451
|
return ASK_URGENCIES.find((urgency) => urgency === value);
|
|
@@ -31403,7 +31484,7 @@ function toolSchema(tool) {
|
|
|
31403
31484
|
return dispatchToolSchema(spec, zodSchemaApi(exports_external), { strict: true });
|
|
31404
31485
|
}
|
|
31405
31486
|
async function resolveIssueArguments(tool, args, cwd, env, exec) {
|
|
31406
|
-
if (tool
|
|
31487
|
+
if (issueFreeTools.has(tool))
|
|
31407
31488
|
return { args, ref: null };
|
|
31408
31489
|
const refArgument = args.ref;
|
|
31409
31490
|
const ref = typeof refArgument === "string" ? parseDispatchRef(refArgument) ?? (() => {
|
|
@@ -31551,7 +31632,9 @@ async function openArtifactMarks(client, resolved) {
|
|
|
31551
31632
|
];
|
|
31552
31633
|
}
|
|
31553
31634
|
async function executeDispatchTool(input) {
|
|
31554
|
-
|
|
31635
|
+
const configUrl = input.config.url;
|
|
31636
|
+
const configToken = input.config.token;
|
|
31637
|
+
if (!input.config.enabled || !configUrl || !configToken) {
|
|
31555
31638
|
throw new Error("Dispatch is disabled; resolve both DISPATCH_URL and DISPATCH_TOKEN");
|
|
31556
31639
|
}
|
|
31557
31640
|
const env = input.env ?? process.env;
|
|
@@ -31559,8 +31642,8 @@ async function executeDispatchTool(input) {
|
|
|
31559
31642
|
const issueArguments = await resolveIssueArguments(input.tool, input.args, input.cwd, env, exec);
|
|
31560
31643
|
const args = toolSchema(input.tool).parse(issueArguments.args);
|
|
31561
31644
|
const actor = toolActor(await resolveOrigin(env, exec, input.cwd), input);
|
|
31562
|
-
const client = new DispatchClient(
|
|
31563
|
-
const issueKey =
|
|
31645
|
+
const client = new DispatchClient(configUrl, configToken, input.fetchImpl);
|
|
31646
|
+
const issueKey = issueFreeTools.has(input.tool) ? null : await ensureIssue(client, stringArg(args, "issue"), actor);
|
|
31564
31647
|
const issue = () => {
|
|
31565
31648
|
if (issueKey === null)
|
|
31566
31649
|
throw new Error("issue is required");
|
|
@@ -31568,20 +31651,62 @@ async function executeDispatchTool(input) {
|
|
|
31568
31651
|
};
|
|
31569
31652
|
switch (input.tool) {
|
|
31570
31653
|
case "dispatch_issue": {
|
|
31654
|
+
const project = stringArg(args, "project");
|
|
31655
|
+
const title = stringArg(args, "title");
|
|
31571
31656
|
const parent = optionalString(args, "parent");
|
|
31572
31657
|
const external = optionalString(args, "external");
|
|
31658
|
+
const force = optionalBoolean(args, "force");
|
|
31573
31659
|
const spec = optionalString(args, "spec");
|
|
31574
|
-
|
|
31575
|
-
|
|
31576
|
-
|
|
31577
|
-
|
|
31578
|
-
|
|
31579
|
-
|
|
31580
|
-
|
|
31660
|
+
try {
|
|
31661
|
+
const created = await client.issue({
|
|
31662
|
+
project,
|
|
31663
|
+
title,
|
|
31664
|
+
...parent === undefined ? {} : { parent },
|
|
31665
|
+
...external === undefined ? {} : { external },
|
|
31666
|
+
...force === undefined ? {} : { force },
|
|
31667
|
+
...spec === undefined ? {} : { spec },
|
|
31668
|
+
actor
|
|
31669
|
+
});
|
|
31670
|
+
return {
|
|
31671
|
+
text: `Created ${created.key}: ${created.title}`,
|
|
31672
|
+
details: { issue: created.key, topic: dispatchIssueSubject(created.key, ">") }
|
|
31673
|
+
};
|
|
31674
|
+
} catch (error) {
|
|
31675
|
+
if (!(error instanceof DispatchServiceError) || error.code !== "POSSIBLE_DUPLICATE") {
|
|
31676
|
+
throw error;
|
|
31677
|
+
}
|
|
31678
|
+
const candidates = duplicateCandidates(error);
|
|
31679
|
+
return {
|
|
31680
|
+
text: [
|
|
31681
|
+
`Not created: "${title}" looks like a duplicate.`,
|
|
31682
|
+
...candidates.map((candidate) => {
|
|
31683
|
+
const href = new URL(candidate.href, configUrl).toString();
|
|
31684
|
+
return `${candidate.key} [${candidate.status}] ${candidate.title} \u2192 ${href}`;
|
|
31685
|
+
}),
|
|
31686
|
+
"Reference the existing issue, or call dispatch_issue again with force: true after reading it."
|
|
31687
|
+
].join(`
|
|
31688
|
+
`),
|
|
31689
|
+
details: { duplicates: candidates }
|
|
31690
|
+
};
|
|
31691
|
+
}
|
|
31692
|
+
}
|
|
31693
|
+
case "dispatch_search": {
|
|
31694
|
+
const query = stringArg(args, "query");
|
|
31695
|
+
const project = optionalString(args, "project");
|
|
31696
|
+
const limit = optionalNumber(args, "limit");
|
|
31697
|
+
const search = await client.search(query, {
|
|
31698
|
+
...project === undefined ? {} : { project },
|
|
31699
|
+
...limit === undefined ? {} : { limit }
|
|
31581
31700
|
});
|
|
31701
|
+
const results = search.results;
|
|
31702
|
+
const count = results.length;
|
|
31582
31703
|
return {
|
|
31583
|
-
text: `
|
|
31584
|
-
|
|
31704
|
+
text: count === 0 ? `No results for "${query}".` : [
|
|
31705
|
+
`${count} ${count === 1 ? "result" : "results"} for "${query}" (${search.took_ms} ms)`,
|
|
31706
|
+
...results.map((result) => searchResultLine(result, configUrl))
|
|
31707
|
+
].join(`
|
|
31708
|
+
`),
|
|
31709
|
+
details: { query, results }
|
|
31585
31710
|
};
|
|
31586
31711
|
}
|
|
31587
31712
|
case "dispatch_resolve_ask": {
|
package/dist/legion.js
CHANGED
|
@@ -29692,6 +29692,56 @@ var ChildStatusEventPayloadSchema = object({
|
|
|
29692
29692
|
from: string2().optional(),
|
|
29693
29693
|
to: string2().optional()
|
|
29694
29694
|
});
|
|
29695
|
+
// ../contracts/src/dispatch-snippet.ts
|
|
29696
|
+
var HTML_ENTITIES = [
|
|
29697
|
+
["<", "<"],
|
|
29698
|
+
[">", ">"],
|
|
29699
|
+
["'", "'"],
|
|
29700
|
+
[""", '"'],
|
|
29701
|
+
["&", "&"]
|
|
29702
|
+
];
|
|
29703
|
+
function decodeEntities(text) {
|
|
29704
|
+
let decoded = text;
|
|
29705
|
+
for (const [entity, character] of HTML_ENTITIES) {
|
|
29706
|
+
decoded = decoded.replaceAll(entity, character);
|
|
29707
|
+
}
|
|
29708
|
+
return decoded;
|
|
29709
|
+
}
|
|
29710
|
+
function hasOnlyBalancedMarkers(snippet) {
|
|
29711
|
+
let marked = false;
|
|
29712
|
+
for (const marker of snippet.matchAll(/<mark>|<\/mark>/gu)) {
|
|
29713
|
+
if (marker[0] === "<mark>") {
|
|
29714
|
+
if (marked)
|
|
29715
|
+
return false;
|
|
29716
|
+
marked = true;
|
|
29717
|
+
} else {
|
|
29718
|
+
if (!marked)
|
|
29719
|
+
return false;
|
|
29720
|
+
marked = false;
|
|
29721
|
+
}
|
|
29722
|
+
}
|
|
29723
|
+
return !marked;
|
|
29724
|
+
}
|
|
29725
|
+
function snippetSegments(snippet) {
|
|
29726
|
+
if (!hasOnlyBalancedMarkers(snippet)) {
|
|
29727
|
+
return snippet === "" ? [] : [{ text: decodeEntities(snippet), mark: false }];
|
|
29728
|
+
}
|
|
29729
|
+
let mark = false;
|
|
29730
|
+
const segments = [];
|
|
29731
|
+
for (const part of snippet.split(/(<mark>|<\/mark>)/u)) {
|
|
29732
|
+
if (part === "<mark>") {
|
|
29733
|
+
mark = true;
|
|
29734
|
+
} else if (part === "</mark>") {
|
|
29735
|
+
mark = false;
|
|
29736
|
+
} else if (part !== "") {
|
|
29737
|
+
segments.push({ text: decodeEntities(part), mark });
|
|
29738
|
+
}
|
|
29739
|
+
}
|
|
29740
|
+
return segments;
|
|
29741
|
+
}
|
|
29742
|
+
function snippetText(snippet) {
|
|
29743
|
+
return snippetSegments(snippet).map(({ text, mark }) => mark ? `**${text}**` : text).join("");
|
|
29744
|
+
}
|
|
29695
29745
|
// ../contracts/src/dispatch-tools.ts
|
|
29696
29746
|
function dispatchToolSchema(spec, z, opts) {
|
|
29697
29747
|
const shape = spec.arguments(z);
|
|
@@ -29713,12 +29763,13 @@ var DOC_EDIT_OPS = ["replace", "delete", "insert"];
|
|
|
29713
29763
|
var dispatchToolSpecs = [
|
|
29714
29764
|
{
|
|
29715
29765
|
name: "dispatch_issue",
|
|
29716
|
-
description: "Create a native Dispatch issue for newly tracked work. Do not use it when an existing issue
|
|
29766
|
+
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}`,
|
|
29717
29767
|
arguments: (z) => ({
|
|
29718
29768
|
project: z.string().describe("Project key for the new issue."),
|
|
29719
29769
|
title: z.string().describe("Concise issue title."),
|
|
29720
29770
|
parent: z.string().describe("Optional parent issue.").optional(),
|
|
29721
29771
|
external: z.string().describe("Optional external issue reference.").optional(),
|
|
29772
|
+
force: z.boolean().describe("Create even though POSSIBLE_DUPLICATE listed similar issues; pass it only after reading them.").optional(),
|
|
29722
29773
|
spec: z.string().describe(`Optional initial primary-document markdown. ${SPEC_WRITING_GUIDANCE}`).optional()
|
|
29723
29774
|
})
|
|
29724
29775
|
},
|
|
@@ -29836,6 +29887,15 @@ var dispatchToolSpecs = [
|
|
|
29836
29887
|
issue: z.string().describe(ISSUE_REFERENCE).optional(),
|
|
29837
29888
|
ref: z.string().describe("Optional dispatch:// issue reference.").optional()
|
|
29838
29889
|
})
|
|
29890
|
+
},
|
|
29891
|
+
{
|
|
29892
|
+
name: "dispatch_search",
|
|
29893
|
+
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.',
|
|
29894
|
+
arguments: (z) => ({
|
|
29895
|
+
query: z.string({ min: 2 }).describe("Keyword, phrase, or websearch expression; at least 2 characters."),
|
|
29896
|
+
project: z.string().describe("Optional project key to search within.").optional(),
|
|
29897
|
+
limit: z.number({ int: true, min: 1, max: 50 }).describe("Maximum results, 1-50; default 20.").optional()
|
|
29898
|
+
})
|
|
29839
29899
|
}
|
|
29840
29900
|
];
|
|
29841
29901
|
// ../contracts/src/envelope.ts
|
|
@@ -31608,6 +31668,9 @@ class DispatchClient {
|
|
|
31608
31668
|
async listIssues(options = {}) {
|
|
31609
31669
|
return this.#json("GET", ["api", "v1", "issues"], undefined, options);
|
|
31610
31670
|
}
|
|
31671
|
+
async search(query, options = {}) {
|
|
31672
|
+
return this.#json("GET", ["api", "v1", "search"], undefined, { q: query, ...options });
|
|
31673
|
+
}
|
|
31611
31674
|
async getIssue(issue) {
|
|
31612
31675
|
return this.#json("GET", ["api", "v1", "issues", await this.#resolveIssue(issue)]);
|
|
31613
31676
|
}
|
|
@@ -31782,6 +31845,7 @@ class DispatchClient {
|
|
|
31782
31845
|
var nativeIssueKeyPattern = /^[A-Z][A-Z0-9]{1,9}-[0-9]+$/;
|
|
31783
31846
|
var externalIssueRefPattern = /^([^/\s]+)\/([^/\s#]+)#([1-9][0-9]*)$/;
|
|
31784
31847
|
var bareIssueNumberPattern = /^[1-9][0-9]*$/;
|
|
31848
|
+
var issueFreeTools = new Set(["dispatch_issue", "dispatch_resolve_ask", "dispatch_search"]);
|
|
31785
31849
|
function canonicalExternalIssueRef(value) {
|
|
31786
31850
|
const match = value.trim().match(externalIssueRefPattern);
|
|
31787
31851
|
return match ? `${canonicalRepo(match[1] ?? "", match[2] ?? "")}#${match[3]}` : value;
|
|
@@ -31804,6 +31868,23 @@ function optionalNumber(args, name) {
|
|
|
31804
31868
|
const value = args[name];
|
|
31805
31869
|
return typeof value === "number" ? value : undefined;
|
|
31806
31870
|
}
|
|
31871
|
+
function isDuplicateCandidate(value) {
|
|
31872
|
+
if (typeof value !== "object" || value === null)
|
|
31873
|
+
return false;
|
|
31874
|
+
const candidate = value;
|
|
31875
|
+
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";
|
|
31876
|
+
}
|
|
31877
|
+
function duplicateCandidates(error) {
|
|
31878
|
+
if (error.candidates === undefined || !error.candidates.every(isDuplicateCandidate))
|
|
31879
|
+
throw error;
|
|
31880
|
+
return error.candidates;
|
|
31881
|
+
}
|
|
31882
|
+
function searchResultLine(result, baseUrl) {
|
|
31883
|
+
const artifactName = result.artifact ? ` ${result.artifact.name}` : "";
|
|
31884
|
+
const label = `${result.issue.key} [${result.issue.status}] ${result.issue.title} - ${result.kind}${artifactName}`;
|
|
31885
|
+
const href = new URL(result.href, baseUrl).toString();
|
|
31886
|
+
return `${label}: ${snippetText(result.snippet)} -> ${href}`;
|
|
31887
|
+
}
|
|
31807
31888
|
function askUrgency(args) {
|
|
31808
31889
|
const value = args.urgency;
|
|
31809
31890
|
return ASK_URGENCIES.find((urgency) => urgency === value);
|
|
@@ -31842,7 +31923,7 @@ function toolSchema(tool) {
|
|
|
31842
31923
|
return dispatchToolSchema(spec, zodSchemaApi(exports_external), { strict: true });
|
|
31843
31924
|
}
|
|
31844
31925
|
async function resolveIssueArguments(tool, args, cwd, env, exec) {
|
|
31845
|
-
if (tool
|
|
31926
|
+
if (issueFreeTools.has(tool))
|
|
31846
31927
|
return { args, ref: null };
|
|
31847
31928
|
const refArgument = args.ref;
|
|
31848
31929
|
const ref = typeof refArgument === "string" ? parseDispatchRef(refArgument) ?? (() => {
|
|
@@ -31990,7 +32071,9 @@ async function openArtifactMarks(client, resolved) {
|
|
|
31990
32071
|
];
|
|
31991
32072
|
}
|
|
31992
32073
|
async function executeDispatchTool(input) {
|
|
31993
|
-
|
|
32074
|
+
const configUrl = input.config.url;
|
|
32075
|
+
const configToken = input.config.token;
|
|
32076
|
+
if (!input.config.enabled || !configUrl || !configToken) {
|
|
31994
32077
|
throw new Error("Dispatch is disabled; resolve both DISPATCH_URL and DISPATCH_TOKEN");
|
|
31995
32078
|
}
|
|
31996
32079
|
const env = input.env ?? process.env;
|
|
@@ -31998,8 +32081,8 @@ async function executeDispatchTool(input) {
|
|
|
31998
32081
|
const issueArguments = await resolveIssueArguments(input.tool, input.args, input.cwd, env, exec);
|
|
31999
32082
|
const args = toolSchema(input.tool).parse(issueArguments.args);
|
|
32000
32083
|
const actor = toolActor(await resolveOrigin(env, exec, input.cwd), input);
|
|
32001
|
-
const client = new DispatchClient(
|
|
32002
|
-
const issueKey =
|
|
32084
|
+
const client = new DispatchClient(configUrl, configToken, input.fetchImpl);
|
|
32085
|
+
const issueKey = issueFreeTools.has(input.tool) ? null : await ensureIssue(client, stringArg(args, "issue"), actor);
|
|
32003
32086
|
const issue = () => {
|
|
32004
32087
|
if (issueKey === null)
|
|
32005
32088
|
throw new Error("issue is required");
|
|
@@ -32007,20 +32090,62 @@ async function executeDispatchTool(input) {
|
|
|
32007
32090
|
};
|
|
32008
32091
|
switch (input.tool) {
|
|
32009
32092
|
case "dispatch_issue": {
|
|
32093
|
+
const project = stringArg(args, "project");
|
|
32094
|
+
const title = stringArg(args, "title");
|
|
32010
32095
|
const parent = optionalString(args, "parent");
|
|
32011
32096
|
const external = optionalString(args, "external");
|
|
32097
|
+
const force = optionalBoolean(args, "force");
|
|
32012
32098
|
const spec = optionalString(args, "spec");
|
|
32013
|
-
|
|
32014
|
-
|
|
32015
|
-
|
|
32016
|
-
|
|
32017
|
-
|
|
32018
|
-
|
|
32019
|
-
|
|
32099
|
+
try {
|
|
32100
|
+
const created = await client.issue({
|
|
32101
|
+
project,
|
|
32102
|
+
title,
|
|
32103
|
+
...parent === undefined ? {} : { parent },
|
|
32104
|
+
...external === undefined ? {} : { external },
|
|
32105
|
+
...force === undefined ? {} : { force },
|
|
32106
|
+
...spec === undefined ? {} : { spec },
|
|
32107
|
+
actor
|
|
32108
|
+
});
|
|
32109
|
+
return {
|
|
32110
|
+
text: `Created ${created.key}: ${created.title}`,
|
|
32111
|
+
details: { issue: created.key, topic: dispatchIssueSubject(created.key, ">") }
|
|
32112
|
+
};
|
|
32113
|
+
} catch (error) {
|
|
32114
|
+
if (!(error instanceof DispatchServiceError) || error.code !== "POSSIBLE_DUPLICATE") {
|
|
32115
|
+
throw error;
|
|
32116
|
+
}
|
|
32117
|
+
const candidates = duplicateCandidates(error);
|
|
32118
|
+
return {
|
|
32119
|
+
text: [
|
|
32120
|
+
`Not created: "${title}" looks like a duplicate.`,
|
|
32121
|
+
...candidates.map((candidate) => {
|
|
32122
|
+
const href = new URL(candidate.href, configUrl).toString();
|
|
32123
|
+
return `${candidate.key} [${candidate.status}] ${candidate.title} \u2192 ${href}`;
|
|
32124
|
+
}),
|
|
32125
|
+
"Reference the existing issue, or call dispatch_issue again with force: true after reading it."
|
|
32126
|
+
].join(`
|
|
32127
|
+
`),
|
|
32128
|
+
details: { duplicates: candidates }
|
|
32129
|
+
};
|
|
32130
|
+
}
|
|
32131
|
+
}
|
|
32132
|
+
case "dispatch_search": {
|
|
32133
|
+
const query = stringArg(args, "query");
|
|
32134
|
+
const project = optionalString(args, "project");
|
|
32135
|
+
const limit = optionalNumber(args, "limit");
|
|
32136
|
+
const search = await client.search(query, {
|
|
32137
|
+
...project === undefined ? {} : { project },
|
|
32138
|
+
...limit === undefined ? {} : { limit }
|
|
32020
32139
|
});
|
|
32140
|
+
const results = search.results;
|
|
32141
|
+
const count = results.length;
|
|
32021
32142
|
return {
|
|
32022
|
-
text: `
|
|
32023
|
-
|
|
32143
|
+
text: count === 0 ? `No results for "${query}".` : [
|
|
32144
|
+
`${count} ${count === 1 ? "result" : "results"} for "${query}" (${search.took_ms} ms)`,
|
|
32145
|
+
...results.map((result) => searchResultLine(result, configUrl))
|
|
32146
|
+
].join(`
|
|
32147
|
+
`),
|
|
32148
|
+
details: { query, results }
|
|
32024
32149
|
};
|
|
32025
32150
|
}
|
|
32026
32151
|
case "dispatch_resolve_ask": {
|
|
@@ -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
|
|