@sjawhar/opencode-legion-envoy 11.4.0 → 12.0.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
@@ -14009,6 +14009,13 @@ var CommentDeliverySchema = object({
14009
14009
  reply_id: string2().nullable(),
14010
14010
  created_at: string2()
14011
14011
  });
14012
+ var ReplyBlockSchema = object({
14013
+ node: string2(),
14014
+ ref: string2().optional(),
14015
+ block_id: string2(),
14016
+ text: string2(),
14017
+ removed: boolean2()
14018
+ });
14012
14019
  var CommentEventPayloadSchema = object({
14013
14020
  id: string2().optional(),
14014
14021
  artifact_name: string2().optional(),
@@ -14034,6 +14041,7 @@ var CommentEventPayloadSchema = object({
14034
14041
  created_at: string2().optional(),
14035
14042
  mentions: array(CommentMentionSchema).optional(),
14036
14043
  deliveries: array(CommentDeliverySchema).optional(),
14044
+ reply_block: ReplyBlockSchema.nullish(),
14037
14045
  thread_logins: array(string2()).optional()
14038
14046
  }).passthrough();
14039
14047
  var MessageEventPayloadSchema = object({
@@ -14044,6 +14052,7 @@ var MessageEventPayloadSchema = object({
14044
14052
  reply_body: string2().optional(),
14045
14053
  thread_target: string2().optional(),
14046
14054
  author: object({ kind: string2(), id: string2() }).optional(),
14055
+ reply_block: ReplyBlockSchema.nullish(),
14047
14056
  thread_logins: array(string2()).optional()
14048
14057
  });
14049
14058
  var DispatchTargetedResourceIDSchema = uuid2();
@@ -14598,7 +14607,7 @@ var dispatchToolSpecs = [
14598
14607
  quote: z.string().describe("Optional exact quoted document text.").optional(),
14599
14608
  occurrence: z.number({ int: true, min: 0 }).describe("Optional zero-based occurrence of quote.").optional(),
14600
14609
  body: z.string({ max: DISPATCH_BODY_MAX }).describe(`Review comment, at most ${DISPATCH_BODY_MAX} characters.`),
14601
- reply_to: z.string().describe("A comment id (uuid); replying to any comment in a thread continues that thread (an " + "ask's clarification thread included). To reply to an ask, use reply_to_ask with the " + "ask id instead.").optional(),
14610
+ reply_to: z.string().describe("A comment id (uuid); replying to any comment in a thread continues that thread (an " + "ask's clarification thread included). A block reference to one block of the comment " + "(dispatch://KEY/comment/<id>#b-<block id>) replies to that comment and names the " + "passage the reply answers. To reply to an ask, use reply_to_ask with the " + "ask id instead.").optional(),
14602
14611
  reply_to_ask: z.string().describe("Optional ask id to reply to, threading this comment under that question. Mutually " + "exclusive with reply_to.").optional(),
14603
14612
  turn: z.enum(["agent", "human"]).describe("Only with reply_to_ask: who holds the turn after this reply. agent: a progress note - " + "you keep the turn and the ask stays 'Waiting on agents' for the human; human (default): " + "you need the human to act - the ask returns to 'Waiting on you'.").optional(),
14604
14613
  images: imagesArgument(z, "the issue (or the project document's project)", `the ${DISPATCH_BODY_MAX}-character body limit`)
@@ -14633,7 +14642,7 @@ var dispatchToolSpecs = [
14633
14642
  arguments: (z) => ({
14634
14643
  issue: z.string().describe(`${ISSUE_REFERENCE} Omit it only when in_reply_to answers a human's direct message to this session.`).optional(),
14635
14644
  body: z.string({ max: DISPATCH_BODY_MAX }).describe(`Update text, at most ${DISPATCH_BODY_MAX} characters.`),
14636
- in_reply_to: z.string().describe("Optional message id or dispatch://KEY/message/<id> reference to reply to, threading " + "this message under it so the reply stays with the original in the Conversation. " + "A bare id with no issue answers a human's direct message to this session; a " + "dispatch://KEY/message/<id> names the issue its message lives on, so that form is a " + "reply on that issue.").optional(),
14645
+ in_reply_to: z.string().describe("Optional message id or dispatch://KEY/message/<id> reference to reply to, threading " + "this message under it so the reply stays with the original in the Conversation. " + "A block reference to one block of the message (dispatch://KEY/message/<id>#b-<block id>) " + "replies to that message and names the passage the reply answers. " + "A bare id with no issue answers a human's direct message to this session; a " + "dispatch://KEY/message/<id> names the issue its message lives on, so that form is a " + "reply on that issue.").optional(),
14637
14646
  images: imagesArgument(z, "the issue (a reply to a direct message: your own conversation, dispatch://agent/<your session id>)", `the ${DISPATCH_BODY_MAX}-character body limit`)
14638
14647
  }),
14639
14648
  validation: messageValidation
@@ -16117,6 +16126,12 @@ class DispatchClient {
16117
16126
  async artifactBlocks(id) {
16118
16127
  return this.#json("GET", ["api", "v1", "artifacts", id, "blocks"]);
16119
16128
  }
16129
+ async commentBlocks(id) {
16130
+ return this.#json("GET", ["api", "v1", "comments", id, "blocks"]);
16131
+ }
16132
+ async messageBlocks(id, session) {
16133
+ return this.#json("GET", ["api", "v1", "messages", id, "blocks"], undefined, session === undefined ? undefined : { session });
16134
+ }
16120
16135
  async docEdit(id, input) {
16121
16136
  return this.#json("POST", ["api", "v1", "artifacts", id, "edits"], input);
16122
16137
  }
@@ -16417,10 +16432,26 @@ function dispatchDocumentRef(owner, slug) {
16417
16432
  function dispatchChildRef(ownerRef, kind, id) {
16418
16433
  return `${ownerRef}/${kind}/${id}`;
16419
16434
  }
16420
- function parseBlockRef(document, fragment) {
16421
- const target = parseBlockFragment(fragment);
16422
- const parsed = parseDispatchRef(document);
16423
- if (target === undefined || parsed === null || parsed.version !== undefined || parsed.kind !== "spec" && parsed.kind !== "artifact") {
16435
+ function parseBlockRef(holder, fragment) {
16436
+ const target = holder.includes("@") ? undefined : parseBlockFragment(fragment);
16437
+ const parsed = parseDispatchRef(holder);
16438
+ if (target === undefined || parsed === null) {
16439
+ return null;
16440
+ }
16441
+ const pin = target.pin === undefined ? {} : { pin: target.pin };
16442
+ const artifact = parsed.artifact === undefined ? {} : { artifact: parsed.artifact };
16443
+ if (parsed.kind === "comment" || parsed.kind === "message" && target.pin === undefined) {
16444
+ return {
16445
+ owner: parsed.owner,
16446
+ kind: "block",
16447
+ id: parsed.id,
16448
+ block: target.block,
16449
+ item: { kind: parsed.kind, id: parsed.id },
16450
+ ...artifact,
16451
+ ...pin
16452
+ };
16453
+ }
16454
+ if (parsed.kind !== "spec" && parsed.kind !== "artifact") {
16424
16455
  return null;
16425
16456
  }
16426
16457
  return {
@@ -16428,9 +16459,9 @@ function parseBlockRef(document, fragment) {
16428
16459
  kind: "block",
16429
16460
  id: parsed.id,
16430
16461
  block: target.block,
16431
- document,
16462
+ document: holder,
16432
16463
  ...parsed.owner.kind === "project" ? { artifact: parsed.id } : {},
16433
- ...target.pin === undefined ? {} : { pin: target.pin }
16464
+ ...pin
16434
16465
  };
16435
16466
  }
16436
16467
  function parseDispatchRef(ref) {
@@ -17457,7 +17488,7 @@ function normalizeUUID(value) {
17457
17488
  }
17458
17489
  var askIdProblem = "ask must be a bare ask id or a dispatch://.../ask/<id> reference";
17459
17490
  var commentIdProblem = "comment must be a bare comment id or a dispatch://.../comment/<id> reference";
17460
- var messageIdProblem = "in_reply_to must be a full message id (uuid) or a dispatch://KEY/message/<id> reference";
17491
+ var messageIdProblem = "in_reply_to must be a full message id (uuid), a dispatch://KEY/message/<id> reference, or a block of one (dispatch://KEY/message/<id>#b-<block id>)";
17461
17492
  function askId(args) {
17462
17493
  const ask = stringArg(args, "ask");
17463
17494
  return ask.startsWith("dispatch://") ? parseDispatchRef(ask)?.id ?? ask : ask;
@@ -17469,6 +17500,28 @@ function messageIdOf(value) {
17469
17500
  })() : value;
17470
17501
  return id === undefined ? undefined : normalizeUUID(id);
17471
17502
  }
17503
+ function itemBlockOf(value) {
17504
+ const ref = parseDispatchRef(value);
17505
+ return ref?.item === undefined || ref.block === undefined ? undefined : { ...ref, item: ref.item, block: ref.block };
17506
+ }
17507
+ function replyBlockProblem(field, kind, value) {
17508
+ if (!/^dispatch:\/\/[^#]*#b-/.test(value))
17509
+ return;
17510
+ const form = `dispatch://KEY/${kind}/<id>#b-<block id>`;
17511
+ const ref = parseDispatchRef(value);
17512
+ if (ref?.kind !== "block") {
17513
+ return `${field} ${value} names no block; a block of a ${kind} reads ${form}`;
17514
+ }
17515
+ const other = ref.item?.kind;
17516
+ if (other !== kind) {
17517
+ const answer = other === "comment" ? `; answer it with ${commandLine("dispatch_comment", { reply_to: value })}` : other === "message" ? `; answer it with ${commandLine("dispatch_message", { in_reply_to: value })}` : "";
17518
+ return `${field} ${value} names a block of ${other === undefined ? "a document" : `a ${other}`}, but a ${kind} reply answers a block of a ${kind} (${form})${answer}`;
17519
+ }
17520
+ if (ref.pin !== undefined) {
17521
+ return `${field} ${value} names revision ${ref.pin} of the block, but a reply answers the block whatever its text becomes: drop @v${ref.pin}`;
17522
+ }
17523
+ return;
17524
+ }
17472
17525
  var idPrefixPattern = /^[0-9a-f][0-9a-f-]{7,}$/i;
17473
17526
  function refOwnerName(ref) {
17474
17527
  return ref.owner.kind === "issue" ? ref.owner.issue : `${ref.owner.project}/${ref.artifact ?? ref.id}`;
@@ -17578,7 +17631,20 @@ async function resolveAskArgument(tool, args, client, sessionId) {
17578
17631
  }
17579
17632
  function messageInReplyTo(args) {
17580
17633
  const inReplyTo = optionalString(args, "in_reply_to");
17581
- return inReplyTo === undefined ? undefined : messageIdOf(inReplyTo);
17634
+ if (inReplyTo === undefined)
17635
+ return;
17636
+ const block = itemBlockOf(inReplyTo);
17637
+ if (block !== undefined && block.item.kind !== "message")
17638
+ return;
17639
+ const id = messageIdOf(block?.item.id ?? inReplyTo);
17640
+ return id === undefined ? undefined : { id, ...block === undefined ? {} : { block: block.block } };
17641
+ }
17642
+ function commentReplyTo(args) {
17643
+ const replyTo = optionalString(args, "reply_to");
17644
+ if (replyTo === undefined)
17645
+ return;
17646
+ const block = itemBlockOf(replyTo);
17647
+ return block === undefined ? { id: replyTo } : { id: block.item.id, block: block.block };
17582
17648
  }
17583
17649
  function argumentProblems(tool, args) {
17584
17650
  const problems = [];
@@ -17613,15 +17679,21 @@ function argumentProblems(tool, args) {
17613
17679
  if (optionalString(args, "quote") !== undefined && optionalString(args, "artifact") === undefined) {
17614
17680
  problems.push("artifact is required when quote is supplied");
17615
17681
  }
17616
- if (optionalString(args, "reply_to") !== undefined && optionalString(args, "reply_to_ask") !== undefined) {
17682
+ const replyTo = optionalString(args, "reply_to");
17683
+ if (replyTo !== undefined && optionalString(args, "reply_to_ask") !== undefined) {
17617
17684
  problems.push("reply_to and reply_to_ask cannot both be set");
17618
17685
  }
17686
+ const replyBlock = replyTo === undefined ? undefined : replyBlockProblem("reply_to", "comment", replyTo);
17687
+ if (replyBlock !== undefined)
17688
+ problems.push(replyBlock);
17619
17689
  break;
17620
17690
  }
17621
17691
  case "dispatch_message": {
17622
17692
  const inReplyTo = optionalString(args, "in_reply_to");
17623
- if (inReplyTo !== undefined && messageIdOf(inReplyTo) === undefined) {
17624
- problems.push(messageIdProblem);
17693
+ if (inReplyTo !== undefined) {
17694
+ const problem = replyBlockProblem("in_reply_to", "message", inReplyTo) ?? (messageInReplyTo(args) === undefined ? messageIdProblem : undefined);
17695
+ if (problem !== undefined)
17696
+ problems.push(problem);
17625
17697
  }
17626
17698
  break;
17627
17699
  }
@@ -17701,7 +17773,7 @@ function dispatchRefFromUrl(value, serverUrl) {
17701
17773
  };
17702
17774
  const blockOf = (versionName) => {
17703
17775
  const id = url.hash.startsWith("#b-") ? decodeBlockId(url.hash.slice(3)) : undefined;
17704
- return id === undefined || url.searchParams.get(versionName) ? "" : `#b-${encodeBlockId(id)}`;
17776
+ return id === undefined || versionName !== undefined && url.searchParams.get(versionName) ? "" : `#b-${encodeBlockId(id)}`;
17705
17777
  };
17706
17778
  const issuePage = url.pathname.match(/^\/issues\/([A-Z][A-Z0-9]{1,9}-[1-9][0-9]*)(?:\/(spec|log|conversation|activity|children)|\/artifacts\/([^/]+)|\/asks\/([^/]+)|\/comments\/([^/]+)|\/messages\/([^/]+))?\/?$/);
17707
17779
  if (issuePage) {
@@ -17727,9 +17799,9 @@ function dispatchRefFromUrl(value, serverUrl) {
17727
17799
  if (ask !== undefined)
17728
17800
  return `dispatch://${key}/ask/${ask}`;
17729
17801
  if (comment !== undefined)
17730
- return `dispatch://${key}/comment/${comment}`;
17802
+ return `dispatch://${key}/comment/${comment}${blockOf()}`;
17731
17803
  if (message !== undefined)
17732
- return `dispatch://${key}/message/${message}`;
17804
+ return `dispatch://${key}/message/${message}${blockOf()}`;
17733
17805
  return `dispatch://${key}`;
17734
17806
  }
17735
17807
  const agentArtifactPage = url.pathname.match(/^\/agents\/([^/]+)\/artifacts\/([^/]+)\/?$/);
@@ -17752,7 +17824,7 @@ function dispatchRefFromUrl(value, serverUrl) {
17752
17824
  return;
17753
17825
  return item === undefined ? `${document}${versionOf("version")}${blockOf("version")}` : `${document}/${item.kind}/${item.id}`;
17754
17826
  }
17755
- var refGrammarProblem = "ref must be a valid dispatch:// reference such as dispatch://KEY-1, " + "dispatch://KEY-1/ask/<uuid>, dispatch://KEY-1/comment/<uuid>, " + "dispatch://KEY-1/message/<uuid>, dispatch://KEY-1/artifact/<slug>, " + "dispatch://PROJECT/artifact/<document-ref> (an artifact id, slug, or filename), " + "dispatch://KEY-1/spec#b-<block id>, dispatch://KEY-1/artifact/<slug>#b-<block id> or " + "dispatch://PROJECT/artifact/<slug>#b-<block id> (one block of a document, its id " + "percent-encoded, optionally pinned to a version of the document with @v<N> after it; for " + "dispatch read), or " + "dispatch://agent/<session id>/artifact/<slug> (a picture in a conversation on the Agents page, " + "for dispatch doc-read); a dashboard URL on this Dispatch server is accepted too";
17827
+ var refGrammarProblem = "ref must be a valid dispatch:// reference such as dispatch://KEY-1, " + "dispatch://KEY-1/ask/<uuid>, dispatch://KEY-1/comment/<uuid>, " + "dispatch://KEY-1/message/<uuid>, dispatch://KEY-1/artifact/<slug>, " + "dispatch://PROJECT/artifact/<document-ref> (an artifact id, slug, or filename), " + "dispatch://KEY-1/spec#b-<block id>, dispatch://KEY-1/artifact/<slug>#b-<block id> or " + "dispatch://PROJECT/artifact/<slug>#b-<block id> (one block of a document, its id " + "percent-encoded, optionally pinned to a version of the document with @v<N> after it; for " + "dispatch read), dispatch://KEY-1/comment/<uuid>#b-<block id>, " + "dispatch://PROJECT/artifact/<slug>/comment/<uuid>#b-<block id> or " + "dispatch://KEY-1/message/<uuid>#b-<block id> (one block of a comment or message, a " + "comment's optionally pinned to a revision of the comment with @v<N> after it; for dispatch " + "read), or " + "dispatch://agent/<session id>/artifact/<slug> (a picture in a conversation on the Agents page, " + "for dispatch doc-read); a dashboard URL on this Dispatch server is accepted too";
17756
17828
  var ownerRequiredProblem = "issue is required; supply issue or set LEGION_ISSUE";
17757
17829
  function innerRefProblem(tool, ref, refText) {
17758
17830
  if (ref.kind !== "ask" && ref.kind !== "comment" && ref.kind !== "message" && ref.kind !== "log" && ref.kind !== "children" && ref.kind !== "block") {
@@ -17761,10 +17833,11 @@ function innerRefProblem(tool, ref, refText) {
17761
17833
  if (tool === "dispatch_read" || tool === "dispatch_ask")
17762
17834
  return;
17763
17835
  const owner = refOwnerName(ref);
17764
- const documentArgument = ref.kind === "block" && tool !== "dispatch_comment" ? ` and artifact ${ref.id}` : tool === "dispatch_suggest" ? " and artifact <the document's slug>" : "";
17836
+ const itemKind = ref.item?.kind ?? ref.kind;
17837
+ const documentArgument = ref.kind === "block" && ref.item === undefined && tool !== "dispatch_comment" ? ` and artifact ${ref.id}` : tool === "dispatch_suggest" ? " and artifact <the document's slug>" : "";
17765
17838
  const ownerArguments = ref.owner.kind === "issue" ? `issue ${ref.owner.issue}${documentArgument}` : `project ${ref.owner.project} and artifact ${ref.artifact}`;
17766
- const fix = tool === "dispatch_comment" && ref.kind === "ask" ? `reply under it with reply_to_ask ${ref.id} and ${ownerArguments}` : tool === "dispatch_comment" && ref.kind === "comment" ? `reply in its thread with reply_to ${ref.id} and ${ownerArguments}` : `read it with dispatch read, or name ${owner} with ${ownerArguments}`;
17767
- const item = ref.kind === "ask" ? "an ask" : ref.kind === "log" ? "the event log" : ref.kind === "children" ? "the children" : ref.kind === "block" ? `block ${ref.block}` : `a ${ref.kind}`;
17839
+ const fix = tool === "dispatch_comment" && itemKind === "ask" ? `reply under it with reply_to_ask ${ref.id} and ${ownerArguments}` : tool === "dispatch_comment" && itemKind === "comment" ? `reply in its thread with reply_to ${ref.id} and ${ownerArguments}` : `read it with dispatch read, or name ${owner} with ${ownerArguments}`;
17840
+ const item = ref.kind === "ask" ? "an ask" : ref.kind === "log" ? "the event log" : ref.kind === "children" ? "the children" : ref.kind === "block" ? `block ${ref.block}${ref.item === undefined ? "" : ` of a ${ref.item.kind}`}` : `a ${ref.kind}`;
17768
17841
  return `ref ${refText} names ${item} on ${owner}; dispatch ${commandName(tool)} would act on ${owner} and drop the ${ref.kind}: ${fix}`;
17769
17842
  }
17770
17843
  var documentItemPattern = /^dispatch:\/\/([A-Z][A-Z0-9]{1,9}-[1-9][0-9]*)\/artifact\/[^/@]+(?:@v\d+)?\/(ask|comment)\/([^/]+)$/;
@@ -17791,7 +17864,7 @@ function ownerConflictProblems(tool, args, parsedRef, refText) {
17791
17864
  const artifact = optionalString(args, "artifact");
17792
17865
  const version = optionalNumber(args, "version");
17793
17866
  const ref = tool === "dispatch_ask" && (issue !== undefined || project !== undefined) ? null : parsedRef;
17794
- const refDocument = ref === null ? undefined : ref.owner.kind === "project" ? ref.artifact ?? ref.id : ref.kind === "spec" || ref.kind === "artifact" || ref.kind === "block" ? ref.id : undefined;
17867
+ const refDocument = ref === null ? undefined : ref.owner.kind === "project" ? ref.artifact ?? ref.id : ref.kind === "spec" || ref.kind === "artifact" || ref.kind === "block" && ref.item === undefined ? ref.id : undefined;
17795
17868
  if (ref !== null && refText !== undefined) {
17796
17869
  const refOwner = refOwnerName(ref);
17797
17870
  if (issue !== undefined && (ref.owner.kind !== "issue" || nativeIssueKeyPattern.test(issue) && issue !== ref.owner.issue)) {
@@ -18198,9 +18271,12 @@ function referenceLines(read) {
18198
18271
  if (read.edges.length === 0)
18199
18272
  return ["- none"];
18200
18273
  return read.edges.map((edge) => {
18274
+ const { node } = edge;
18201
18275
  const excerpt = edge.excerpt === undefined ? "" : `${textHead(edge.excerpt.text)} \xB7 `;
18202
18276
  const block = edge.block === undefined ? "" : ` (block ${edge.block.ref ?? edge.block.id})`;
18203
- return `- ${edge.kind} ${edge.node.kind} ${edge.node.ref ?? edge.node.id} (${excerpt}${edge.created_at})${block}`;
18277
+ const item = node.kind === "block" && node.document === undefined && (node.parent?.kind === "comment" || node.parent?.kind === "message") ? node.parent : undefined;
18278
+ const source = item === undefined ? `${node.kind} ${node.ref ?? node.id}` : `${item.kind} ${item.ref ?? item.id} block ${node.block_id ?? node.id}`;
18279
+ return `- ${edge.kind} ${source} (${excerpt}${edge.created_at})${block}`;
18204
18280
  });
18205
18281
  }
18206
18282
  function unavailableReason(error) {
@@ -18243,27 +18319,32 @@ function nodeLine(node) {
18243
18319
  const made = fact(node.made_by, node.made_at);
18244
18320
  const changed = fact(node.changed_by, node.changed_at);
18245
18321
  const { parent } = node;
18246
- const partOf = parent == null ? "" : ` \xB7 part of ${parent.ref ?? `${parent.kind} ${parent.id}`}${node.parent_version == null ? "" : ` (version ${node.parent_version})`}`;
18322
+ const partOf = parent == null ? "" : ` \xB7 part of ${parent.ref ?? `${parent.kind} ${parent.id}`}${node.parent_version == null ? "" : ` (${parent.kind === "comment" ? "revision" : "version"} ${node.parent_version})`}`;
18247
18323
  return [
18248
18324
  `Node: ${node.kind} ${node.ref ?? node.id} \xB7 made by ${made} \xB7 changed by ${changed}${partOf}`
18249
18325
  ];
18250
18326
  }
18251
18327
  function blockLines(node, ref) {
18252
- const head = [
18253
- `Block: ${node.block_id ?? ref.block}`,
18328
+ const unit = ref.item === undefined ? "version" : "revision";
18329
+ const blockID = node.block_id ?? ref.block;
18330
+ const head = ref.item === undefined ? [
18331
+ `Block: ${blockID}`,
18254
18332
  `Document: ${node.document ?? ref.document}`,
18255
18333
  ...node.changed_version === undefined ? [] : [`Changed in version: ${node.changed_version}`],
18256
18334
  ...node.text === undefined ? [] : ["Text:", node.text]
18335
+ ] : [
18336
+ `Block ${blockID} of ${ref.item.kind} ${ref.item.id}${node.changed_version === undefined ? "" : ` (changed in revision ${node.changed_version})`}:`,
18337
+ ...node.text === undefined ? [] : [node.text]
18257
18338
  ];
18258
18339
  const { pin } = node;
18259
18340
  if (pin === undefined)
18260
18341
  return head;
18261
- const state = pin.state === "unchanged" ? `Unchanged since version ${pin.version}.` : pin.state === "changed" ? `Changed since version ${pin.version}; last changed in version ${node.changed_version}.` : `Removed in version ${pin.removed_version}.`;
18342
+ const state = pin.state === "unchanged" ? `Unchanged since ${unit} ${pin.version}.` : pin.state === "changed" ? `Changed since ${unit} ${pin.version}; last changed in ${unit} ${node.changed_version}.` : `Removed in ${unit} ${pin.removed_version}.`;
18262
18343
  return [
18263
18344
  ...head,
18264
- `Pinned to version: ${pin.version}`,
18345
+ `Pinned to ${unit}: ${pin.version}`,
18265
18346
  state,
18266
- `Text at version ${pin.version}:`,
18347
+ `Text at ${unit} ${pin.version}:`,
18267
18348
  pin.text
18268
18349
  ];
18269
18350
  }
@@ -18410,11 +18491,13 @@ function commentSummary({ comment, replies }, graph) {
18410
18491
  const root = [
18411
18492
  `${comment.id} \xB7 ${actorText(comment.author)}`,
18412
18493
  ...anchorLines(comment),
18494
+ ...replyBlockLines(comment),
18413
18495
  `Body: ${comment.body}`
18414
18496
  ];
18415
18497
  const chain = replies.flatMap((reply) => [
18416
18498
  `${reply.id} \xB7 ${actorText(reply.author)}`,
18417
18499
  ...anchorLines(reply),
18500
+ ...replyBlockLines(reply),
18418
18501
  `Body: ${reply.body}`
18419
18502
  ]);
18420
18503
  return [
@@ -18426,6 +18509,14 @@ function commentSummary({ comment, replies }, graph) {
18426
18509
  ].join(`
18427
18510
  `);
18428
18511
  }
18512
+ function replyBlockLines(record) {
18513
+ const block = record.reply_block;
18514
+ if (block === undefined || block === null)
18515
+ return [];
18516
+ return [
18517
+ block.removed ? `Re: block ${block.block_id} (removed)` : `Re: block ${block.block_id}: ${textHead(block.text)}`
18518
+ ];
18519
+ }
18429
18520
  function anchorLines(record) {
18430
18521
  return [
18431
18522
  ...record.anchor?.quote === undefined ? [] : [`> ${record.anchor.quote}`],
@@ -18449,9 +18540,14 @@ function positionText(block) {
18449
18540
  ].join(" \u203A ");
18450
18541
  }
18451
18542
  function messageSummary({ message, replies }, graph) {
18452
- const root = [`${message.id} \xB7 ${actorText(message.author)}`, `Body: ${message.body}`];
18543
+ const root = [
18544
+ `${message.id} \xB7 ${actorText(message.author)}`,
18545
+ ...replyBlockLines(message),
18546
+ `Body: ${message.body}`
18547
+ ];
18453
18548
  const chain = replies.flatMap((reply) => [
18454
18549
  `${reply.id} \xB7 ${actorText(reply.author)}`,
18550
+ ...replyBlockLines(reply),
18455
18551
  `Body: ${reply.body}`
18456
18552
  ]);
18457
18553
  return [
@@ -19309,7 +19405,7 @@ async function executeDispatchTool(input) {
19309
19405
  const owner = documentOwner();
19310
19406
  const resolved = owner.kind === "project" || artifactReference === undefined ? owner.kind === "project" ? await resolveDocument(owner, artifactReference) : undefined : await resolveDocument(owner, artifactReference);
19311
19407
  const anchored = resolved ? anchor(resolved.artifact, args) : undefined;
19312
- const replyTo = optionalString(args, "reply_to");
19408
+ const replyTo = commentReplyTo(args);
19313
19409
  const replyToAskReference = optionalString(args, "reply_to_ask");
19314
19410
  const replyToAsk = replyToAskReference === undefined ? undefined : normalizeUUID(replyToAskReference);
19315
19411
  if (replyToAskReference !== undefined && replyToAsk === undefined) {
@@ -19326,7 +19422,8 @@ async function executeDispatchTool(input) {
19326
19422
  const commentInput = {
19327
19423
  body,
19328
19424
  ...anchored === undefined ? {} : { anchor: anchored },
19329
- ...replyTo === undefined ? {} : { reply_to: replyTo },
19425
+ ...replyTo === undefined ? {} : { reply_to: replyTo.id },
19426
+ ...replyTo?.block === undefined ? {} : { reply_to_block: replyTo.block },
19330
19427
  ...replyToAsk === undefined ? {} : { ask_id: replyToAsk },
19331
19428
  ...requestedTurn === undefined ? {} : { turn: requestedTurn },
19332
19429
  actor
@@ -19386,7 +19483,8 @@ async function executeDispatchTool(input) {
19386
19483
  };
19387
19484
  }
19388
19485
  case "dispatch_message": {
19389
- const inReplyTo = messageInReplyTo(args);
19486
+ const target = messageInReplyTo(args);
19487
+ const inReplyTo = target?.id;
19390
19488
  const written = stringArg(args, "body");
19391
19489
  const bodyLimit = {
19392
19490
  cap: DISPATCH_BODY_MAX,
@@ -19433,6 +19531,7 @@ async function executeDispatchTool(input) {
19433
19531
  const message = await client.message(issueKey, {
19434
19532
  body,
19435
19533
  ...inReplyTo === undefined ? {} : { in_reply_to: inReplyTo },
19534
+ ...target?.block === undefined ? {} : { in_reply_to_block: target.block },
19436
19535
  actor
19437
19536
  });
19438
19537
  const messageRef = dispatchChildRef(dispatchIssueRef(issueKey), "message", message.id);
@@ -19727,10 +19826,11 @@ Document token: ${listed.token}`,
19727
19826
  if (ownerArguments.ref?.kind === "block") {
19728
19827
  const ref = ownerArguments.ref;
19729
19828
  const blockRef = stringArg(args, "ref");
19829
+ const holder = blockRef.slice(0, blockRef.indexOf("#"));
19730
19830
  const [incoming, outgoing] = await Promise.all([
19731
19831
  client.getReferences({ to: blockRef }).catch((error) => {
19732
19832
  if (ref.pin === undefined && dispatchAnswered(error, 404)) {
19733
- throw new Error(`No block ${ref.block} in ${ref.document}.`);
19833
+ throw new Error(`No block ${ref.block} in ${holder}.`);
19734
19834
  }
19735
19835
  throw error;
19736
19836
  }),
@@ -13698,6 +13698,13 @@ var CommentDeliverySchema = object({
13698
13698
  reply_id: string2().nullable(),
13699
13699
  created_at: string2()
13700
13700
  });
13701
+ var ReplyBlockSchema = object({
13702
+ node: string2(),
13703
+ ref: string2().optional(),
13704
+ block_id: string2(),
13705
+ text: string2(),
13706
+ removed: boolean2()
13707
+ });
13701
13708
  var CommentEventPayloadSchema = object({
13702
13709
  id: string2().optional(),
13703
13710
  artifact_name: string2().optional(),
@@ -13723,6 +13730,7 @@ var CommentEventPayloadSchema = object({
13723
13730
  created_at: string2().optional(),
13724
13731
  mentions: array(CommentMentionSchema).optional(),
13725
13732
  deliveries: array(CommentDeliverySchema).optional(),
13733
+ reply_block: ReplyBlockSchema.nullish(),
13726
13734
  thread_logins: array(string2()).optional()
13727
13735
  }).passthrough();
13728
13736
  var MessageEventPayloadSchema = object({
@@ -13733,6 +13741,7 @@ var MessageEventPayloadSchema = object({
13733
13741
  reply_body: string2().optional(),
13734
13742
  thread_target: string2().optional(),
13735
13743
  author: object({ kind: string2(), id: string2() }).optional(),
13744
+ reply_block: ReplyBlockSchema.nullish(),
13736
13745
  thread_logins: array(string2()).optional()
13737
13746
  });
13738
13747
  var DispatchTargetedResourceIDSchema = uuid2();
@@ -14116,7 +14125,7 @@ var dispatchToolSpecs = [
14116
14125
  quote: z.string().describe("Optional exact quoted document text.").optional(),
14117
14126
  occurrence: z.number({ int: true, min: 0 }).describe("Optional zero-based occurrence of quote.").optional(),
14118
14127
  body: z.string({ max: DISPATCH_BODY_MAX }).describe(`Review comment, at most ${DISPATCH_BODY_MAX} characters.`),
14119
- reply_to: z.string().describe("A comment id (uuid); replying to any comment in a thread continues that thread (an " + "ask's clarification thread included). To reply to an ask, use reply_to_ask with the " + "ask id instead.").optional(),
14128
+ reply_to: z.string().describe("A comment id (uuid); replying to any comment in a thread continues that thread (an " + "ask's clarification thread included). A block reference to one block of the comment " + "(dispatch://KEY/comment/<id>#b-<block id>) replies to that comment and names the " + "passage the reply answers. To reply to an ask, use reply_to_ask with the " + "ask id instead.").optional(),
14120
14129
  reply_to_ask: z.string().describe("Optional ask id to reply to, threading this comment under that question. Mutually " + "exclusive with reply_to.").optional(),
14121
14130
  turn: z.enum(["agent", "human"]).describe("Only with reply_to_ask: who holds the turn after this reply. agent: a progress note - " + "you keep the turn and the ask stays 'Waiting on agents' for the human; human (default): " + "you need the human to act - the ask returns to 'Waiting on you'.").optional(),
14122
14131
  images: imagesArgument(z, "the issue (or the project document's project)", `the ${DISPATCH_BODY_MAX}-character body limit`)
@@ -14151,7 +14160,7 @@ var dispatchToolSpecs = [
14151
14160
  arguments: (z) => ({
14152
14161
  issue: z.string().describe(`${ISSUE_REFERENCE} Omit it only when in_reply_to answers a human's direct message to this session.`).optional(),
14153
14162
  body: z.string({ max: DISPATCH_BODY_MAX }).describe(`Update text, at most ${DISPATCH_BODY_MAX} characters.`),
14154
- in_reply_to: z.string().describe("Optional message id or dispatch://KEY/message/<id> reference to reply to, threading " + "this message under it so the reply stays with the original in the Conversation. " + "A bare id with no issue answers a human's direct message to this session; a " + "dispatch://KEY/message/<id> names the issue its message lives on, so that form is a " + "reply on that issue.").optional(),
14163
+ in_reply_to: z.string().describe("Optional message id or dispatch://KEY/message/<id> reference to reply to, threading " + "this message under it so the reply stays with the original in the Conversation. " + "A block reference to one block of the message (dispatch://KEY/message/<id>#b-<block id>) " + "replies to that message and names the passage the reply answers. " + "A bare id with no issue answers a human's direct message to this session; a " + "dispatch://KEY/message/<id> names the issue its message lives on, so that form is a " + "reply on that issue.").optional(),
14155
14164
  images: imagesArgument(z, "the issue (a reply to a direct message: your own conversation, dispatch://agent/<your session id>)", `the ${DISPATCH_BODY_MAX}-character body limit`)
14156
14165
  }),
14157
14166
  validation: messageValidation
@@ -15610,7 +15619,7 @@ function sourceSession(sessionID) {
15610
15619
  // src/server.ts
15611
15620
  import { tool } from "@opencode-ai/plugin/tool";
15612
15621
  // package.json
15613
- var version2 = "11.4.0";
15622
+ var version2 = "12.0.0";
15614
15623
 
15615
15624
  // src/log.ts
15616
15625
  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": "11.4.0",
3
+ "version": "12.0.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "dist/src/server.js",
@@ -10,7 +10,7 @@ Dispatch is a record for the people who decide, never a log of your work.
10
10
  - Use the `dispatch` command; the HTTP API only for what no command does. `dispatch <command> --help` gives its rules and an example.
11
11
  - Put long text on stdin: `--<field>-file -` with a quoted here-document (`<<'EOF'`). Give object and list fields as JSON.
12
12
  - Exit 1 is a refusal, 2 a usage error. A refusal lists each problem: fix them all, then call once.
13
- - `DOC_SERVICE_UNAVAILABLE`: the document is reloading, nothing was written; retry in a few seconds.
13
+ - `DOC_SERVICE_UNAVAILABLE`: the document is reloading, nothing was written; retry shortly.
14
14
 
15
15
  ## Asks and ask blocks
16
16
 
@@ -27,7 +27,7 @@ Which machines, files or people, the numbers, then "A happens to X, or B to Y?"
27
27
 
28
28
  - A to-do or permission only a person can give, or a decision with no document, is a `dispatch ask`.
29
29
  - In Legion, fix a clear defect in code your tree uses, hand others to the controller, and ask about decisions (`skill://legion-worker/references/findings.md`).
30
- - An open ask is the request. Do not raise it again in each message.
30
+ - An open ask is the request. Do not repeat it in each message.
31
31
  - Change an open ask's block with `dispatch edit-ask`, or `doc-edit`'s `replace` or `move`. An answered ask is fixed: ask anew.
32
32
  - Answer a person's reply to an open ask with `dispatch comment --reply-to-ask`; `--turn agent` for a note that needs nothing from them.
33
33
  - When an answer's option and text disagree, the text wins.
@@ -72,9 +72,12 @@ dispatch://KEY/spec
72
72
  dispatch://KEY/artifact/<slug>[@vN]
73
73
  dispatch://KEY/ask/<id>
74
74
  dispatch://KEY/comment/<id>
75
+ dispatch://KEY/comment/<id>#b-<block>[@vN]
75
76
  dispatch://KEY/message/<id>
77
+ dispatch://KEY/message/<id>#b-<block>
76
78
  dispatch://PROJECT/artifact/<slug>[@vN]
77
79
  dispatch://PROJECT/artifact/<slug>/ask/<id>
80
+ dispatch://PROJECT/artifact/<slug>/comment/<id>#b-<block>[@vN]
78
81
  ```
79
82
 
80
83
  A bare id or filename is not a reference. An ask or comment id can be an 8-hex prefix.
@@ -13,7 +13,7 @@ You own one issue tree from its first decision to the merge that closes it. The
13
13
 
14
14
  - Act on delivered wakes and the live artifacts they name; build no wait loop, since every wake reaches you.
15
15
  - Check each claim against the code as fetched now (the default branch your addressing line names, or the head under review), never the workspace as it was at admission, and tell every fresh reader you dispatch to do the same: the branch may have moved since.
16
- - Before you end a turn that waits on a human, open what they must answer: a decision block, a `dispatch ask` or `dispatch request-approval`. A turn that ends with nothing open asks nobody, and the tree stops. Open an ordinary ask on the root issue itself, never a child: only a question a session of this tree opens on the root starts a wait on a person, so an ask on a child leaves the root holding its slot while nothing runs. Once you have handed a root question to the human, end your turn: when nothing else holds the slot — no other issue of the tree in a phase, the `legion` label still on, and no person or outside session holding the root's Dispatch claim — the root returns its admission slot (`awaiting_person`), the daemon suspends this session at the end of the turn, and the human's answer resumes it with a task naming what answered and what is still with them. The same holds once you request approval and `register_gate` that version: the registration hands the spec to the human, so end your turn there too, and the human's review, or a newer version once Dispatch shows the request back on your turn, resumes you.
16
+ - Before you end a turn that waits on a human, open what they must answer: a decision block, a `dispatch ask` or `dispatch request-approval`. A turn that ends with nothing open asks nobody, and the tree stops. Open an ordinary ask on the root issue itself, never a child: only a question a session of this tree opens on the root starts a wait on a person, so an ask on a child leaves the root holding its slot while nothing runs. Once you have handed a root question to the human, end your turn: when nothing else holds the slot — no other issue of the tree in a phase, the `legion` label still on, and no person or outside session holding the root's Dispatch claim — the root returns its admission slot (`awaiting_person`) while this session stays live. Read the human's comments and replies as they arrive. Their answer gives this live claim a task naming what answered and what is still with them. The same holds once you request approval and `register_gate` that version: the registration hands the spec to the human, so end your turn there too, and the human's review, or a newer version once Dispatch shows the request back on your turn, gives you that task.
17
17
  - Deliver every criterion of the root spec in this tree. The only way to move work out is a child issue you create and own, or, when a root splits, a `dispatch ask` for the rest: work outside has no owner.
18
18
  - A problem outside the tree is a `dispatch ask`, never an issue you file, because a person decides whether it needs one (`skill://legion-worker/references/findings.md`). A clear defect is the exception. Fix one in code this tree touches or depends on in this tree's pull request. Send any other to the controller, which gives it to the tree that owns that code.
19
19
  - Before you write a spec or ask a person anything, read the repository's `STRATEGY.md` when it has one, then use a standard best-practice solution; ask only what neither settles, with one recommendation. A design that crosses a Boundary in that file goes to its owner before it is built. Find the standard with a web search and with how two or three well-regarded projects solve the problem. Cite them in a few lines of the spec's `Design`. A design far from the standard is a sign to fix the underlying problem, not to add a workaround.
@@ -105,15 +105,15 @@ Facts about the request:
105
105
 
106
106
  Then end your turn. The registration hands the spec to the human: when nothing else holds the
107
107
  slot, the root returns its admission slot and waits on its design gate (`awaiting_person`), as a
108
- root question handed to the human does ("Every turn" in `skill://legion-architect/SKILL.md`). The
109
- human's approval or changes request resumes you, and so does a newer spec version once Dispatch
110
- shows the request back on your turn. While the root waits on a question instead, the human's
111
- approval or changes request of the spec answers that wait too, and the resume task says how the
112
- spec stands. Release no wave until a delivered wake shows `design-approved` on the root, or a
113
- resume task says its design gate is open: the daemon sends a root waiting on a person no
114
- `design-approved` notice, and "this resume is your design-approved wake" is that wake. On
115
- `design-changes-requested`: revise the spec as the human's reason asks, run step 3's two reviews on
116
- the new version, resolve every finding, request approval again and end your turn. A
108
+ root question handed to the human does ("Every turn" in `skill://legion-architect/SKILL.md`). Stay
109
+ live to read the human's comments and replies. Their approval or changes request delivers your next
110
+ task, and so does a newer spec version once Dispatch shows the request back on your turn. While the
111
+ root waits on a question instead, the human's approval or changes request of the spec answers that
112
+ wait too, and the task says how the spec stands. Release no wave until a delivered wake shows
113
+ `design-approved` on the root, or a task says its design gate is open: the daemon sends a root
114
+ waiting on a person no `design-approved` notice, and "this resume is your design-approved wake" is
115
+ that wake. On `design-changes-requested`: revise the spec as the human's reason asks, run step 3's
116
+ two reviews on the new version, resolve every finding, request approval again and end your turn. A
117
117
  `design-changes-requested` may name a version below the latest, or arrive after another for the
118
118
  same version: it is a request the human made that reached the daemon late, and no approval came
119
119
  after it. Each request arrives once. The gate is as the newest review left it; still read the
@@ -8,7 +8,7 @@ waits, or neither. Read its key against `admission` and its tree's root.
8
8
  |---|---|---|
9
9
  | The key is in `admission.active` | A root holding a slot: its tree runs, `admitted` until its architect starts the first phase | Nothing, beyond [trees waiting on a root claim](skill://legion-controller/SKILL.md#trees-waiting-on-a-root-claim) |
10
10
  | The key is in `admission.waiting` and not in `admission.blocked` | A root handed to Legion in `todo`, queued for the next free slot: a root a person answered while it waited in `awaiting_person` comes first, oldest answer first, then every root that never ran, in Dispatch rank order. A root that left another tree (handed over by its finished tree, or admitted as an orphan) waits here as its own root even under a live tree, the old root's return included: no tree adopts it | Nothing: it takes the next free slot by itself |
11
- | `phase: awaiting_person`, `waitingOn` | A root that already ran and now waits only on a person: an answer to its architect's question (`waitingOn.kind` `ask`) or a review of its spec (`gate`). It holds no slot, its architect session is suspended, and its tree stays live. Unanswered, it is in neither list; `waitingOn.answeredAt` means the daemon has promoted it to the front of `admission.waiting` | Nothing: it is not something to admit, label or park, and the slot it returned is no reason to take an issue ([Free slots](skill://legion-controller/SKILL.md#free-slots)). The daemon resumes it in its own architect session at the next free slot. If its label goes or a person or outside session claims it on Dispatch, the daemon sends `unslotted` and does not resume it |
11
+ | `phase: awaiting_person`, `waitingOn` | A root that already ran and now waits only on a person: an answer to its architect's question (`waitingOn.kind` `ask`) or a review of its spec (`gate`). It holds no slot, its architect stays live to read the person's comments and replies, and its tree stays live. Unanswered, it is in neither list; `waitingOn.answeredAt` means the daemon has promoted it to the front of `admission.waiting` | Nothing: it is not something to admit, label or park, and the slot it returned is no reason to take an issue ([Free slots](skill://legion-controller/SKILL.md#free-slots)). The daemon delivers the answer task to its architect once the root takes the next free slot. If its label goes or a person or outside session claims it on Dispatch, the daemon sends `unslotted` and does not resume work under another owner |
12
12
  | The key is in `admission.waiting` and in `admission.blocked` | A root in the line that waits on the Dispatch issues `admission.blocked` names: admission passes it over, and it takes no slot, until every one of them is `done` | Nothing: the wait ends when each blocker reaches `done` (a person's close, or the blocker's own merge) or its link is taken off (by a person or the tree's architect). Name the wait in the day's report |
13
13
  | `phase: admitted`, in neither list, and Dispatch shows a `child_of` parent | A child: it runs or waits with its tree, the root its `child_of` chain reaches. Under a root whose `phase` is `done` it waits with it. A labelled `todo` issue here is not in the line either: one whose recorded parent is in a live tree runs in that tree (the daemon adopted it there), or is held with that parent while the parent itself is held under an unreleased child, and one whose parent is not recorded but sits under a live tree is held under an unreleased child of that tree until its architect releases it or its parent. That hold is not `admission.blocked`, which names only blocked-by links | Leave it to its tree. It runs again when its root does, or as a root of its own once it is `todo` with the `legion` label and no live tree is above it |
14
14
  | `phase: admitted`, `status: todo`, in neither list, no parent | A root a person took out of the line by taking its label off | Nothing: the label puts it back in the line, and a person's label is their decision |
@@ -63,13 +63,12 @@ wake gives it>; `todo` with the `legion` label queues it again". Other admission
63
63
  ## `awaiting-person on <KEY>` (`{kind: "awaiting-person", reason}`)
64
64
 
65
65
  A root whose architect asked a person a question, or handed its spec to a person for approval, and
66
- now waits only on them: the daemon returned
67
- its slot and suspends the architect after its turn, keeping its session. Read
68
- `issues.<KEY>.waitingOn`. Do not park it or start anything, and take nothing for the slot it
69
- returned: it is no reason to label or admit an issue ([Free
66
+ now waits only on them: the daemon returned its slot while the architect stays live to read the
67
+ person's comments and replies. Read `issues.<KEY>.waitingOn`. Do not park it or start anything, and
68
+ take nothing for the slot it returned: it is no reason to label or admit an issue ([Free
70
69
  slots](skill://legion-controller/SKILL.md#free-slots)). The person's answer puts it at the front of
71
- `admission.waiting` by itself, and the next free slot resumes its architect. A lost label or a
72
- foreign Dispatch claim sends the separate `unslotted` wake.
70
+ `admission.waiting` by itself, and the next free slot delivers its task. A lost label or a foreign
71
+ Dispatch claim sends the separate `unslotted` wake.
73
72
 
74
73
  ## `held on <KEY>` (`{kind: "held", phase, role?, reason?}`)
75
74