@sjawhar/opencode-legion-envoy 1.37.0 → 1.38.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/dist/src/server.js +149 -62
- package/package.json +1 -1
- package/skills/dispatch/SKILL.md +15 -0
- package/skills/legion-architect/SKILL.md +4 -0
package/dist/src/server.js
CHANGED
|
@@ -13552,6 +13552,7 @@ function date4(params) {
|
|
|
13552
13552
|
// ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/classic/external.js
|
|
13553
13553
|
config(en_default());
|
|
13554
13554
|
// ../contracts/src/dispatch-api.ts
|
|
13555
|
+
var ASK_TURNS = ["human", "agent"];
|
|
13555
13556
|
var DELIVERY_CAPABILITIES = ["aside", "btw", "steer"];
|
|
13556
13557
|
var DispatchEventSchema = object({
|
|
13557
13558
|
issue_key: string2().nullable(),
|
|
@@ -13632,8 +13633,8 @@ var CommentEventPayloadSchema = object({
|
|
|
13632
13633
|
ask_id: string2().nullish(),
|
|
13633
13634
|
ask_question: string2().optional(),
|
|
13634
13635
|
ask_state: _enum2(["open", "answered", "resolved"]).optional(),
|
|
13635
|
-
ask_waiting_on: _enum2(
|
|
13636
|
-
turn: _enum2(
|
|
13636
|
+
ask_waiting_on: _enum2(ASK_TURNS).optional(),
|
|
13637
|
+
turn: _enum2(ASK_TURNS).nullish(),
|
|
13637
13638
|
anchor: object({ block_id: string2().nullable().optional(), quote: string2().optional() }).nullish(),
|
|
13638
13639
|
suggestion: object({ replace_with: string2().optional() }).nullish(),
|
|
13639
13640
|
author: object({ kind: string2(), id: string2() }).optional(),
|
|
@@ -13756,16 +13757,22 @@ function documentOwnerValidation(requireArtifact, alwaysRequireArtifact = false)
|
|
|
13756
13757
|
message: alwaysRequireArtifact ? "Exactly one of issue and project is required; artifact or ref must name the document." : "Exactly one of issue and project is required; with project, artifact names the document."
|
|
13757
13758
|
};
|
|
13758
13759
|
}
|
|
13759
|
-
var
|
|
13760
|
-
|
|
13761
|
-
|
|
13762
|
-
|
|
13763
|
-
|
|
13764
|
-
|
|
13765
|
-
|
|
13766
|
-
|
|
13767
|
-
|
|
13768
|
-
|
|
13760
|
+
var commentOwner = documentOwnerValidation(true);
|
|
13761
|
+
var commentValidation = {
|
|
13762
|
+
check: (value) => {
|
|
13763
|
+
const input = value;
|
|
13764
|
+
return commentOwner.check(value) && (input.turn === undefined || typeof input.reply_to_ask === "string");
|
|
13765
|
+
},
|
|
13766
|
+
message: `${commentOwner.message} turn requires reply_to_ask.`
|
|
13767
|
+
};
|
|
13768
|
+
var ISSUE_COMPONENTS_MODES = ["inherit", "explicit", "none"];
|
|
13769
|
+
function componentsArgument(z) {
|
|
13770
|
+
return z.object({
|
|
13771
|
+
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."),
|
|
13772
|
+
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(),
|
|
13773
|
+
reason: z.string({ min: 1 }).describe("For mode none: why this issue is not architectural (process, hiring, ops).").optional()
|
|
13774
|
+
}).describe("Attach the issue to architecture components. Attach the root before decomposing it; children inherit unless they choose.");
|
|
13775
|
+
}
|
|
13769
13776
|
var SPEC_SECTIONS = [
|
|
13770
13777
|
"Summary",
|
|
13771
13778
|
"Decisions needed",
|
|
@@ -13779,6 +13786,7 @@ var SPEC_SECTIONS = [
|
|
|
13779
13786
|
];
|
|
13780
13787
|
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
13788
|
var ASK_URGENCIES = ["low", "med", "high", "blocking"];
|
|
13789
|
+
var ASK_QUESTION_MAX = 800;
|
|
13782
13790
|
var ISSUE_STATUSES = [
|
|
13783
13791
|
"triage",
|
|
13784
13792
|
"icebox",
|
|
@@ -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,26 +13826,27 @@ 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
|
},
|
|
13831
13841
|
{
|
|
13832
13842
|
name: "dispatch_ask",
|
|
13833
|
-
description: "Open a durable, answerable decision on an issue or project document. Do not use it for a status update or discussion; " + "use dispatch_message instead. A to-do a human must complete is a question phrased as that to-do, with the options you want (for example Done / Can't). " + "Anchor a document question, thread reply_to/reply_to_ask, or cite a dispatch:// " +
|
|
13843
|
+
description: "Open a durable, answerable decision on an issue or project document. Do not use it for a status update or discussion; " + "use dispatch_message instead. A to-do a human must complete is a question phrased as that to-do, with the options you want (for example Done / Can't). " + "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 ${ASK_QUESTION_MAX} ` + `characters and has at most 8 options. ${OWNER_REFERENCE}`,
|
|
13834
13844
|
arguments: (z) => ({
|
|
13835
13845
|
issue: z.string().describe(ISSUE_REFERENCE).optional(),
|
|
13836
13846
|
project: z.string().describe("Project key owning the document.").optional(),
|
|
13837
13847
|
artifact: z.string().describe("Project document artifact id, slug, or filename.").optional(),
|
|
13838
13848
|
ref: z.string().describe("Optional dispatch:// reference (issue, document, message, or ask); appended to the question and rendered as a link.").optional(),
|
|
13839
|
-
question: z.string({ max:
|
|
13849
|
+
question: z.string({ max: ASK_QUESTION_MAX }).describe(`Decision question, at most ${ASK_QUESTION_MAX} characters.`),
|
|
13840
13850
|
options: z.array(z.object({
|
|
13841
13851
|
label: z.string().describe("Selectable option label."),
|
|
13842
13852
|
description: z.string().describe("Optional option context.").optional()
|
|
@@ -13856,7 +13866,7 @@ var dispatchToolSpecs = [
|
|
|
13856
13866
|
description: "Edit an open question in place. Use it to correct or refine the same decision; retract the " + "old ask and open a new one when the decision itself changes. Previous text remains in the " + "event log. Only the asking session can edit it; answered or resolved asks cannot be edited.",
|
|
13857
13867
|
arguments: (z) => ({
|
|
13858
13868
|
ask: z.string().describe("Ask id to edit."),
|
|
13859
|
-
question: z.string({ max:
|
|
13869
|
+
question: z.string({ max: ASK_QUESTION_MAX }).describe(`Replacement decision question, at most ${ASK_QUESTION_MAX} characters.`).optional(),
|
|
13860
13870
|
options: z.array(z.object({
|
|
13861
13871
|
label: z.string().describe("Selectable option label."),
|
|
13862
13872
|
description: z.string().describe("Optional option context.").optional()
|
|
@@ -14690,9 +14700,9 @@ import { resolve as resolvePath } from "path";
|
|
|
14690
14700
|
|
|
14691
14701
|
// ../envoy-client/src/ask-answer.ts
|
|
14692
14702
|
var HEAD_LENGTH = 120;
|
|
14693
|
-
function textHead(text
|
|
14703
|
+
function textHead(text) {
|
|
14694
14704
|
const flat = text.replace(/\s+/g, " ").trim();
|
|
14695
|
-
return flat.length >
|
|
14705
|
+
return flat.length > HEAD_LENGTH ? `${flat.slice(0, HEAD_LENGTH)}\u2026` : flat;
|
|
14696
14706
|
}
|
|
14697
14707
|
function askAnswerText(answer) {
|
|
14698
14708
|
if (answer === null || answer === undefined)
|
|
@@ -15116,6 +15126,29 @@ class DispatchClient {
|
|
|
15116
15126
|
}
|
|
15117
15127
|
}
|
|
15118
15128
|
|
|
15129
|
+
// ../envoy-client/src/dispatch-owner.ts
|
|
15130
|
+
function documentLabel(project, slug) {
|
|
15131
|
+
return `${project}/${slug}`;
|
|
15132
|
+
}
|
|
15133
|
+
function issueTopic(key) {
|
|
15134
|
+
return { label: key, topic: dispatchIssueSubject(key, ">") };
|
|
15135
|
+
}
|
|
15136
|
+
function documentTopicOf(project, slug) {
|
|
15137
|
+
return {
|
|
15138
|
+
label: documentLabel(project, slug),
|
|
15139
|
+
topic: dispatchDocumentSubject(project, slug, ">")
|
|
15140
|
+
};
|
|
15141
|
+
}
|
|
15142
|
+
function dispatchIssueRef(key) {
|
|
15143
|
+
return `dispatch://${key}`;
|
|
15144
|
+
}
|
|
15145
|
+
function dispatchDocumentRef(owner, slug) {
|
|
15146
|
+
return `dispatch://${owner}/artifact/${slug}`;
|
|
15147
|
+
}
|
|
15148
|
+
function dispatchChildRef(ownerRef, kind, id) {
|
|
15149
|
+
return `${ownerRef}/${kind}/${id}`;
|
|
15150
|
+
}
|
|
15151
|
+
|
|
15119
15152
|
// ../envoy-client/src/tool-input-errors.ts
|
|
15120
15153
|
class ToolInputError extends Error {
|
|
15121
15154
|
tool;
|
|
@@ -15227,14 +15260,8 @@ function formatZodIssues(issues, schema) {
|
|
|
15227
15260
|
}
|
|
15228
15261
|
|
|
15229
15262
|
// ../envoy-client/src/dispatch-execute.ts
|
|
15230
|
-
function issueTopic(key) {
|
|
15231
|
-
return { label: key, topic: dispatchIssueSubject(key, ">") };
|
|
15232
|
-
}
|
|
15233
15263
|
function documentTopic(artifact) {
|
|
15234
|
-
return
|
|
15235
|
-
label: `${artifact.project}/${artifact.slug}`,
|
|
15236
|
-
topic: dispatchDocumentSubject(artifact.project, artifact.slug, ">")
|
|
15237
|
-
};
|
|
15264
|
+
return documentTopicOf(artifact.project, artifact.slug);
|
|
15238
15265
|
}
|
|
15239
15266
|
function resolvedTopic(resolved) {
|
|
15240
15267
|
if (resolved.owner.kind === "project")
|
|
@@ -15253,7 +15280,7 @@ function documentResultDetails(artifact) {
|
|
|
15253
15280
|
return {
|
|
15254
15281
|
project: artifact.project,
|
|
15255
15282
|
artifact: artifact.id,
|
|
15256
|
-
document:
|
|
15283
|
+
document: documentLabel(artifact.project, artifact.slug)
|
|
15257
15284
|
};
|
|
15258
15285
|
}
|
|
15259
15286
|
function writeResultDetails(resolved, fields) {
|
|
@@ -15264,15 +15291,15 @@ function writeResultDetails(resolved, fields) {
|
|
|
15264
15291
|
throw new Error("issue document is missing its issue");
|
|
15265
15292
|
return { issue: resolved.issue.key, ...fields };
|
|
15266
15293
|
}
|
|
15267
|
-
async function askOwnerDetails(client, ask,
|
|
15294
|
+
async function askOwnerDetails(client, ask, artifact) {
|
|
15268
15295
|
if (ask.issue_key !== null) {
|
|
15269
15296
|
return { issue: ask.issue_key, ask: ask.id };
|
|
15270
15297
|
}
|
|
15271
15298
|
if (ask.artifact_id === undefined || ask.artifact_id === null) {
|
|
15272
15299
|
throw new Error("document ask is missing its artifact ID");
|
|
15273
15300
|
}
|
|
15274
|
-
const
|
|
15275
|
-
return { ...documentResultDetails(
|
|
15301
|
+
const owner = artifact ?? await client.getArtifact(ask.artifact_id);
|
|
15302
|
+
return { ...documentResultDetails(owner), ask: ask.id };
|
|
15276
15303
|
}
|
|
15277
15304
|
async function commentOwnerResult(client, comment, artifact) {
|
|
15278
15305
|
if (comment.issue_key !== null) {
|
|
@@ -15283,12 +15310,12 @@ async function commentOwnerResult(client, comment, artifact) {
|
|
|
15283
15310
|
}
|
|
15284
15311
|
const owner = artifact ?? await client.getArtifact(comment.artifact_id);
|
|
15285
15312
|
return {
|
|
15286
|
-
label:
|
|
15313
|
+
label: documentLabel(owner.project, owner.slug),
|
|
15287
15314
|
details: { ...documentResultDetails(owner), comment: comment.id }
|
|
15288
15315
|
};
|
|
15289
15316
|
}
|
|
15290
|
-
async function followedAskDetails(client, ask,
|
|
15291
|
-
return { ...await askOwnerDetails(client, ask,
|
|
15317
|
+
async function followedAskDetails(client, ask, artifact) {
|
|
15318
|
+
return { ...await askOwnerDetails(client, ask, artifact), follows: { ask: ask.id } };
|
|
15292
15319
|
}
|
|
15293
15320
|
var nativeIssueKeyPattern = /^[A-Z][A-Z0-9]{1,9}-[0-9]+$/;
|
|
15294
15321
|
var externalIssueRefPattern = /^([^/\s]+)\/([^/\s#]+)#([1-9][0-9]*)$/;
|
|
@@ -15327,6 +15354,27 @@ function optionalNumber(args, name) {
|
|
|
15327
15354
|
const value = args[name];
|
|
15328
15355
|
return typeof value === "number" ? value : undefined;
|
|
15329
15356
|
}
|
|
15357
|
+
function optionalComponents(args, name) {
|
|
15358
|
+
const value = args[name];
|
|
15359
|
+
if (typeof value !== "object" || value === null)
|
|
15360
|
+
return;
|
|
15361
|
+
const input = value;
|
|
15362
|
+
return {
|
|
15363
|
+
mode: input.mode,
|
|
15364
|
+
...input.ids === undefined ? {} : { ids: input.ids },
|
|
15365
|
+
...input.reason === undefined ? {} : { reason: input.reason }
|
|
15366
|
+
};
|
|
15367
|
+
}
|
|
15368
|
+
function componentsChange(input, after) {
|
|
15369
|
+
switch (input.mode) {
|
|
15370
|
+
case "explicit":
|
|
15371
|
+
return `components -> explicit [${after.ids.join(", ")}]`;
|
|
15372
|
+
case "none":
|
|
15373
|
+
return `components -> none (${after.reason ?? input.reason ?? ""})`;
|
|
15374
|
+
case "inherit":
|
|
15375
|
+
return "components -> inherit";
|
|
15376
|
+
}
|
|
15377
|
+
}
|
|
15330
15378
|
function isDuplicateCandidate(value) {
|
|
15331
15379
|
if (typeof value !== "object" || value === null)
|
|
15332
15380
|
return false;
|
|
@@ -15342,7 +15390,7 @@ function searchResultLine(result, baseUrl) {
|
|
|
15342
15390
|
const href = new URL(result.href, baseUrl).toString();
|
|
15343
15391
|
const { owner } = result;
|
|
15344
15392
|
if (owner.kind === "document") {
|
|
15345
|
-
const reference =
|
|
15393
|
+
const reference = dispatchDocumentRef(owner.project, owner.slug);
|
|
15346
15394
|
return `${reference} [document] ${owner.name} - ${result.kind}: ${snippetText(result.snippet)} -> ${href}`;
|
|
15347
15395
|
}
|
|
15348
15396
|
const artifactName = result.artifact ? ` ${result.artifact.name}` : "";
|
|
@@ -15353,7 +15401,6 @@ function askUrgency(args) {
|
|
|
15353
15401
|
const value = args.urgency;
|
|
15354
15402
|
return ASK_URGENCIES.find((urgency) => urgency === value);
|
|
15355
15403
|
}
|
|
15356
|
-
var maxAskQuestion16 = 800;
|
|
15357
15404
|
function questionWithRef(question, ref) {
|
|
15358
15405
|
return ref === undefined || question.includes(ref) ? question : `${question}
|
|
15359
15406
|
|
|
@@ -15363,7 +15410,7 @@ function askQuestionWithRef(args) {
|
|
|
15363
15410
|
return questionWithRef(stringArg(args, "question"), optionalString(args, "ref"));
|
|
15364
15411
|
}
|
|
15365
15412
|
function askQuestionProblem(withRef) {
|
|
15366
|
-
return withRef.length >
|
|
15413
|
+
return withRef.length > ASK_QUESTION_MAX ? `question plus ref ${overCapMessage(withRef.length, ASK_QUESTION_MAX)}; shorten the question or drop the ref` : undefined;
|
|
15367
15414
|
}
|
|
15368
15415
|
function parseDispatchRef(ref) {
|
|
15369
15416
|
const projectDocument = ref.match(/^dispatch:\/\/([A-Z][A-Z0-9]{1,9})\/artifact\/([^/@]+)(?:@v(\d+))?(?:\/(ask|comment)\/([^/]+))?$/);
|
|
@@ -15418,6 +15465,10 @@ function parseDispatchRef(ref) {
|
|
|
15418
15465
|
return { owner, kind: "message", id: message };
|
|
15419
15466
|
return { owner, kind: "issue", id: issue };
|
|
15420
15467
|
}
|
|
15468
|
+
function refTarget(ref, kind, id) {
|
|
15469
|
+
const ownerRef = ref.owner.kind === "issue" ? dispatchIssueRef(ref.owner.issue) : dispatchDocumentRef(ref.owner.project, `${ref.artifact}`);
|
|
15470
|
+
return dispatchChildRef(ownerRef, kind, id);
|
|
15471
|
+
}
|
|
15421
15472
|
var uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
15422
15473
|
var askIdProblem = "ask must be a bare ask id or a dispatch://.../ask/<id> reference";
|
|
15423
15474
|
var commentIdProblem = "comment must be a bare comment id or a dispatch://.../comment/<id> reference";
|
|
@@ -15461,7 +15512,7 @@ function argumentProblems(tool, args) {
|
|
|
15461
15512
|
case "dispatch_ask": {
|
|
15462
15513
|
const question = optionalString(args, "question");
|
|
15463
15514
|
const ref = optionalString(args, "ref");
|
|
15464
|
-
if (question !== undefined && ref !== undefined && question.length <=
|
|
15515
|
+
if (question !== undefined && ref !== undefined && question.length <= ASK_QUESTION_MAX) {
|
|
15465
15516
|
const problem = askQuestionProblem(questionWithRef(question, ref));
|
|
15466
15517
|
if (problem !== undefined)
|
|
15467
15518
|
problems.push(problem);
|
|
@@ -15551,11 +15602,17 @@ function dispatchRefFromUrl(value, serverUrl) {
|
|
|
15551
15602
|
}
|
|
15552
15603
|
var refGrammarProblem = "ref must be a valid dispatch:// reference such as dispatch://KEY-1, " + "dispatch://KEY-1/ask/<uuid>, dispatch://KEY-1/comment/<uuid>, " + "dispatch://KEY-1/message/<uuid>, dispatch://KEY-1/artifact/<slug>, or " + "dispatch://PROJECT/artifact/<document-ref> (an artifact id, slug, or filename); " + "a dashboard URL on this Dispatch server is accepted too";
|
|
15553
15604
|
var ownerRequiredProblem = "issue is required; supply issue or set LEGION_ISSUE";
|
|
15605
|
+
var toolSchemas = new Map;
|
|
15554
15606
|
function toolSchema(tool) {
|
|
15607
|
+
const cached = toolSchemas.get(tool);
|
|
15608
|
+
if (cached !== undefined)
|
|
15609
|
+
return cached;
|
|
15555
15610
|
const spec = dispatchToolSpecs.find((candidate) => candidate.name === tool);
|
|
15556
15611
|
if (!spec)
|
|
15557
15612
|
throw new Error(`Unknown Dispatch tool: ${tool}`);
|
|
15558
|
-
|
|
15613
|
+
const schema = dispatchToolSchema(spec, zodSchemaApi(exports_external), { strict: true });
|
|
15614
|
+
toolSchemas.set(tool, schema);
|
|
15615
|
+
return schema;
|
|
15559
15616
|
}
|
|
15560
15617
|
async function resolveOwnerArguments(tool, input, cwd, env, exec, serverUrl, problems) {
|
|
15561
15618
|
if (issueFreeTools[tool] === true)
|
|
@@ -15710,6 +15767,18 @@ function approvalLine(artifact) {
|
|
|
15710
15767
|
return `Approval: changes requested on v${approval.version} by ${approval.by?.id ?? "unknown"}: ${approval.reason ?? ""}`;
|
|
15711
15768
|
}
|
|
15712
15769
|
}
|
|
15770
|
+
function componentsLine(components) {
|
|
15771
|
+
const inherited = components.inherited_from === null ? "" : ` (inherited from ${components.inherited_from})`;
|
|
15772
|
+
const retired = components.unknown.length === 0 ? "" : ` (retired: ${components.unknown.join(", ")})`;
|
|
15773
|
+
switch (components.mode) {
|
|
15774
|
+
case "explicit":
|
|
15775
|
+
return `Components: ${components.ids.length === 0 ? "none live" : components.ids.join(", ")}${inherited}${retired}`;
|
|
15776
|
+
case "none":
|
|
15777
|
+
return `Components: none \u2014 ${components.reason ?? ""}${inherited}`;
|
|
15778
|
+
case "inherit":
|
|
15779
|
+
return "Components: unassigned";
|
|
15780
|
+
}
|
|
15781
|
+
}
|
|
15713
15782
|
function issueSummary(issue, events, references, graph) {
|
|
15714
15783
|
const asks = issue.open_asks;
|
|
15715
15784
|
const spec = issue.artifacts?.find((artifact) => artifact.primary);
|
|
@@ -15718,6 +15787,8 @@ function issueSummary(issue, events, references, graph) {
|
|
|
15718
15787
|
throw new Error("Dispatch issue is missing priority");
|
|
15719
15788
|
if (issue.assignee === undefined)
|
|
15720
15789
|
throw new Error("Dispatch issue is missing assignee");
|
|
15790
|
+
if (issue.components === undefined)
|
|
15791
|
+
throw new Error("Dispatch issue is missing components");
|
|
15721
15792
|
return [
|
|
15722
15793
|
`Title: ${issue.title}`,
|
|
15723
15794
|
`Key: ${issue.key}`,
|
|
@@ -15725,6 +15796,7 @@ function issueSummary(issue, events, references, graph) {
|
|
|
15725
15796
|
`Assignee: ${issue.assignee ?? "unassigned"}`,
|
|
15726
15797
|
...issue.priority === null ? [] : [`Priority: P${issue.priority}`],
|
|
15727
15798
|
`Labels: ${issue.labels.length === 0 ? "none" : issue.labels.join(", ")}`,
|
|
15799
|
+
componentsLine(issue.components),
|
|
15728
15800
|
`Route: ${issue.route ?? "none"}`,
|
|
15729
15801
|
...specApproval === undefined ? [] : [`Spec ${specApproval.replace(/^Approval/, "approval")}`],
|
|
15730
15802
|
"Open asks:",
|
|
@@ -15748,11 +15820,21 @@ function referenceLines(edges) {
|
|
|
15748
15820
|
return `- ${edge.kind} ${edge.node.kind} ${edge.node.ref ?? edge.node.id} (${excerpt}${edge.created_at})`;
|
|
15749
15821
|
});
|
|
15750
15822
|
}
|
|
15823
|
+
function unavailableReason(error) {
|
|
15824
|
+
return error instanceof DispatchServiceError && error.status === 404 ? "unavailable" : `unavailable: ${messageFor(error)}`;
|
|
15825
|
+
}
|
|
15751
15826
|
async function graphEdges(client, query) {
|
|
15752
15827
|
try {
|
|
15753
15828
|
return (await client.getReferences(query)).edges;
|
|
15754
15829
|
} catch (error) {
|
|
15755
|
-
return
|
|
15830
|
+
return unavailableReason(error);
|
|
15831
|
+
}
|
|
15832
|
+
}
|
|
15833
|
+
async function issueReferencesOrUnavailable(client, key) {
|
|
15834
|
+
try {
|
|
15835
|
+
return await client.getIssueReferences(key);
|
|
15836
|
+
} catch (error) {
|
|
15837
|
+
return unavailableReason(error);
|
|
15756
15838
|
}
|
|
15757
15839
|
}
|
|
15758
15840
|
async function graphSections(client, ref) {
|
|
@@ -15962,8 +16044,9 @@ async function executeDispatchTool(input) {
|
|
|
15962
16044
|
problems.push(...argumentProblems(input.tool, ownerArguments.args));
|
|
15963
16045
|
if (problems.length > 0)
|
|
15964
16046
|
throw new ToolInputError(input.tool, problems);
|
|
16047
|
+
const dispatchClient = () => new DispatchClient(configUrl, configToken, fetchImpl, input.signal);
|
|
15965
16048
|
if (input.tool === "dispatch_open_asks") {
|
|
15966
|
-
const client =
|
|
16049
|
+
const client = dispatchClient();
|
|
15967
16050
|
const project = optionalString(ownerArguments.args, "project");
|
|
15968
16051
|
if (project !== undefined) {
|
|
15969
16052
|
const response = await client.openAsksForProject(project);
|
|
@@ -15979,7 +16062,7 @@ async function executeDispatchTool(input) {
|
|
|
15979
16062
|
const sessionId = input.sessionId?.trim();
|
|
15980
16063
|
if (!sessionId)
|
|
15981
16064
|
throw new Error("host session id is required for dispatch_whoami");
|
|
15982
|
-
const client =
|
|
16065
|
+
const client = dispatchClient();
|
|
15983
16066
|
const identity = await client.whoami();
|
|
15984
16067
|
const owner = identity.kind === "agent" ? identity.owner : identity.login.toLowerCase();
|
|
15985
16068
|
return {
|
|
@@ -15989,7 +16072,7 @@ async function executeDispatchTool(input) {
|
|
|
15989
16072
|
}
|
|
15990
16073
|
const args = parsed.success ? parsed.data : ownerArguments.args;
|
|
15991
16074
|
const actor = toolActor(await resolveOrigin(env, exec, input.cwd), input);
|
|
15992
|
-
const client =
|
|
16075
|
+
const client = dispatchClient();
|
|
15993
16076
|
const owner = ownerArguments.owner?.kind === "issue" ? {
|
|
15994
16077
|
kind: "issue",
|
|
15995
16078
|
issue: await ensureIssue(client, ownerArguments.owner.issue, actor)
|
|
@@ -16014,6 +16097,7 @@ async function executeDispatchTool(input) {
|
|
|
16014
16097
|
const spec = optionalString(args, "spec");
|
|
16015
16098
|
const priority = optionalNumber(args, "priority");
|
|
16016
16099
|
const assignee = optionalString(args, "assignee");
|
|
16100
|
+
const components = optionalComponents(args, "components");
|
|
16017
16101
|
const labels = args.labels;
|
|
16018
16102
|
try {
|
|
16019
16103
|
const created = await client.issue({
|
|
@@ -16025,6 +16109,7 @@ async function executeDispatchTool(input) {
|
|
|
16025
16109
|
...spec === undefined ? {} : { spec },
|
|
16026
16110
|
...priority === undefined ? {} : { priority },
|
|
16027
16111
|
...assignee === undefined ? {} : { assignee },
|
|
16112
|
+
...components === undefined ? {} : { components },
|
|
16028
16113
|
...Array.isArray(labels) ? { labels } : {},
|
|
16029
16114
|
actor
|
|
16030
16115
|
});
|
|
@@ -16057,6 +16142,7 @@ async function executeDispatchTool(input) {
|
|
|
16057
16142
|
const title = optionalString(args, "title");
|
|
16058
16143
|
const route = optionalString(args, "route");
|
|
16059
16144
|
const parent = optionalString(args, "parent");
|
|
16145
|
+
const components = optionalComponents(args, "components");
|
|
16060
16146
|
const labels = Array.isArray(args.labels) ? args.labels : undefined;
|
|
16061
16147
|
const requestedLinks = Array.isArray(args.external_links) ? [...new Set(args.external_links)] : undefined;
|
|
16062
16148
|
let newLinks = [];
|
|
@@ -16070,6 +16156,7 @@ async function executeDispatchTool(input) {
|
|
|
16070
16156
|
...labels === undefined ? {} : { labels },
|
|
16071
16157
|
...route === undefined ? {} : { route },
|
|
16072
16158
|
...parent === undefined ? {} : { parent: parent === "" ? null : parent },
|
|
16159
|
+
...components === undefined ? {} : { components },
|
|
16073
16160
|
...requestedLinks === undefined ? {} : { external_links: [...before.external_links, ...newLinks.map((url) => ({ url }))] },
|
|
16074
16161
|
actor
|
|
16075
16162
|
});
|
|
@@ -16082,7 +16169,8 @@ async function executeDispatchTool(input) {
|
|
|
16082
16169
|
newLinks.length === 0 ? `already linked ${requestedLinks.join(", ")} ${linkCount}` : `linked ${newLinks.join(", ")} ${linkCount}`
|
|
16083
16170
|
],
|
|
16084
16171
|
...route === undefined ? [] : [after.route === null ? "route cleared" : `route ${after.route}`],
|
|
16085
|
-
...parent === undefined ? [] : [after.parent === null ? "parent cleared" : `parent -> ${after.parent}`]
|
|
16172
|
+
...parent === undefined ? [] : [after.parent === null ? "parent cleared" : `parent -> ${after.parent}`],
|
|
16173
|
+
...components === undefined ? [] : [componentsChange(components, after.components)]
|
|
16086
16174
|
];
|
|
16087
16175
|
return {
|
|
16088
16176
|
text: `${after.key}: ${changes.join("; ")} ${notSubscribed(issueTopic(after.key))}`,
|
|
@@ -16223,7 +16311,7 @@ async function executeDispatchTool(input) {
|
|
|
16223
16311
|
return {
|
|
16224
16312
|
text: `Asked ${ask.id} on ${askOwner.label} (urgency ${ask.urgency}): ${ask.question}
|
|
16225
16313
|
${followsAsk(askOwner)}`,
|
|
16226
|
-
details: await followedAskDetails(client, ask, resolved)
|
|
16314
|
+
details: await followedAskDetails(client, ask, resolved?.artifact)
|
|
16227
16315
|
};
|
|
16228
16316
|
}
|
|
16229
16317
|
case "dispatch_edit_ask": {
|
|
@@ -16305,7 +16393,7 @@ ${followsAsk(askOwner)}`,
|
|
|
16305
16393
|
...inReplyTo === undefined ? {} : { in_reply_to: inReplyTo },
|
|
16306
16394
|
actor
|
|
16307
16395
|
});
|
|
16308
|
-
const messageRef =
|
|
16396
|
+
const messageRef = dispatchChildRef(dispatchIssueRef(issueKey), "message", message.id);
|
|
16309
16397
|
return {
|
|
16310
16398
|
text: `Posted message ${message.id} (${messageRef}) ${notSubscribed(issueTopic(issueKey))}`,
|
|
16311
16399
|
details: { issue: issueKey, message: message.id }
|
|
@@ -16367,7 +16455,7 @@ ${trailer.join(`
|
|
|
16367
16455
|
}
|
|
16368
16456
|
};
|
|
16369
16457
|
}
|
|
16370
|
-
const details = await followedAskDetails(client, result.ask, resolved);
|
|
16458
|
+
const details = await followedAskDetails(client, result.ask, resolved.artifact);
|
|
16371
16459
|
return {
|
|
16372
16460
|
text: `Approval requested for ${resolved.artifact.name} (document id ${resolved.artifact.id}) at version ${result.version} (ask ${result.ask.id}). The answer arrives as artifact.approved or artifact.changes_requested; an edit after approval makes it stale, so request again for the new version.`,
|
|
16373
16461
|
details: { ...details, artifact: resolved.artifact.id, version: result.version }
|
|
@@ -16390,7 +16478,7 @@ ${trailer.join(`
|
|
|
16390
16478
|
};
|
|
16391
16479
|
const artifactOwner = documentOwner();
|
|
16392
16480
|
const result = artifactOwner.kind === "project" ? await client.projectArtifact(artifactOwner.project, artifactInput) : await client.artifact(issue(), artifactInput);
|
|
16393
|
-
const artifactRef = artifactOwner.kind === "project" ?
|
|
16481
|
+
const artifactRef = dispatchDocumentRef(artifactOwner.kind === "project" ? artifactOwner.project : issue(), result.artifact.slug);
|
|
16394
16482
|
const uploadOwner = artifactOwner.kind === "project" ? documentTopic(result.artifact) : issueTopic(issue());
|
|
16395
16483
|
return {
|
|
16396
16484
|
text: `Uploaded ${result.artifact.name} as version ${result.version.number} (artifact slug ${result.artifact.slug}; ${artifactRef}) ${notSubscribed(uploadOwner)}`,
|
|
@@ -16426,7 +16514,7 @@ ${trailer.join(`
|
|
|
16426
16514
|
const ref = ownerArguments.ref;
|
|
16427
16515
|
const id = await resolveIdPrefix(input.tool, "ask", ref, async () => ref.owner.kind === "issue" ? client.listIssueAsks(ref.owner.issue) : client.getArtifactAsks((await resolveArtifact(client, ref.owner, ref.artifact)).artifact.id, "all"));
|
|
16428
16516
|
const askRead = await client.getAsk(id);
|
|
16429
|
-
const askRef = ref
|
|
16517
|
+
const askRef = refTarget(ref, "ask", id);
|
|
16430
16518
|
return {
|
|
16431
16519
|
text: askSummary(askRead, await graphSections(client, askRef)),
|
|
16432
16520
|
details: ref.owner.kind === "project" ? { project: ref.owner.project } : { issue: ref.owner.issue }
|
|
@@ -16436,7 +16524,7 @@ ${trailer.join(`
|
|
|
16436
16524
|
const ref = ownerArguments.ref;
|
|
16437
16525
|
const id = await resolveIdPrefix(input.tool, "comment", ref, async () => ref.owner.kind === "issue" ? client.getComments(ref.owner.issue) : client.getArtifactComments((await resolveArtifact(client, ref.owner, ref.artifact)).artifact.id));
|
|
16438
16526
|
const comment = await client.getComment(id);
|
|
16439
|
-
const commentRef = ref
|
|
16527
|
+
const commentRef = refTarget(ref, "comment", id);
|
|
16440
16528
|
return {
|
|
16441
16529
|
text: commentSummary(comment, await graphSections(client, commentRef)),
|
|
16442
16530
|
details: ref.owner.kind === "project" ? { project: ref.owner.project } : { issue: comment.comment.issue_key }
|
|
@@ -16447,7 +16535,7 @@ ${trailer.join(`
|
|
|
16447
16535
|
throw new Error("message references are issue-scoped");
|
|
16448
16536
|
}
|
|
16449
16537
|
const messageRead = await client.getMessage(ownerArguments.ref.owner.issue, ownerArguments.ref.id);
|
|
16450
|
-
const messageRef =
|
|
16538
|
+
const messageRef = dispatchChildRef(dispatchIssueRef(ownerArguments.ref.owner.issue), "message", ownerArguments.ref.id);
|
|
16451
16539
|
return {
|
|
16452
16540
|
text: messageSummary(messageRead, await graphSections(client, messageRef)),
|
|
16453
16541
|
details: { issue: messageRead.message.issue_key }
|
|
@@ -16455,7 +16543,7 @@ ${trailer.join(`
|
|
|
16455
16543
|
}
|
|
16456
16544
|
if (documentOwner().kind === "project") {
|
|
16457
16545
|
const resolved = await resolveArtifact(client, documentOwner(), stringArg(args, "artifact"));
|
|
16458
|
-
const documentRef =
|
|
16546
|
+
const documentRef = dispatchDocumentRef(resolved.artifact.project, resolved.artifact.slug);
|
|
16459
16547
|
return {
|
|
16460
16548
|
text: [
|
|
16461
16549
|
`Document: ${resolved.artifact.project} / ${resolved.artifact.name}`,
|
|
@@ -16467,7 +16555,7 @@ ${trailer.join(`
|
|
|
16467
16555
|
`),
|
|
16468
16556
|
details: {
|
|
16469
16557
|
project: resolved.artifact.project,
|
|
16470
|
-
document:
|
|
16558
|
+
document: documentLabel(resolved.artifact.project, resolved.artifact.slug)
|
|
16471
16559
|
}
|
|
16472
16560
|
};
|
|
16473
16561
|
}
|
|
@@ -16484,14 +16572,12 @@ ${trailer.join(`
|
|
|
16484
16572
|
details: { issue: read.issue.key }
|
|
16485
16573
|
};
|
|
16486
16574
|
}
|
|
16487
|
-
|
|
16488
|
-
|
|
16489
|
-
|
|
16490
|
-
|
|
16491
|
-
references = error instanceof DispatchServiceError && error.status === 404 ? "unavailable" : `unavailable: ${error instanceof Error ? error.message : String(error)}`;
|
|
16492
|
-
}
|
|
16575
|
+
const [references, graph] = await Promise.all([
|
|
16576
|
+
issueReferencesOrUnavailable(client, read.issue.key),
|
|
16577
|
+
graphSections(client, dispatchIssueRef(read.issue.key))
|
|
16578
|
+
]);
|
|
16493
16579
|
return {
|
|
16494
|
-
text: issueSummary(read.issue, read.events, references,
|
|
16580
|
+
text: issueSummary(read.issue, read.events, references, graph),
|
|
16495
16581
|
details: { issue: read.issue.key }
|
|
16496
16582
|
};
|
|
16497
16583
|
}
|
|
@@ -16634,6 +16720,7 @@ var envoyToolSpecs = [
|
|
|
16634
16720
|
requiresSubscriptionCapability: false
|
|
16635
16721
|
}
|
|
16636
16722
|
];
|
|
16723
|
+
var envoyToolSchemas = new Map;
|
|
16637
16724
|
|
|
16638
16725
|
// ../envoy-client/src/transport.ts
|
|
16639
16726
|
var DEFAULT_TIMEOUT_MS = 5000;
|
package/package.json
CHANGED
package/skills/dispatch/SKILL.md
CHANGED
|
@@ -30,6 +30,8 @@ vocabulary, and is often on a phone. Write for that person.
|
|
|
30
30
|
what each costs, and your recommendation with its reason.
|
|
31
31
|
- Before posting, test it: could Sami, reading only this text on his phone, know what he is being
|
|
32
32
|
told or asked? If not, rewrite it. Length is not the problem; density is.
|
|
33
|
+
- When an ask or message communicates a judgment, lead with that judgment in one sentence and put the mechanism underneath it. Do not make the reader ask a second time whether the result is a win. This shapes communication only when a judgment exists; it does not pre-decide an open question or remove its genuine options. Inferred from the AGENTC-186 12-hour-cap incident (platform PO, 2026-09-17).
|
|
34
|
+
- When a Dispatch message states a root cause, include the reproducing command or test in that same message. Without it, label the diagnosis a hypothesis; a diagnosis still in progress may say so plainly. This boundary applies to causal claims, not to reporting that an investigation has started. Inferred from the astro lane's 2026-09-16 retro (platform PO, 2026-09-17).
|
|
33
35
|
|
|
34
36
|
## Agent authentication
|
|
35
37
|
|
|
@@ -248,6 +250,17 @@ that follow from it:
|
|
|
248
250
|
the question carries `dispatch://KEY/artifact/<slug>` (or `ref`), never just its filename. Text
|
|
249
251
|
they must read to decide belongs in the spec in the first place — see [Artifacts](#artifacts).
|
|
250
252
|
|
|
253
|
+
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:
|
|
254
|
+
|
|
255
|
+
```
|
|
256
|
+
dispatch_issue({ project: "CORE", title: "...", components: { mode: "explicit", ids: ["dispatch-server", "web"] } })
|
|
257
|
+
dispatch_issue_update({ issue: "CORE-12", components: { mode: "explicit", ids: ["web"] } }) // this issue's own set, replacing what it inherited
|
|
258
|
+
dispatch_issue_update({ issue: "CORE-13", components: { mode: "none", reason: "hiring, not code" } })
|
|
259
|
+
dispatch_issue_update({ issue: "CORE-14", components: { mode: "inherit" } }) // back to the parent chain's attachment
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
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.
|
|
263
|
+
|
|
251
264
|
Import a project's architecture model from its configured source repository now (a human configures the source in Settings):
|
|
252
265
|
```ts
|
|
253
266
|
dispatch_architecture_sync({ project: "CORE" })
|
|
@@ -256,6 +269,8 @@ It returns the imported commit, or the recorded error when the model was rejecte
|
|
|
256
269
|
|
|
257
270
|
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.
|
|
258
271
|
|
|
272
|
+
**Unsettled product shape needs a decision before implementation.** When a page, navigation entry, table key, customer-scoping rule, or persisted sidecar would set product shape that Sami has not already settled, send a one-line ask before the first implementation commit. A platform-PO schema or contract ruling does not settle product shape. This does not turn a user-specified decision or routine implementation into an approval request; it is inferred from AGENTC-186's 2026-09-16 retro (platform PO, 2026-09-17).
|
|
273
|
+
|
|
259
274
|
**Anything that needs the human is an ask, or it does not exist.** An approval, a credential,
|
|
260
275
|
a setting only they can change, a review click, a conflict between two of their own rules - if
|
|
261
276
|
your work waits on it, open a `dispatch_ask` the moment you know, the action as the question.
|
|
@@ -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.
|