@sjawhar/opencode-legion-envoy 9.5.3 → 9.7.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 +64 -19
- package/dist/src/server.js +15 -9
- 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/references/daily-report.md +3 -3
- package/skills/legion-worker/references/findings.md +3 -3
- package/skills/legion-worker/references/merge-gate.md +4 -3
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."),
|
|
@@ -14719,7 +14725,7 @@ var dispatchToolSpecs = [
|
|
|
14719
14725
|
{
|
|
14720
14726
|
name: "dispatch_search",
|
|
14721
14727
|
example: { query: "astrolabe" },
|
|
14722
|
-
description: "Search every issue, document, comment, ask, and message for a keyword or phrase and get deep links. " + "Use it before creating an issue or a design document, and to find where a word was written. " + 'Websearch syntax: "quoted phrase", -excluded, OR. ' + "Each kind of content is ranked on its own and the lists are merged, so the top holds the best " + "issue, document, ask, comment and message; an issue key searched alone lists that issue first. " + `The answer names how many results match; offset pages through them, and each kind lists at most its best ${SEARCH_KIND_DEPTH}, ` + "so narrow the query or name a project to reach the rest. Paging is exact only while the " + "corpus holds still: content added, changed, or removed between two offsets can shift rows " + "across a page boundary, so one hit can come back twice and another never.",
|
|
14728
|
+
description: "Search every issue, document, comment, ask, and message for a keyword or phrase and get deep links. " + "Use it before creating an issue or a design document, and to find where a word was written. " + 'Websearch syntax: "quoted phrase", -excluded, OR. ' + "Each kind of content is ranked on its own and the lists are merged, so the top holds the best " + "issue, document, ask, comment and message; an issue key searched alone lists that issue first. " + "An ask hit says whether it is answered: an answered one shows the latest answer, who gave it, " + "when, the options chosen and its text; a closed one says it was closed with no answer, or " + "shows the last answer it had. " + `The answer names how many results match; offset pages through them, and each kind lists at most its best ${SEARCH_KIND_DEPTH}, ` + "so narrow the query or name a project to reach the rest. Paging is exact only while the " + "corpus holds still: content added, changed, or removed between two offsets can shift rows " + "across a page boundary, so one hit can come back twice and another never.",
|
|
14723
14729
|
arguments: (z) => ({
|
|
14724
14730
|
query: z.string({
|
|
14725
14731
|
min: 2,
|
|
@@ -15365,6 +15371,15 @@ function commandsHelp() {
|
|
|
15365
15371
|
// ../envoy-client/src/dispatch-execute.ts
|
|
15366
15372
|
import { resolve as resolvePath2 } from "path";
|
|
15367
15373
|
|
|
15374
|
+
// ../envoy-client/src/actor-text.ts
|
|
15375
|
+
function actorText(actor) {
|
|
15376
|
+
const service = actor.kind === "session" ? actor.service : undefined;
|
|
15377
|
+
if (service === undefined) {
|
|
15378
|
+
return `${actor.kind} ${actor.id}`;
|
|
15379
|
+
}
|
|
15380
|
+
return `${actor.kind} ${actor.id} (as ${serviceSubjectLabel(service)})`;
|
|
15381
|
+
}
|
|
15382
|
+
|
|
15368
15383
|
// ../envoy-client/src/ask-answer.ts
|
|
15369
15384
|
var HEAD_LENGTH = 120;
|
|
15370
15385
|
function textHead(text) {
|
|
@@ -16633,7 +16648,7 @@ function humanEditHead(edit) {
|
|
|
16633
16648
|
function pageSummaryText(offset, count, total) {
|
|
16634
16649
|
return `showing ${offset + 1}-${offset + count} of ${total}`;
|
|
16635
16650
|
}
|
|
16636
|
-
function
|
|
16651
|
+
function searchHitLine(result, baseUrl) {
|
|
16637
16652
|
const href = new URL(result.href, baseUrl).toString();
|
|
16638
16653
|
const { owner } = result;
|
|
16639
16654
|
if (owner.kind === "document") {
|
|
@@ -16644,6 +16659,42 @@ function searchResultLine(result, baseUrl) {
|
|
|
16644
16659
|
const label = `${owner.key} [${owner.status}] ${owner.title} - ${result.kind}${artifactName}`;
|
|
16645
16660
|
return `${label}: ${snippetText(result.snippet)} -> ${href}`;
|
|
16646
16661
|
}
|
|
16662
|
+
function indentedText(text) {
|
|
16663
|
+
return text.split(/\r?\n/u).join(`
|
|
16664
|
+
`);
|
|
16665
|
+
}
|
|
16666
|
+
function answerLines(lead, answer) {
|
|
16667
|
+
const selected = answer.selected.length === 0 ? "none" : answer.selected.join(", ");
|
|
16668
|
+
const text = answer.text === null || answer.text === "" ? "none" : indentedText(answer.text);
|
|
16669
|
+
return [
|
|
16670
|
+
` ${lead} by ${answer.user} at ${answer.at} \xB7 options chosen: ${selected}`,
|
|
16671
|
+
` Answer text: ${text}`
|
|
16672
|
+
];
|
|
16673
|
+
}
|
|
16674
|
+
function resolutionText(resolution) {
|
|
16675
|
+
return `${resolution.kind} by ${actorText(resolution.actor)} at ${resolution.at}: ${indentedText(resolution.reason)}`;
|
|
16676
|
+
}
|
|
16677
|
+
function askStateLines(ask) {
|
|
16678
|
+
if (ask === undefined) {
|
|
16679
|
+
return [" Answer state not reported by this Dispatch; open the link to read the ask."];
|
|
16680
|
+
}
|
|
16681
|
+
switch (ask.state) {
|
|
16682
|
+
case "open":
|
|
16683
|
+
return [" Open: no answer yet."];
|
|
16684
|
+
case "answered":
|
|
16685
|
+
return answerLines("Answered", ask.answer);
|
|
16686
|
+
case "resolved":
|
|
16687
|
+
return ask.answer === undefined ? [` Closed with no answer: ${resolutionText(ask.resolution)}`] : [
|
|
16688
|
+
` Closed after an answer: ${resolutionText(ask.resolution)}`,
|
|
16689
|
+
...answerLines("Last answer", ask.answer)
|
|
16690
|
+
];
|
|
16691
|
+
}
|
|
16692
|
+
}
|
|
16693
|
+
function searchResultLine(result, baseUrl) {
|
|
16694
|
+
const line = searchHitLine(result, baseUrl);
|
|
16695
|
+
return result.kind === "ask" ? [line, ...askStateLines(result.ask)].join(`
|
|
16696
|
+
`) : line;
|
|
16697
|
+
}
|
|
16647
16698
|
function searchAnswer(search, query, offset, configUrl) {
|
|
16648
16699
|
const results = search.results;
|
|
16649
16700
|
const count = results.length;
|
|
@@ -17836,13 +17887,6 @@ function eventHead(event) {
|
|
|
17836
17887
|
return;
|
|
17837
17888
|
}
|
|
17838
17889
|
}
|
|
17839
|
-
function actorText(actor) {
|
|
17840
|
-
const service = actor.kind === "session" ? actor.service : undefined;
|
|
17841
|
-
if (service === undefined) {
|
|
17842
|
-
return `${actor.kind} ${actor.id}`;
|
|
17843
|
-
}
|
|
17844
|
-
return `${actor.kind} ${actor.id} (as ${serviceSubjectLabel(service)})`;
|
|
17845
|
-
}
|
|
17846
17890
|
function eventRef(event) {
|
|
17847
17891
|
const payload = event.payload;
|
|
17848
17892
|
const id = typeof payload === "object" && payload !== null && "id" in payload ? payload.id : undefined;
|
|
@@ -18536,7 +18580,8 @@ async function executeDispatchTool(input) {
|
|
|
18536
18580
|
throw new Error(`Dispatch claimed ${issueKey} but answered with no claim`);
|
|
18537
18581
|
}
|
|
18538
18582
|
const claimed = `${issueKey}: claimed by you since ${held.at}. Its status is ${after.status}`;
|
|
18539
|
-
|
|
18583
|
+
const reaches = `A person's answers, comments and messages on ${issueKey} now reach you directly.`;
|
|
18584
|
+
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}`;
|
|
18540
18585
|
}
|
|
18541
18586
|
return {
|
|
18542
18587
|
text: `${text}${await notSubscribed(issueTopic(issueKey), input.subscriptions)}`,
|
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."),
|
|
@@ -14248,7 +14254,7 @@ var dispatchToolSpecs = [
|
|
|
14248
14254
|
{
|
|
14249
14255
|
name: "dispatch_search",
|
|
14250
14256
|
example: { query: "astrolabe" },
|
|
14251
|
-
description: "Search every issue, document, comment, ask, and message for a keyword or phrase and get deep links. " + "Use it before creating an issue or a design document, and to find where a word was written. " + 'Websearch syntax: "quoted phrase", -excluded, OR. ' + "Each kind of content is ranked on its own and the lists are merged, so the top holds the best " + "issue, document, ask, comment and message; an issue key searched alone lists that issue first. " + `The answer names how many results match; offset pages through them, and each kind lists at most its best ${SEARCH_KIND_DEPTH}, ` + "so narrow the query or name a project to reach the rest. Paging is exact only while the " + "corpus holds still: content added, changed, or removed between two offsets can shift rows " + "across a page boundary, so one hit can come back twice and another never.",
|
|
14257
|
+
description: "Search every issue, document, comment, ask, and message for a keyword or phrase and get deep links. " + "Use it before creating an issue or a design document, and to find where a word was written. " + 'Websearch syntax: "quoted phrase", -excluded, OR. ' + "Each kind of content is ranked on its own and the lists are merged, so the top holds the best " + "issue, document, ask, comment and message; an issue key searched alone lists that issue first. " + "An ask hit says whether it is answered: an answered one shows the latest answer, who gave it, " + "when, the options chosen and its text; a closed one says it was closed with no answer, or " + "shows the last answer it had. " + `The answer names how many results match; offset pages through them, and each kind lists at most its best ${SEARCH_KIND_DEPTH}, ` + "so narrow the query or name a project to reach the rest. Paging is exact only while the " + "corpus holds still: content added, changed, or removed between two offsets can shift rows " + "across a page boundary, so one hit can come back twice and another never.",
|
|
14252
14258
|
arguments: (z) => ({
|
|
14253
14259
|
query: z.string({
|
|
14254
14260
|
min: 2,
|
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.
|
|
@@ -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
|
|
@@ -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
|