@sjawhar/pi-legion-envoy 1.40.2 → 1.42.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 -3
- package/dist/envoy.js +111 -15
- package/dist/legion.js +45 -29
- package/dist/skills/dispatch/SKILL.md +34 -3
- package/dist/skills/legion-architect/SKILL.md +4 -1
- package/dist/skills/legion-controller/SKILL.md +8 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,10 +85,10 @@ extension files it does not contain.
|
|
|
85
85
|
|
|
86
86
|
## Native Dispatch tools
|
|
87
87
|
|
|
88
|
-
The extension registers
|
|
88
|
+
The extension registers eighteen native Dispatch tools: `dispatch_issue`, `dispatch_issue_update`, `dispatch_ask`, `dispatch_edit_ask`,
|
|
89
89
|
`dispatch_resolve_ask`, `dispatch_resolve_comment`, `dispatch_follow`, `dispatch_comment`, `dispatch_suggest`,
|
|
90
90
|
`dispatch_message`, `dispatch_doc_edit`, `dispatch_doc_read`, `dispatch_request_approval`, `dispatch_artifact`,
|
|
91
|
-
`dispatch_read`, `dispatch_search`, and `
|
|
91
|
+
`dispatch_read`, `dispatch_search`, `dispatch_open_asks`, and `dispatch_whoami`, when Dispatch configuration resolves both a base URL and bearer token.
|
|
92
92
|
|
|
93
93
|
Configure the shared `envoy.json` with:
|
|
94
94
|
|
|
@@ -112,7 +112,7 @@ whose trimmed contents are the token — how the Legion daemon delivers it to a
|
|
|
112
112
|
pane) wins over every other token source and never falls back when unreadable.
|
|
113
113
|
Omitting `dispatch.serverUrl` while `dispatch.enabled` is true targets
|
|
114
114
|
`http://localhost:8766`, the Go server's listen address. Invalid configuration,
|
|
115
|
-
an invalid URL, or an empty token leaves the
|
|
115
|
+
an invalid URL, or an empty token leaves the eighteen tools unavailable and
|
|
116
116
|
reports the source of the error.
|
|
117
117
|
|
|
118
118
|
Owner-scoped calls use either an issue (a native `KEY` or external `owner/repo#n` reference) or
|
package/dist/envoy.js
CHANGED
|
@@ -29876,6 +29876,17 @@ var SPEC_SECTIONS = [
|
|
|
29876
29876
|
];
|
|
29877
29877
|
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.";
|
|
29878
29878
|
var ASK_URGENCIES = ["low", "med", "high", "blocking"];
|
|
29879
|
+
var ISSUE_STATUSES = [
|
|
29880
|
+
"triage",
|
|
29881
|
+
"icebox",
|
|
29882
|
+
"backlog",
|
|
29883
|
+
"todo",
|
|
29884
|
+
"in_progress",
|
|
29885
|
+
"testing",
|
|
29886
|
+
"needs_review",
|
|
29887
|
+
"retro",
|
|
29888
|
+
"done"
|
|
29889
|
+
];
|
|
29879
29890
|
var DOC_EDIT_OPS = ["replace", "delete", "insert", "retype", "move"];
|
|
29880
29891
|
var dispatchToolSpecs = [
|
|
29881
29892
|
{
|
|
@@ -29889,9 +29900,30 @@ var dispatchToolSpecs = [
|
|
|
29889
29900
|
force: z.boolean().describe("Create even though POSSIBLE_DUPLICATE listed similar issues; pass it only after reading them.").optional(),
|
|
29890
29901
|
spec: z.string().describe(`Optional initial primary-document markdown. ${SPEC_WRITING_GUIDANCE}`).optional(),
|
|
29891
29902
|
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(),
|
|
29892
|
-
priority: z.number({ int: true, min: 0, max: 3 }).describe("Optional coarse priority: P0 is highest and P3 is lowest.").optional()
|
|
29903
|
+
priority: z.number({ int: true, min: 0, max: 3 }).describe("Optional coarse priority: P0 is highest and P3 is lowest.").optional(),
|
|
29904
|
+
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()
|
|
29893
29905
|
})
|
|
29894
29906
|
},
|
|
29907
|
+
{
|
|
29908
|
+
name: "dispatch_issue_update",
|
|
29909
|
+
description: "Update an existing issue: move its lifecycle status, retitle it, replace its labels, link a URL " + "(the pull request that delivers it, a run, a document), or set its route. Status is one of " + `${ISSUE_STATUSES.join(", ")}; outside Legion, move it yourself as the work advances; inside ` + "Legion the daemon moves it. external_links are " + "merged into the issue's existing links by URL, so linking the pull request you just opened " + "keeps every earlier link. Priority is the human's and is not settable here. At least one " + `field besides issue is required. ${ISSUE_REFERENCE}`,
|
|
29910
|
+
arguments: (z) => ({
|
|
29911
|
+
issue: z.string().describe(ISSUE_REFERENCE),
|
|
29912
|
+
status: z.enum(ISSUE_STATUSES).describe("New lifecycle status.").optional(),
|
|
29913
|
+
title: z.string({ min: 1 }).describe("Replacement title.").optional(),
|
|
29914
|
+
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(),
|
|
29915
|
+
external_links: z.array(z.string({ min: 1 })).describe("URLs to link; merged into the issue's existing external links by URL.").optional(),
|
|
29916
|
+
route: z.string().describe("Route the issue to role:<name> or session:<id>; an empty string clears it.").optional()
|
|
29917
|
+
}),
|
|
29918
|
+
validation: {
|
|
29919
|
+
check: (value) => {
|
|
29920
|
+
const input = value;
|
|
29921
|
+
return typeof input.status === "string" || typeof input.title === "string" || Array.isArray(input.labels) || Array.isArray(input.external_links) || typeof input.route === "string";
|
|
29922
|
+
},
|
|
29923
|
+
message: "Issue update requires at least one field besides issue: status, title, labels, external_links, or route."
|
|
29924
|
+
},
|
|
29925
|
+
strict: true
|
|
29926
|
+
},
|
|
29895
29927
|
{
|
|
29896
29928
|
name: "dispatch_ask",
|
|
29897
29929
|
description: "Open a durable, answerable decision or human to-do on an issue or project document. Do not use it for a status update or discussion; " + "use dispatch_message instead. Use kind: action for a to-do a human must complete; it has fixed Done / Can't answers. " + "Anchor a document question, thread reply_to/reply_to_ask, or cite a dispatch:// " + 'reference \u2014 it must be answerable from its own text and anchor alone, never "see above". A quote anchor is pinned to its block. Question is at most 800 ' + `characters and has at most 8 options. ${OWNER_REFERENCE}`,
|
|
@@ -30094,6 +30126,12 @@ var dispatchToolSpecs = [
|
|
|
30094
30126
|
description: "List this session's active unanswered asks across issues and project documents, including age and whose reply is needed. Call before saying you are waiting for human input.",
|
|
30095
30127
|
arguments: () => ({}),
|
|
30096
30128
|
strict: true
|
|
30129
|
+
},
|
|
30130
|
+
{
|
|
30131
|
+
name: "dispatch_whoami",
|
|
30132
|
+
description: "Who Dispatch takes this session for: {session, owner}. owner is the lowercase GitHub login of the human whose personal token you run under (the default assignee of issues you create), or null under the shared token.",
|
|
30133
|
+
arguments: () => ({}),
|
|
30134
|
+
strict: true
|
|
30097
30135
|
}
|
|
30098
30136
|
];
|
|
30099
30137
|
// ../contracts/src/envelope.ts
|
|
@@ -30273,17 +30311,6 @@ var legionRole = _enum2(LEGION_ROLES);
|
|
|
30273
30311
|
var requiredUnknown = unknown().refine((value) => value !== undefined, {
|
|
30274
30312
|
message: "Required"
|
|
30275
30313
|
});
|
|
30276
|
-
var LIFECYCLE_STATUSES = [
|
|
30277
|
-
"triage",
|
|
30278
|
-
"icebox",
|
|
30279
|
-
"backlog",
|
|
30280
|
-
"todo",
|
|
30281
|
-
"in_progress",
|
|
30282
|
-
"testing",
|
|
30283
|
-
"needs_review",
|
|
30284
|
-
"retro",
|
|
30285
|
-
"done"
|
|
30286
|
-
];
|
|
30287
30314
|
var architectCapability = strictObject({
|
|
30288
30315
|
tree: nonEmptyString,
|
|
30289
30316
|
sessionId: nonEmptyString,
|
|
@@ -30315,7 +30342,7 @@ var stateTreeLocator = discriminatedUnion("runtime", [
|
|
|
30315
30342
|
var stateIssue = strictObject({
|
|
30316
30343
|
key: nonEmptyString,
|
|
30317
30344
|
title: string2(),
|
|
30318
|
-
status: _enum2(
|
|
30345
|
+
status: _enum2(ISSUE_STATUSES).optional(),
|
|
30319
30346
|
children: array(nonEmptyString),
|
|
30320
30347
|
parent: nonEmptyString.optional(),
|
|
30321
30348
|
lastAppliedSeq: number2().int().nonnegative().optional()
|
|
@@ -30484,7 +30511,7 @@ var LegionDaemonApi = {
|
|
|
30484
30511
|
},
|
|
30485
30512
|
IssueStatus: {
|
|
30486
30513
|
request: controllerIssue.extend({
|
|
30487
|
-
status: _enum2(
|
|
30514
|
+
status: _enum2(ISSUE_STATUSES),
|
|
30488
30515
|
tree: nonEmptyString.optional(),
|
|
30489
30516
|
sessionId: nonEmptyString.optional()
|
|
30490
30517
|
}),
|
|
@@ -31855,6 +31882,9 @@ class DispatchClient {
|
|
|
31855
31882
|
async getIssue(issue) {
|
|
31856
31883
|
return this.#json("GET", ["api", "v1", "issues", await this.#resolveIssue(issue)]);
|
|
31857
31884
|
}
|
|
31885
|
+
async updateIssue(issue, input) {
|
|
31886
|
+
return this.#json("PATCH", ["api", "v1", "issues", await this.#resolveIssue(issue)], input);
|
|
31887
|
+
}
|
|
31858
31888
|
async getIssueEvents(issue, after = 0, limit = 200) {
|
|
31859
31889
|
return this.#json("GET", ["api", "v1", "issues", await this.#resolveIssue(issue), "events"], undefined, { after, limit });
|
|
31860
31890
|
}
|
|
@@ -31873,6 +31903,9 @@ class DispatchClient {
|
|
|
31873
31903
|
...since === undefined ? {} : { since }
|
|
31874
31904
|
});
|
|
31875
31905
|
}
|
|
31906
|
+
async whoami() {
|
|
31907
|
+
return this.#json("GET", ["api", "v1", "whoami"]);
|
|
31908
|
+
}
|
|
31876
31909
|
async resolveAsk(id, input) {
|
|
31877
31910
|
return this.#json("POST", ["api", "v1", "asks", id, "resolve"], input);
|
|
31878
31911
|
}
|
|
@@ -32286,7 +32319,8 @@ var issueFreeTools = {
|
|
|
32286
32319
|
dispatch_resolve_comment: true,
|
|
32287
32320
|
dispatch_follow: true,
|
|
32288
32321
|
dispatch_search: true,
|
|
32289
|
-
dispatch_open_asks: true
|
|
32322
|
+
dispatch_open_asks: true,
|
|
32323
|
+
dispatch_whoami: true
|
|
32290
32324
|
};
|
|
32291
32325
|
function canonicalExternalIssueRef(value) {
|
|
32292
32326
|
const match = value.trim().match(externalIssueRefPattern);
|
|
@@ -32702,10 +32736,13 @@ function issueSummary(issue, events, references, graph) {
|
|
|
32702
32736
|
const specApproval = spec === undefined ? undefined : approvalLine(spec);
|
|
32703
32737
|
if (issue.priority === undefined)
|
|
32704
32738
|
throw new Error("Dispatch issue is missing priority");
|
|
32739
|
+
if (issue.assignee === undefined)
|
|
32740
|
+
throw new Error("Dispatch issue is missing assignee");
|
|
32705
32741
|
return [
|
|
32706
32742
|
`Title: ${issue.title}`,
|
|
32707
32743
|
`Key: ${issue.key}`,
|
|
32708
32744
|
`Status: ${issue.status}`,
|
|
32745
|
+
`Assignee: ${issue.assignee ?? "unassigned"}`,
|
|
32709
32746
|
...issue.priority === null ? [] : [`Priority: P${issue.priority}`],
|
|
32710
32747
|
`Labels: ${issue.labels.length === 0 ? "none" : issue.labels.join(", ")}`,
|
|
32711
32748
|
`Route: ${issue.route ?? "none"}`,
|
|
@@ -32953,6 +32990,18 @@ async function executeDispatchTool(input) {
|
|
|
32953
32990
|
const response = await client.openAsks(sessionId);
|
|
32954
32991
|
return { text: formatOpenAsksSummary(response, configUrl), details: { ...response } };
|
|
32955
32992
|
}
|
|
32993
|
+
if (input.tool === "dispatch_whoami") {
|
|
32994
|
+
const sessionId = input.sessionId?.trim();
|
|
32995
|
+
if (!sessionId)
|
|
32996
|
+
throw new Error("host session id is required for dispatch_whoami");
|
|
32997
|
+
const client = new DispatchClient(configUrl, configToken, fetchImpl, input.signal);
|
|
32998
|
+
const identity = await client.whoami();
|
|
32999
|
+
const owner = identity.kind === "agent" ? identity.owner : identity.login.toLowerCase();
|
|
33000
|
+
return {
|
|
33001
|
+
text: owner === null ? `Session ${sessionId} runs under the shared token: no owner, so issues you create without an assignee are unassigned (or inherit their parent's).` : `Session ${sessionId} acts for ${owner}: issues you create without an assignee are assigned to ${owner}.`,
|
|
33002
|
+
details: { session: sessionId, owner }
|
|
33003
|
+
};
|
|
33004
|
+
}
|
|
32956
33005
|
const args = parsed.success ? parsed.data : ownerArguments.args;
|
|
32957
33006
|
const actor = toolActor(await resolveOrigin(env, exec, input.cwd), input);
|
|
32958
33007
|
const client = new DispatchClient(configUrl, configToken, fetchImpl, input.signal);
|
|
@@ -32979,6 +33028,7 @@ async function executeDispatchTool(input) {
|
|
|
32979
33028
|
const force = optionalBoolean(args, "force");
|
|
32980
33029
|
const spec = optionalString(args, "spec");
|
|
32981
33030
|
const priority = optionalNumber(args, "priority");
|
|
33031
|
+
const assignee = optionalString(args, "assignee");
|
|
32982
33032
|
const labels = args.labels;
|
|
32983
33033
|
try {
|
|
32984
33034
|
const created = await client.issue({
|
|
@@ -32989,6 +33039,7 @@ async function executeDispatchTool(input) {
|
|
|
32989
33039
|
...force === undefined ? {} : { force },
|
|
32990
33040
|
...spec === undefined ? {} : { spec },
|
|
32991
33041
|
...priority === undefined ? {} : { priority },
|
|
33042
|
+
...assignee === undefined ? {} : { assignee },
|
|
32992
33043
|
...Array.isArray(labels) ? { labels } : {},
|
|
32993
33044
|
actor
|
|
32994
33045
|
});
|
|
@@ -33015,6 +33066,51 @@ async function executeDispatchTool(input) {
|
|
|
33015
33066
|
};
|
|
33016
33067
|
}
|
|
33017
33068
|
}
|
|
33069
|
+
case "dispatch_issue_update": {
|
|
33070
|
+
const issueKey = issue();
|
|
33071
|
+
const status = optionalString(args, "status");
|
|
33072
|
+
const title = optionalString(args, "title");
|
|
33073
|
+
const route = optionalString(args, "route");
|
|
33074
|
+
const labels = Array.isArray(args.labels) ? args.labels : undefined;
|
|
33075
|
+
const requestedLinks = Array.isArray(args.external_links) ? [...new Set(args.external_links)] : undefined;
|
|
33076
|
+
let newLinks = [];
|
|
33077
|
+
try {
|
|
33078
|
+
const before = await client.getIssue(issueKey);
|
|
33079
|
+
const linked = before.external_links.map((link) => link.url);
|
|
33080
|
+
newLinks = requestedLinks?.filter((url) => !linked.includes(url)) ?? [];
|
|
33081
|
+
const after = await client.updateIssue(issueKey, {
|
|
33082
|
+
...status === undefined ? {} : { status },
|
|
33083
|
+
...title === undefined ? {} : { title },
|
|
33084
|
+
...labels === undefined ? {} : { labels },
|
|
33085
|
+
...route === undefined ? {} : { route },
|
|
33086
|
+
...requestedLinks === undefined ? {} : { external_links: [...before.external_links, ...newLinks.map((url) => ({ url }))] },
|
|
33087
|
+
actor
|
|
33088
|
+
});
|
|
33089
|
+
const linkCount = `(${after.external_links.length} ${after.external_links.length === 1 ? "link" : "links"})`;
|
|
33090
|
+
const changes = [
|
|
33091
|
+
...status === undefined ? [] : [`status ${before.status} -> ${after.status}`],
|
|
33092
|
+
...title === undefined ? [] : [`title "${after.title}"`],
|
|
33093
|
+
...labels === undefined ? [] : [after.labels.length === 0 ? "labels cleared" : `labels ${after.labels.join(", ")}`],
|
|
33094
|
+
...requestedLinks === undefined ? [] : [
|
|
33095
|
+
newLinks.length === 0 ? `already linked ${requestedLinks.join(", ")} ${linkCount}` : `linked ${newLinks.join(", ")} ${linkCount}`
|
|
33096
|
+
],
|
|
33097
|
+
...route === undefined ? [] : [after.route === null ? "route cleared" : `route ${after.route}`]
|
|
33098
|
+
];
|
|
33099
|
+
return {
|
|
33100
|
+
text: `${after.key}: ${changes.join("; ")} ${notSubscribed(issueTopic(after.key))}`,
|
|
33101
|
+
details: {
|
|
33102
|
+
issue: after.key,
|
|
33103
|
+
status: after.status,
|
|
33104
|
+
external_links: after.external_links.map((link) => link.url)
|
|
33105
|
+
}
|
|
33106
|
+
};
|
|
33107
|
+
} catch (error) {
|
|
33108
|
+
if (!(error instanceof DispatchServiceError))
|
|
33109
|
+
throw error;
|
|
33110
|
+
const taken = error.status === 500 && newLinks.length > 0 ? `; one of ${newLinks.join(", ")} may already be linked from another issue (a URL links exactly one issue)` : "";
|
|
33111
|
+
throw new DispatchServiceError(error.code, error.status, `${error.code}: ${error.message}${taken}`, error.candidates);
|
|
33112
|
+
}
|
|
33113
|
+
}
|
|
33018
33114
|
case "dispatch_search": {
|
|
33019
33115
|
const query = stringArg(args, "query");
|
|
33020
33116
|
const project = optionalString(args, "project");
|
package/dist/legion.js
CHANGED
|
@@ -29150,6 +29150,17 @@ var SPEC_SECTIONS = [
|
|
|
29150
29150
|
];
|
|
29151
29151
|
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.";
|
|
29152
29152
|
var ASK_URGENCIES = ["low", "med", "high", "blocking"];
|
|
29153
|
+
var ISSUE_STATUSES = [
|
|
29154
|
+
"triage",
|
|
29155
|
+
"icebox",
|
|
29156
|
+
"backlog",
|
|
29157
|
+
"todo",
|
|
29158
|
+
"in_progress",
|
|
29159
|
+
"testing",
|
|
29160
|
+
"needs_review",
|
|
29161
|
+
"retro",
|
|
29162
|
+
"done"
|
|
29163
|
+
];
|
|
29153
29164
|
var DOC_EDIT_OPS = ["replace", "delete", "insert", "retype", "move"];
|
|
29154
29165
|
var dispatchToolSpecs = [
|
|
29155
29166
|
{
|
|
@@ -29163,9 +29174,30 @@ var dispatchToolSpecs = [
|
|
|
29163
29174
|
force: z.boolean().describe("Create even though POSSIBLE_DUPLICATE listed similar issues; pass it only after reading them.").optional(),
|
|
29164
29175
|
spec: z.string().describe(`Optional initial primary-document markdown. ${SPEC_WRITING_GUIDANCE}`).optional(),
|
|
29165
29176
|
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(),
|
|
29166
|
-
priority: z.number({ int: true, min: 0, max: 3 }).describe("Optional coarse priority: P0 is highest and P3 is lowest.").optional()
|
|
29177
|
+
priority: z.number({ int: true, min: 0, max: 3 }).describe("Optional coarse priority: P0 is highest and P3 is lowest.").optional(),
|
|
29178
|
+
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()
|
|
29167
29179
|
})
|
|
29168
29180
|
},
|
|
29181
|
+
{
|
|
29182
|
+
name: "dispatch_issue_update",
|
|
29183
|
+
description: "Update an existing issue: move its lifecycle status, retitle it, replace its labels, link a URL " + "(the pull request that delivers it, a run, a document), or set its route. Status is one of " + `${ISSUE_STATUSES.join(", ")}; outside Legion, move it yourself as the work advances; inside ` + "Legion the daemon moves it. external_links are " + "merged into the issue's existing links by URL, so linking the pull request you just opened " + "keeps every earlier link. Priority is the human's and is not settable here. At least one " + `field besides issue is required. ${ISSUE_REFERENCE}`,
|
|
29184
|
+
arguments: (z) => ({
|
|
29185
|
+
issue: z.string().describe(ISSUE_REFERENCE),
|
|
29186
|
+
status: z.enum(ISSUE_STATUSES).describe("New lifecycle status.").optional(),
|
|
29187
|
+
title: z.string({ min: 1 }).describe("Replacement title.").optional(),
|
|
29188
|
+
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(),
|
|
29189
|
+
external_links: z.array(z.string({ min: 1 })).describe("URLs to link; merged into the issue's existing external links by URL.").optional(),
|
|
29190
|
+
route: z.string().describe("Route the issue to role:<name> or session:<id>; an empty string clears it.").optional()
|
|
29191
|
+
}),
|
|
29192
|
+
validation: {
|
|
29193
|
+
check: (value) => {
|
|
29194
|
+
const input = value;
|
|
29195
|
+
return typeof input.status === "string" || typeof input.title === "string" || Array.isArray(input.labels) || Array.isArray(input.external_links) || typeof input.route === "string";
|
|
29196
|
+
},
|
|
29197
|
+
message: "Issue update requires at least one field besides issue: status, title, labels, external_links, or route."
|
|
29198
|
+
},
|
|
29199
|
+
strict: true
|
|
29200
|
+
},
|
|
29169
29201
|
{
|
|
29170
29202
|
name: "dispatch_ask",
|
|
29171
29203
|
description: "Open a durable, answerable decision or human to-do on an issue or project document. Do not use it for a status update or discussion; " + "use dispatch_message instead. Use kind: action for a to-do a human must complete; it has fixed Done / Can't answers. " + "Anchor a document question, thread reply_to/reply_to_ask, or cite a dispatch:// " + 'reference \u2014 it must be answerable from its own text and anchor alone, never "see above". A quote anchor is pinned to its block. Question is at most 800 ' + `characters and has at most 8 options. ${OWNER_REFERENCE}`,
|
|
@@ -29368,6 +29400,12 @@ var dispatchToolSpecs = [
|
|
|
29368
29400
|
description: "List this session's active unanswered asks across issues and project documents, including age and whose reply is needed. Call before saying you are waiting for human input.",
|
|
29369
29401
|
arguments: () => ({}),
|
|
29370
29402
|
strict: true
|
|
29403
|
+
},
|
|
29404
|
+
{
|
|
29405
|
+
name: "dispatch_whoami",
|
|
29406
|
+
description: "Who Dispatch takes this session for: {session, owner}. owner is the lowercase GitHub login of the human whose personal token you run under (the default assignee of issues you create), or null under the shared token.",
|
|
29407
|
+
arguments: () => ({}),
|
|
29408
|
+
strict: true
|
|
29371
29409
|
}
|
|
29372
29410
|
];
|
|
29373
29411
|
// ../contracts/src/envelope.ts
|
|
@@ -29547,17 +29585,6 @@ var legionRole = _enum2(LEGION_ROLES);
|
|
|
29547
29585
|
var requiredUnknown = unknown().refine((value) => value !== undefined, {
|
|
29548
29586
|
message: "Required"
|
|
29549
29587
|
});
|
|
29550
|
-
var LIFECYCLE_STATUSES = [
|
|
29551
|
-
"triage",
|
|
29552
|
-
"icebox",
|
|
29553
|
-
"backlog",
|
|
29554
|
-
"todo",
|
|
29555
|
-
"in_progress",
|
|
29556
|
-
"testing",
|
|
29557
|
-
"needs_review",
|
|
29558
|
-
"retro",
|
|
29559
|
-
"done"
|
|
29560
|
-
];
|
|
29561
29588
|
var architectCapability = strictObject({
|
|
29562
29589
|
tree: nonEmptyString,
|
|
29563
29590
|
sessionId: nonEmptyString,
|
|
@@ -29589,7 +29616,7 @@ var stateTreeLocator = discriminatedUnion("runtime", [
|
|
|
29589
29616
|
var stateIssue = strictObject({
|
|
29590
29617
|
key: nonEmptyString,
|
|
29591
29618
|
title: string2(),
|
|
29592
|
-
status: _enum2(
|
|
29619
|
+
status: _enum2(ISSUE_STATUSES).optional(),
|
|
29593
29620
|
children: array(nonEmptyString),
|
|
29594
29621
|
parent: nonEmptyString.optional(),
|
|
29595
29622
|
lastAppliedSeq: number2().int().nonnegative().optional()
|
|
@@ -29758,7 +29785,7 @@ var LegionDaemonApi = {
|
|
|
29758
29785
|
},
|
|
29759
29786
|
IssueStatus: {
|
|
29760
29787
|
request: controllerIssue.extend({
|
|
29761
|
-
status: _enum2(
|
|
29788
|
+
status: _enum2(ISSUE_STATUSES),
|
|
29762
29789
|
tree: nonEmptyString.optional(),
|
|
29763
29790
|
sessionId: nonEmptyString.optional()
|
|
29764
29791
|
}),
|
|
@@ -30287,19 +30314,8 @@ function toolFailure(error) {
|
|
|
30287
30314
|
|
|
30288
30315
|
// src/legion/tools.ts
|
|
30289
30316
|
var jsonSuccess = (details) => toolSuccess(JSON.stringify(details), details);
|
|
30290
|
-
|
|
30291
|
-
|
|
30292
|
-
"icebox",
|
|
30293
|
-
"backlog",
|
|
30294
|
-
"todo",
|
|
30295
|
-
"in_progress",
|
|
30296
|
-
"testing",
|
|
30297
|
-
"needs_review",
|
|
30298
|
-
"retro",
|
|
30299
|
-
"done"
|
|
30300
|
-
];
|
|
30301
|
-
function isLifecycleStatus(value) {
|
|
30302
|
-
return LIFECYCLE_STATUSES2.includes(value);
|
|
30317
|
+
function isIssueStatus(value) {
|
|
30318
|
+
return ISSUE_STATUSES.includes(value);
|
|
30303
30319
|
}
|
|
30304
30320
|
function isDispatchArtifactId(value) {
|
|
30305
30321
|
return LegionDaemonApi.GatesRegister.request.shape.artifactId.safeParse(value).success;
|
|
@@ -30316,7 +30332,7 @@ function legionToolSchema(pi) {
|
|
|
30316
30332
|
return z.object({
|
|
30317
30333
|
op: z.enum(["set_status", "register_gate", "release_wave", "escalate", "spawn_worker"]),
|
|
30318
30334
|
issue: z.string().optional(),
|
|
30319
|
-
status: z.enum(
|
|
30335
|
+
status: z.enum(ISSUE_STATUSES).optional(),
|
|
30320
30336
|
artifactId: z.string().optional(),
|
|
30321
30337
|
version: z.number().optional(),
|
|
30322
30338
|
kind: z.enum(["re-file", "capacity", "cross-tree"]).optional(),
|
|
@@ -30358,7 +30374,7 @@ function createLegionTool(deps) {
|
|
|
30358
30374
|
switch (parameters.op) {
|
|
30359
30375
|
case "set_status": {
|
|
30360
30376
|
const status = parameters.status;
|
|
30361
|
-
if (typeof status !== "string" || !
|
|
30377
|
+
if (typeof status !== "string" || !isIssueStatus(status)) {
|
|
30362
30378
|
throw new Error("set_status requires a valid Legion issue status");
|
|
30363
30379
|
}
|
|
30364
30380
|
await daemon.issueStatus({
|
|
@@ -90,16 +90,47 @@ exactly one owner to every owner-scoped tool: `issue` for an issue, or `project`
|
|
|
90
90
|
[References](#references) for the resulting ref shape). On first use, an external issue reference creates its native issue in the
|
|
91
91
|
project configured for that repository in Dispatch Settings, then falls back to `DISPATCH_DEFAULT_PROJECT`.
|
|
92
92
|
|
|
93
|
-
Issue reads include `rank`, the server-owned ordering key used by project boards; reorder through `PATCH /api/v1/issues/{key}` with neighboring issue keys. They also include nullable coarse priority (`P0` highest through `P3` lowest).
|
|
93
|
+
Issue reads include `rank`, the server-owned ordering key used by project boards; reorder through `PATCH /api/v1/issues/{key}` with neighboring issue keys. They also include nullable coarse priority (`P0` highest through `P3` lowest) and `assignee`: the lowercase GitHub login of the human who answers the issue's asks, or `null` when nobody holds it. `dispatch_read` of an issue prints it as `Assignee: <login>` or `Assignee: unassigned`.
|
|
94
|
+
|
|
95
|
+
### Who answers an ask
|
|
96
|
+
|
|
97
|
+
An ask goes to the issue's assignee: their Inbox opens on **Mine**, which lists asks on the issues they hold plus an Unassigned band; an ask on an unassigned issue waits in that band for someone to take it. Find out who Dispatch takes you for with:
|
|
98
|
+
```ts
|
|
99
|
+
dispatch_whoami({})
|
|
100
|
+
```
|
|
101
|
+
It returns `details` `{ session, owner }`: `owner` is the lowercase login of the human whose personal token you run under, or `null` under the shared token. An issue you create without `assignee` goes to your owner; under the shared token it inherits its parent's assignee, or stays unassigned without a parent. If an issue you are asking on is unassigned and the answer matters, assign it to your owner (`PATCH /api/v1/issues/{key}` with `{"assignee": "<login>"}`; any authenticated caller may reassign, and an unlisted login is refused with `ASSIGNEE_NOT_ALLOWED`) or name in the question who should answer it. Never reassign an issue a human holds to get an answer faster: that is the human's call.
|
|
94
102
|
|
|
95
103
|
Architects create newly tracked child work with:
|
|
96
104
|
```ts
|
|
97
|
-
dispatch_issue({ project, title, parent?, external?, spec?, force?, labels?: string[], priority?: 0 | 1 | 2 | 3 })
|
|
105
|
+
dispatch_issue({ project, title, parent?, external?, spec?, force?, labels?: string[], priority?: 0 | 1 | 2 | 3, assignee?: string })
|
|
98
106
|
```
|
|
99
|
-
`labels` are optional initial labels: Dispatch trims them, preserves their case, and removes case-insensitive duplicates. Set `priority` on creation only when the human's intent makes the bucket clear; otherwise priority remains the human's decision. It returns
|
|
107
|
+
`labels` are optional initial labels: Dispatch trims them, preserves their case, and removes case-insensitive duplicates. Set `priority` on creation only when the human's intent makes the bucket clear; otherwise priority remains the human's decision. Set `assignee` (a GitHub login on the sign-in allowlist) only when the human said who owns the work; otherwise the default above applies, so a child inherits its parent's assignee. It returns
|
|
100
108
|
`details` `{ issue }`; creating an issue does not subscribe you to it (see [Following](#following)). Use `dispatch_issue` only to create an issue; never use it to park a question. When `spec` is supplied,
|
|
101
109
|
follow [Writing a spec](#writing-a-spec).
|
|
102
110
|
|
|
111
|
+
## Issue status is yours to move
|
|
112
|
+
|
|
113
|
+
The issue's status is how a human sees delivery without asking a session. Outside Legion (where
|
|
114
|
+
the daemon writes it), the session doing the work moves it, the way a person moves a card:
|
|
115
|
+
`in_progress` when implementation starts, `testing` when the change is being proven on a
|
|
116
|
+
production-like surface, `needs_review` when its pull request is open and waiting on the merge
|
|
117
|
+
queue, `done` when the change has been driven in production (a merge is not `done`). Move child
|
|
118
|
+
issues you own as well as the root. An issue left at `triage` while work is underway is a defect:
|
|
119
|
+
Sami, 2026-09-15, on the roadmap he could not read — "I'm not even sure what their development
|
|
120
|
+
status is." Waiting for the deploy lane is not a status and is never announced. Priority stays the
|
|
121
|
+
human's: set it on creation only when their intent is clear, and change it only on their word.
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
// PATCH /api/v1/issues/{key} — status, title, labels, external_links (merged by URL), route
|
|
125
|
+
dispatch_issue_update({ issue: "AGENTC-175", status: "testing" })
|
|
126
|
+
dispatch_issue_update({ issue: "AGENTC-175", external_links: ["https://github.com/owner/repo/pull/7"] })
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Link the pull request that delivers the issue in `external_links` when you open it; the issue page
|
|
130
|
+
renders its state and checks from that link. The call is authenticated with the same bearer as every
|
|
131
|
+
other `dispatch_*` tool: a Legion pane reads it from the `DISPATCH_TOKEN_FILE` path the daemon sets on
|
|
132
|
+
the pane; an OMP session outside Legion reads `dispatch.token` from `~/.config/opencode/envoy.json`.
|
|
133
|
+
|
|
103
134
|
## Search first
|
|
104
135
|
|
|
105
136
|
Before you create an issue or start a design document, search:
|
|
@@ -75,7 +75,10 @@ exercise a criterion end to end, building that path is a child issue of this tre
|
|
|
75
75
|
the role-token `<project>` (the daemon's own project, e.g. `acme`), a different string. A
|
|
76
76
|
root session has `LEGION_TREE == LEGION_ISSUE`. The daemon establishes the sub-issue
|
|
77
77
|
relationship from `parent`. Keep the returned issue keys in ordered waves; a child is
|
|
78
|
-
inert until released.
|
|
78
|
+
inert until released. Do not pass `assignee`: the default keeps the tree's questions in one
|
|
79
|
+
Inbox — under the shared token a child inherits its parent's assignee (the human who
|
|
80
|
+
answers the tree's asks); under a personal token it goes to that token's owner, whom
|
|
81
|
+
`dispatch_whoami` names. Set it only when a human told you a specific person owns that child.
|
|
79
82
|
|
|
80
83
|
Specifications written into Dispatch follow [`skills/dispatch`'s Writing a spec](../dispatch/SKILL.md#writing-a-spec).
|
|
81
84
|
Wave releases, child closures, and your own status are visible from the issue tree and the
|
|
@@ -107,6 +107,9 @@ override a Sami ruling quoted here.
|
|
|
107
107
|
1. Read `legion state --json`, then inspect the reported Dispatch issue with `dispatch_read`.
|
|
108
108
|
Verify the issue is in this project, is eligible for a root process, and whether it
|
|
109
109
|
has pre-existing children. Dispatch and daemon state, not the wake text, decide triage.
|
|
110
|
+
Note the `Assignee:` line: that human answers the tree's asks, and their Inbox opens on
|
|
111
|
+
the issues they hold. Never reassign during triage — who holds an issue is the humans'
|
|
112
|
+
decision, made from the issue header.
|
|
110
113
|
2. If it should run now, admit the root issue:
|
|
111
114
|
|
|
112
115
|
```text
|
|
@@ -123,6 +126,11 @@ override a Sami ruling quoted here.
|
|
|
123
126
|
(or `icebox` for longer-term deferral). Dispatch status is the durable record;
|
|
124
127
|
there is no separate marker to maintain. Do not triage a system-created child as a root
|
|
125
128
|
issue.
|
|
129
|
+
4. When you post a triage note (a `dispatch_comment` on the issue saying what you decided and
|
|
130
|
+
why), name who will be asked: `Assigned to <login>, who will get this tree's questions`,
|
|
131
|
+
or, when the `Assignee:` line says `unassigned`, `Unassigned — nobody's Inbox shows this
|
|
132
|
+
tree's questions until someone takes it from the issue header (Assignee, beside Priority)`.
|
|
133
|
+
An unassigned root still runs; the architect's asks wait in every Inbox's Unassigned band.
|
|
126
134
|
|
|
127
135
|
## Backlog eligibility
|
|
128
136
|
|