@sjawhar/opencode-legion-envoy 9.6.0 → 9.8.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/dist/dispatch.js +92 -36
- package/dist/src/server.js +28 -13
- package/package.json +1 -1
- package/skills/AGENTS.md +1 -1
- package/skills/dispatch/SKILL.md +2 -2
- package/skills/dispatch-writing-specs/SKILL.md +5 -6
- package/skills/legion-controller/SKILL.md +1 -1
- package/skills/legion-controller/references/daily-report.md +3 -3
- package/skills/legion-controller/references/session.md +2 -2
- package/skills/legion-worker/references/findings.md +3 -3
- package/skills/legion-worker/references/merge-gate.md +4 -3
- package/src/server.ts +6 -0
package/dist/dispatch.js
CHANGED
|
@@ -13894,7 +13894,10 @@ var IssueEventPayloadSchema = object({
|
|
|
13894
13894
|
status: string2().optional(),
|
|
13895
13895
|
priority: number2().int().min(0).max(3).nullable().optional(),
|
|
13896
13896
|
rank: string2().optional(),
|
|
13897
|
-
route: string2().nullish()
|
|
13897
|
+
route: string2().nullish(),
|
|
13898
|
+
changed: array(string2()).optional(),
|
|
13899
|
+
ancestors: array(string2()).optional(),
|
|
13900
|
+
thread_logins: array(string2()).optional()
|
|
13898
13901
|
});
|
|
13899
13902
|
var ArtifactCreatedEventPayloadSchema = object({
|
|
13900
13903
|
artifact: object({ id: string2().optional(), slug: string2().optional(), name: string2().optional() }).optional()
|
|
@@ -13948,11 +13951,12 @@ var askEventPayloadFields = {
|
|
|
13948
13951
|
primary: boolean2().optional()
|
|
13949
13952
|
}).optional(),
|
|
13950
13953
|
resolution: object({
|
|
13951
|
-
kind: _enum2(["retracted", "resolved"]).optional(),
|
|
13954
|
+
kind: _enum2(["retracted", "resolved", "withdrawn"]).optional(),
|
|
13952
13955
|
reason: string2().optional(),
|
|
13953
13956
|
actor: object({ kind: string2().optional(), id: string2().optional() }).passthrough().optional(),
|
|
13954
13957
|
at: string2().optional()
|
|
13955
|
-
}).nullish()
|
|
13958
|
+
}).nullish(),
|
|
13959
|
+
thread_logins: array(string2()).optional()
|
|
13956
13960
|
};
|
|
13957
13961
|
var AskEventPayloadSchema = intersection(object(askEventPayloadFields), object({
|
|
13958
13962
|
previous: never().optional(),
|
|
@@ -14014,7 +14018,8 @@ var CommentEventPayloadSchema = object({
|
|
|
14014
14018
|
author: object({ kind: string2(), id: string2() }).optional(),
|
|
14015
14019
|
created_at: string2().optional(),
|
|
14016
14020
|
mentions: array(CommentMentionSchema).optional(),
|
|
14017
|
-
deliveries: array(CommentDeliverySchema).optional()
|
|
14021
|
+
deliveries: array(CommentDeliverySchema).optional(),
|
|
14022
|
+
thread_logins: array(string2()).optional()
|
|
14018
14023
|
}).passthrough();
|
|
14019
14024
|
var MessageEventPayloadSchema = object({
|
|
14020
14025
|
id: string2().optional(),
|
|
@@ -14023,7 +14028,8 @@ var MessageEventPayloadSchema = object({
|
|
|
14023
14028
|
in_reply_to: string2().nullish(),
|
|
14024
14029
|
reply_body: string2().optional(),
|
|
14025
14030
|
thread_target: string2().optional(),
|
|
14026
|
-
author: object({ kind: string2(), id: string2() }).optional()
|
|
14031
|
+
author: object({ kind: string2(), id: string2() }).optional(),
|
|
14032
|
+
thread_logins: array(string2()).optional()
|
|
14027
14033
|
});
|
|
14028
14034
|
var DispatchTargetedResourceIDSchema = uuid2();
|
|
14029
14035
|
var DispatchTargetedMessagePayloadSchema = MessageEventPayloadSchema.extend({
|
|
@@ -14093,7 +14099,7 @@ var AskFollowerEventPayloadSchema = object({
|
|
|
14093
14099
|
|
|
14094
14100
|
// ../contracts/src/actor-label.ts
|
|
14095
14101
|
function shortSessionId(id) {
|
|
14096
|
-
return `session:${id.slice(0, 8)}\u2026`;
|
|
14102
|
+
return id.startsWith("legion-daemon:") ? id : `session:${id.slice(0, 8)}\u2026`;
|
|
14097
14103
|
}
|
|
14098
14104
|
function sessionLabel(sessionId, title) {
|
|
14099
14105
|
const trimmed = title?.trim() ?? "";
|
|
@@ -14424,7 +14430,7 @@ var dispatchToolSpecs = [
|
|
|
14424
14430
|
priority: 1,
|
|
14425
14431
|
before: "DSP-2"
|
|
14426
14432
|
},
|
|
14427
|
-
description: "Update an existing issue: move its lifecycle status, retitle it, replace its labels, set " + "its priority, link a URL (the pull request that delivers it, a run, a document), set its " + "route, set or clear its parent, replace the issues it is blocked by, attach it to " + "architecture components, set the repository it targets, or move it on its project's " + "board. Status is one of " + `${ISSUE_STATUSES.join(", ")}; outside Legion, move it yourself as the work advances; inside ` + "Legion the daemon moves it. A non-empty external_links is " + "merged into the issue's existing links by URL, so linking the pull request you just opened " + "keeps every earlier link; an empty external_links removes every link instead. components " + "replaces the issue's own attachment. Closing an issue " + "(status done) requires reason, the note that says why: it is posted on the issue as a " + "message, then the issue closes, because a closed issue refuses messages, comments, and " + "artifacts; reason goes only with status done. A closed issue takes only a board move " + "(before, after), components, " + "a reopening status (any status but done), and an external_links write that adds no new " + "link (so a link can move to the issue that replaced it); everything else, priority " + "included, waits for the reopen. " + "priority is yours to set and a human overrides it. before and after move the issue on " + "its project's board: before puts it above that issue, after below, and both together " + "land it between them. A project's board order is one agent's to keep; in a Legion " + "deployment that is the controller. Never reassign an issue a person holds to get a " + "faster answer: who holds it is their call. At least one " + `field besides issue is required. ${ISSUE_REFERENCE}`,
|
|
14433
|
+
description: "Update an existing issue: move its lifecycle status, retitle it, replace its labels, set " + "its priority, link a URL (the pull request that delivers it, a run, a document), set its " + "route, set or clear its parent, replace the issues it is blocked by, attach it to " + "architecture components, set the repository it targets, or move it on its project's " + "board. Status is one of " + `${ISSUE_STATUSES.join(", ")}; outside Legion, move it yourself as the work advances; inside ` + "Legion the daemon moves it. A non-empty external_links is " + "merged into the issue's existing links by URL, so linking the pull request you just opened " + "keeps every earlier link; an empty external_links removes every link instead. components " + "replaces the issue's own attachment. Closing an issue " + "(status done) requires reason, the note that says why: it is posted on the issue as a " + "message, then the issue closes, because a closed issue refuses messages, comments, and " + "artifacts; reason goes only with status done. Closing an issue withdraws every open ask " + "it holds (resolution kind withdrawn, naming the closer), and reopening it brings exactly " + "those asks back. A closed issue takes only a board move " + "(before, after), components, " + "a reopening status (any status but done), and an external_links write that adds no new " + "link (so a link can move to the issue that replaced it); everything else, priority " + "included, waits for the reopen. " + "priority is yours to set and a human overrides it. before and after move the issue on " + "its project's board: before puts it above that issue, after below, and both together " + "land it between them. A project's board order is one agent's to keep; in a Legion " + "deployment that is the controller. Never reassign an issue a person holds to get a " + "faster answer: who holds it is their call. At least one " + `field besides issue is required. ${ISSUE_REFERENCE}`,
|
|
14428
14434
|
arguments: (z) => ({
|
|
14429
14435
|
issue: z.string().describe(ISSUE_REFERENCE),
|
|
14430
14436
|
status: z.enum(ISSUE_STATUSES).describe("New lifecycle status.").optional(),
|
|
@@ -14451,7 +14457,7 @@ var dispatchToolSpecs = [
|
|
|
14451
14457
|
{
|
|
14452
14458
|
name: "dispatch_claim",
|
|
14453
14459
|
example: { issue: "DSP-1" },
|
|
14454
|
-
description: "Claim a Dispatch issue before you start implementing it, so no other session takes the same work, " + "and release it when you stop. Pass the issue alone to claim it, or --release to give it up. " + "Your claim records your own session and shows on every read of the issue: the dashboard header, the " + "issue list and board, dispatch read, and dispatch issues. Claiming is refused with 409 ISSUE_CLAIMED " + "when another session holds the issue and is still running; the refusal names that session, so talk to " + "it instead of working the same issue in parallel. When a human holds the claim the refusal names the " + "person, not a session: there is nothing running to message, so ask them on the issue rather than " + "taking it. 409 CLAIM_CONTENDED means the issue changed " + "hands twice while your call ran, so nothing was applied and nobody's liveness was checked: read " + "the issue and decide again. A claim whose session is no longer running may be " + "taken: the takeover is recorded on the issue and the session that lost it is told. A claim is not the " + "issue's status \u2014 claiming moves nothing, and the result names who moves the status: you, with " + "dispatch issue-update, or Legion's daemon on an issue it runs, where a status written by anyone " + "else is undone. A claim is released by its holder or any human, and by " + "any agent once the holder's session is no longer running. " + ISSUE_REFERENCE,
|
|
14460
|
+
description: "Claim a Dispatch issue before you start implementing it, so no other session takes the same work, " + "and release it when you stop. Pass the issue alone to claim it, or --release to give it up. " + "Your claim records your own session and shows on every read of the issue: the dashboard header, the " + "issue list and board, dispatch read, and dispatch issues. While you hold it, a person's answers, " + "comments and messages on the issue reach your session directly (a new comment that mentions you " + "arrives once, as the mention, when the mention reached you; when the mention's send fails, the " + "comment still reaches you directly; a mention whose receipt timed out can arrive twice, and the " + "second copy is the same comment); an agent's events and every other change on the issue still " + "need a subscription to it, except in an ask you follow: anyone else's answer, hand-back, " + "resolution or edit of it (other than a quiet edit), and every reply there (new, edited, resolved " + "or reopened), reach you " + "as its follower. Claiming is refused with 409 ISSUE_CLAIMED " + "when another session holds the issue and is still running; the refusal names that session, so talk to " + "it instead of working the same issue in parallel. When a human holds the claim the refusal names the " + "person, not a session: there is nothing running to message, so ask them on the issue rather than " + "taking it. 409 CLAIM_CONTENDED means the issue changed " + "hands twice while your call ran, so nothing was applied and nobody's liveness was checked: read " + "the issue and decide again. A claim whose session is no longer running may be " + "taken: the takeover is recorded on the issue and the session that lost it is told. A claim is not the " + "issue's status \u2014 claiming moves nothing, and the result names who moves the status: you, with " + "dispatch issue-update, or Legion's daemon on an issue it runs, where a status written by anyone " + "else is undone. A claim is released by its holder or any human, and by " + "any agent once the holder's session is no longer running. " + ISSUE_REFERENCE,
|
|
14455
14461
|
arguments: (z) => ({
|
|
14456
14462
|
issue: z.string().describe(ISSUE_REFERENCE),
|
|
14457
14463
|
release: z.boolean().describe("Give up your claim instead of taking it; the issue's status does not change.").optional()
|
|
@@ -14538,7 +14544,7 @@ var dispatchToolSpecs = [
|
|
|
14538
14544
|
kind: "retracted",
|
|
14539
14545
|
reason: "A newer question supersedes this one."
|
|
14540
14546
|
},
|
|
14541
|
-
description: "Retract an open question that is moot or resolve one after finding the answer. This closes " + "the question without answering it. An ask that lives as an `ask` block in a document is " + 'closed in the document too. A reason beginning "removed from the document in version" is ' + "refused: that marks a retraction the document's own settlement wrote. Use retracted for an ask " + "a newer one replaces or the design made moot, resolved for one whose answer you found; an " + "answered ask cannot be resolved, and resolving records no decision by a person.",
|
|
14547
|
+
description: "Retract an open question that is moot or resolve one after finding the answer. This closes " + "the question without answering it. An ask that lives as an `ask` block in a document is " + 'closed in the document too. A reason beginning "removed from the document in version" is ' + "refused: that marks a retraction the document's own settlement wrote. Use retracted for an ask " + "a newer one replaces or the design made moot, resolved for one whose answer you found; an " + "answered ask cannot be resolved, and resolving records no decision by a person. An issue's " + "close withdrew its open asks (kind withdrawn, which only Dispatch writes, never this tool); " + "an ask on a closed issue is refused (409 ISSUE_CLOSED): reopen the issue first.",
|
|
14542
14548
|
arguments: (z) => ({
|
|
14543
14549
|
ask: z.string().describe("Ask id (uuid) to close; an 8+ hex prefix unique among this session's own open asks " + "works too."),
|
|
14544
14550
|
kind: z.enum(["retracted", "resolved"]).describe("Whether the ask is retracted or self-resolved."),
|
|
@@ -14657,7 +14663,7 @@ var dispatchToolSpecs = [
|
|
|
14657
14663
|
{
|
|
14658
14664
|
name: "dispatch_doc_read",
|
|
14659
14665
|
example: { issue: "DSP-1" },
|
|
14660
|
-
description: "Read a live document or a named document version, or the text of an uploaded file at its latest or named version. " + "Do not use it for issue status, asks, or events; " + "use dispatch read instead. Supply ref, issue, or project plus artifact; issue plus an omitted artifact reads the primary document. " + "A live read returns its document token for an optional dispatch doc-edit precondition. With blocks it lists the live document's blocks instead of its markdown, in document order: each block's id, type, token, anchored comments and asks, and opening words, and under a table each row by the index delete_row takes and each cell by its row and the column delete_column takes; block ids, block tokens and those indexes are what dispatch doc-edit takes. " + "A picture (an uploaded PNG, JPEG, GIF or WebP of at most 3,750,000 bytes, 5 MB once base64-encoded) comes back as an image you see, with its name, type, size and version, every time you ask, including one a read already showed this session; " + "any other file that is not UTF-8 text is described, with the route that serves its bytes. " + OWNER_REFERENCE,
|
|
14666
|
+
description: "Read a live document or a named document version, or the text of an uploaded file at its latest or named version. " + "Do not use it for issue status, asks, or events; " + "use dispatch read instead. Supply ref, issue, or project plus artifact; issue plus an omitted artifact reads the primary document. " + "A live read returns its document token for an optional dispatch doc-edit precondition. With blocks it lists the live document's blocks instead of its markdown, in document order: each block's id, type, token, anchored comments and asks, and opening words, and under a table each row by the index delete_row takes and each cell by its row and the column delete_column takes; block ids, block tokens and those indexes are what dispatch doc-edit takes. " + "After the markdown it prints every open thread anchored on the document in full: an open ask with its id and question and each reply's author, time and text, and a comment thread with its root's id, author, quote and text and each reply's author, time and text, in order; a resolved thread, and an ask that is answered or resolved, is not printed. " + "A picture (an uploaded PNG, JPEG, GIF or WebP of at most 3,750,000 bytes, 5 MB once base64-encoded) comes back as an image you see, with its name, type, size and version, every time you ask, including one a read already showed this session; " + "any other file that is not UTF-8 text is described, with the route that serves its bytes. " + OWNER_REFERENCE,
|
|
14661
14667
|
arguments: (z) => ({
|
|
14662
14668
|
issue: z.string().describe(ISSUE_REFERENCE).optional(),
|
|
14663
14669
|
project: z.string().describe("Project key owning the document.").optional(),
|
|
@@ -15927,8 +15933,8 @@ class DispatchClient {
|
|
|
15927
15933
|
async resolveComment(id, actor) {
|
|
15928
15934
|
return this.#json("POST", ["api", "v1", "comments", id, "resolve"], { actor });
|
|
15929
15935
|
}
|
|
15930
|
-
async getComments(issue
|
|
15931
|
-
return this.#json("GET", ["api", "v1", "issues", await this.#resolveIssue(issue), "comments"]
|
|
15936
|
+
async getComments(issue) {
|
|
15937
|
+
return this.#json("GET", ["api", "v1", "issues", await this.#resolveIssue(issue), "comments"]);
|
|
15932
15938
|
}
|
|
15933
15939
|
async getIssueReferences(issue) {
|
|
15934
15940
|
return this.#json("GET", [
|
|
@@ -18034,22 +18040,64 @@ function messageSummary({ message, replies }, graph) {
|
|
|
18034
18040
|
].join(`
|
|
18035
18041
|
`);
|
|
18036
18042
|
}
|
|
18037
|
-
async function
|
|
18043
|
+
async function openAnchoredThreads(client, resolved) {
|
|
18038
18044
|
const asksPromise = resolved.owner.kind === "project" ? client.getArtifactAsks(resolved.artifact.id) : Promise.resolve(resolved.issue?.open_asks ?? []);
|
|
18039
|
-
const commentsPromise = resolved.owner.kind === "project" ? client.getArtifactComments(resolved.artifact.id) : client.getComments(resolved.issue?.key ?? ""
|
|
18045
|
+
const commentsPromise = resolved.owner.kind === "project" ? client.getArtifactComments(resolved.artifact.id) : client.getComments(resolved.issue?.key ?? "");
|
|
18040
18046
|
const commentsResultPromise = commentsPromise.then((value) => ({ status: "fulfilled", value }), (reason) => ({ status: "rejected", reason }));
|
|
18041
|
-
const asks = await asksPromise;
|
|
18042
|
-
const marks = asks.filter((ask) => ask.state === "open" && ask.anchor?.artifact_id === resolved.artifact.id).map((ask) => `ask ${ask.id}`);
|
|
18047
|
+
const asks = (await asksPromise).filter((ask) => ask.state === "open" && ask.anchor?.artifact_id === resolved.artifact.id);
|
|
18043
18048
|
const commentsResult = await commentsResultPromise;
|
|
18044
|
-
if (commentsResult.status === "rejected") {
|
|
18045
|
-
if (dispatchAnswered(commentsResult.reason, 404))
|
|
18046
|
-
return marks;
|
|
18049
|
+
if (commentsResult.status === "rejected" && !dispatchAnswered(commentsResult.reason, 404)) {
|
|
18047
18050
|
throw commentsResult.reason;
|
|
18048
18051
|
}
|
|
18049
|
-
|
|
18050
|
-
|
|
18051
|
-
|
|
18052
|
-
|
|
18052
|
+
const comments = commentsResult.status === "fulfilled" ? commentsResult.value : [];
|
|
18053
|
+
const askReplies = new Map(asks.map((ask) => [ask.id, []]));
|
|
18054
|
+
for (const comment of comments) {
|
|
18055
|
+
if (comment.ask_id)
|
|
18056
|
+
askReplies.get(comment.ask_id)?.push(comment);
|
|
18057
|
+
}
|
|
18058
|
+
const askThreads = asks.map((ask) => [
|
|
18059
|
+
`ask ${ask.id} \xB7 ${actorText(ask.author)} \xB7 ${ask.created_at}`,
|
|
18060
|
+
...anchorLines(ask),
|
|
18061
|
+
`Question: ${ask.question}`,
|
|
18062
|
+
...threadReplyLines(askReplies.get(ask.id) ?? [])
|
|
18063
|
+
].join(`
|
|
18064
|
+
`));
|
|
18065
|
+
const byId = new Map(comments.map((comment) => [comment.id, comment]));
|
|
18066
|
+
const roots = comments.filter((comment) => comment.reply_to === null && !comment.ask_id && !comment.resolved && comment.anchor?.artifact_id === resolved.artifact.id);
|
|
18067
|
+
const replies = new Map(roots.map((root) => [root.id, []]));
|
|
18068
|
+
for (const comment of comments) {
|
|
18069
|
+
const root = comment.reply_to === null ? undefined : threadRootId(comment, byId);
|
|
18070
|
+
if (root !== undefined)
|
|
18071
|
+
replies.get(root)?.push(comment);
|
|
18072
|
+
}
|
|
18073
|
+
const commentThreads = roots.map((root) => [
|
|
18074
|
+
`comment ${root.id} \xB7 ${actorText(root.author)} \xB7 ${root.created_at}`,
|
|
18075
|
+
...anchorLines(root),
|
|
18076
|
+
`Body: ${root.body}`,
|
|
18077
|
+
...threadReplyLines(replies.get(root.id) ?? [])
|
|
18078
|
+
].join(`
|
|
18079
|
+
`));
|
|
18080
|
+
return [...askThreads, ...commentThreads];
|
|
18081
|
+
}
|
|
18082
|
+
function threadReplyLines(replies) {
|
|
18083
|
+
return replies.flatMap((reply) => [
|
|
18084
|
+
`reply ${reply.id} \xB7 ${actorText(reply.author)} \xB7 ${reply.created_at}`,
|
|
18085
|
+
`Body: ${reply.body}`
|
|
18086
|
+
]);
|
|
18087
|
+
}
|
|
18088
|
+
function threadRootId(comment, byId) {
|
|
18089
|
+
const seen = new Set;
|
|
18090
|
+
let current = comment;
|
|
18091
|
+
while (current.reply_to !== null) {
|
|
18092
|
+
if (seen.has(current.id))
|
|
18093
|
+
return;
|
|
18094
|
+
seen.add(current.id);
|
|
18095
|
+
const parent = byId.get(current.reply_to);
|
|
18096
|
+
if (parent === undefined)
|
|
18097
|
+
return;
|
|
18098
|
+
current = parent;
|
|
18099
|
+
}
|
|
18100
|
+
return current.id;
|
|
18053
18101
|
}
|
|
18054
18102
|
async function blockAsks(client, resolved, state) {
|
|
18055
18103
|
const asks = await (resolved.issue === undefined ? client.getArtifactAsks(resolved.artifact.id, state) : client.listIssueAsks(resolved.issue.key, state));
|
|
@@ -18574,7 +18622,8 @@ async function executeDispatchTool(input) {
|
|
|
18574
18622
|
throw new Error(`Dispatch claimed ${issueKey} but answered with no claim`);
|
|
18575
18623
|
}
|
|
18576
18624
|
const claimed = `${issueKey}: claimed by you since ${held.at}. Its status is ${after.status}`;
|
|
18577
|
-
|
|
18625
|
+
const reaches = `A person's answers, comments and messages on ${issueKey} now reach you directly.`;
|
|
18626
|
+
text = legionManaged ? `${claimed}; ${LEGION_OWNS_STATUS} Release the claim when you stop. ${reaches}` : `${claimed}; a claim moves nothing, so move it to in_progress with dispatch issue-update when you start, and release the claim when you stop. ${reaches}`;
|
|
18578
18627
|
}
|
|
18579
18628
|
return {
|
|
18580
18629
|
text: `${text}${await notSubscribed(issueTopic(issueKey), input.subscriptions)}`,
|
|
@@ -19013,24 +19062,27 @@ Document token: ${listed.token}`,
|
|
|
19013
19062
|
} : { issue: resolved.issue?.key }, version, resolved.owner.kind === "project" ? resolved.owner.project : resolved.owner.issue, shown);
|
|
19014
19063
|
}
|
|
19015
19064
|
const documentPromise = client.docRead(resolved.artifact.id, version);
|
|
19016
|
-
const
|
|
19017
|
-
const
|
|
19065
|
+
const threadsPromise = openAnchoredThreads(client, resolved);
|
|
19066
|
+
const threadsResultPromise = threadsPromise.then((value) => ({ status: "fulfilled", value }), (reason) => ({ status: "rejected", reason }));
|
|
19018
19067
|
const document = await documentPromise;
|
|
19019
|
-
const
|
|
19020
|
-
if (
|
|
19021
|
-
throw
|
|
19022
|
-
const
|
|
19068
|
+
const threadsResult = await threadsResultPromise;
|
|
19069
|
+
if (threadsResult.status === "rejected")
|
|
19070
|
+
throw threadsResult.reason;
|
|
19071
|
+
const threads = threadsResult.value;
|
|
19023
19072
|
const approval = approvalLine(resolved.artifact);
|
|
19024
19073
|
const trailer = [
|
|
19025
19074
|
..."token" in document && document.token !== undefined ? [`Document token: ${document.token}`] : [],
|
|
19026
|
-
...marks.length === 0 ? [] : [`Open anchored asks/comments: ${marks.join(", ")}`],
|
|
19027
19075
|
...approval === undefined ? [] : [approval]
|
|
19028
19076
|
];
|
|
19029
19077
|
return {
|
|
19030
|
-
text:
|
|
19078
|
+
text: [
|
|
19079
|
+
document.markdown,
|
|
19080
|
+
...trailer.length === 0 ? [] : [trailer.join(`
|
|
19081
|
+
`)],
|
|
19082
|
+
...threads.length === 0 ? [] : ["Open anchored threads:", ...threads]
|
|
19083
|
+
].join(`
|
|
19031
19084
|
|
|
19032
|
-
|
|
19033
|
-
`)}`,
|
|
19085
|
+
`),
|
|
19034
19086
|
details: resolved.owner.kind === "project" ? {
|
|
19035
19087
|
project: resolved.artifact.project,
|
|
19036
19088
|
document: `${resolved.artifact.project}/${resolved.artifact.slug}`
|
|
@@ -20184,7 +20236,9 @@ var SessionWireSchema = object({
|
|
|
20184
20236
|
roles: array(string2()).optional(),
|
|
20185
20237
|
capabilities: array(string2()).optional(),
|
|
20186
20238
|
self_subscribed: boolean2(),
|
|
20187
|
-
last_seen: number2().int().optional()
|
|
20239
|
+
last_seen: number2().int().optional(),
|
|
20240
|
+
plugin: string2().optional(),
|
|
20241
|
+
plugin_version: string2().optional()
|
|
20188
20242
|
});
|
|
20189
20243
|
var RoleWireSchema = object({
|
|
20190
20244
|
role: string2(),
|
|
@@ -20320,7 +20374,9 @@ function createEnvoyClient(config) {
|
|
|
20320
20374
|
...input.capabilities === undefined ? {} : { capabilities: input.capabilities },
|
|
20321
20375
|
driving: input.driving,
|
|
20322
20376
|
...input.selfSubscribed === undefined ? {} : { self_subscribed: input.selfSubscribed },
|
|
20323
|
-
host: machineID()
|
|
20377
|
+
host: machineID(),
|
|
20378
|
+
plugin: input.plugin.name,
|
|
20379
|
+
plugin_version: input.plugin.version
|
|
20324
20380
|
}))),
|
|
20325
20381
|
unsubscribe: async (input) => {
|
|
20326
20382
|
await post("/v1/interests/unsubscribe", {
|
package/dist/src/server.js
CHANGED
|
@@ -13594,7 +13594,10 @@ var IssueEventPayloadSchema = object({
|
|
|
13594
13594
|
status: string2().optional(),
|
|
13595
13595
|
priority: number2().int().min(0).max(3).nullable().optional(),
|
|
13596
13596
|
rank: string2().optional(),
|
|
13597
|
-
route: string2().nullish()
|
|
13597
|
+
route: string2().nullish(),
|
|
13598
|
+
changed: array(string2()).optional(),
|
|
13599
|
+
ancestors: array(string2()).optional(),
|
|
13600
|
+
thread_logins: array(string2()).optional()
|
|
13598
13601
|
});
|
|
13599
13602
|
var ArtifactCreatedEventPayloadSchema = object({
|
|
13600
13603
|
artifact: object({ id: string2().optional(), slug: string2().optional(), name: string2().optional() }).optional()
|
|
@@ -13648,11 +13651,12 @@ var askEventPayloadFields = {
|
|
|
13648
13651
|
primary: boolean2().optional()
|
|
13649
13652
|
}).optional(),
|
|
13650
13653
|
resolution: object({
|
|
13651
|
-
kind: _enum2(["retracted", "resolved"]).optional(),
|
|
13654
|
+
kind: _enum2(["retracted", "resolved", "withdrawn"]).optional(),
|
|
13652
13655
|
reason: string2().optional(),
|
|
13653
13656
|
actor: object({ kind: string2().optional(), id: string2().optional() }).passthrough().optional(),
|
|
13654
13657
|
at: string2().optional()
|
|
13655
|
-
}).nullish()
|
|
13658
|
+
}).nullish(),
|
|
13659
|
+
thread_logins: array(string2()).optional()
|
|
13656
13660
|
};
|
|
13657
13661
|
var AskEventPayloadSchema = intersection(object(askEventPayloadFields), object({
|
|
13658
13662
|
previous: never().optional(),
|
|
@@ -13714,7 +13718,8 @@ var CommentEventPayloadSchema = object({
|
|
|
13714
13718
|
author: object({ kind: string2(), id: string2() }).optional(),
|
|
13715
13719
|
created_at: string2().optional(),
|
|
13716
13720
|
mentions: array(CommentMentionSchema).optional(),
|
|
13717
|
-
deliveries: array(CommentDeliverySchema).optional()
|
|
13721
|
+
deliveries: array(CommentDeliverySchema).optional(),
|
|
13722
|
+
thread_logins: array(string2()).optional()
|
|
13718
13723
|
}).passthrough();
|
|
13719
13724
|
var MessageEventPayloadSchema = object({
|
|
13720
13725
|
id: string2().optional(),
|
|
@@ -13723,7 +13728,8 @@ var MessageEventPayloadSchema = object({
|
|
|
13723
13728
|
in_reply_to: string2().nullish(),
|
|
13724
13729
|
reply_body: string2().optional(),
|
|
13725
13730
|
thread_target: string2().optional(),
|
|
13726
|
-
author: object({ kind: string2(), id: string2() }).optional()
|
|
13731
|
+
author: object({ kind: string2(), id: string2() }).optional(),
|
|
13732
|
+
thread_logins: array(string2()).optional()
|
|
13727
13733
|
});
|
|
13728
13734
|
var DispatchTargetedResourceIDSchema = uuid2();
|
|
13729
13735
|
var DispatchTargetedMessagePayloadSchema = MessageEventPayloadSchema.extend({
|
|
@@ -13953,7 +13959,7 @@ var dispatchToolSpecs = [
|
|
|
13953
13959
|
priority: 1,
|
|
13954
13960
|
before: "DSP-2"
|
|
13955
13961
|
},
|
|
13956
|
-
description: "Update an existing issue: move its lifecycle status, retitle it, replace its labels, set " + "its priority, link a URL (the pull request that delivers it, a run, a document), set its " + "route, set or clear its parent, replace the issues it is blocked by, attach it to " + "architecture components, set the repository it targets, or move it on its project's " + "board. Status is one of " + `${ISSUE_STATUSES.join(", ")}; outside Legion, move it yourself as the work advances; inside ` + "Legion the daemon moves it. A non-empty external_links is " + "merged into the issue's existing links by URL, so linking the pull request you just opened " + "keeps every earlier link; an empty external_links removes every link instead. components " + "replaces the issue's own attachment. Closing an issue " + "(status done) requires reason, the note that says why: it is posted on the issue as a " + "message, then the issue closes, because a closed issue refuses messages, comments, and " + "artifacts; reason goes only with status done. A closed issue takes only a board move " + "(before, after), components, " + "a reopening status (any status but done), and an external_links write that adds no new " + "link (so a link can move to the issue that replaced it); everything else, priority " + "included, waits for the reopen. " + "priority is yours to set and a human overrides it. before and after move the issue on " + "its project's board: before puts it above that issue, after below, and both together " + "land it between them. A project's board order is one agent's to keep; in a Legion " + "deployment that is the controller. Never reassign an issue a person holds to get a " + "faster answer: who holds it is their call. At least one " + `field besides issue is required. ${ISSUE_REFERENCE}`,
|
|
13962
|
+
description: "Update an existing issue: move its lifecycle status, retitle it, replace its labels, set " + "its priority, link a URL (the pull request that delivers it, a run, a document), set its " + "route, set or clear its parent, replace the issues it is blocked by, attach it to " + "architecture components, set the repository it targets, or move it on its project's " + "board. Status is one of " + `${ISSUE_STATUSES.join(", ")}; outside Legion, move it yourself as the work advances; inside ` + "Legion the daemon moves it. A non-empty external_links is " + "merged into the issue's existing links by URL, so linking the pull request you just opened " + "keeps every earlier link; an empty external_links removes every link instead. components " + "replaces the issue's own attachment. Closing an issue " + "(status done) requires reason, the note that says why: it is posted on the issue as a " + "message, then the issue closes, because a closed issue refuses messages, comments, and " + "artifacts; reason goes only with status done. Closing an issue withdraws every open ask " + "it holds (resolution kind withdrawn, naming the closer), and reopening it brings exactly " + "those asks back. A closed issue takes only a board move " + "(before, after), components, " + "a reopening status (any status but done), and an external_links write that adds no new " + "link (so a link can move to the issue that replaced it); everything else, priority " + "included, waits for the reopen. " + "priority is yours to set and a human overrides it. before and after move the issue on " + "its project's board: before puts it above that issue, after below, and both together " + "land it between them. A project's board order is one agent's to keep; in a Legion " + "deployment that is the controller. Never reassign an issue a person holds to get a " + "faster answer: who holds it is their call. At least one " + `field besides issue is required. ${ISSUE_REFERENCE}`,
|
|
13957
13963
|
arguments: (z) => ({
|
|
13958
13964
|
issue: z.string().describe(ISSUE_REFERENCE),
|
|
13959
13965
|
status: z.enum(ISSUE_STATUSES).describe("New lifecycle status.").optional(),
|
|
@@ -13980,7 +13986,7 @@ var dispatchToolSpecs = [
|
|
|
13980
13986
|
{
|
|
13981
13987
|
name: "dispatch_claim",
|
|
13982
13988
|
example: { issue: "DSP-1" },
|
|
13983
|
-
description: "Claim a Dispatch issue before you start implementing it, so no other session takes the same work, " + "and release it when you stop. Pass the issue alone to claim it, or --release to give it up. " + "Your claim records your own session and shows on every read of the issue: the dashboard header, the " + "issue list and board, dispatch read, and dispatch issues. Claiming is refused with 409 ISSUE_CLAIMED " + "when another session holds the issue and is still running; the refusal names that session, so talk to " + "it instead of working the same issue in parallel. When a human holds the claim the refusal names the " + "person, not a session: there is nothing running to message, so ask them on the issue rather than " + "taking it. 409 CLAIM_CONTENDED means the issue changed " + "hands twice while your call ran, so nothing was applied and nobody's liveness was checked: read " + "the issue and decide again. A claim whose session is no longer running may be " + "taken: the takeover is recorded on the issue and the session that lost it is told. A claim is not the " + "issue's status \u2014 claiming moves nothing, and the result names who moves the status: you, with " + "dispatch issue-update, or Legion's daemon on an issue it runs, where a status written by anyone " + "else is undone. A claim is released by its holder or any human, and by " + "any agent once the holder's session is no longer running. " + ISSUE_REFERENCE,
|
|
13989
|
+
description: "Claim a Dispatch issue before you start implementing it, so no other session takes the same work, " + "and release it when you stop. Pass the issue alone to claim it, or --release to give it up. " + "Your claim records your own session and shows on every read of the issue: the dashboard header, the " + "issue list and board, dispatch read, and dispatch issues. While you hold it, a person's answers, " + "comments and messages on the issue reach your session directly (a new comment that mentions you " + "arrives once, as the mention, when the mention reached you; when the mention's send fails, the " + "comment still reaches you directly; a mention whose receipt timed out can arrive twice, and the " + "second copy is the same comment); an agent's events and every other change on the issue still " + "need a subscription to it, except in an ask you follow: anyone else's answer, hand-back, " + "resolution or edit of it (other than a quiet edit), and every reply there (new, edited, resolved " + "or reopened), reach you " + "as its follower. Claiming is refused with 409 ISSUE_CLAIMED " + "when another session holds the issue and is still running; the refusal names that session, so talk to " + "it instead of working the same issue in parallel. When a human holds the claim the refusal names the " + "person, not a session: there is nothing running to message, so ask them on the issue rather than " + "taking it. 409 CLAIM_CONTENDED means the issue changed " + "hands twice while your call ran, so nothing was applied and nobody's liveness was checked: read " + "the issue and decide again. A claim whose session is no longer running may be " + "taken: the takeover is recorded on the issue and the session that lost it is told. A claim is not the " + "issue's status \u2014 claiming moves nothing, and the result names who moves the status: you, with " + "dispatch issue-update, or Legion's daemon on an issue it runs, where a status written by anyone " + "else is undone. A claim is released by its holder or any human, and by " + "any agent once the holder's session is no longer running. " + ISSUE_REFERENCE,
|
|
13984
13990
|
arguments: (z) => ({
|
|
13985
13991
|
issue: z.string().describe(ISSUE_REFERENCE),
|
|
13986
13992
|
release: z.boolean().describe("Give up your claim instead of taking it; the issue's status does not change.").optional()
|
|
@@ -14067,7 +14073,7 @@ var dispatchToolSpecs = [
|
|
|
14067
14073
|
kind: "retracted",
|
|
14068
14074
|
reason: "A newer question supersedes this one."
|
|
14069
14075
|
},
|
|
14070
|
-
description: "Retract an open question that is moot or resolve one after finding the answer. This closes " + "the question without answering it. An ask that lives as an `ask` block in a document is " + 'closed in the document too. A reason beginning "removed from the document in version" is ' + "refused: that marks a retraction the document's own settlement wrote. Use retracted for an ask " + "a newer one replaces or the design made moot, resolved for one whose answer you found; an " + "answered ask cannot be resolved, and resolving records no decision by a person.",
|
|
14076
|
+
description: "Retract an open question that is moot or resolve one after finding the answer. This closes " + "the question without answering it. An ask that lives as an `ask` block in a document is " + 'closed in the document too. A reason beginning "removed from the document in version" is ' + "refused: that marks a retraction the document's own settlement wrote. Use retracted for an ask " + "a newer one replaces or the design made moot, resolved for one whose answer you found; an " + "answered ask cannot be resolved, and resolving records no decision by a person. An issue's " + "close withdrew its open asks (kind withdrawn, which only Dispatch writes, never this tool); " + "an ask on a closed issue is refused (409 ISSUE_CLOSED): reopen the issue first.",
|
|
14071
14077
|
arguments: (z) => ({
|
|
14072
14078
|
ask: z.string().describe("Ask id (uuid) to close; an 8+ hex prefix unique among this session's own open asks " + "works too."),
|
|
14073
14079
|
kind: z.enum(["retracted", "resolved"]).describe("Whether the ask is retracted or self-resolved."),
|
|
@@ -14186,7 +14192,7 @@ var dispatchToolSpecs = [
|
|
|
14186
14192
|
{
|
|
14187
14193
|
name: "dispatch_doc_read",
|
|
14188
14194
|
example: { issue: "DSP-1" },
|
|
14189
|
-
description: "Read a live document or a named document version, or the text of an uploaded file at its latest or named version. " + "Do not use it for issue status, asks, or events; " + "use dispatch read instead. Supply ref, issue, or project plus artifact; issue plus an omitted artifact reads the primary document. " + "A live read returns its document token for an optional dispatch doc-edit precondition. With blocks it lists the live document's blocks instead of its markdown, in document order: each block's id, type, token, anchored comments and asks, and opening words, and under a table each row by the index delete_row takes and each cell by its row and the column delete_column takes; block ids, block tokens and those indexes are what dispatch doc-edit takes. " + "A picture (an uploaded PNG, JPEG, GIF or WebP of at most 3,750,000 bytes, 5 MB once base64-encoded) comes back as an image you see, with its name, type, size and version, every time you ask, including one a read already showed this session; " + "any other file that is not UTF-8 text is described, with the route that serves its bytes. " + OWNER_REFERENCE,
|
|
14195
|
+
description: "Read a live document or a named document version, or the text of an uploaded file at its latest or named version. " + "Do not use it for issue status, asks, or events; " + "use dispatch read instead. Supply ref, issue, or project plus artifact; issue plus an omitted artifact reads the primary document. " + "A live read returns its document token for an optional dispatch doc-edit precondition. With blocks it lists the live document's blocks instead of its markdown, in document order: each block's id, type, token, anchored comments and asks, and opening words, and under a table each row by the index delete_row takes and each cell by its row and the column delete_column takes; block ids, block tokens and those indexes are what dispatch doc-edit takes. " + "After the markdown it prints every open thread anchored on the document in full: an open ask with its id and question and each reply's author, time and text, and a comment thread with its root's id, author, quote and text and each reply's author, time and text, in order; a resolved thread, and an ask that is answered or resolved, is not printed. " + "A picture (an uploaded PNG, JPEG, GIF or WebP of at most 3,750,000 bytes, 5 MB once base64-encoded) comes back as an image you see, with its name, type, size and version, every time you ask, including one a read already showed this session; " + "any other file that is not UTF-8 text is described, with the route that serves its bytes. " + OWNER_REFERENCE,
|
|
14190
14196
|
arguments: (z) => ({
|
|
14191
14197
|
issue: z.string().describe(ISSUE_REFERENCE).optional(),
|
|
14192
14198
|
project: z.string().describe("Project key owning the document.").optional(),
|
|
@@ -15336,7 +15342,9 @@ var SessionWireSchema = object({
|
|
|
15336
15342
|
roles: array(string2()).optional(),
|
|
15337
15343
|
capabilities: array(string2()).optional(),
|
|
15338
15344
|
self_subscribed: boolean2(),
|
|
15339
|
-
last_seen: number2().int().optional()
|
|
15345
|
+
last_seen: number2().int().optional(),
|
|
15346
|
+
plugin: string2().optional(),
|
|
15347
|
+
plugin_version: string2().optional()
|
|
15340
15348
|
});
|
|
15341
15349
|
var RoleWireSchema = object({
|
|
15342
15350
|
role: string2(),
|
|
@@ -15472,7 +15480,9 @@ function createEnvoyClient(config) {
|
|
|
15472
15480
|
...input.capabilities === undefined ? {} : { capabilities: input.capabilities },
|
|
15473
15481
|
driving: input.driving,
|
|
15474
15482
|
...input.selfSubscribed === undefined ? {} : { self_subscribed: input.selfSubscribed },
|
|
15475
|
-
host: machineID()
|
|
15483
|
+
host: machineID(),
|
|
15484
|
+
plugin: input.plugin.name,
|
|
15485
|
+
plugin_version: input.plugin.version
|
|
15476
15486
|
}))),
|
|
15477
15487
|
unsubscribe: async (input) => {
|
|
15478
15488
|
await post("/v1/interests/unsubscribe", {
|
|
@@ -15575,6 +15585,8 @@ function sourceSession(sessionID) {
|
|
|
15575
15585
|
|
|
15576
15586
|
// src/server.ts
|
|
15577
15587
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
15588
|
+
// package.json
|
|
15589
|
+
var version2 = "9.8.0";
|
|
15578
15590
|
|
|
15579
15591
|
// src/log.ts
|
|
15580
15592
|
import { appendFile, mkdir } from "fs/promises";
|
|
@@ -15665,6 +15677,7 @@ var skillsDirectory = [
|
|
|
15665
15677
|
path3.resolve(moduleDirectory, "../../../skills")
|
|
15666
15678
|
].find((dir) => existsSync2(dir));
|
|
15667
15679
|
var PACKAGE_NAME = "@sjawhar/opencode-legion-envoy";
|
|
15680
|
+
var PLUGIN = { name: PACKAGE_NAME, version: version2 };
|
|
15668
15681
|
function packageRoot(from) {
|
|
15669
15682
|
for (let directory = from;; directory = path3.dirname(directory)) {
|
|
15670
15683
|
if (manifestName(path3.join(directory, "package.json")) === PACKAGE_NAME)
|
|
@@ -15710,7 +15723,8 @@ function envoyTools(client, port, title) {
|
|
|
15710
15723
|
topics: args.topics,
|
|
15711
15724
|
port: port() ?? 0,
|
|
15712
15725
|
title: title() ?? "",
|
|
15713
|
-
driving: true
|
|
15726
|
+
driving: true,
|
|
15727
|
+
plugin: PLUGIN
|
|
15714
15728
|
}));
|
|
15715
15729
|
}
|
|
15716
15730
|
}),
|
|
@@ -15873,7 +15887,8 @@ var server_default = async (input) => {
|
|
|
15873
15887
|
topics: [agentSubject(sessionID)],
|
|
15874
15888
|
port,
|
|
15875
15889
|
title: title ?? "",
|
|
15876
|
-
driving
|
|
15890
|
+
driving,
|
|
15891
|
+
plugin: PLUGIN
|
|
15877
15892
|
}).catch(() => {});
|
|
15878
15893
|
let stopEnvoy = () => {};
|
|
15879
15894
|
if (envoyOn !== undefined) {
|
package/package.json
CHANGED
package/skills/AGENTS.md
CHANGED
|
@@ -22,7 +22,7 @@ They are Markdown instructions loaded by Oh My Pi sessions; the daemon and the t
|
|
|
22
22
|
| `thermonuclear-deep-review/` | the `thermonuclear-deep-review` agent, and the reviewer (the Security Guidelines) | the correctness rubric of the reviewer's pair, with its tagged, diff-triggered Security Guidelines and the attack on the PR body's claims (adapted from the MIT-licensed Thermos plugin in `cursor/plugins`; `LICENSE` beside it) |
|
|
23
23
|
|
|
24
24
|
The owning skill above is where each contract is defined; a role prompt that needs a contract from its own seat points there or restates only its own step. This file lists and does not restate.
|
|
25
|
-
The skills here ship in two Oh My Pi plugins, and the partition is kept in one place, `scripts/pi-plugin-prepack.sh` at the repository root, keyed by package name: `@sjawhar/pi-envoy` (`packages/pi-envoy`) ships `asd-ste100`, `dispatch`, `dispatch-first`, `dispatch-writing-specs` and `envoy`, the skills every session with Dispatch reads; `@sjawhar/pi-legion` (`packages/pi-legion`) ships `legion-architect`, `legion-controller`, `legion-oracle`, `legion-retro`, `legion-worker`, `leading-a-work-stream`, `ce-simplify-code`, `thermonuclear-code-quality` and `thermonuclear-deep-review`, the skills a Legion pane's prompts load. Each plugin's prepack stages its list into its `dist/skills`, and `packages/pi-shared/test/skills-partition.test.ts` holds the two lists to this directory and to the plugin's skill lists in `.github/workflows/release.yaml` (
|
|
25
|
+
The skills here ship in two Oh My Pi plugins, and the partition is kept in one place, `scripts/pi-plugin-prepack.sh` at the repository root, keyed by package name: `@sjawhar/pi-envoy` (`packages/pi-envoy`) ships `asd-ste100`, `dispatch`, `dispatch-first`, `dispatch-writing-specs` and `envoy`, the skills every session with Dispatch reads; `@sjawhar/pi-legion` (`packages/pi-legion`) ships `legion-architect`, `legion-controller`, `legion-oracle`, `legion-retro`, `legion-worker`, `leading-a-work-stream`, `ce-simplify-code`, `thermonuclear-code-quality` and `thermonuclear-deep-review`, the skills a Legion pane's prompts load. Each plugin's prepack stages its list into its `dist/skills`, and `packages/pi-shared/test/skills-partition.test.ts` holds the two lists to this directory and to the plugin's skill lists in `.github/workflows/release.yaml` (the `--patch-path` and the positional `skills/<name>/` entries of its version arguments in the workflow's `env:`, `PI_ENVOY_VERSION_ARGS` and `PI_LEGION_VERSION_ARGS`): together they are every skill directory here, and none is in both. A link from a skill in one partition into a skill in the other (a Legion skill's link into `dispatch`) is written `skill://<name>/<path>`, never as a relative path: a staged partition is installed on its own, so a relative link out of it dangles, while a `skill://` link resolves by name once both plugins are installed.
|
|
26
26
|
A Legion prompt (a skill here, a role prompt, or an agent definition in `packages/pi-legion/agents/`) names a task agent only as `task(agent="<name>")` and a skill it tells the model to load only as `skill://<name>`. Those are the two forms the Go daemon's boot gate and `legion probe-image` resolve through Oh My Pi, refusing by name one it cannot find; a dispatch or a load written any other way goes unchecked. An agent or skill Legion's prompts name is shipped here or in `packages/pi-legion/agents/`, unless Oh My Pi bundles it.
|
|
27
27
|
A skill file an agent loads stays under 51,200 bytes, Oh My Pi's spill threshold (a longer one arrives with its middle cut out), and a skill's detail lives in `references/` files linked as `skill://<name>/references/<file>.md`, never by relative path, which a read by path cuts at 300 lines. The rules are `packages/pi-shared/test/skills-guard.ts`, run over each plugin's staged partition: `packages/pi-envoy/src/skills-guard.test.ts` over the Envoy plugin's five skills, with the `dispatch-first` budget check and the word budget of the three Dispatch skills (`dispatch`, `dispatch-first` and `dispatch-writing-specs` together under 2,000 words, every file and frontmatter counted), and `packages/pi-legion/src/skills-guard.test.ts` over the Legion plugin's nine, with the daemon's prompts as linking roots and every `legion-worker` reference linked from somewhere. Each fails on a file at the threshold, on any `SKILL.md` body of 500 lines or more, on a skill whose frontmatter `name` is not its directory's name, on a `skill://<name>/<path>` link that names a missing file or a missing `#heading` (resolved against this directory, where both partitions live), and on a relative link that leaves the staged partition or names no file. The Envoy guard also fails on a `task(agent="<name>")` in the Envoy plugin's skills that names an agent neither that plugin nor Oh My Pi bundles (the bundled list read off the pinned binary's `omp agents unpack` under `LEGION_TEST_OMP`, never written down), since those skills reach a session with no other plugin installed; a `skill://legion-*` mention there is a sentence conditioned on a Legion role, inert for a person and right for a pane with both plugins, which the guard leaves alone.
|
|
28
28
|
The text a worker boots with (its role prompt) lives in `packages/daemon/internal/prompts/roles/`,
|
package/skills/dispatch/SKILL.md
CHANGED
|
@@ -43,9 +43,9 @@ Which machines, files or people, the numbers, then "A happens to X, or B to Y?"
|
|
|
43
43
|
|
|
44
44
|
## Issue status is yours to move
|
|
45
45
|
|
|
46
|
-
Outside Legion, claim your issue (`dispatch claim`) and move it and your children: `in_progress` at implementation, `testing` on a production-like surface, `needs_review` while its PR waits, `done` with `--reason` once driven in production. An issue with the `legion` label, or under one: never write its status, and claim it only as a Legion role of its tree, even when its claim reads `· not running`, as a suspended architect's does: taking it
|
|
46
|
+
Outside Legion, claim your issue (`dispatch claim`) and move it and your children: `in_progress` at implementation, `testing` on a production-like surface, `needs_review` while its PR waits, `done` with `--reason` once driven in production. An issue with the `legion` label, or under one: never write its status or route it, and claim it only as a Legion role of its tree, even when its claim reads `· not running`, as a suspended architect's does: taking it removes the tree from the workflow.
|
|
47
47
|
|
|
48
|
-
A claim
|
|
48
|
+
A claim, or `dispatch issue-update --route session:<your id>` (`--route ''` on release), brings a person's answers, comments and messages; for agents' activity, `envoy_subscribe notifications.dispatch.issue.<KEY>.>`.
|
|
49
49
|
|
|
50
50
|
## Close what you opened
|
|
51
51
|
|
|
@@ -30,11 +30,10 @@ Another agent can build and check the work from the spec alone. A small change g
|
|
|
30
30
|
For each spec, ask, comment and message:
|
|
31
31
|
|
|
32
32
|
- Write in `skill://asd-ste100`'s strict mode.
|
|
33
|
-
-
|
|
34
|
-
- Link each source where the person wrote it
|
|
33
|
+
- Write about the current design, never the person or how people reached it. Never quote people or upload their words.
|
|
34
|
+
- Link each source where the person wrote it. With no link, state the design.
|
|
35
35
|
- In a spec, cite code by file and symbol, never a line number: it goes stale at the next edit.
|
|
36
36
|
- A sentence that cites a gate passing (a test, review, check or proof) also says, in that sentence, what the pass does not show: "The tests pass; they catch a wrong verdict, not a proof of a contradiction."
|
|
37
|
-
- Write about the design, never the person. Never quote people or upload their words.
|
|
38
37
|
- Name your session only if a reader must reach it.
|
|
39
38
|
- Keep only text that helps a reader understand, decide, build, check.
|
|
40
39
|
- Explain each name or term a new reader may not know. Say in a clause what each issue you name does for this reader.
|
|
@@ -60,11 +59,11 @@ For each spec, ask, comment and message:
|
|
|
60
59
|
- Each design choice no source states is a `###` entry: choice, reason, options weighed, parts shaped.
|
|
61
60
|
- An unsure entry adds an `Unsure:` paragraph: what would change your mind.
|
|
62
61
|
- A replaced entry moves under a final `### Replaced`.
|
|
63
|
-
-
|
|
62
|
+
- Link a person's answered ask block from each part it shapes.
|
|
64
63
|
|
|
65
64
|
## Old specs
|
|
66
65
|
|
|
67
|
-
At your next edit, rewrite a spec outside the outline whole, in one version. Move its ask blocks with `doc-edit`'s `move`.
|
|
66
|
+
At your next edit, rewrite a spec outside the outline whole, in one version. Move its ask blocks with `doc-edit`'s `move`. Kept text follows the text rules: restate each retelling, quotation or unlinked attribution as design, or cut it.
|
|
68
67
|
|
|
69
68
|
## Approval
|
|
70
69
|
|
|
@@ -73,4 +72,4 @@ At your next edit, rewrite a spec outside the outline whole, in one version. Mov
|
|
|
73
72
|
|
|
74
73
|
## The plan
|
|
75
74
|
|
|
76
|
-
Write the
|
|
75
|
+
Write the plan with `writing-plans` as the issue's `plan.md`, not a repository file.
|
|
@@ -12,7 +12,7 @@ except a clear defect a role reports ([Issues handed to Legion](#issues-handed-t
|
|
|
12
12
|
|
|
13
13
|
## Rules
|
|
14
14
|
|
|
15
|
-
- Handle no wake until the extension's startup
|
|
15
|
+
- Handle no wake until the extension has registered this session with the daemon as the project's controller, at session startup or, in a controller an operator started, from its first command once Oh My Pi has started, because without that registration the daemon does not know you as its controller.
|
|
16
16
|
- Answer a person's direct message first, in the conversation. A plain user turn that is neither the start message nor a task sent with `legion claims deliver` is a person writing from Dispatch's Agents page.
|
|
17
17
|
- A direct Dispatch message that decides scope, priority or an owner is a ruling: after you answer it, run the weekly-plan reconciliation (`skill://legion-controller/references/backlog-stewardship.md`), because the plan must record the rule.
|
|
18
18
|
- Handle one wake per turn, then end the turn. Never poll or wait for an event, because the daemon wakes you again (`tick` at least every `controller_wake_interval_seconds`).
|
|
@@ -30,9 +30,9 @@ unassigned.
|
|
|
30
30
|
|
|
31
31
|
Each time you read it:
|
|
32
32
|
|
|
33
|
-
- `Status: done` means someone closed it
|
|
34
|
-
|
|
35
|
-
in the day's report.
|
|
33
|
+
- `Status: done` means someone closed it: the close withdrew its open asks (each recorded
|
|
34
|
+
`withdrawn`; a reopen brings them back), and Dispatch refuses an ask or a message there
|
|
35
|
+
(`409 ISSUE_CLOSED`). Reopen it with `set_status` to `icebox` and say so in the day's report.
|
|
36
36
|
- `Assignee: unassigned` with a person known: assign it, naming your session (`dispatch whoami`):
|
|
37
37
|
`curl -sS -X PATCH -H "Authorization: Bearer $(cat "$DISPATCH_TOKEN_FILE")" -H 'Content-Type: application/json' -d '{"assignee": "<email>", "actor": {"kind": "session", "id": "<session id>"}}' "$DISPATCH_URL/api/v1/issues/<REPORT KEY>"`.
|
|
38
38
|
An answer that does not print the issue with its new `assignee` left the assignee as it was
|
|
@@ -83,8 +83,8 @@ environment, then run:
|
|
|
83
83
|
It checks `LEGION_PROJECT` against the daemon, registers the session, claims the Envoy role, and
|
|
84
84
|
moves the daemon's recorded controller session id to this session; the `legion` tool's controller
|
|
85
85
|
operations then work there. Never pass the secret as a command argument or copy it into a
|
|
86
|
-
transcript, because others read transcripts. Do not set `LEGION_CONTROLLER=1
|
|
87
|
-
|
|
86
|
+
transcript, because others read transcripts. Do not set `LEGION_CONTROLLER=1`: that marker makes
|
|
87
|
+
the session the launched controller, which needs a start message. Run `/legion-claim-controller`
|
|
88
88
|
once per session; do not run it again after an Envoy listener restart, because the heartbeat
|
|
89
89
|
re-claims the role.
|
|
90
90
|
|
|
@@ -118,9 +118,9 @@ An ask goes on the controller's report issue whenever it is open: the issue the
|
|
|
118
118
|
instructions name for Legion's reports, else the project's `Legion daily report`, which
|
|
119
119
|
`dispatch search --query '"Legion daily report"' --project <PROJECT>` finds. It stays open in
|
|
120
120
|
`icebox`, so the ask stays answerable whatever happens to the issue it came from; once an issue
|
|
121
|
-
closes,
|
|
122
|
-
(`409 ISSUE_CLOSED`). The question names the issue the finding came from by its
|
|
123
|
-
reference.
|
|
121
|
+
closes, its close withdraws its open asks (a reopen brings them back), and Dispatch refuses their
|
|
122
|
+
answers (`409 ISSUE_CLOSED`). The question names the issue the finding came from by its
|
|
123
|
+
`dispatch://` reference.
|
|
124
124
|
|
|
125
125
|
When the project has no report issue yet, or a person closed it (`dispatch read` shows `Status:
|
|
126
126
|
done`, and Dispatch refuses the ask with `409 ISSUE_CLOSED`), do not create or reopen one yourself
|
|
@@ -289,10 +289,11 @@ reviewing until you finish.
|
|
|
289
289
|
repository's deploy workflow (`projects.<KEY>.deploys`), at each completed run of that workflow
|
|
290
290
|
on the default branch whose named step (such as `Apply`) succeeded after the merge, once per such
|
|
291
291
|
job; or, for `sjawhar/legion`, on a Legion daemon's boot on a build containing the merge, on
|
|
292
|
-
Dispatch's `server.started`
|
|
293
|
-
its boot. The woken look's task names the event
|
|
292
|
+
Dispatch's `server.started` announcing that every server process serves such a build, or on the
|
|
293
|
+
build the daemon read from Dispatch at its boot. The woken look's task names the event
|
|
294
|
+
(`Dispatch's issue.deployed: run <id> completed <t>`,
|
|
294
295
|
`the deploy workflow's run: <repo> <workflow> run <id> attempt <n>, job <job name>: <step> succeeded at <t>`,
|
|
295
|
-
`the Legion daemon's boot on build <sha>`, `Dispatch
|
|
296
|
+
`the Legion daemon's boot on build <sha>`, `Dispatch serving build <sha> on every server process`,
|
|
296
297
|
`Dispatch's build <sha>`), and its `Production:` line names it too, so the pull request carries
|
|
297
298
|
the cause. Never poll for the deploy, never wait on a reply, and never ask anyone for approval
|
|
298
299
|
to deploy. Open a `dispatch ask` to a person only when the instructions name nobody for that
|
package/src/server.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from "@legion/envoy-client/tool-contract";
|
|
13
13
|
import { createEnvoyClient, type EnvoyClient } from "@legion/envoy-client/transport";
|
|
14
14
|
import { type ToolDefinition, tool } from "@opencode-ai/plugin/tool";
|
|
15
|
+
import { version } from "../package.json" with { type: "json" };
|
|
15
16
|
import { logger } from "./log";
|
|
16
17
|
import { resolvePort } from "./port";
|
|
17
18
|
|
|
@@ -29,6 +30,9 @@ const skillsDirectory = [
|
|
|
29
30
|
|
|
30
31
|
const PACKAGE_NAME = "@sjawhar/opencode-legion-envoy";
|
|
31
32
|
|
|
33
|
+
/** The plugin every registration names: this package and the version its manifest carries. */
|
|
34
|
+
const PLUGIN = { name: PACKAGE_NAME, version };
|
|
35
|
+
|
|
32
36
|
/** The package root: the nearest directory above `from` whose package.json names this package.
|
|
33
37
|
* The packed server.js sits in dist/src/, the repo's server.ts in src/. A manifest on the way up
|
|
34
38
|
* that is missing, unreadable or not JSON is passed over. */
|
|
@@ -93,6 +97,7 @@ function envoyTools(
|
|
|
93
97
|
port: port() ?? 0,
|
|
94
98
|
title: title() ?? "",
|
|
95
99
|
driving: true,
|
|
100
|
+
plugin: PLUGIN,
|
|
96
101
|
})
|
|
97
102
|
);
|
|
98
103
|
},
|
|
@@ -307,6 +312,7 @@ export default async (input: { serverUrl: URL }) => {
|
|
|
307
312
|
port,
|
|
308
313
|
title: title ?? "",
|
|
309
314
|
driving,
|
|
315
|
+
plugin: PLUGIN,
|
|
310
316
|
})
|
|
311
317
|
.catch(() => {});
|
|
312
318
|
|