@sjawhar/pi-legion-envoy 0.22.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -98,7 +98,9 @@ Configure the shared `envoy.json` with:
98
98
 
99
99
  The user file is `~/.config/opencode/envoy.json`; a
100
100
  `<cwd>/.opencode/envoy.json` file shallow-merges over it. `DISPATCH_URL` and
101
- `DISPATCH_TOKEN` override the file values for one process. Invalid configuration,
101
+ `DISPATCH_TOKEN` override the file values for one process. Omitting
102
+ `dispatch.serverUrl` while `dispatch.enabled` is true targets
103
+ `http://localhost:8766`, the Go server's listen address. Invalid configuration,
102
104
  an invalid URL, or an empty token leaves the nine tools unavailable and reports
103
105
  the source of the error.
104
106
 
package/dist/envoy.js CHANGED
@@ -29677,6 +29677,8 @@ var CommentEventPayloadSchema = object({
29677
29677
  artifact_name: string2().optional(),
29678
29678
  body: string2().optional(),
29679
29679
  reply_to: string2().nullish(),
29680
+ ask_id: string2().nullish(),
29681
+ ask_question: string2().optional(),
29680
29682
  anchor: object({ quote: string2().optional() }).nullish(),
29681
29683
  suggestion: object({ replace_with: string2().optional() }).nullish()
29682
29684
  });
@@ -29727,14 +29729,15 @@ var dispatchToolSpecs = [
29727
29729
  },
29728
29730
  {
29729
29731
  name: "dispatch_comment",
29730
- description: "Add review feedback to an issue or document quote. Do not use it for an exact replacement; use " + `dispatch_suggest instead. Body is at most 2,000 characters. ${ISSUE_REFERENCE}`,
29732
+ description: "Add review feedback to an issue or document quote, or reply to a question asked with dispatch_ask. " + "Do not use it for an exact replacement; use " + `dispatch_suggest instead. Body is at most 2,000 characters. ${ISSUE_REFERENCE}`,
29731
29733
  arguments: (z) => ({
29732
29734
  issue: z.string().describe(ISSUE_REFERENCE),
29733
29735
  artifact: z.string().describe("Artifact slug or id required when quote is given.").optional(),
29734
29736
  quote: z.string().describe("Optional exact quoted document text.").optional(),
29735
29737
  occurrence: z.number({ int: true, min: 0 }).describe("Optional zero-based occurrence of quote.").optional(),
29736
29738
  body: z.string({ max: 2000 }).describe("Review comment, at most 2,000 characters."),
29737
- reply_to: z.string().describe("Optional comment id to reply to.").optional()
29739
+ reply_to: z.string().describe("Optional comment id to reply to.").optional(),
29740
+ reply_to_ask: z.string().describe("Optional ask id to reply to, threading this comment under that question. Mutually " + "exclusive with reply_to.").optional()
29738
29741
  })
29739
29742
  },
