@sjawhar/opencode-legion-envoy 1.46.0 → 1.47.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.
@@ -13552,6 +13552,13 @@ function date4(params) {
13552
13552
  // ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/classic/external.js
13553
13553
  config(en_default());
13554
13554
  // ../contracts/src/dispatch-api.ts
13555
+ function serviceSubjectLabel(subject) {
13556
+ const parts = subject.split(":");
13557
+ if (parts.length === 4 && parts[0] === "system" && parts[1] === "serviceaccount" && parts[2] !== "" && parts[3] !== "") {
13558
+ return `${parts[2]}/${parts[3]}`;
13559
+ }
13560
+ return subject;
13561
+ }
13555
13562
  var ASK_TURNS = ["human", "agent"];
13556
13563
  var DELIVERY_CAPABILITIES = ["aside", "btw", "steer"];
13557
13564
  var DispatchEventSchema = object({
@@ -13952,7 +13959,7 @@ var dispatchToolSpecs = [
13952
13959
  },
13953
13960
  description: "Edit an open question in place. Use it to correct or refine the same decision; retract the " + "old ask and open a new one when the decision itself changes. Previous text remains in the " + "event log. Only the asking session can edit it; answered or resolved asks cannot be edited.",
13954
13961
  arguments: (z) => ({
13955
- ask: z.string().describe("Ask id to edit."),
13962
+ ask: z.string().describe("Ask id (uuid); an 8+ hex prefix unique among this session's own open asks works too."),
13956
13963
  question: z.string({ max: ASK_QUESTION_MAX }).describe(`Replacement decision question, at most ${ASK_QUESTION_MAX} characters.`).optional(),
13957
13964
  options: z.array(z.object({
13958
13965
  label: z.string().describe("Selectable option label."),
@@ -13978,7 +13985,7 @@ var dispatchToolSpecs = [
13978
13985
  },
13979
13986
  description: "Retract an open question that is moot or resolve one after finding the answer. This closes the question without answering it.",
13980
13987
  arguments: (z) => ({
13981
- ask: z.string().describe("Ask id to close."),
13988
+ ask: z.string().describe("Ask id (uuid) to close; an 8+ hex prefix unique among this session's own open asks " + "works too."),
13982
13989
  kind: z.enum(["retracted", "resolved"]).describe("Whether the ask is retracted or self-resolved."),
13983
13990
  reason: z.string({ min: 1 }).describe("Why the open ask no longer needs a human answer.")
13984
13991
  })
@@ -13997,7 +14004,7 @@ var dispatchToolSpecs = [
13997
14004
  example: { ask: "01234567-0000-4000-8000-000000000001", action: "follow" },
13998
14005
  description: "Follow or unfollow an ask. Every session that opens or replies to an ask follows it: its answer, " + "edits, resolution, and replies reach that session directly. Unfollow to stop; follow to rejoin or " + "to hear an ask you never wrote to. Whole-issue subscription is separate: envoy_subscribe " + "notifications.dispatch.issue.<KEY>.>",
13999
14006
  arguments: (z) => ({
14000
- ask: z.string().describe("Full ask id (uuid)."),
14007
+ ask: z.string().describe("Ask id (uuid); an 8+ hex prefix unique among this session's own open asks works too."),
14001
14008
  action: z.enum(["follow", "unfollow"]).describe("follow | unfollow")
14002
14009
  }),
14003
14010
  strict: true
@@ -15688,24 +15695,51 @@ function messageIdOf(value) {
15688
15695
  return id === undefined ? undefined : normalizeUUID(id);
15689
15696
  }
15690
15697
  var idPrefixPattern = /^[0-9a-f][0-9a-f-]{7,}$/i;
15691
- async function resolveIdPrefix(tool, kind, ref, list) {
15692
- const fullID = normalizeUUID(ref.id);
15698
+ function refOwnerName(ref) {
15699
+ return ref.owner.kind === "issue" ? ref.owner.issue : `${ref.owner.project}/${ref.artifact}`;
15700
+ }
15701
+ async function resolveIdPrefix(tool, kind, id, ownerName, list) {
15702
+ const fullID = normalizeUUID(id);
15693
15703
  if (fullID !== undefined)
15694
15704
  return fullID;
15695
- const ownerName = ref.owner.kind === "issue" ? ref.owner.issue : `${ref.owner.project}/${ref.artifact}`;
15696
- if (!idPrefixPattern.test(ref.id)) {
15705
+ if (!idPrefixPattern.test(id)) {
15697
15706
  throw new ToolInputError(tool, [
15698
- `${kind} id ${ref.id} must be a full uuid or a prefix of at least 8 hex characters`
15707
+ `${kind} id ${id} must be a full uuid or a prefix of at least 8 hex characters`
15699
15708
  ]);
15700
15709
  }
15701
- const prefix = ref.id.toLowerCase();
15710
+ const prefix = id.toLowerCase();
15702
15711
  const matches = (await list()).filter((item) => item.id.toLowerCase().startsWith(prefix));
15703
15712
  if (matches.length === 1 && matches[0] !== undefined)
15704
15713
  return matches[0].id;
15705
15714
  throw new ToolInputError(tool, [
15706
- matches.length === 0 ? `${kind} id ${ref.id} matches none of the ${kind}s on ${ownerName}; use the full id` : `${kind} id ${ref.id} matches ${matches.length} ${kind}s on ${ownerName}; use the full id`
15715
+ matches.length === 0 ? `${kind} id ${id} matches none of the ${kind}s on ${ownerName}; use the full id` : `${kind} id ${id} matches ${matches.length} ${kind}s on ${ownerName}; use the full id`
15707
15716
  ]);
15708
15717
  }
15718
+ var askIdShapeProblem = "ask ids are uuids (a prefix of at least 8 hex characters works)";
15719
+ async function sessionOpenAsks(client, sessionId) {
15720
+ const session = sessionId?.trim();
15721
+ if (!session)
15722
+ throw new Error("host session id is required to read this session's open asks");
15723
+ return (await client.openAsks(session)).asks;
15724
+ }
15725
+ function openAskHints(asks) {
15726
+ if (asks.length === 0)
15727
+ return "you have no open asks";
15728
+ const hints = asks.slice(0, askHintLimit).map((ask) => `${ask.id.slice(0, 8)} \u2014 ${textHead(ask.question)}`);
15729
+ return `your open asks: ${hints.join("; ")}`;
15730
+ }
15731
+ async function resolveAskArgument(tool, args, client, sessionId) {
15732
+ const id = askId(args);
15733
+ if (normalizeUUID(id) === undefined && !idPrefixPattern.test(id)) {
15734
+ const asks = await sessionOpenAsks(client, sessionId).catch(() => {
15735
+ return;
15736
+ });
15737
+ throw new ToolInputError(tool, [
15738
+ asks === undefined ? askIdShapeProblem : `${askIdShapeProblem}; ${openAskHints(asks)}`
15739
+ ]);
15740
+ }
15741
+ return resolveIdPrefix(tool, "ask", id, "this session", () => sessionOpenAsks(client, sessionId));
15742
+ }
15709
15743
  function messageInReplyTo(args) {
15710
15744
  const inReplyTo = optionalString(args, "in_reply_to");
15711
15745
  return inReplyTo === undefined ? undefined : messageIdOf(inReplyTo);
@@ -15724,7 +15758,8 @@ function argumentProblems(tool, args) {
15724
15758
  break;
15725
15759
  }
15726
15760
  case "dispatch_edit_ask":
15727
- case "dispatch_resolve_ask": {
15761
+ case "dispatch_resolve_ask":
15762
+ case "dispatch_follow": {
15728
15763
  const ask = optionalString(args, "ask");
15729
15764
  if (ask?.startsWith("dispatch://") && parseDispatchRef(ask)?.kind !== "ask") {
15730
15765
  problems.push(askIdProblem);
@@ -16117,9 +16152,16 @@ function eventHead(event) {
16117
16152
  return;
16118
16153
  }
16119
16154
  }
16155
+ function actorText(actor) {
16156
+ const service = actor.kind === "session" ? actor.service : undefined;
16157
+ if (service === undefined) {
16158
+ return `${actor.kind} ${actor.id}`;
16159
+ }
16160
+ return `${actor.kind} ${actor.id} (as ${serviceSubjectLabel(service)})`;
16161
+ }
16120
16162
  function eventLine(event) {
16121
16163
  const head = eventHead(event);
16122
- return `- #${event.seq} ${event.type} \xB7 ${event.actor.kind} ${event.actor.id} \xB7 ${event.created_at}${head === undefined || head === "" ? "" : ` \xB7 ${head}`}`;
16164
+ return `- #${event.seq} ${event.type} \xB7 ${actorText(event.actor)} \xB7 ${event.created_at}${head === undefined || head === "" ? "" : ` \xB7 ${head}`}`;
16123
16165
  }
16124
16166
  function childrenSummary(issue) {
16125
16167
  return [
@@ -16132,7 +16174,7 @@ function childrenSummary(issue) {
16132
16174
  function askSummary({ ask, replies }, graph) {
16133
16175
  const answer = ask.answer;
16134
16176
  const chain = replies.flatMap((reply) => [
16135
- `${reply.id} \xB7 ${reply.author.kind} ${reply.author.id}`,
16177
+ `${reply.id} \xB7 ${actorText(reply.author)}`,
16136
16178
  `Body: ${reply.body}`
16137
16179
  ]);
16138
16180
  return [
@@ -16199,12 +16241,12 @@ function formatOpenAsksSummary(response, baseUrl) {
16199
16241
  }
16200
16242
  function commentSummary({ comment, replies }, graph) {
16201
16243
  const root = [
16202
- `${comment.id} \xB7 ${comment.author.kind} ${comment.author.id}`,
16244
+ `${comment.id} \xB7 ${actorText(comment.author)}`,
16203
16245
  ...comment.anchor?.quote === undefined ? [] : [`> ${comment.anchor.quote}`],
16204
16246
  `Body: ${comment.body}`
16205
16247
  ];
16206
16248
  const chain = replies.flatMap((reply) => [
16207
- `${reply.id} \xB7 ${reply.author.kind} ${reply.author.id}`,
16249
+ `${reply.id} \xB7 ${actorText(reply.author)}`,
16208
16250
  ...reply.anchor?.quote === undefined ? [] : [`> ${reply.anchor.quote}`],
16209
16251
  `Body: ${reply.body}`
16210
16252
  ]);
@@ -16218,12 +16260,9 @@ function commentSummary({ comment, replies }, graph) {
16218
16260
  `);
16219
16261
  }
16220
16262
  function messageSummary({ message, replies }, graph) {
16221
- const root = [
16222
- `${message.id} \xB7 ${message.author.kind} ${message.author.id}`,
16223
- `Body: ${message.body}`
16224
- ];
16263
+ const root = [`${message.id} \xB7 ${actorText(message.author)}`, `Body: ${message.body}`];
16225
16264
  const chain = replies.flatMap((reply) => [
16226
- `${reply.id} \xB7 ${reply.author.kind} ${reply.author.id}`,
16265
+ `${reply.id} \xB7 ${actorText(reply.author)}`,
16227
16266
  `Body: ${reply.body}`
16228
16267
  ]);
16229
16268
  return [
@@ -16305,9 +16344,11 @@ async function executeDispatchTool(input) {
16305
16344
  const client = dispatchClient();
16306
16345
  const identity = await client.whoami();
16307
16346
  const owner = identity.kind === "agent" ? identity.owner : identity.login.toLowerCase();
16347
+ const service = identity.kind === "agent" ? identity.service ?? null : null;
16348
+ const unowned = "no owner, so issues you create without an assignee are unassigned (or inherit their parent's).";
16308
16349
  return {
16309
- text: owner === null ? `Session ${sessionId} runs under the shared token: no owner, so issues you create without an assignee are unassigned (or inherit their parent's).` : `Session ${sessionId} acts for ${owner}: issues you create without an assignee are assigned to ${owner}.`,
16310
- details: { session: sessionId, owner }
16350
+ text: owner !== null ? `Session ${sessionId} acts for ${owner}: issues you create without an assignee are assigned to ${owner}.` : service !== null ? `Session ${sessionId} runs as service ${serviceSubjectLabel(service)}: ${unowned}` : `Session ${sessionId} runs under the shared token: ${unowned}`,
16351
+ details: { session: sessionId, owner, service }
16311
16352
  };
16312
16353
  }
16313
16354
  const args = parsed.success ? parsed.data : ownerArguments.args;
@@ -16518,7 +16559,8 @@ async function executeDispatchTool(input) {
16518
16559
  }
16519
16560
  case "dispatch_resolve_ask": {
16520
16561
  const kind = stringArg(args, "kind");
16521
- const ask = await client.resolveAsk(stringArg(args, "ask"), {
16562
+ const id = await resolveAskArgument(input.tool, args, client, input.sessionId);
16563
+ const ask = await client.resolveAsk(id, {
16522
16564
  kind,
16523
16565
  reason: stringArg(args, "reason"),
16524
16566
  actor
@@ -16537,11 +16579,11 @@ async function executeDispatchTool(input) {
16537
16579
  let document;
16538
16580
  if (ref?.owner.kind === "issue") {
16539
16581
  const issueKey = ref.owner.issue;
16540
- id = await resolveIdPrefix(input.tool, "comment", ref, () => client.getComments(issueKey));
16582
+ id = await resolveIdPrefix(input.tool, "comment", ref.id, refOwnerName(ref), () => client.getComments(issueKey));
16541
16583
  } else if (ref !== null) {
16542
16584
  const artifact = (await resolveArtifact(client, ref.owner, ref.artifact)).artifact;
16543
16585
  document = artifact;
16544
- id = await resolveIdPrefix(input.tool, "comment", ref, () => client.getArtifactComments(artifact.id));
16586
+ id = await resolveIdPrefix(input.tool, "comment", ref.id, refOwnerName(ref), () => client.getArtifactComments(artifact.id));
16545
16587
  }
16546
16588
  const comment = await client.resolveComment(id, actor);
16547
16589
  const { label, details } = await commentOwnerResult(client, comment, document);
@@ -16585,7 +16627,8 @@ ${followsAsk(askOwner)}`,
16585
16627
  const options = args.options;
16586
16628
  const multiple = optionalBoolean(args, "multiple");
16587
16629
  const urgency = askUrgency(args);
16588
- const ask = await client.editAsk(askId(args), {
16630
+ const id = await resolveAskArgument(input.tool, args, client, input.sessionId);
16631
+ const ask = await client.editAsk(id, {
16589
16632
  ...question === undefined ? {} : { question },
16590
16633
  ...Array.isArray(options) ? { options } : {},
16591
16634
  ...multiple === undefined ? {} : { multiple },
@@ -16812,7 +16855,7 @@ ${trailer.join(`
16812
16855
  const sessionId = input.sessionId?.trim();
16813
16856
  if (!sessionId)
16814
16857
  throw new Error("host session id is required for dispatch_follow");
16815
- const ask = askId(args);
16858
+ const ask = await resolveAskArgument(input.tool, args, client, sessionId);
16816
16859
  const action = stringArg(args, "action");
16817
16860
  if (action === "unfollow") {
16818
16861
  await client.unfollowAsk(ask, sessionId, actor);
@@ -16828,7 +16871,7 @@ ${trailer.join(`
16828
16871
  case "dispatch_read": {
16829
16872
  if (ownerArguments.ref?.kind === "ask") {
16830
16873
  const ref = ownerArguments.ref;
16831
- const id = await resolveIdPrefix(input.tool, "ask", ref, async () => ref.owner.kind === "issue" ? client.listIssueAsks(ref.owner.issue) : client.getArtifactAsks((await resolveArtifact(client, ref.owner, ref.artifact)).artifact.id, "all"));
16874
+ const id = await resolveIdPrefix(input.tool, "ask", ref.id, refOwnerName(ref), async () => ref.owner.kind === "issue" ? client.listIssueAsks(ref.owner.issue) : client.getArtifactAsks((await resolveArtifact(client, ref.owner, ref.artifact)).artifact.id, "all"));
16832
16875
  const askRead = await client.getAsk(id);
16833
16876
  const askRef = refTarget(ref, "ask", id);
16834
16877
  return {
@@ -16838,7 +16881,7 @@ ${trailer.join(`
16838
16881
  }
16839
16882
  if (ownerArguments.ref?.kind === "comment") {
16840
16883
  const ref = ownerArguments.ref;
16841
- const id = await resolveIdPrefix(input.tool, "comment", ref, async () => ref.owner.kind === "issue" ? client.getComments(ref.owner.issue) : client.getArtifactComments((await resolveArtifact(client, ref.owner, ref.artifact)).artifact.id));
16884
+ const id = await resolveIdPrefix(input.tool, "comment", ref.id, refOwnerName(ref), async () => ref.owner.kind === "issue" ? client.getComments(ref.owner.issue) : client.getArtifactComments((await resolveArtifact(client, ref.owner, ref.artifact)).artifact.id));
16842
16885
  const comment = await client.getComment(id);
16843
16886
  const commentRef = refTarget(ref, "comment", id);
16844
16887
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/opencode-legion-envoy",
3
- "version": "1.46.0",
3
+ "version": "1.47.0",
4
4
  "type": "module",
5
5
  "main": "dist/src/server.js",
6
6
  "exports": {
@@ -148,7 +148,7 @@ An ask goes to the issue's assignee: their Inbox opens on **Mine**, which lists
148
148
  ```ts
149
149
  dispatch_whoami({})
150
150
  ```
151
- It returns `details` `{ session, owner }`: `owner` is the lowercase login of the human whose personal token you run under, or `null` under the shared token. An issue you create without `assignee` goes to your owner; under the shared token it inherits its parent's assignee, or stays unassigned without a parent. If an issue you are asking on is unassigned and the answer matters, assign it to your owner (`PATCH /api/v1/issues/{key}` with `{"assignee": "<login>"}`; any authenticated caller may reassign, and an unlisted login is refused with `ASSIGNEE_NOT_ALLOWED`) or name in the question who should answer it. Never reassign an issue a human holds to get an answer faster: that is the human's call.
151
+ It returns `details` `{ session, owner, service }`: `owner` is the lowercase login of the human whose personal token you run under, or `null` under the shared token or a verified service token, in which case `service` is that token's subject (`system:serviceaccount:<namespace>:<name>`, `null` otherwise) and every write you make is rendered `(as <namespace>/<name>)` — the namespace is kept because every namespace has a `default` service account, and a subject that is not a Kubernetes one is shown whole. An issue you create without `assignee` goes to your owner; with no owner it inherits its parent's assignee, or stays unassigned without a parent. If an issue you are asking on is unassigned and the answer matters, assign it to your owner (`PATCH /api/v1/issues/{key}` with `{"assignee": "<login>"}`; any authenticated caller may reassign, and an unlisted login is refused with `ASSIGNEE_NOT_ALLOWED`) or name in the question who should answer it. Never reassign an issue a human holds to get an answer faster: that is the human's call.
152
152
 
153
153
  Architects create newly tracked child work with:
154
154
  ```ts
@@ -708,7 +708,9 @@ the host tells you once per ask. Leave a thread you no longer need, or rejoin on
708
708
  dispatch_follow({ ask, action: "follow" | "unfollow" })
709
709
  ```
710
710
 
711
- `ask` is the full ask id or a `dispatch://KEY/ask/<id>` reference. A human may also remove you from the ask card; either way you are
711
+ `ask` is the full ask id or a `dispatch://KEY/ask/<id>` reference; `dispatch_follow`, `dispatch_edit_ask`, and
712
+ `dispatch_resolve_ask` also take an 8+ hex prefix that is unique among your own open asks, and refuse anything shorter by naming
713
+ those asks. A human may also remove you from the ask card; either way you are
712
714
  told with an `ask.follower_removed` notice, and a human adding you arrives as `ask.follower_added`.
713
715
 
714
716
  No write subscribes you to an issue or document. Following covers your own asks and the threads you joined; everything else on the