@sjawhar/opencode-legion-envoy 11.3.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
@@ -14895,6 +14904,45 @@ function dispatchIssueSubject(issueKey, type) {
14895
14904
  function dispatchDocumentSubject(project, slug, type) {
14896
14905
  return `${DISPATCH_DOCUMENT_TOPIC_PREFIX}${project}.${slug}.${type}`;
14897
14906
  }
14907
+ // ../contracts/src/related-forms.ts
14908
+ function relatedCheckFormOf(suggestions) {
14909
+ const { outcome, missing } = suggestions;
14910
+ const searchQuery = suggestions.search_query;
14911
+ if (outcome === "not_made" || outcome === undefined && missing !== undefined) {
14912
+ return { form: "not_made", reason: missing ?? "the server gave no reason", searchQuery };
14913
+ }
14914
+ const mode = relatedCheckModeWords(suggestions);
14915
+ if (suggestions.related.length > 0) {
14916
+ return { form: "items", mode, related: suggestions.related };
14917
+ }
14918
+ if (outcome === undefined) {
14919
+ return {
14920
+ form: "not_made",
14921
+ reason: "the server did not say whether it searched",
14922
+ searchQuery
14923
+ };
14924
+ }
14925
+ if (outcome === "found" || outcome === "none") {
14926
+ return { form: "nothing", mode };
14927
+ }
14928
+ return {
14929
+ form: "not_made",
14930
+ reason: `the server answered an outcome this client does not know (${String(outcome)})`,
14931
+ searchQuery
14932
+ };
14933
+ }
14934
+ function relatedCheckModeWords(suggestions) {
14935
+ const { search, search_note: note } = suggestions;
14936
+ if (search === undefined)
14937
+ return;
14938
+ if (search === "meaning_and_keywords")
14939
+ return "meaning and keywords";
14940
+ const words = search === "keywords_only" ? "keywords only" : String(search);
14941
+ return note === undefined || note === "" ? words : `${words}: ${note}`;
14942
+ }
14943
+ function relatedCheckSearchWords(text) {
14944
+ return (text.match(/[\p{L}\p{N}_]+/gu) ?? []).filter((word) => (word.match(/\p{L}/gu) ?? []).length >= 4).slice(0, 3).join(" ");
14945
+ }
14898
14946
  // ../contracts/src/repo.ts
14899
14947
  function canonicalRepo(owner, repo) {
14900
14948
  return `${owner.trim().toLowerCase()}/${repo.trim().toLowerCase().replace(/\.git$/, "")}`;
@@ -16078,6 +16126,12 @@ class DispatchClient {
16078
16126
  async artifactBlocks(id) {
16079
16127
  return this.#json("GET", ["api", "v1", "artifacts", id, "blocks"]);
16080
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
+ }
16081
16135
  async docEdit(id, input) {
16082
16136
  return this.#json("POST", ["api", "v1", "artifacts", id, "edits"], input);
16083
16137
  }
@@ -16378,10 +16432,26 @@ function dispatchDocumentRef(owner, slug) {
16378
16432
  function dispatchChildRef(ownerRef, kind, id) {
16379
16433
  return `${ownerRef}/${kind}/${id}`;
16380
16434
  }
16381
- function parseBlockRef(document, fragment) {
16382
- const target = parseBlockFragment(fragment);
16383
- const parsed = parseDispatchRef(document);
16384
- 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") {
16385
16455
  return null;
16386
16456
  }
16387
16457
  return {
@@ -16389,9 +16459,9 @@ function parseBlockRef(document, fragment) {
16389
16459
  kind: "block",
16390
16460
  id: parsed.id,
16391
16461
  block: target.block,
16392
- document,
16462
+ document: holder,
16393
16463
  ...parsed.owner.kind === "project" ? { artifact: parsed.id } : {},
16394
- ...target.pin === undefined ? {} : { pin: target.pin }
16464
+ ...pin
16395
16465
  };
16396
16466
  }
16397
16467
  function parseDispatchRef(ref) {
@@ -17104,39 +17174,18 @@ function decisionBlockLines(advice, context) {
17104
17174
  }
17105
17175
  }
17106
17176
  function relatedCheckForm(suggestions, context) {
17107
- const { outcome, missing } = suggestions;
17108
- if (outcome === "not_made" || outcome === undefined && missing !== undefined) {
17109
- return [notMadeLine(missing ?? "the server gave no reason", context, suggestions.search_query)];
17110
- }
17111
- const related = suggestions.related ?? [];
17112
- const mode = searchModeWords(suggestions);
17113
- if (related.length > 0) {
17114
- const header = mode === undefined ? "Related items:" : `Related items (searched by ${mode}):`;
17115
- return [header, ...related.flatMap((item) => relatedItemLines(item, context.configUrl))];
17116
- }
17117
- if (outcome === undefined) {
17118
- return [
17119
- notMadeLine("the server did not say whether it searched", context, suggestions.search_query)
17120
- ];
17177
+ const form = relatedCheckFormOf(suggestions);
17178
+ if (form.form === "not_made") {
17179
+ return [notMadeLine(form.reason, context, form.searchQuery)];
17121
17180
  }
17122
- if (outcome === "found" || outcome === "none") {
17123
- return [
17124
- mode === undefined ? "Related-item check searched and found nothing." : `Related-item check searched (${mode}) and found nothing.`
17125
- ];
17181
+ if (form.form === "items") {
17182
+ const header = form.mode === undefined ? "Related items:" : `Related items (searched by ${form.mode}):`;
17183
+ return [header, ...form.related.flatMap((item) => relatedItemLines(item, context.configUrl))];
17126
17184
  }
17127
17185
  return [
17128
- notMadeLine(`the server answered an outcome this client does not know (${String(outcome)})`, context, suggestions.search_query)
17186
+ form.mode === undefined ? "Related-item check searched and found nothing." : `Related-item check searched (${form.mode}) and found nothing.`
17129
17187
  ];
17130
17188
  }
17131
- function searchModeWords(suggestions) {
17132
- const { search, search_note: note } = suggestions;
17133
- if (search === undefined)
17134
- return;
17135
- if (search === "meaning_and_keywords")
17136
- return "meaning and keywords";
17137
- const words = search === "keywords_only" ? "keywords only" : String(search);
17138
- return note === undefined || note === "" ? words : `${words}: ${note}`;
17139
- }
17140
17189
  function relatedItemLines(item, configUrl) {
17141
17190
  try {
17142
17191
  const lines = [`- ${suggestionLabel(item)} \u2192 ${absoluteHref(item.href, configUrl)}`];
@@ -17173,13 +17222,10 @@ function absoluteHref(href, configUrl) {
17173
17222
  }
17174
17223
  }
17175
17224
  function notMadeLine(reason, context, searchQuery) {
17176
- const words = searchQuery?.trim() || searchWords(context.text);
17225
+ const words = searchQuery?.trim() || relatedCheckSearchWords(context.text);
17177
17226
  const query = words === "" ? " with two or three of its words as --query" : ` --query '${words.replaceAll("'", "'\\''")}'`;
17178
17227
  return `No related-item check was made: ${reason}. Search before you rely on this being new: dispatch search --project ${context.project}${query}`;
17179
17228
  }
17180
- function searchWords(text) {
17181
- return (text.match(/[\p{L}\p{N}_]+/gu) ?? []).filter((word) => (word.match(/\p{L}/gu) ?? []).length >= 4).slice(0, 3).join(" ");
17182
- }
17183
17229
 
17184
17230
  // ../envoy-client/src/dispatch-execute.ts
17185
17231
  function documentTopic(artifact) {
@@ -17442,7 +17488,7 @@ function normalizeUUID(value) {
17442
17488
  }
17443
17489
  var askIdProblem = "ask must be a bare ask id or a dispatch://.../ask/<id> reference";
17444
17490
  var commentIdProblem = "comment must be a bare comment id or a dispatch://.../comment/<id> reference";
17445
- 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>)";
17446
17492
  function askId(args) {
17447
17493
  const ask = stringArg(args, "ask");
17448
17494
  return ask.startsWith("dispatch://") ? parseDispatchRef(ask)?.id ?? ask : ask;
@@ -17454,6 +17500,28 @@ function messageIdOf(value) {
17454
17500
  })() : value;
17455
17501
  return id === undefined ? undefined : normalizeUUID(id);
17456
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
+ }
17457
17525
  var idPrefixPattern = /^[0-9a-f][0-9a-f-]{7,}$/i;
17458
17526
  function refOwnerName(ref) {
17459
17527
  return ref.owner.kind === "issue" ? ref.owner.issue : `${ref.owner.project}/${ref.artifact ?? ref.id}`;
@@ -17563,7 +17631,20 @@ async function resolveAskArgument(tool, args, client, sessionId) {
17563
17631
  }
17564
17632
  function messageInReplyTo(args) {
17565
17633
  const inReplyTo = optionalString(args, "in_reply_to");
17566
- 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 };
17567
17648
  }
17568
17649
  function argumentProblems(tool, args) {
17569
17650
  const problems = [];
@@ -17598,15 +17679,21 @@ function argumentProblems(tool, args) {
17598
17679
  if (optionalString(args, "quote") !== undefined && optionalString(args, "artifact") === undefined) {
17599
17680
  problems.push("artifact is required when quote is supplied");
17600
17681
  }
17601
- 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) {
17602
17684
  problems.push("reply_to and reply_to_ask cannot both be set");
17603
17685
  }
17686
+ const replyBlock = replyTo === undefined ? undefined : replyBlockProblem("reply_to", "comment", replyTo);
17687
+ if (replyBlock !== undefined)
17688
+ problems.push(replyBlock);
17604
17689
  break;
17605
17690
  }
17606
17691
  case "dispatch_message": {
17607
17692
  const inReplyTo = optionalString(args, "in_reply_to");
17608
- if (inReplyTo !== undefined && messageIdOf(inReplyTo) === undefined) {
17609
- 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);
17610
17697
  }
17611
17698
  break;
17612
17699
  }
@@ -17686,7 +17773,7 @@ function dispatchRefFromUrl(value, serverUrl) {
17686
17773
  };
17687
17774
  const blockOf = (versionName) => {
17688
17775
  const id = url.hash.startsWith("#b-") ? decodeBlockId(url.hash.slice(3)) : undefined;
17689
- return id === undefined || url.searchParams.get(versionName) ? "" : `#b-${encodeBlockId(id)}`;
17776
+ return id === undefined || versionName !== undefined && url.searchParams.get(versionName) ? "" : `#b-${encodeBlockId(id)}`;
17690
17777
  };
17691
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\/([^/]+))?\/?$/);
17692
17779
  if (issuePage) {
@@ -17712,9 +17799,9 @@ function dispatchRefFromUrl(value, serverUrl) {
17712
17799
  if (ask !== undefined)
17713
17800
  return `dispatch://${key}/ask/${ask}`;
17714
17801
  if (comment !== undefined)
17715
- return `dispatch://${key}/comment/${comment}`;
17802
+ return `dispatch://${key}/comment/${comment}${blockOf()}`;
17716
17803
  if (message !== undefined)
17717
- return `dispatch://${key}/message/${message}`;
17804
+ return `dispatch://${key}/message/${message}${blockOf()}`;
17718
17805
  return `dispatch://${key}`;
17719
17806
  }
17720
17807
  const agentArtifactPage = url.pathname.match(/^\/agents\/([^/]+)\/artifacts\/([^/]+)\/?$/);
@@ -17737,7 +17824,7 @@ function dispatchRefFromUrl(value, serverUrl) {
17737
17824
  return;
17738
17825
  return item === undefined ? `${document}${versionOf("version")}${blockOf("version")}` : `${document}/${item.kind}/${item.id}`;
17739
17826
  }
17740
- 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";
17741
17828
  var ownerRequiredProblem = "issue is required; supply issue or set LEGION_ISSUE";
17742
17829
  function innerRefProblem(tool, ref, refText) {
17743
17830
  if (ref.kind !== "ask" && ref.kind !== "comment" && ref.kind !== "message" && ref.kind !== "log" && ref.kind !== "children" && ref.kind !== "block") {
@@ -17746,10 +17833,11 @@ function innerRefProblem(tool, ref, refText) {
17746
17833
  if (tool === "dispatch_read" || tool === "dispatch_ask")
17747
17834
  return;
17748
17835
  const owner = refOwnerName(ref);
17749
- 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>" : "";
17750
17838
  const ownerArguments = ref.owner.kind === "issue" ? `issue ${ref.owner.issue}${documentArgument}` : `project ${ref.owner.project} and artifact ${ref.artifact}`;
17751
- 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}`;
17752
- 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}`;
17753
17841
  return `ref ${refText} names ${item} on ${owner}; dispatch ${commandName(tool)} would act on ${owner} and drop the ${ref.kind}: ${fix}`;
17754
17842
  }
17755
17843
  var documentItemPattern = /^dispatch:\/\/([A-Z][A-Z0-9]{1,9}-[1-9][0-9]*)\/artifact\/[^/@]+(?:@v\d+)?\/(ask|comment)\/([^/]+)$/;
@@ -17776,7 +17864,7 @@ function ownerConflictProblems(tool, args, parsedRef, refText) {
17776
17864
  const artifact = optionalString(args, "artifact");
17777
17865
  const version = optionalNumber(args, "version");
17778
17866
  const ref = tool === "dispatch_ask" && (issue !== undefined || project !== undefined) ? null : parsedRef;
17779
- 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;
17780
17868
  if (ref !== null && refText !== undefined) {
17781
17869
  const refOwner = refOwnerName(ref);
17782
17870
  if (issue !== undefined && (ref.owner.kind !== "issue" || nativeIssueKeyPattern.test(issue) && issue !== ref.owner.issue)) {
@@ -18183,9 +18271,12 @@ function referenceLines(read) {
18183
18271
  if (read.edges.length === 0)
18184
18272
  return ["- none"];
18185
18273
  return read.edges.map((edge) => {
18274
+ const { node } = edge;
18186
18275
  const excerpt = edge.excerpt === undefined ? "" : `${textHead(edge.excerpt.text)} \xB7 `;
18187
18276
  const block = edge.block === undefined ? "" : ` (block ${edge.block.ref ?? edge.block.id})`;
18188
- 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}`;
18189
18280
  });
18190
18281
  }
18191
18282
  function unavailableReason(error) {
@@ -18228,27 +18319,32 @@ function nodeLine(node) {
18228
18319
  const made = fact(node.made_by, node.made_at);
18229
18320
  const changed = fact(node.changed_by, node.changed_at);
18230
18321
  const { parent } = node;
18231
- 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})`}`;
18232
18323
  return [
18233
18324
  `Node: ${node.kind} ${node.ref ?? node.id} \xB7 made by ${made} \xB7 changed by ${changed}${partOf}`
18234
18325
  ];
18235
18326
  }
18236
18327
  function blockLines(node, ref) {
18237
- const head = [
18238
- `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}`,
18239
18332
  `Document: ${node.document ?? ref.document}`,
18240
18333
  ...node.changed_version === undefined ? [] : [`Changed in version: ${node.changed_version}`],
18241
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]
18242
18338
  ];
18243
18339
  const { pin } = node;
18244
18340
  if (pin === undefined)
18245
18341
  return head;
18246
- 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}.`;
18247
18343
  return [
18248
18344
  ...head,
18249
- `Pinned to version: ${pin.version}`,
18345
+ `Pinned to ${unit}: ${pin.version}`,
18250
18346
  state,
18251
- `Text at version ${pin.version}:`,
18347
+ `Text at ${unit} ${pin.version}:`,
18252
18348
  pin.text
18253
18349
  ];
18254
18350
  }
@@ -18395,11 +18491,13 @@ function commentSummary({ comment, replies }, graph) {
18395
18491
  const root = [
18396
18492
  `${comment.id} \xB7 ${actorText(comment.author)}`,
18397
18493
  ...anchorLines(comment),
18494
+ ...replyBlockLines(comment),
18398
18495
  `Body: ${comment.body}`
18399
18496
  ];
18400
18497
  const chain = replies.flatMap((reply) => [
18401
18498
  `${reply.id} \xB7 ${actorText(reply.author)}`,
18402
18499
  ...anchorLines(reply),
18500
+ ...replyBlockLines(reply),
18403
18501
  `Body: ${reply.body}`
18404
18502
  ]);
18405
18503
  return [
@@ -18411,6 +18509,14 @@ function commentSummary({ comment, replies }, graph) {
18411
18509
  ].join(`
18412
18510
  `);
18413
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
+ }
18414
18520
  function anchorLines(record) {
18415
18521
  return [
18416
18522
  ...record.anchor?.quote === undefined ? [] : [`> ${record.anchor.quote}`],
@@ -18434,9 +18540,14 @@ function positionText(block) {
18434
18540
  ].join(" \u203A ");
18435
18541
  }
18436
18542
  function messageSummary({ message, replies }, graph) {
18437
- 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
+ ];
18438
18548
  const chain = replies.flatMap((reply) => [
18439
18549
  `${reply.id} \xB7 ${actorText(reply.author)}`,
18550
+ ...replyBlockLines(reply),
18440
18551
  `Body: ${reply.body}`
18441
18552
  ]);
18442
18553
  return [
@@ -19294,7 +19405,7 @@ async function executeDispatchTool(input) {
19294
19405
  const owner = documentOwner();
19295
19406
  const resolved = owner.kind === "project" || artifactReference === undefined ? owner.kind === "project" ? await resolveDocument(owner, artifactReference) : undefined : await resolveDocument(owner, artifactReference);
19296
19407
  const anchored = resolved ? anchor(resolved.artifact, args) : undefined;
19297
- const replyTo = optionalString(args, "reply_to");
19408
+ const replyTo = commentReplyTo(args);
19298
19409
  const replyToAskReference = optionalString(args, "reply_to_ask");
19299
19410
  const replyToAsk = replyToAskReference === undefined ? undefined : normalizeUUID(replyToAskReference);
19300
19411
  if (replyToAskReference !== undefined && replyToAsk === undefined) {
@@ -19311,7 +19422,8 @@ async function executeDispatchTool(input) {
19311
19422
  const commentInput = {
19312
19423
  body,
19313
19424
  ...anchored === undefined ? {} : { anchor: anchored },
19314
- ...replyTo === undefined ? {} : { reply_to: replyTo },
19425
+ ...replyTo === undefined ? {} : { reply_to: replyTo.id },
19426
+ ...replyTo?.block === undefined ? {} : { reply_to_block: replyTo.block },
19315
19427
  ...replyToAsk === undefined ? {} : { ask_id: replyToAsk },
19316
19428
  ...requestedTurn === undefined ? {} : { turn: requestedTurn },
19317
19429
  actor
@@ -19371,7 +19483,8 @@ async function executeDispatchTool(input) {
19371
19483
  };
19372
19484
  }
19373
19485
  case "dispatch_message": {
19374
- const inReplyTo = messageInReplyTo(args);
19486
+ const target = messageInReplyTo(args);
19487
+ const inReplyTo = target?.id;
19375
19488
  const written = stringArg(args, "body");
19376
19489
  const bodyLimit = {
19377
19490
  cap: DISPATCH_BODY_MAX,
@@ -19418,6 +19531,7 @@ async function executeDispatchTool(input) {
19418
19531
  const message = await client.message(issueKey, {
19419
19532
  body,
19420
19533
  ...inReplyTo === undefined ? {} : { in_reply_to: inReplyTo },
19534
+ ...target?.block === undefined ? {} : { in_reply_to_block: target.block },
19421
19535
  actor
19422
19536
  });
19423
19537
  const messageRef = dispatchChildRef(dispatchIssueRef(issueKey), "message", message.id);
@@ -19712,10 +19826,11 @@ Document token: ${listed.token}`,
19712
19826
  if (ownerArguments.ref?.kind === "block") {
19713
19827
  const ref = ownerArguments.ref;
19714
19828
  const blockRef = stringArg(args, "ref");
19829
+ const holder = blockRef.slice(0, blockRef.indexOf("#"));
19715
19830
  const [incoming, outgoing] = await Promise.all([
19716
19831
  client.getReferences({ to: blockRef }).catch((error) => {
19717
19832
  if (ref.pin === undefined && dispatchAnswered(error, 404)) {
19718
- throw new Error(`No block ${ref.block} in ${ref.document}.`);
19833
+ throw new Error(`No block ${ref.block} in ${holder}.`);
19719
19834
  }
19720
19835
  throw error;
19721
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.3.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.3.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