29740
29743
  {
@@ -30065,25 +30068,10 @@ var LegionDaemonApi = {
30065
30068
  request: architectCapability.extend({ issue: nonEmptyString, comment: string2().optional() }),
30066
30069
  response: object({})
30067
30070
  },
30068
- SpawnToken: {
30069
- request: architectCapability.extend({ issue: nonEmptyString, role: legionRole }),
30070
- response: object({ spawnToken: nonEmptyString })
30071
- },
30072
30071
  ProvisioningCredential: {
30073
30072
  request: architectCapability.extend({ issue: nonEmptyString }),
30074
30073
  response: object({ token: nonEmptyString })
30075
30074
  },
30076
- RoleBacking: {
30077
- request: strictObject({
30078
- tree: nonEmptyString,
30079
- issue: nonEmptyString,
30080
- role: legionRole,
30081
- sessionId: nonEmptyString,
30082
- agentId: nonEmptyString,
30083
- spawnToken: nonEmptyString
30084
- }),
30085
- response: object({})
30086
- },
30087
30075
  WorkerStarted: {
30088
30076
  request: strictObject({
30089
30077
  tree: nonEmptyString,
@@ -30130,20 +30118,6 @@ var LegionDaemonApi = {
30130
30118
  roleToken: nonEmptyString
30131
30119
  })
30132
30120
  },
30133
- Phase: {
30134
- request: strictObject({
30135
- tree: nonEmptyString,
30136
- issue: nonEmptyString,
30137
- phase: nonEmptyString,
30138
- sessionId: nonEmptyString,
30139
- spawnToken: nonEmptyString
30140
- }),
30141
- response: object({
30142
- secret: nonEmptyString,
30143
- gitName: nonEmptyString,
30144
- gitEmail: nonEmptyString
30145
- })
30146
- },
30147
30121
  WorkerSession: {
30148
30122
  request: strictObject({
30149
30123
  sessionId: nonEmptyString,
@@ -30838,6 +30812,12 @@ function dispatchPayload(event) {
30838
30812
  const parsed = schema.safeParse(event.payload);
30839
30813
  return parsed.success ? parsed.data : event.payload;
30840
30814
  }
30815
+ function dispatchAskQuestion(event) {
30816
+ if (event.type !== "comment.created")
30817
+ return;
30818
+ const parsed = CommentEventPayloadSchema.safeParse(event.payload);
30819
+ return parsed.success && parsed.data.ask_question !== "" ? parsed.data.ask_question : undefined;
30820
+ }
30841
30821
  function parseDispatchFrame(rawPayload) {
30842
30822
  let value;
30843
30823
  try {
@@ -30887,6 +30867,7 @@ function renderInbound(raw, sessionID, subject) {
30887
30867
  }
30888
30868
  let dispatchEvent;
30889
30869
  let dispatchIssue;
30870
+ let askQuestion;
30890
30871
  const dispatchRendered = envelope.source === "dispatch" && envelope.payload !== undefined;
30891
30872
  if (envelope.source === "dispatch") {
30892
30873
  if (envelope.payload === undefined) {
@@ -30900,6 +30881,7 @@ function renderInbound(raw, sessionID, subject) {
30900
30881
  if (frame.event.actor.kind === "session" && frame.event.actor.id === sessionID) {
30901
30882
  return { skip: true, content: "", envelope };
30902
30883
  }
30884
+ askQuestion = dispatchAskQuestion(frame.event);
30903
30885
  dispatchEvent = {
30904
30886
  issue_key: frame.event.issue_key,
30905
30887
  type: frame.event.type,
@@ -30947,7 +30929,7 @@ ${envelope.payload ?? ""}`;
30947
30929
  ...envelope.expires_at === undefined ? {} : { by: inboundTimestamp(envelope.expires_at) },
30948
30930
  ...envelope.urgency === undefined ? {} : { urgency: envelope.urgency },
30949
30931
  ...envelope.expects_reply === undefined ? {} : { expects_reply: envelope.expects_reply },
30950
- ...envelope.in_reply_to === undefined ? {} : { re: envelope.in_reply_to },
30932
+ ...envelope.in_reply_to === undefined ? {} : { re: askQuestion ?? envelope.in_reply_to },
30951
30933
  ...envelope.supersedes === undefined ? {} : { supersedes: envelope.supersedes },
30952
30934
  ...reply === undefined ? {} : { reply_with: reply },
30953
30935
  ...role === undefined ? {} : {
@@ -31500,8 +31482,12 @@ function childrenSummary(issue) {
31500
31482
  ].join(`
31501
31483
  `);
31502
31484
  }
31503
- function askSummary(ask) {
31485
+ function askSummary({ ask, replies }) {
31504
31486
  const answer = ask.answer;
31487
+ const chain = replies.flatMap((reply) => [
31488
+ `${reply.id} \xB7 ${reply.author.kind} ${reply.author.id}`,
31489
+ `Body: ${reply.body}`
31490
+ ]);
31505
31491
  return [
31506
31492
  `Question: ${ask.question}`,
31507
31493
  "Options:",
@@ -31512,7 +31498,9 @@ function askSummary(ask) {
31512
31498
  `- By: ${answer.user}`,
31513
31499
  `- Selected: ${answer.selected.length === 0 ? "none" : answer.selected.join(", ")}`,
31514
31500
  ...answer.text === null ? [] : [`- Text: ${answer.text}`]
31515
- ]
31501
+ ],
31502
+ "Replies:",
31503
+ ...chain.length === 0 ? ["- none"] : chain
31516
31504
  ].join(`
31517
31505
  `);
31518
31506
  }
@@ -31611,10 +31599,15 @@ async function executeDispatchTool(input) {
31611
31599
  const resolved = artifactReference ? await resolveArtifact(client, issue(), artifactReference) : undefined;
31612
31600
  const anchored = resolved ? anchor(resolved.artifact, args) : undefined;
31613
31601
  const replyTo = optionalString(args, "reply_to");
31602
+ const replyToAsk = optionalString(args, "reply_to_ask");
31603
+ if (replyTo !== undefined && replyToAsk !== undefined) {
31604
+ throw new Error("reply_to and reply_to_ask cannot both be set");
31605
+ }
31614
31606
  const comment = await client.comment(issue(), {
31615
31607
  body: stringArg(args, "body"),
31616
31608
  ...anchored === undefined ? {} : { anchor: anchored },
31617
31609
  ...replyTo === undefined ? {} : { reply_to: replyTo },
31610
+ ...replyToAsk === undefined ? {} : { ask_id: replyToAsk },
31618
31611
  actor
31619
31612
  });
31620
31613
  return {
@@ -31723,10 +31716,13 @@ Open anchored asks/comments: ${marks.join(", ")}`,
31723
31716
  }
31724
31717
  case "dispatch_read": {
31725
31718
  if (issueArguments.ref?.kind === "ask") {
31726
- const ask = await client.getAsk(issueArguments.ref.id);
31719
+ const askRead = await client.getAsk(issueArguments.ref.id);
31727
31720
  return {
31728
- text: askSummary(ask),
31729
- details: { issue: ask.issue_key, topic: dispatchIssueSubject(ask.issue_key, ">") }
31721
+ text: askSummary(askRead),
31722
+ details: {
31723
+ issue: askRead.ask.issue_key,
31724
+ topic: dispatchIssueSubject(askRead.ask.issue_key, ">")
31725
+ }
31730
31726
  };
31731
31727
  }
31732
31728
  if (issueArguments.ref?.kind === "comment") {
package/dist/legion.js CHANGED
@@ -29675,6 +29675,8 @@ var CommentEventPayloadSchema = object({
29675
29675
  artifact_name: string2().optional(),
29676
29676
  body: string2().optional(),
29677
29677
  reply_to: string2().nullish(),
29678
+ ask_id: string2().nullish(),
29679
+ ask_question: string2().optional(),
29678
29680
  anchor: object({ quote: string2().optional() }).nullish(),
29679
29681
  suggestion: object({ replace_with: string2().optional() }).nullish()
29680
29682
  });
@@ -29725,14 +29727,15 @@ var dispatchToolSpecs = [
29725
29727
  },
29726
29728
  {
29727
29729
  name: "dispatch_comment",
29728
- description: "Add review feedback to an issue or document quote. Do not use it for an exact replacement; use " + `dispatch_suggest instead. Body is at most 2,000 characters. ${ISSUE_REFERENCE}`,
29730
+ description: "Add review feedback to an issue or document quote, or reply to a question asked with dispatch_ask. " + "Do not use it for an exact replacement; use " + `dispatch_suggest instead. Body is at most 2,000 characters. ${ISSUE_REFERENCE}`,
29729
29731
  arguments: (z) => ({
29730
29732
  issue: z.string().describe(ISSUE_REFERENCE),
29731
29733
  artifact: z.string().describe("Artifact slug or id required when quote is given.").optional(),
29732
29734
  quote: z.string().describe("Optional exact quoted document text.").optional(),
29733
29735
  occurrence: z.number({ int: true, min: 0 }).describe("Optional zero-based occurrence of quote.").optional(),
29734
29736
  body: z.string({ max: 2000 }).describe("Review comment, at most 2,000 characters."),
29735
- reply_to: z.string().describe("Optional comment id to reply to.").optional()
29737
+ reply_to: z.string().describe("Optional comment id to reply to.").optional(),
29738
+ reply_to_ask: z.string().describe("Optional ask id to reply to, threading this comment under that question. Mutually " + "exclusive with reply_to.").optional()
29736
29739
  })
29737
29740
  },
29738
29741
  {
@@ -30063,25 +30066,10 @@ var LegionDaemonApi = {
30063
30066
  request: architectCapability.extend({ issue: nonEmptyString, comment: string2().optional() }),
30064
30067
  response: object({})
30065
30068
  },
30066
- SpawnToken: {
30067
- request: architectCapability.extend({ issue: nonEmptyString, role: legionRole }),
30068
- response: object({ spawnToken: nonEmptyString })
30069
- },
30070
30069
  ProvisioningCredential: {
30071
30070
  request: architectCapability.extend({ issue: nonEmptyString }),
30072
30071
  response: object({ token: nonEmptyString })
30073
30072
  },
30074
- RoleBacking: {
30075
- request: strictObject({
30076
- tree: nonEmptyString,
30077
- issue: nonEmptyString,
30078
- role: legionRole,
30079
- sessionId: nonEmptyString,
30080
- agentId: nonEmptyString,
30081
- spawnToken: nonEmptyString
30082
- }),
30083
- response: object({})
30084
- },
30085
30073
  WorkerStarted: {
30086
30074
  request: strictObject({
30087
30075
  tree: nonEmptyString,
@@ -30128,20 +30116,6 @@ var LegionDaemonApi = {
30128
30116
  roleToken: nonEmptyString
30129
30117
  })
30130
30118
  },
30131
- Phase: {
30132
- request: strictObject({
30133
- tree: nonEmptyString,
30134
- issue: nonEmptyString,
30135
- phase: nonEmptyString,
30136
- sessionId: nonEmptyString,
30137
- spawnToken: nonEmptyString
30138
- }),
30139
- response: object({
30140
- secret: nonEmptyString,
30141
- gitName: nonEmptyString,
30142
- gitEmail: nonEmptyString
30143
- })
30144
- },
30145
30119
  WorkerSession: {
30146
30120
  request: strictObject({
30147
30121
  sessionId: nonEmptyString,
@@ -31330,6 +31304,12 @@ function dispatchPayload(event) {
31330
31304
  const parsed = schema.safeParse(event.payload);
31331
31305
  return parsed.success ? parsed.data : event.payload;
31332
31306
  }
31307
+ function dispatchAskQuestion(event) {
31308
+ if (event.type !== "comment.created")
31309
+ return;
31310
+ const parsed = CommentEventPayloadSchema.safeParse(event.payload);
31311
+ return parsed.success && parsed.data.ask_question !== "" ? parsed.data.ask_question : undefined;
31312
+ }
31333
31313
  function parseDispatchFrame(rawPayload) {
31334
31314
  let value;
31335
31315
  try {
@@ -31379,6 +31359,7 @@ function renderInbound(raw, sessionID, subject) {
31379
31359
  }
31380
31360
  let dispatchEvent;
31381
31361
  let dispatchIssue;
31362
+ let askQuestion;
31382
31363
  const dispatchRendered = envelope.source === "dispatch" && envelope.payload !== undefined;
31383
31364
  if (envelope.source === "dispatch") {
31384
31365
  if (envelope.payload === undefined) {
@@ -31392,6 +31373,7 @@ function renderInbound(raw, sessionID, subject) {
31392
31373
  if (frame.event.actor.kind === "session" && frame.event.actor.id === sessionID) {
31393
31374
  return { skip: true, content: "", envelope };
31394
31375
  }
31376
+ askQuestion = dispatchAskQuestion(frame.event);
31395
31377
  dispatchEvent = {
31396
31378
  issue_key: frame.event.issue_key,
31397
31379
  type: frame.event.type,
@@ -31439,7 +31421,7 @@ ${envelope.payload ?? ""}`;
31439
31421
  ...envelope.expires_at === undefined ? {} : { by: inboundTimestamp(envelope.expires_at) },
31440
31422
  ...envelope.urgency === undefined ? {} : { urgency: envelope.urgency },
31441
31423
  ...envelope.expects_reply === undefined ? {} : { expects_reply: envelope.expects_reply },
31442
- ...envelope.in_reply_to === undefined ? {} : { re: envelope.in_reply_to },
31424
+ ...envelope.in_reply_to === undefined ? {} : { re: askQuestion ?? envelope.in_reply_to },
31443
31425
  ...envelope.supersedes === undefined ? {} : { supersedes: envelope.supersedes },
31444
31426
  ...reply === undefined ? {} : { reply_with: reply },
31445
31427
  ...role === undefined ? {} : {
@@ -31985,8 +31967,12 @@ function childrenSummary(issue) {
31985
31967
  ].join(`
31986
31968
  `);
31987
31969
  }
31988
- function askSummary(ask) {
31970
+ function askSummary({ ask, replies }) {
31989
31971
  const answer = ask.answer;
31972
+ const chain = replies.flatMap((reply) => [
31973
+ `${reply.id} \xB7 ${reply.author.kind} ${reply.author.id}`,
31974
+ `Body: ${reply.body}`
31975
+ ]);
31990
31976
  return [
31991
31977
  `Question: ${ask.question}`,
31992
31978
  "Options:",
@@ -31997,7 +31983,9 @@ function askSummary(ask) {
31997
31983
  `- By: ${answer.user}`,
31998
31984
  `- Selected: ${answer.selected.length === 0 ? "none" : answer.selected.join(", ")}`,
31999
31985
  ...answer.text === null ? [] : [`- Text: ${answer.text}`]
32000
- ]
31986
+ ],
31987
+ "Replies:",
31988
+ ...chain.length === 0 ? ["- none"] : chain
32001
31989
  ].join(`
32002
31990
  `);
32003
31991
  }
@@ -32096,10 +32084,15 @@ async function executeDispatchTool(input) {
32096
32084
  const resolved = artifactReference ? await resolveArtifact(client, issue(), artifactReference) : undefined;
32097
32085
  const anchored = resolved ? anchor(resolved.artifact, args) : undefined;
32098
32086
  const replyTo = optionalString(args, "reply_to");
32087
+ const replyToAsk = optionalString(args, "reply_to_ask");
32088
+ if (replyTo !== undefined && replyToAsk !== undefined) {
32089
+ throw new Error("reply_to and reply_to_ask cannot both be set");
32090
+ }
32099
32091
  const comment = await client.comment(issue(), {
32100
32092
  body: stringArg(args, "body"),
32101
32093
  ...anchored === undefined ? {} : { anchor: anchored },
32102
32094
  ...replyTo === undefined ? {} : { reply_to: replyTo },
32095
+ ...replyToAsk === undefined ? {} : { ask_id: replyToAsk },
32103
32096
  actor
32104
32097
  });
32105
32098
  return {
@@ -32208,10 +32201,13 @@ Open anchored asks/comments: ${marks.join(", ")}`,
32208
32201
  }
32209
32202
  case "dispatch_read": {
32210
32203
  if (issueArguments.ref?.kind === "ask") {
32211
- const ask = await client.getAsk(issueArguments.ref.id);
32204
+ const askRead = await client.getAsk(issueArguments.ref.id);
32212
32205
  return {
32213
- text: askSummary(ask),
32214
- details: { issue: ask.issue_key, topic: dispatchIssueSubject(ask.issue_key, ">") }
32206
+ text: askSummary(askRead),
32207
+ details: {
32208
+ issue: askRead.ask.issue_key,
32209
+ topic: dispatchIssueSubject(askRead.ask.issue_key, ">")
32210
+ }
32215
32211
  };
32216
32212
  }
32217
32213
  if (issueArguments.ref?.kind === "comment") {
@@ -46,6 +46,12 @@ Anchor a document question with `anchor: { artifact, quote, occurrence? }`; `occ
46
46
  zero-based and selects a repeated quote. An anchor whose quote disappears becomes orphaned but
47
47
  remains readable against its original document version. An ask stays open until a human answers;
48
48
  you cannot withdraw it. Ask once and well. The host steers you when a human answers.
49
+
50
+ An ask is a thread, not a dead end: a human can reply to it before or after answering, and you
51
+ (the asker) can reply too — e.g. acknowledging a clarifying question, or following up after the
52
+ answer. Use `reply_to_ask` on `dispatch_comment` to reply under your own ask; it is mutually
53
+ exclusive with `reply_to`.
54
+
49
55
  ## The spec is where narrative goes
50
56
 
51
57
  Read the current document before changing it:
@@ -85,11 +91,12 @@ message.
85
91
  Add feedback with:
86
92
 
87
93
  ```ts
88
- dispatch_comment({ issue, artifact?, quote?, occurrence?, body, reply_to? })
94
+ dispatch_comment({ issue, artifact?, quote?, occurrence?, body, reply_to?, reply_to_ask? })
89
95
  ```
90
96
 
91
97
  It returns `details` `{ issue, topic, comment }`. `quote` requires `artifact`; omit both for a
92
- floating issue comment. Use `reply_to` to continue a comment.
98
+ floating issue comment. Use `reply_to` to continue a comment thread; use `reply_to_ask` to reply
99
+ directly under a question asked with `dispatch_ask`. The two are mutually exclusive.
93
100
 
94
101
  Propose an exact replacement instead of describing it:
95
102
 
@@ -149,9 +156,9 @@ dispatch_read({ issue?, ref? })
149
156
  ```
150
157
 
151
158
  With an issue ref, it returns the issue summary, open asks, and recent events with `details`
152
- `{ issue }`. With an ask ref, it returns that ask's question, options, state, and answer. With a
153
- comment ref, it returns that comment and its quoted reply chain. Use `dispatch_doc_read` for
154
- document contents.
159
+ `{ issue }`. With an ask ref, it returns that ask's question, options, state, answer, and its
160
+ reply thread. With a comment ref, it returns that comment and its quoted reply chain. Use
161
+ `dispatch_doc_read` for document contents.
155
162
 
156
163
  ## References
157
164
 
@@ -213,7 +213,7 @@ corresponding lifecycle procedure.
213
213
  | `pr-closed-unmerged` | Decide from current scope whether to reopen the work, send a fresh implementer, or cancel it with a reason. Delegate the repository action to the responsible phase worker and keep ownership. |
214
214
  | `issue-comment` | Interpret the comment in the issue's design context. Answer it, adjust the plan, or relay it via `envoy_publish` to the responsible worker's role token; scope and product decisions remain with you. |
215
215
  | `catchup-overseer` | Verify its gates, child counts, and PR verdicts against current artifacts, then resume the applicable numbered lifecycle step. It is a current-state snapshot, not a raw-event replay. For each entry in its `phaseCompletions` (`{issue, role, summary, at}`, phases that completed while you were not live), handle it exactly as a `phase-complete` wake. |
216
- | `revive-worker` | The extension has revived the backed worker. Do not create a duplicate; message the restored worker via `envoy_publish` to its role token if action is needed and rely on its committed handoff over recollection. |
216
+ | `worker-died` | Payload `{type:"worker-died", issue, role}`. The daemon probed and retried this role's worker through `MAX_LAUNCH_FAILURES` attempts and could not confirm a boot — never a raw-event replay or a silent revive. Reassess the work and `spawn_worker` again for the role (it resumes the same agent via `--resume` if a session file survived) or reassign it if the failure looks environmental, not agent-specific. |
217
217
  | `reopened` | Reopen the root lifecycle: inspect the reason and current artifacts, reassess scope and children, and resume at the first applicable numbered step. |
218
218
 
219
219
  ## Escalation judgment
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-legion-envoy",
3
- "version": "0.22.0",
3
+ "version": "0.24.0",
4
4
  "type": "module",
5
5
  "omp": {
6
6
  "extensions": [