@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 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 searchResultLine(result, baseUrl) {
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
- text = legionManaged ? `${claimed}; ${LEGION_OWNS_STATUS} Release the claim when you stop.` : `${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.`;
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)}`,
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/opencode-legion-envoy",
3
- "version": "9.5.3",
3
+ "version": "9.7.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "dist/src/server.js",
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` (its `changes` filter and its version step): 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.
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/`,
@@ -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 takes the tree out of the workflow.
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 delivers no comments: that turn, `dispatch issue-update --route session:<your id>` (`--route ''` on release) or `envoy_subscribe notifications.dispatch.issue.<KEY>.>`. Never route such an issue.
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
- - State the current design, never how people reached it.
34
- - Link each source where the person wrote it: `dispatch://KEY/comment/<id>`, `/ask/<id>`, `/message/<id>`, or a Slack URL. With no link, state the design.
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
- - A person's decision is their answered ask block. Each part it shapes links it.
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 approved spec's plan with `writing-plans` as the issue's `plan.md`, not a repository file.
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, and Dispatch then refuses an ask or a message there
34
- (`409 ISSUE_CLOSED`) and hides its open asks. Reopen it with `set_status` to `icebox` and say so
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, Dispatch takes its open asks out of the Inbox and refuses their answers
122
- (`409 ISSUE_CLOSED`). The question names the issue the finding came from by its `dispatch://`
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` naming such a build, or on the build the daemon read from Dispatch at
293
- its boot. The woken look's task names the event (`Dispatch's issue.deployed: run <id> completed <t>`,
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's start on build <sha>`,
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