@sjawhar/opencode-legion-envoy 12.0.1 → 12.1.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
@@ -14688,7 +14688,7 @@ var dispatchToolSpecs = [
14688
14688
  {
14689
14689
  name: "dispatch_doc_read",
14690
14690
  example: { issue: "DSP-1" },
14691
- description: "Read a live document or a named document version, or the text of an uploaded file at its latest or named version. " + "Do not use it for issue status, asks, or events; " + "use dispatch read instead. Supply ref, issue, or project plus artifact; issue plus an omitted artifact reads the primary document. " + "A live read returns its document token for an optional dispatch doc-edit precondition. With blocks it lists the live document's blocks instead of its markdown, in document order: each block's id, type, token, anchored comments and asks, and opening words, and under a table each row by the index delete_row takes and each cell by its row and the column delete_column takes; block ids, block tokens and those indexes are what dispatch doc-edit takes. " + "After the markdown it prints every open thread anchored on the document in full: an open ask with its id and question and each reply's author, time and text, and a comment thread with its root's id, author, quote and text and each reply's author, time and text, in order; a resolved thread, and an ask that is answered or resolved, is not printed. " + "A picture (an uploaded PNG, JPEG, GIF or WebP of at most 3,750,000 bytes, 5 MB once base64-encoded) comes back as an image you see, with its name, type, size and version, every time you ask, including one a read already showed this session; " + "any other file that is not UTF-8 text is described, with the route that serves its bytes. " + OWNER_REFERENCE,
14691
+ description: "Read a live document or a named document version, or the text of an uploaded file at its latest or named version. " + "Do not use it for issue status, asks, or events; " + "use dispatch read instead. Supply ref, issue, or project plus artifact; issue plus an omitted artifact reads the primary document. " + "A live read returns its document token for an optional dispatch doc-edit precondition. With blocks it lists the live document's blocks instead of its markdown, in document order: each block's id, type, token, anchored comments and asks, and opening words, and under a table each row by the index delete_row takes and each cell by its row and the column delete_column takes; block ids, block tokens and those indexes are what dispatch doc-edit takes. " + "After the markdown it prints every open thread anchored on the document in full: an open ask with its id and question and each reply's author, time and text, and a comment thread with its root's id, author, time, quote and text and each reply's author, time and text, in order; a resolved thread, and an ask that is answered or resolved, is not printed. " + "A picture (an uploaded PNG, JPEG, GIF or WebP of at most 3,750,000 bytes, 5 MB once base64-encoded) comes back as an image you see, with its name, type, size and version, every time you ask, including one a read already showed this session; " + "any other file that is not UTF-8 text is described, with the route that serves its bytes. " + OWNER_REFERENCE,
14692
14692
  arguments: (z) => ({
14693
14693
  issue: z.string().describe(ISSUE_REFERENCE).optional(),
14694
14694
  project: z.string().describe("Project key owning the document.").optional(),
@@ -14848,6 +14848,13 @@ var dispatchToolSpecs = [
14848
14848
  }),
14849
14849
  strict: true
14850
14850
  },
14851
+ {
14852
+ name: "dispatch_answers",
14853
+ example: {},
14854
+ description: "List every answer and comment a person wrote to this session in the last 72 hours, newest first, each in full: " + "answers to its asks, replies on its asks and in its comment threads, comments on its documents and on issues routed to it, " + "mentions of it, messages to it and replies to its messages. Each item names its time, its writer, what it is, its " + "dispatch:// reference and its issue or document, an answer's question and choices, and the words, never cut. " + "Run it when the list of people's answers on a model request says it left older items out. " + "It reads this session's id from CLAUDE_CODE_SESSION_ID under Claude Code and DISPATCH_SESSION_ID elsewhere, so a subagent reads its parent's.",
14855
+ arguments: () => ({}),
14856
+ strict: true
14857
+ },
14851
14858
  {
14852
14859
  name: "dispatch_whoami",
14853
14860
  example: {},
@@ -15961,6 +15968,12 @@ class DispatchClient {
15961
15968
  async openAsksForProject(project) {
15962
15969
  return this.#json("GET", ["api", "v1", "asks", "open"], undefined, { project });
15963
15970
  }
15971
+ async sessionAnswers(sessionID, options = {}) {
15972
+ return this.#json("GET", ["api", "v1", "agents", sessionID, "answers"], undefined, {
15973
+ ...options.since === undefined ? {} : { since: options.since },
15974
+ ...options.maxChars === undefined ? {} : { max_chars: options.maxChars }
15975
+ }, options.signal);
15976
+ }
15964
15977
  async listAgents() {
15965
15978
  return this.#json("GET", ["api", "v1", "agents"]);
15966
15979
  }
@@ -16189,12 +16202,12 @@ class DispatchClient {
16189
16202
  throw error;
16190
16203
  }
16191
16204
  }
16192
- async#json(method, path, body, query) {
16193
- return (await this.#jsonAnswer(method, path, body, query)).payload;
16205
+ async#json(method, path, body, query, signal) {
16206
+ return (await this.#jsonAnswer(method, path, body, query, signal)).payload;
16194
16207
  }
16195
- async#jsonAnswer(method, path, body, query) {
16208
+ async#jsonAnswer(method, path, body, query, signal) {
16196
16209
  const url = this.#url(path, query);
16197
- const response = await this.#send(method, url, body === undefined ? undefined : { content: JSON.stringify(body), type: "application/json" });
16210
+ const response = await this.#send(method, url, body === undefined ? undefined : { content: JSON.stringify(body), type: "application/json" }, undefined, signal);
16198
16211
  return { status: response.status, payload: await this.#response(method, url, response) };
16199
16212
  }
16200
16213
  async#upload(path, input) {
@@ -16217,7 +16230,7 @@ class DispatchClient {
16217
16230
  const response = await this.#send(method, url, { content, type });
16218
16231
  return this.#response(method, url, response);
16219
16232
  }
16220
- async#send(method, url, body, accept = "application/json") {
16233
+ async#send(method, url, body, accept = "application/json", signal) {
16221
16234
  const headers = {
16222
16235
  Accept: accept,
16223
16236
  Authorization: `Bearer ${this.token}`
@@ -16228,7 +16241,7 @@ class DispatchClient {
16228
16241
  return this.fetchImpl(url, {
16229
16242
  method,
16230
16243
  headers,
16231
- signal: this.#signal,
16244
+ signal: signal === undefined ? this.#signal : AbortSignal.any([this.#signal, signal]),
16232
16245
  ...body === undefined ? {} : { body: body.content }
16233
16246
  });
16234
16247
  }
@@ -16414,6 +16427,9 @@ class DispatchClient {
16414
16427
  function documentLabel(project, slug) {
16415
16428
  return `${project}/${slug}`;
16416
16429
  }
16430
+ function ownerTitle(owner) {
16431
+ return "issue" in owner ? `${owner.issue.key}: ${owner.issue.title}` : `${owner.document.project} / ${owner.document.name}`;
16432
+ }
16417
16433
  function issueTopic(key) {
16418
16434
  return { label: key, topic: dispatchIssueSubject(key, ">") };
16419
16435
  }
@@ -17227,6 +17243,99 @@ function notMadeLine(reason, context, searchQuery) {
17227
17243
  return `No related-item check was made: ${reason}. Search before you rely on this being new: dispatch search --project ${context.project}${query}`;
17228
17244
  }
17229
17245
 
17246
+ // ../envoy-client/src/dispatch-session-directory.ts
17247
+ import { randomUUID } from "crypto";
17248
+ import { renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
17249
+ import { homedir } from "os";
17250
+ import { join as join2 } from "path";
17251
+ var SESSION_ID = /^[A-Za-z0-9._-]{1,128}$/;
17252
+ var PRIVATE_FILE = 384;
17253
+ function sessionStateRoot(env) {
17254
+ const stateDir = env["DISPATCH_STATE_DIR"];
17255
+ if (stateDir)
17256
+ return stateDir;
17257
+ return join2(env["XDG_STATE_HOME"] || join2(homedir(), ".local", "state"), "dispatch");
17258
+ }
17259
+ function sessionDirectory(env, sessionId) {
17260
+ if (!SESSION_ID.test(sessionId) || sessionId === "." || sessionId === "..") {
17261
+ throw new Error(`dispatch: the session id ${JSON.stringify(sessionId)} is not 1-128 letters, digits, ".", "_" or "-"`);
17262
+ }
17263
+ return join2(sessionStateRoot(env), "sessions", sessionId);
17264
+ }
17265
+ function writeAtomically(path, text) {
17266
+ const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
17267
+ try {
17268
+ writeFileSync2(temporary, text, { mode: PRIVATE_FILE });
17269
+ renameSync2(temporary, path);
17270
+ } catch (error) {
17271
+ rmSync2(temporary, { force: true });
17272
+ throw error;
17273
+ }
17274
+ }
17275
+
17276
+ // ../envoy-client/src/session-answers.ts
17277
+ var SESSION_ANSWERS_WINDOW_MS = 72 * 3600000;
17278
+ var SESSION_ANSWERS_MAX_CHARS = 8000;
17279
+ var NO_ROUTE_BACKOFF_MS = 5 * 60000;
17280
+ var QUESTION_MAX = 200;
17281
+ var RELATION_PHRASES = {
17282
+ answered_your_ask: "answered your ask",
17283
+ replied_on_your_ask: "replied on your ask",
17284
+ replied_in_a_comment_thread: "replied in a comment thread",
17285
+ commented_on_a_document: "commented on a document",
17286
+ commented_on_an_issue: "commented on an issue",
17287
+ mentioned_you: "mentioned you",
17288
+ sent_you_a_message: "sent you a message",
17289
+ replied_to_your_message: "replied to your message"
17290
+ };
17291
+ function isNoAnswersRoute(error) {
17292
+ return error instanceof DispatchServiceError && error.status === 404;
17293
+ }
17294
+ function renderSessionAnswerItem(item) {
17295
+ const relation = `${RELATION_PHRASES[item.relation]}${item.replaced === true ? " (replaces an earlier answer)" : ""}`;
17296
+ const head = [item.at, `user ${item.author.id}`, relation];
17297
+ if (item.edited_at !== undefined)
17298
+ head.push(`edited ${item.edited_at}`);
17299
+ const owner = item.owner === null ? undefined : ownerTitle(item.owner);
17300
+ const where = item.ref === null ? `Direct message ${item.id}, on no issue` : owner === undefined ? item.ref : `${item.ref} \xB7 ${owner}`;
17301
+ const lines = [head.join(" \xB7 "), where];
17302
+ if (item.ask !== undefined) {
17303
+ const question = item.ask.question.replace(/\s+/g, " ").trim();
17304
+ lines.push(`Question: ${question.length > QUESTION_MAX ? `${question.slice(0, QUESTION_MAX)}\u2026` : question}`);
17305
+ }
17306
+ if (item.kind === "answer") {
17307
+ const selected = item.selected ?? [];
17308
+ lines.push(`Selected: ${selected.length === 0 ? "none" : selected.join(", ")}`);
17309
+ }
17310
+ if (item.text !== "")
17311
+ lines.push(item.text);
17312
+ return lines.join(`
17313
+ `);
17314
+ }
17315
+ function withinWindow(items, now) {
17316
+ const start = now - SESSION_ANSWERS_WINDOW_MS;
17317
+ return items.filter((item) => Date.parse(item.at) >= start);
17318
+ }
17319
+ var MAX_CHARS_TEXT = SESSION_ANSWERS_MAX_CHARS.toLocaleString("en-US");
17320
+ function renderSessionAnswersList(items, options) {
17321
+ const shown = withinWindow(items, Number(options.now));
17322
+ if (shown.length === 0)
17323
+ return "No person wrote to this session in the last 72 hours.";
17324
+ return [
17325
+ `${shown.length} ${shown.length === 1 ? "item" : "items"} people wrote to this session in the last 72 hours, newest first, word for word:`,
17326
+ ...shown.map(renderSessionAnswerItem)
17327
+ ].join(`
17328
+
17329
+ `);
17330
+ }
17331
+ var STATE_KEY = Symbol.for("legion.pi-envoy.session-answers.v2");
17332
+ var processGlobal = globalThis;
17333
+ var state = processGlobal[STATE_KEY] ?? {
17334
+ sessions: new Map,
17335
+ noRouteUntil: new Map
17336
+ };
17337
+ processGlobal[STATE_KEY] = state;
17338
+
17230
17339
  // ../envoy-client/src/dispatch-execute.ts
17231
17340
  function documentTopic(artifact) {
17232
17341
  return documentTopicOf(artifact.project, artifact.slug);
@@ -17349,6 +17458,7 @@ var issueFreeTools = {
17349
17458
  dispatch_project: true,
17350
17459
  dispatch_projects: true,
17351
17460
  dispatch_open_asks: true,
17461
+ dispatch_answers: true,
17352
17462
  dispatch_whoami: true,
17353
17463
  dispatch_architecture_sync: true
17354
17464
  };
@@ -18461,12 +18571,9 @@ function openAskAge(ageSeconds) {
18461
18571
  const days = Math.floor(hours / 24);
18462
18572
  return `${days}d${hours % 24 === 0 ? "" : ` ${hours % 24}h`}`;
18463
18573
  }
18464
- function openAskOwner(ask) {
18465
- return "issue" in ask.owner ? `${ask.owner.issue.key}: ${ask.owner.issue.title}` : `${ask.owner.document.project} / ${ask.owner.document.name}`;
18466
- }
18467
18574
  function openAskLine(ask, baseUrl) {
18468
18575
  const priority = ask.priority === null ? "" : `P${ask.priority} \xB7 `;
18469
- return `- ${openAskAge(ask.age_seconds)} \xB7 ${priority}${openAskOwner(ask)} \xB7 ${ask.question} \xB7 ${new URL(ask.ref, baseUrl).toString()}`;
18576
+ return `- ${openAskAge(ask.age_seconds)} \xB7 ${priority}${ownerTitle(ask.owner)} \xB7 ${ask.question} \xB7 ${new URL(ask.ref, baseUrl).toString()}`;
18470
18577
  }
18471
18578
  function formatOpenAsksSummary(response, baseUrl) {
18472
18579
  const scope = "active open asks you authored on open issues and project documents";
@@ -18882,6 +18989,24 @@ async function executeDispatchTool(input) {
18882
18989
  const response = await client.openAsks(sessionId);
18883
18990
  return { text: formatOpenAsksSummary(response, linkBase), details: { ...response } };
18884
18991
  }
18992
+ if (input.tool === "dispatch_answers") {
18993
+ const sessionId = input.sessionId?.trim();
18994
+ if (!sessionId)
18995
+ throw new Error("host session id is required for dispatch answers");
18996
+ let response;
18997
+ try {
18998
+ response = await dispatchClient().sessionAnswers(sessionId);
18999
+ } catch (error) {
19000
+ if (isNoAnswersRoute(error)) {
19001
+ throw new Error(`This Dispatch (${configUrl}) has no answers route yet: its build is older than the answers route.`);
19002
+ }
19003
+ throw error;
19004
+ }
19005
+ return {
19006
+ text: renderSessionAnswersList(response.items, { now: Date.now() }),
19007
+ details: { session: sessionId, since: response.since, total: response.total }
19008
+ };
19009
+ }
18885
19010
  if (input.tool === "dispatch_whoami") {
18886
19011
  const sessionId = input.sessionId?.trim();
18887
19012
  if (!sessionId)
@@ -19939,16 +20064,13 @@ import {
19939
20064
  readdirSync as readdirSync2,
19940
20065
  readFileSync as readFileSync2,
19941
20066
  readSync,
19942
- renameSync as renameSync2,
19943
- rmSync as rmSync2,
20067
+ rmSync as rmSync3,
19944
20068
  statSync as statSync2,
19945
- writeFileSync as writeFileSync2
20069
+ writeFileSync as writeFileSync3
19946
20070
  } from "fs";
19947
- import { homedir } from "os";
19948
- import { join as join2 } from "path";
19949
- var SESSION_ID = /^[A-Za-z0-9._-]{1,128}$/;
20071
+ import { join as join3 } from "path";
19950
20072
  var PRIVATE_DIR = 448;
19951
- var PRIVATE_FILE = 384;
20073
+ var PRIVATE_FILE2 = 384;
19952
20074
  var IDLE_SESSION_MS = 14 * 86400000;
19953
20075
  var PRUNE_INTERVAL_MS = 86400000;
19954
20076
  var PRUNED_STAMP = ".pruned";
@@ -19958,18 +20080,6 @@ var PICTURE_EXTENSIONS = {
19958
20080
  "image/gif": "gif",
19959
20081
  "image/webp": "webp"
19960
20082
  };
19961
- function sessionStateRoot(env) {
19962
- const stateDir = env["DISPATCH_STATE_DIR"];
19963
- if (stateDir)
19964
- return stateDir;
19965
- return join2(env["XDG_STATE_HOME"] || join2(homedir(), ".local", "state"), "dispatch");
19966
- }
19967
- function sessionDirectory(env, sessionId) {
19968
- if (!SESSION_ID.test(sessionId) || sessionId === "." || sessionId === "..") {
19969
- throw new Error(`dispatch: the session id ${JSON.stringify(sessionId)} is not 1-128 letters, digits, ".", "_" or "-"`);
19970
- }
19971
- return join2(sessionStateRoot(env), "sessions", sessionId);
19972
- }
19973
20083
  function ensureDirectory(dir) {
19974
20084
  mkdirSync2(dir, { recursive: true, mode: PRIVATE_DIR });
19975
20085
  }
@@ -19982,16 +20092,11 @@ function readIfPresent(path) {
19982
20092
  throw error;
19983
20093
  }
19984
20094
  }
19985
- function writeAtomically(path, text) {
19986
- const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
19987
- writeFileSync2(temporary, text, { mode: PRIVATE_FILE });
19988
- renameSync2(temporary, path);
19989
- }
19990
20095
  function stringList(value) {
19991
20096
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
19992
20097
  }
19993
20098
  function loadSessionMemory(dir, sessionId) {
19994
- const path = join2(dir, "state.json");
20099
+ const path = join3(dir, "state.json");
19995
20100
  const text = readIfPresent(path);
19996
20101
  if (text === undefined)
19997
20102
  return new Set;
@@ -20015,7 +20120,7 @@ function writeMemory(dir, sessionId, follows) {
20015
20120
  pictures: [...shownPictures(sessionId)],
20016
20121
  follows: [...follows]
20017
20122
  };
20018
- writeAtomically(join2(dir, "state.json"), `${JSON.stringify(state)}
20123
+ writeAtomically(join3(dir, "state.json"), `${JSON.stringify(state)}
20019
20124
  `);
20020
20125
  }
20021
20126
  function appendResult(dir, entry) {
@@ -20024,8 +20129,8 @@ function appendResult(dir, entry) {
20024
20129
  }
20025
20130
  function writeResult(dir, entry) {
20026
20131
  const line = { at: new Date().toISOString(), ...entry };
20027
- appendFileSync(join2(dir, "results.jsonl"), `${JSON.stringify(line)}
20028
- `, { mode: PRIVATE_FILE });
20132
+ appendFileSync(join3(dir, "results.jsonl"), `${JSON.stringify(line)}
20133
+ `, { mode: PRIVATE_FILE2 });
20029
20134
  }
20030
20135
  function recordOutcome(dir, sessionId, follows, entry) {
20031
20136
  ensureDirectory(dir);
@@ -20035,11 +20140,11 @@ function recordOutcome(dir, sessionId, follows, entry) {
20035
20140
  function writePicture(dir, image) {
20036
20141
  const bytes = Buffer.from(image.data, "base64");
20037
20142
  const name = createHash2("sha256").update(bytes).digest("hex").slice(0, 16);
20038
- const pictures = join2(dir, "pictures");
20039
- const path = join2(pictures, `${name}.${PICTURE_EXTENSIONS[image.mimeType]}`);
20143
+ const pictures = join3(dir, "pictures");
20144
+ const path = join3(pictures, `${name}.${PICTURE_EXTENSIONS[image.mimeType]}`);
20040
20145
  ensureDirectory(pictures);
20041
20146
  try {
20042
- writeFileSync2(path, bytes, { mode: PRIVATE_FILE, flag: "wx" });
20147
+ writeFileSync3(path, bytes, { mode: PRIVATE_FILE2, flag: "wx" });
20043
20148
  } catch (error) {
20044
20149
  if (!hasErrnoCode(error, "EEXIST"))
20045
20150
  throw error;
@@ -20047,17 +20152,17 @@ function writePicture(dir, image) {
20047
20152
  return { path, bytes: bytes.length };
20048
20153
  }
20049
20154
  function writeLongOutput(dir, text) {
20050
- const out = join2(dir, "out");
20155
+ const out = join3(dir, "out");
20051
20156
  ensureDirectory(out);
20052
- const path = join2(out, `${new Date().toISOString().replaceAll(":", "-")}-${process.pid}.md`);
20053
- writeFileSync2(path, text, { mode: PRIVATE_FILE });
20157
+ const path = join3(out, `${new Date().toISOString().replaceAll(":", "-")}-${process.pid}.md`);
20158
+ writeFileSync3(path, text, { mode: PRIVATE_FILE2 });
20054
20159
  return path;
20055
20160
  }
20056
20161
  function readSessionTitle(dir) {
20057
- return readIfPresent(join2(dir, "title")) || undefined;
20162
+ return readIfPresent(join3(dir, "title")) || undefined;
20058
20163
  }
20059
20164
  function pruneSessions(root, now) {
20060
- const stamp = join2(root, PRUNED_STAMP);
20165
+ const stamp = join3(root, PRUNED_STAMP);
20061
20166
  try {
20062
20167
  if (now - statSync2(stamp).mtimeMs < PRUNE_INTERVAL_MS)
20063
20168
  return;
@@ -20065,7 +20170,7 @@ function pruneSessions(root, now) {
20065
20170
  if (!hasErrnoCode(error, "ENOENT"))
20066
20171
  throw error;
20067
20172
  }
20068
- const sessions = join2(root, "sessions");
20173
+ const sessions = join3(root, "sessions");
20069
20174
  let entries = [];
20070
20175
  try {
20071
20176
  entries = readdirSync2(sessions, { withFileTypes: true });
@@ -20076,10 +20181,10 @@ function pruneSessions(root, now) {
20076
20181
  for (const entry of entries) {
20077
20182
  if (!entry.isDirectory())
20078
20183
  continue;
20079
- const dir = join2(sessions, entry.name);
20184
+ const dir = join3(sessions, entry.name);
20080
20185
  try {
20081
20186
  if (now - statSync2(dir).mtimeMs > IDLE_SESSION_MS && askgateState(dir).kind === "off") {
20082
- rmSync2(dir, { recursive: true, force: true });
20187
+ rmSync3(dir, { recursive: true, force: true });
20083
20188
  }
20084
20189
  } catch (error) {
20085
20190
  if (!hasErrnoCode(error, "ENOENT"))
@@ -20088,7 +20193,7 @@ function pruneSessions(root, now) {
20088
20193
  }
20089
20194
  sweepStores(root, now);
20090
20195
  ensureDirectory(root);
20091
- writeFileSync2(stamp, "", { mode: PRIVATE_FILE });
20196
+ writeFileSync3(stamp, "", { mode: PRIVATE_FILE2 });
20092
20197
  }
20093
20198
 
20094
20199
  // ../envoy-client/src/envoy-config.ts
@@ -20101,7 +20206,7 @@ import {
20101
20206
  openSync as openSync3,
20102
20207
  readFileSync as readFileSync4,
20103
20208
  readlinkSync,
20104
- rmSync as rmSync3,
20209
+ rmSync as rmSync4,
20105
20210
  statSync as statSync3
20106
20211
  } from "fs";
20107
20212
  import { homedir as homedir2, tmpdir } from "os";
@@ -20367,9 +20472,9 @@ function parseFile(filePath, schema, checkout) {
20367
20472
  }
20368
20473
  var FAILURE_KEPT_MS = 60000;
20369
20474
  var COMMAND_OUTCOMES = Symbol.for("legion.envoy-config.commands.v1");
20370
- var processGlobal = globalThis;
20371
- var commandOutcomes = processGlobal[COMMAND_OUTCOMES] ?? new Map;
20372
- processGlobal[COMMAND_OUTCOMES] = commandOutcomes;
20475
+ var processGlobal2 = globalThis;
20476
+ var commandOutcomes = processGlobal2[COMMAND_OUTCOMES] ?? new Map;
20477
+ processGlobal2[COMMAND_OUTCOMES] = commandOutcomes;
20373
20478
  function runCommand(command, env, cwd) {
20374
20479
  const childEnv = {};
20375
20480
  for (const [name, variable] of Object.entries(env)) {
@@ -20423,10 +20528,10 @@ function runCommand(command, env, cwd) {
20423
20528
  }
20424
20529
  return { value };
20425
20530
  } finally {
20426
- rmSync3(directory, { recursive: true, force: true });
20531
+ rmSync4(directory, { recursive: true, force: true });
20427
20532
  }
20428
20533
  }
20429
- function fileValue(value, field, filePath, env) {
20534
+ function fileValue(value, field, filePath, env, commands = "run") {
20430
20535
  if (!value.startsWith("!")) {
20431
20536
  if (!sendable(value)) {
20432
20537
  throw new Error(`${filePath}: ${field} holds whitespace or a control character`);
@@ -20437,7 +20542,11 @@ function fileValue(value, field, filePath, env) {
20437
20542
  const cwd = path.dirname(path.resolve(filePath));
20438
20543
  const key = JSON.stringify([command, filePath, cwd]);
20439
20544
  let outcome = commandOutcomes.get(key);
20440
- if (outcome === undefined || "failure" in outcome && !outcome.timedOut && Date.now() - outcome.at >= FAILURE_KEPT_MS) {
20545
+ if (commands === "kept") {
20546
+ if (outcome === undefined) {
20547
+ throw new Error(`${filePath}: ${field}'s command has not run in this process yet, and this read does not run it`);
20548
+ }
20549
+ } else if (outcome === undefined || "failure" in outcome && !outcome.timedOut && Date.now() - outcome.at >= FAILURE_KEPT_MS) {
20441
20550
  outcome = { ...runCommand(command, env, cwd), at: Date.now() };
20442
20551
  commandOutcomes.set(key, outcome);
20443
20552
  }
@@ -20547,7 +20656,7 @@ function bearerSource(env, section, written) {
20547
20656
  const bearer = written();
20548
20657
  return bearer === undefined ? { kind: "none" } : { kind: "file", ...bearer };
20549
20658
  }
20550
- function fileBearer(bearer, url, section, env) {
20659
+ function fileBearer(bearer, url, section, env, commands = "run") {
20551
20660
  const named = httpUrl2(url)?.origin;
20552
20661
  const paired = httpUrl2(bearer.server)?.origin;
20553
20662
  if (named !== paired) {
@@ -20559,7 +20668,7 @@ function fileBearer(bearer, url, section, env) {
20559
20668
  if (bearer.token === "") {
20560
20669
  throw new Error(`${bearer.field} in ${bearer.path} must be a non-empty bearer token`);
20561
20670
  }
20562
- return fileValue(bearer.token, bearer.field, bearer.path, env);
20671
+ return fileValue(bearer.token, bearer.field, bearer.path, env, commands);
20563
20672
  }
20564
20673
 
20565
20674
  // ../envoy-client/src/dispatch-config.ts
@@ -20594,7 +20703,30 @@ function writtenToken(config, checkout) {
20594
20703
  checkout: false
20595
20704
  };
20596
20705
  }
20597
- function dispatchSettings(env, config) {
20706
+ function dispatchSettings(env, config, options = {}) {
20707
+ const target = dispatchTarget(env, config);
20708
+ if (!target.enabled)
20709
+ return target;
20710
+ const { url, source } = target;
20711
+ let token;
20712
+ try {
20713
+ token = source.kind === "variable" ? source.token : fileBearer(source, url, "DISPATCH", env, options.commands);
20714
+ } catch (error) {
20715
+ return offWithError(messageFor(error));
20716
+ }
20717
+ const publicUrl = nonEmpty(env.DISPATCH_PUBLIC_URL);
20718
+ const publicParsed = publicUrl === undefined ? undefined : httpUrl2(publicUrl);
20719
+ if (publicUrl !== undefined && (publicParsed === undefined || publicParsed.port === "0")) {
20720
+ return offWithError("DISPATCH_PUBLIC_URL must be an http or https URL");
20721
+ }
20722
+ return {
20723
+ enabled: true,
20724
+ url,
20725
+ token,
20726
+ ...publicUrl === undefined ? {} : { publicUrl: publicUrl.replace(/\/+$/, "") }
20727
+ };
20728
+ }
20729
+ function dispatchTarget(env, config) {
20598
20730
  if (config.kind === "failed")
20599
20731
  return offWithError(config.error);
20600
20732
  const explicitUrl = nonEmpty(env.DISPATCH_URL);
@@ -20617,27 +20749,17 @@ function dispatchSettings(env, config) {
20617
20749
  if (explicitUrl !== undefined && httpUrl2(explicitUrl) === undefined) {
20618
20750
  return offWithError("DISPATCH_URL must be an http or https URL");
20619
20751
  }
20752
+ let source;
20620
20753
  try {
20621
- const source = checkout !== null && checkoutServer !== undefined ? checkoutPair(checkout, checkoutServer) : bearerSource(env, "DISPATCH", () => writtenToken(config, checkout));
20622
- if (source.kind === "none") {
20623
- return offWithError(explicitUrl === undefined ? `${config.path}: dispatch is enabled and no token is: set dispatch.token, or DISPATCH_TOKEN or DISPATCH_TOKEN_FILE` : `DISPATCH_URL is set and no token is: set DISPATCH_TOKEN or DISPATCH_TOKEN_FILE, or dispatch.token in ${config.path}`);
20624
- }
20625
- const url = (explicitUrl ?? (source.kind === "file" ? source.server : own.serverUrl ?? DEFAULT_SERVER_URL)).replace(/\/+$/, "");
20626
- const token = source.kind === "variable" ? source.token : fileBearer(source, url, "DISPATCH", env);
20627
- const publicUrl = nonEmpty(env.DISPATCH_PUBLIC_URL);
20628
- const publicParsed = publicUrl === undefined ? undefined : httpUrl2(publicUrl);
20629
- if (publicUrl !== undefined && (publicParsed === undefined || publicParsed.port === "0")) {
20630
- return offWithError("DISPATCH_PUBLIC_URL must be an http or https URL");
20631
- }
20632
- return {
20633
- enabled: true,
20634
- url,
20635
- token,
20636
- ...publicUrl === undefined ? {} : { publicUrl: publicUrl.replace(/\/+$/, "") }
20637
- };
20754
+ source = checkout !== null && checkoutServer !== undefined ? checkoutPair(checkout, checkoutServer) : bearerSource(env, "DISPATCH", () => writtenToken(config, checkout));
20638
20755
  } catch (error) {
20639
20756
  return offWithError(messageFor(error));
20640
20757
  }
20758
+ if (source.kind === "none") {
20759
+ return offWithError(explicitUrl === undefined ? `${config.path}: dispatch is enabled and no token is: set dispatch.token, or DISPATCH_TOKEN or DISPATCH_TOKEN_FILE` : `DISPATCH_URL is set and no token is: set DISPATCH_TOKEN or DISPATCH_TOKEN_FILE, or dispatch.token in ${config.path}`);
20760
+ }
20761
+ const url = (explicitUrl ?? (source.kind === "file" ? source.server : own.serverUrl ?? DEFAULT_SERVER_URL)).replace(/\/+$/, "");
20762
+ return { enabled: true, url, source };
20641
20763
  }
20642
20764
 
20643
20765
  // ../envoy-client/src/envoy-settings.ts
@@ -20774,7 +20896,7 @@ function sessionDispatchSettings(file, env, options) {
20774
20896
  return { dispatch: offWithError(file.error), warnings: [], missing: null };
20775
20897
  const config = readEnvoyConfig(file.path, env, options);
20776
20898
  return {
20777
- dispatch: dispatchSettings(env, config),
20899
+ dispatch: dispatchSettings(env, config, { commands: options.commands }),
20778
20900
  warnings: config.kind === "read" ? config.warnings : [],
20779
20901
  missing: config.kind === "read" ? config.missing : null
20780
20902
  };
@@ -14206,7 +14206,7 @@ var dispatchToolSpecs = [
14206
14206
  {
14207
14207
  name: "dispatch_doc_read",
14208
14208
  example: { issue: "DSP-1" },
14209
- description: "Read a live document or a named document version, or the text of an uploaded file at its latest or named version. " + "Do not use it for issue status, asks, or events; " + "use dispatch read instead. Supply ref, issue, or project plus artifact; issue plus an omitted artifact reads the primary document. " + "A live read returns its document token for an optional dispatch doc-edit precondition. With blocks it lists the live document's blocks instead of its markdown, in document order: each block's id, type, token, anchored comments and asks, and opening words, and under a table each row by the index delete_row takes and each cell by its row and the column delete_column takes; block ids, block tokens and those indexes are what dispatch doc-edit takes. " + "After the markdown it prints every open thread anchored on the document in full: an open ask with its id and question and each reply's author, time and text, and a comment thread with its root's id, author, quote and text and each reply's author, time and text, in order; a resolved thread, and an ask that is answered or resolved, is not printed. " + "A picture (an uploaded PNG, JPEG, GIF or WebP of at most 3,750,000 bytes, 5 MB once base64-encoded) comes back as an image you see, with its name, type, size and version, every time you ask, including one a read already showed this session; " + "any other file that is not UTF-8 text is described, with the route that serves its bytes. " + OWNER_REFERENCE,
14209
+ description: "Read a live document or a named document version, or the text of an uploaded file at its latest or named version. " + "Do not use it for issue status, asks, or events; " + "use dispatch read instead. Supply ref, issue, or project plus artifact; issue plus an omitted artifact reads the primary document. " + "A live read returns its document token for an optional dispatch doc-edit precondition. With blocks it lists the live document's blocks instead of its markdown, in document order: each block's id, type, token, anchored comments and asks, and opening words, and under a table each row by the index delete_row takes and each cell by its row and the column delete_column takes; block ids, block tokens and those indexes are what dispatch doc-edit takes. " + "After the markdown it prints every open thread anchored on the document in full: an open ask with its id and question and each reply's author, time and text, and a comment thread with its root's id, author, time, quote and text and each reply's author, time and text, in order; a resolved thread, and an ask that is answered or resolved, is not printed. " + "A picture (an uploaded PNG, JPEG, GIF or WebP of at most 3,750,000 bytes, 5 MB once base64-encoded) comes back as an image you see, with its name, type, size and version, every time you ask, including one a read already showed this session; " + "any other file that is not UTF-8 text is described, with the route that serves its bytes. " + OWNER_REFERENCE,
14210
14210
  arguments: (z) => ({
14211
14211
  issue: z.string().describe(ISSUE_REFERENCE).optional(),
14212
14212
  project: z.string().describe("Project key owning the document.").optional(),
@@ -14366,6 +14366,13 @@ var dispatchToolSpecs = [
14366
14366
  }),
14367
14367
  strict: true
14368
14368
  },
14369
+ {
14370
+ name: "dispatch_answers",
14371
+ example: {},
14372
+ description: "List every answer and comment a person wrote to this session in the last 72 hours, newest first, each in full: " + "answers to its asks, replies on its asks and in its comment threads, comments on its documents and on issues routed to it, " + "mentions of it, messages to it and replies to its messages. Each item names its time, its writer, what it is, its " + "dispatch:// reference and its issue or document, an answer's question and choices, and the words, never cut. " + "Run it when the list of people's answers on a model request says it left older items out. " + "It reads this session's id from CLAUDE_CODE_SESSION_ID under Claude Code and DISPATCH_SESSION_ID elsewhere, so a subagent reads its parent's.",
14373
+ arguments: () => ({}),
14374
+ strict: true
14375
+ },
14369
14376
  {
14370
14377
  name: "dispatch_whoami",
14371
14378
  example: {},
@@ -14834,7 +14841,7 @@ function runCommand(command, env, cwd) {
14834
14841
  rmSync(directory, { recursive: true, force: true });
14835
14842
  }
14836
14843
  }
14837
- function fileValue(value, field, filePath, env) {
14844
+ function fileValue(value, field, filePath, env, commands = "run") {
14838
14845
  if (!value.startsWith("!")) {
14839
14846
  if (!sendable(value)) {
14840
14847
  throw new Error(`${filePath}: ${field} holds whitespace or a control character`);
@@ -14845,7 +14852,11 @@ function fileValue(value, field, filePath, env) {
14845
14852
  const cwd = path.dirname(path.resolve(filePath));
14846
14853
  const key = JSON.stringify([command, filePath, cwd]);
14847
14854
  let outcome = commandOutcomes.get(key);
14848
- if (outcome === undefined || "failure" in outcome && !outcome.timedOut && Date.now() - outcome.at >= FAILURE_KEPT_MS) {
14855
+ if (commands === "kept") {
14856
+ if (outcome === undefined) {
14857
+ throw new Error(`${filePath}: ${field}'s command has not run in this process yet, and this read does not run it`);
14858
+ }
14859
+ } else if (outcome === undefined || "failure" in outcome && !outcome.timedOut && Date.now() - outcome.at >= FAILURE_KEPT_MS) {
14849
14860
  outcome = { ...runCommand(command, env, cwd), at: Date.now() };
14850
14861
  commandOutcomes.set(key, outcome);
14851
14862
  }
@@ -14955,7 +14966,7 @@ function bearerSource(env, section, written) {
14955
14966
  const bearer = written();
14956
14967
  return bearer === undefined ? { kind: "none" } : { kind: "file", ...bearer };
14957
14968
  }
14958
- function fileBearer(bearer, url, section, env) {
14969
+ function fileBearer(bearer, url, section, env, commands = "run") {
14959
14970
  const named = httpUrl2(url)?.origin;
14960
14971
  const paired = httpUrl2(bearer.server)?.origin;
14961
14972
  if (named !== paired) {
@@ -14967,7 +14978,7 @@ function fileBearer(bearer, url, section, env) {
14967
14978
  if (bearer.token === "") {
14968
14979
  throw new Error(`${bearer.field} in ${bearer.path} must be a non-empty bearer token`);
14969
14980
  }
14970
- return fileValue(bearer.token, bearer.field, bearer.path, env);
14981
+ return fileValue(bearer.token, bearer.field, bearer.path, env, commands);
14971
14982
  }
14972
14983
 
14973
14984
  // ../envoy-client/src/dispatch-config.ts
@@ -15002,7 +15013,30 @@ function writtenToken(config, checkout) {
15002
15013
  checkout: false
15003
15014
  };
15004
15015
  }
15005
- function dispatchSettings(env, config) {
15016
+ function dispatchSettings(env, config, options = {}) {
15017
+ const target = dispatchTarget(env, config);
15018
+ if (!target.enabled)
15019
+ return target;
15020
+ const { url, source } = target;
15021
+ let token;
15022
+ try {
15023
+ token = source.kind === "variable" ? source.token : fileBearer(source, url, "DISPATCH", env, options.commands);
15024
+ } catch (error) {
15025
+ return offWithError(messageFor(error));
15026
+ }
15027
+ const publicUrl = nonEmpty(env.DISPATCH_PUBLIC_URL);
15028
+ const publicParsed = publicUrl === undefined ? undefined : httpUrl2(publicUrl);
15029
+ if (publicUrl !== undefined && (publicParsed === undefined || publicParsed.port === "0")) {
15030
+ return offWithError("DISPATCH_PUBLIC_URL must be an http or https URL");
15031
+ }
15032
+ return {
15033
+ enabled: true,
15034
+ url,
15035
+ token,
15036
+ ...publicUrl === undefined ? {} : { publicUrl: publicUrl.replace(/\/+$/, "") }
15037
+ };
15038
+ }
15039
+ function dispatchTarget(env, config) {
15006
15040
  if (config.kind === "failed")
15007
15041
  return offWithError(config.error);
15008
15042
  const explicitUrl = nonEmpty(env.DISPATCH_URL);
@@ -15025,27 +15059,17 @@ function dispatchSettings(env, config) {
15025
15059
  if (explicitUrl !== undefined && httpUrl2(explicitUrl) === undefined) {
15026
15060
  return offWithError("DISPATCH_URL must be an http or https URL");
15027
15061
  }
15062
+ let source;
15028
15063
  try {
15029
- const source = checkout !== null && checkoutServer !== undefined ? checkoutPair(checkout, checkoutServer) : bearerSource(env, "DISPATCH", () => writtenToken(config, checkout));
15030
- if (source.kind === "none") {
15031
- return offWithError(explicitUrl === undefined ? `${config.path}: dispatch is enabled and no token is: set dispatch.token, or DISPATCH_TOKEN or DISPATCH_TOKEN_FILE` : `DISPATCH_URL is set and no token is: set DISPATCH_TOKEN or DISPATCH_TOKEN_FILE, or dispatch.token in ${config.path}`);
15032
- }
15033
- const url = (explicitUrl ?? (source.kind === "file" ? source.server : own.serverUrl ?? DEFAULT_SERVER_URL)).replace(/\/+$/, "");
15034
- const token = source.kind === "variable" ? source.token : fileBearer(source, url, "DISPATCH", env);
15035
- const publicUrl = nonEmpty(env.DISPATCH_PUBLIC_URL);
15036
- const publicParsed = publicUrl === undefined ? undefined : httpUrl2(publicUrl);
15037
- if (publicUrl !== undefined && (publicParsed === undefined || publicParsed.port === "0")) {
15038
- return offWithError("DISPATCH_PUBLIC_URL must be an http or https URL");
15039
- }
15040
- return {
15041
- enabled: true,
15042
- url,
15043
- token,
15044
- ...publicUrl === undefined ? {} : { publicUrl: publicUrl.replace(/\/+$/, "") }
15045
- };
15064
+ source = checkout !== null && checkoutServer !== undefined ? checkoutPair(checkout, checkoutServer) : bearerSource(env, "DISPATCH", () => writtenToken(config, checkout));
15046
15065
  } catch (error) {
15047
15066
  return offWithError(messageFor(error));
15048
15067
  }
15068
+ if (source.kind === "none") {
15069
+ return offWithError(explicitUrl === undefined ? `${config.path}: dispatch is enabled and no token is: set dispatch.token, or DISPATCH_TOKEN or DISPATCH_TOKEN_FILE` : `DISPATCH_URL is set and no token is: set DISPATCH_TOKEN or DISPATCH_TOKEN_FILE, or dispatch.token in ${config.path}`);
15070
+ }
15071
+ const url = (explicitUrl ?? (source.kind === "file" ? source.server : own.serverUrl ?? DEFAULT_SERVER_URL)).replace(/\/+$/, "");
15072
+ return { enabled: true, url, source };
15049
15073
  }
15050
15074
 
15051
15075
  // ../envoy-client/src/envoy-settings.ts
@@ -15619,7 +15643,7 @@ function sourceSession(sessionID) {
15619
15643
  // src/server.ts
15620
15644
  import { tool } from "@opencode-ai/plugin/tool";
15621
15645
  // package.json
15622
- var version2 = "12.0.1";
15646
+ var version2 = "12.1.0";
15623
15647
 
15624
15648
  // src/log.ts
15625
15649
  import { appendFile, mkdir } from "fs/promises";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/opencode-legion-envoy",
3
- "version": "12.0.1",
3
+ "version": "12.1.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "dist/src/server.js",
@@ -45,12 +45,12 @@ Which machines, files or people, the numbers, then "A happens to X, or B to Y?"
45
45
 
46
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, 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>.>`.
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>.>`, which shows no event Dispatch marks quiet, like an unnamed autosave.
49
49
 
50
50
  ## Close what you opened
51
51
 
52
- - An ask you opened is yours until answered or resolved. If it is answered another way or made moot, `dispatch resolve-ask` it that turn.
53
- - Resolve each open ask a later write names that is no longer needed; retract one a new ask replaces.
52
+ - An ask you opened is yours until answered or resolved. `dispatch resolve-ask` it that turn once anything makes it needless: another answer, a later write, work dropped.
53
+ - Retract an ask a new one replaces.
54
54
 
55
55
  ## Structure over stream
56
56
 
@@ -33,7 +33,7 @@ For each spec, ask, comment and message:
33
33
  - Write about the current design, never the person or how people reached it. Never quote people or upload their words.
34
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
- - 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."
36
+ - A sentence that cites a gate passing (a test, review, check or proof) also says what the pass does not show: "The tests pass; they catch a wrong verdict, not a proof of a contradiction."
37
37
  - Name your session only if a reader must reach it.
38
38
  - Keep only text that helps a reader understand, decide, build, check.
39
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.