@sjawhar/pi-legion-envoy 0.41.1 → 0.42.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/envoy.js CHANGED
@@ -29663,14 +29663,16 @@ var IssueEventPayloadSchema = object({
29663
29663
  route: string2().nullish()
29664
29664
  });
29665
29665
  var ArtifactCreatedEventPayloadSchema = object({
29666
- artifact: object({ name: string2().optional() }).optional()
29666
+ artifact: object({ id: string2().optional(), slug: string2().optional(), name: string2().optional() }).optional()
29667
29667
  });
29668
29668
  var ArtifactVersionEventPayloadSchema = object({
29669
+ artifact_id: string2().optional(),
29669
29670
  name: string2().optional(),
29670
29671
  version: object({ number: number2().optional(), summary: string2().nullish() }).optional(),
29671
29672
  diff: string2().optional()
29672
29673
  });
29673
29674
  var askEventPayloadFields = {
29675
+ id: string2().optional(),
29674
29676
  opened_event_id: number2().int().positive(),
29675
29677
  question: string2().optional(),
29676
29678
  options: array(object({ label: string2().optional() })).nullish(),
@@ -29708,13 +29710,15 @@ var CommentEventPayloadSchema = object({
29708
29710
  ask_question: string2().optional(),
29709
29711
  anchor: object({ quote: string2().optional() }).nullish(),
29710
29712
  suggestion: object({ replace_with: string2().optional() }).nullish(),
29711
- author: object({ kind: string2(), id: string2().optional() }).passthrough().optional(),
29713
+ author: object({ kind: string2(), id: string2() }).optional(),
29712
29714
  created_at: string2().optional()
29713
29715
  });
29714
29716
  var MessageEventPayloadSchema = object({
29715
29717
  id: string2().optional(),
29716
29718
  body: string2().optional(),
29717
- author: object({ kind: string2(), id: string2().optional() }).passthrough().optional()
29719
+ reply_to: string2().nullish(),
29720
+ reply_body: string2().optional(),
29721
+ author: object({ kind: string2(), id: string2() }).optional()
29718
29722
  });
29719
29723
  var ChildStatusEventPayloadSchema = object({
29720
29724
  child_key: string2().optional(),
@@ -29816,7 +29820,7 @@ var dispatchToolSpecs = [
29816
29820
  },
29817
29821
  {
29818
29822
  name: "dispatch_ask",
29819
- description: "Open a durable, answerable decision on an issue or project document. Do not use it for a status update or discussion; " + `use dispatch_message instead. Question is at most 800 characters and has at most 8 options. ${OWNER_REFERENCE}`,
29823
+ description: "Open a durable, answerable decision on an issue or project document. Do not use it for a status update or discussion; " + "use dispatch_message instead. Anchor a document question, thread reply_to/reply_to_ask, or cite a dispatch:// " + `reference \u2014 it must be answerable from its own text and anchor alone, never "see above". Question is at most 800 ` + `characters and has at most 8 options. ${OWNER_REFERENCE}`,
29820
29824
  arguments: (z) => ({
29821
29825
  issue: z.string().describe(ISSUE_REFERENCE).optional(),
29822
29826
  project: z.string().describe("Project key owning the document.").optional(),
@@ -29897,15 +29901,16 @@ var dispatchToolSpecs = [
29897
29901
  },
29898
29902
  {
29899
29903
  name: "dispatch_message",
29900
- description: "Post a plain issue update. Do not use it for a decision or line-specific review; use dispatch_ask " + `or dispatch_comment instead. Body is at most 2,000 characters. ${ISSUE_REFERENCE}`,
29904
+ description: "Post a note humans must read now: a reply to a human's message, a deliverable that landed, or a blocker only " + "they can clear. Never progress or status updates - Dispatch is a high-signal record, not a log. Not a decision " + `(dispatch_ask) or document feedback (dispatch_comment). Body is at most 2,000 characters. ${ISSUE_REFERENCE}`,
29901
29905
  arguments: (z) => ({
29902
29906
  issue: z.string().describe(ISSUE_REFERENCE),
29903
- body: z.string({ max: 2000 }).describe("Update text, at most 2,000 characters.")
29907
+ body: z.string({ max: 2000 }).describe("Update text, at most 2,000 characters."),
29908
+ 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.").optional()
29904
29909
  })
29905
29910
  },
29906
29911
  {
29907
29912
  name: "dispatch_doc_edit",
29908
- description: "Apply deterministic text edits to an issue or project document. Do not use it for review feedback or for reading; use " + `dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. ${OWNER_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
29913
+ description: "Apply deterministic text edits to an issue or project document. Do not use it for review feedback or for reading; use " + "dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. The spec (or any document) holds requirements, " + `design, and decisions - never progress, status, or timestamps. ${OWNER_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
29909
29914
  arguments: (z) => ({
29910
29915
  issue: z.string().describe(ISSUE_REFERENCE).optional(),
29911
29916
  project: z.string().describe("Project key owning the document.").optional(),
@@ -31026,6 +31031,12 @@ function dispatchAskQuestion(event) {
31026
31031
  const parsed = CommentEventPayloadSchema.safeParse(event.payload);
31027
31032
  return parsed.success && parsed.data.ask_question !== "" ? parsed.data.ask_question : undefined;
31028
31033
  }
31034
+ function dispatchMessageReplyPreview(event) {
31035
+ if (event.type !== "message.created")
31036
+ return;
31037
+ const parsed = MessageEventPayloadSchema.safeParse(event.payload);
31038
+ return parsed.success && parsed.data.reply_body !== undefined && parsed.data.reply_body !== "" ? parsed.data.reply_body : undefined;
31039
+ }
31029
31040
  function parseDispatchFrame(rawPayload) {
31030
31041
  let value;
31031
31042
  try {
@@ -31076,6 +31087,7 @@ function renderInbound(raw, sessionID, subject) {
31076
31087
  let dispatchEvent;
31077
31088
  let dispatchIssue;
31078
31089
  let askQuestion;
31090
+ let messageReplyPreview;
31079
31091
  const dispatchRendered = envelope.source === "dispatch" && envelope.payload !== undefined;
31080
31092
  if (envelope.source === "dispatch") {
31081
31093
  if (envelope.payload === undefined) {
@@ -31090,6 +31102,7 @@ function renderInbound(raw, sessionID, subject) {
31090
31102
  return { skip: true, content: "", envelope };
31091
31103
  }
31092
31104
  askQuestion = dispatchAskQuestion(frame.event);
31105
+ messageReplyPreview = dispatchMessageReplyPreview(frame.event);
31093
31106
  dispatchEvent = {
31094
31107
  owner: dispatchOwner(frame.event, subject ?? envelope.topic),
31095
31108
  ...frame.event.issue_key === null ? {
@@ -31143,7 +31156,7 @@ ${envelope.payload ?? ""}`;
31143
31156
  ...envelope.expires_at === undefined ? {} : { by: inboundTimestamp(envelope.expires_at) },
31144
31157
  ...envelope.urgency === undefined ? {} : { urgency: envelope.urgency },
31145
31158
  ...envelope.expects_reply === undefined ? {} : { expects_reply: envelope.expects_reply },
31146
- ...envelope.in_reply_to === undefined ? {} : { re: askQuestion ?? envelope.in_reply_to },
31159
+ ...envelope.in_reply_to === undefined ? {} : { re: askQuestion ?? messageReplyPreview ?? envelope.in_reply_to },
31147
31160
  ...envelope.supersedes === undefined ? {} : { supersedes: envelope.supersedes },
31148
31161
  ...reply === undefined ? {} : { reply_with: reply },
31149
31162
  ...role === undefined ? {} : {
@@ -31454,6 +31467,16 @@ class DispatchClient {
31454
31467
  async message(issue, input) {
31455
31468
  return this.#json("POST", ["api", "v1", "issues", await this.#resolveIssue(issue), "messages"], input);
31456
31469
  }
31470
+ async getMessage(issue, id) {
31471
+ return this.#json("GET", [
31472
+ "api",
31473
+ "v1",
31474
+ "issues",
31475
+ await this.#resolveIssue(issue),
31476
+ "messages",
31477
+ id
31478
+ ]);
31479
+ }
31457
31480
  async artifact(issue, input) {
31458
31481
  const artifactPath = ["api", "v1", "issues", await this.#resolveIssue(issue), "artifacts"];
31459
31482
  if ("content" in input)
@@ -31718,10 +31741,10 @@ function parseDispatchRef(ref) {
31718
31741
  id: targetID
31719
31742
  };
31720
31743
  }
31721
- const issueReference = ref.match(/^dispatch:\/\/([A-Z][A-Z0-9]{1,9}-[1-9][0-9]*)(?:\/(spec)|\/(log)|\/(children)|\/artifact\/([^/@]+)(?:@v(\d+))?|\/ask\/([^/]+)|\/comment\/([^/]+))?$/);
31744
+ const issueReference = ref.match(/^dispatch:\/\/([A-Z][A-Z0-9]{1,9}-[1-9][0-9]*)(?:\/(spec)|\/(log)|\/(children)|\/artifact\/([^/@]+)(?:@v(\d+))?|\/ask\/([^/]+)|\/comment\/([^/]+)|\/message\/([^/]+))?$/);
31722
31745
  if (!issueReference)
31723
31746
  return null;
31724
- const [, issue, spec, log, children, artifact, version, ask, comment] = issueReference;
31747
+ const [, issue, spec, log, children, artifact, version, ask, comment, message] = issueReference;
31725
31748
  if (!issue || version !== undefined && Number(version) < 1)
31726
31749
  return null;
31727
31750
  const owner = { kind: "issue", issue };
@@ -31743,6 +31766,8 @@ function parseDispatchRef(ref) {
31743
31766
  return { owner, kind: "ask", id: ask };
31744
31767
  if (comment)
31745
31768
  return { owner, kind: "comment", id: comment };
31769
+ if (message)
31770
+ return { owner, kind: "message", id: message };
31746
31771
  return { owner, kind: "issue", id: issue };
31747
31772
  }
31748
31773
  function askId(args) {
@@ -31755,6 +31780,16 @@ function askId(args) {
31755
31780
  }
31756
31781
  return reference.id;
31757
31782
  }
31783
+ function messageReplyTo(args) {
31784
+ const replyTo = optionalString(args, "reply_to");
31785
+ if (replyTo === undefined || !replyTo.startsWith("dispatch://"))
31786
+ return replyTo;
31787
+ const reference = parseDispatchRef(replyTo);
31788
+ if (reference?.kind !== "message") {
31789
+ throw new Error("reply_to must be a bare message id or a dispatch://.../message/<id> reference");
31790
+ }
31791
+ return reference.id;
31792
+ }
31758
31793
  function toolSchema(tool) {
31759
31794
  const spec = dispatchToolSpecs.find((candidate) => candidate.name === tool);
31760
31795
  if (!spec)
@@ -31766,7 +31801,7 @@ async function resolveOwnerArguments(tool, args, cwd, env, exec) {
31766
31801
  return { args, ref: null, owner: null };
31767
31802
  const refArgument = args.ref;
31768
31803
  const ref = typeof refArgument === "string" ? parseDispatchRef(refArgument) ?? (() => {
31769
- throw new Error("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/artifact/<slug>, or dispatch://PROJECT/artifact/<slug>");
31804
+ throw new Error("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>, or " + "dispatch://PROJECT/artifact/<slug>");
31770
31805
  })() : null;
31771
31806
  const issueArgument = args.issue;
31772
31807
  const projectArgument = args.project;
@@ -31846,8 +31881,12 @@ async function resolveArtifact(client, owner, artifactReference) {
31846
31881
  } catch (error) {
31847
31882
  if (!(error instanceof DispatchServiceError) || error.status !== 404)
31848
31883
  throw error;
31849
- const artifacts = await client.listProjectArtifacts(owner.project);
31850
- const artifact = artifacts.find((candidate) => candidate.name === artifactReference);
31884
+ const artifacts = await client.listProjectArtifacts(owner.project, true);
31885
+ const matches = artifacts.filter((candidate) => candidate.name === artifactReference);
31886
+ if (matches.length > 1) {
31887
+ throw new Error(`artifact name ${artifactReference} is ambiguous in project ${owner.project}; ` + `${matches.length} documents share it \u2014 use its slug instead`);
31888
+ }
31889
+ const artifact = matches[0];
31851
31890
  if (!artifact)
31852
31891
  throw error;
31853
31892
  return { owner, artifact };
@@ -31953,6 +31992,18 @@ function commentSummary({ comment, replies }) {
31953
31992
  return ["Comment:", ...root, "Reply chain:", ...chain.length === 0 ? ["- none"] : chain].join(`
31954
31993
  `);
31955
31994
  }
31995
+ function messageSummary({ message, replies }) {
31996
+ const root = [
31997
+ `${message.id} \xB7 ${message.author.kind} ${message.author.id}`,
31998
+ `Body: ${message.body}`
31999
+ ];
32000
+ const chain = replies.flatMap((reply) => [
32001
+ `${reply.id} \xB7 ${reply.author.kind} ${reply.author.id}`,
32002
+ `Body: ${reply.body}`
32003
+ ]);
32004
+ return ["Message:", ...root, "Reply chain:", ...chain.length === 0 ? ["- none"] : chain].join(`
32005
+ `);
32006
+ }
31956
32007
  async function openArtifactMarks(client, resolved) {
31957
32008
  const asks = resolved.owner.kind === "project" ? await client.getArtifactAsks(resolved.artifact.id) : resolved.issue?.open_asks ?? [];
31958
32009
  const marks = asks.filter((ask) => ask.state === "open" && ask.anchor?.artifact_id === resolved.artifact.id).map((ask) => `ask ${ask.id}`);
@@ -32160,9 +32211,15 @@ async function executeDispatchTool(input) {
32160
32211
  };
32161
32212
  }
32162
32213
  case "dispatch_message": {
32163
- const message = await client.message(issue(), { body: stringArg(args, "body"), actor });
32214
+ const replyTo = messageReplyTo(args);
32215
+ const message = await client.message(issue(), {
32216
+ body: stringArg(args, "body"),
32217
+ ...replyTo === undefined ? {} : { reply_to: replyTo },
32218
+ actor
32219
+ });
32220
+ const messageRef = `dispatch://${message.issue_key}/message/${message.id}`;
32164
32221
  return {
32165
- text: `Posted message ${message.id}`,
32222
+ text: `Posted message ${message.id} (${messageRef})`,
32166
32223
  details: {
32167
32224
  issue: message.issue_key,
32168
32225
  topic: dispatchIssueSubject(message.issue_key, ">"),
@@ -32249,6 +32306,16 @@ Open anchored asks/comments: ${marks.join(", ")}`,
32249
32306
  details: ownerArguments.ref.owner.kind === "project" ? { project: ownerArguments.ref.owner.project } : { issue: comment.comment.issue_key }
32250
32307
  };
32251
32308
  }
32309
+ if (ownerArguments.ref?.kind === "message") {
32310
+ if (ownerArguments.ref.owner.kind !== "issue") {
32311
+ throw new Error("message references are issue-scoped");
32312
+ }
32313
+ const messageRead = await client.getMessage(ownerArguments.ref.owner.issue, ownerArguments.ref.id);
32314
+ return {
32315
+ text: messageSummary(messageRead),
32316
+ details: { issue: messageRead.message.issue_key }
32317
+ };
32318
+ }
32252
32319
  if (documentOwner().kind === "project") {
32253
32320
  const resolved = await resolveArtifact(client, documentOwner(), stringArg(args, "artifact"));
32254
32321
  return {
package/dist/legion.js CHANGED
@@ -29662,14 +29662,16 @@ var IssueEventPayloadSchema = object({
29662
29662
  route: string2().nullish()
29663
29663
  });
29664
29664
  var ArtifactCreatedEventPayloadSchema = object({
29665
- artifact: object({ name: string2().optional() }).optional()
29665
+ artifact: object({ id: string2().optional(), slug: string2().optional(), name: string2().optional() }).optional()
29666
29666
  });
29667
29667
  var ArtifactVersionEventPayloadSchema = object({
29668
+ artifact_id: string2().optional(),
29668
29669
  name: string2().optional(),
29669
29670
  version: object({ number: number2().optional(), summary: string2().nullish() }).optional(),
29670
29671
  diff: string2().optional()
29671
29672
  });
29672
29673
  var askEventPayloadFields = {
29674
+ id: string2().optional(),
29673
29675
  opened_event_id: number2().int().positive(),
29674
29676
  question: string2().optional(),
29675
29677
  options: array(object({ label: string2().optional() })).nullish(),
@@ -29707,13 +29709,15 @@ var CommentEventPayloadSchema = object({
29707
29709
  ask_question: string2().optional(),
29708
29710
  anchor: object({ quote: string2().optional() }).nullish(),
29709
29711
  suggestion: object({ replace_with: string2().optional() }).nullish(),
29710
- author: object({ kind: string2(), id: string2().optional() }).passthrough().optional(),
29712
+ author: object({ kind: string2(), id: string2() }).optional(),
29711
29713
  created_at: string2().optional()
29712
29714
  });
29713
29715
  var MessageEventPayloadSchema = object({
29714
29716
  id: string2().optional(),
29715
29717
  body: string2().optional(),
29716
- author: object({ kind: string2(), id: string2().optional() }).passthrough().optional()
29718
+ reply_to: string2().nullish(),
29719
+ reply_body: string2().optional(),
29720
+ author: object({ kind: string2(), id: string2() }).optional()
29717
29721
  });
29718
29722
  var ChildStatusEventPayloadSchema = object({
29719
29723
  child_key: string2().optional(),
@@ -29815,7 +29819,7 @@ var dispatchToolSpecs = [
29815
29819
  },
29816
29820
  {
29817
29821
  name: "dispatch_ask",
29818
- description: "Open a durable, answerable decision on an issue or project document. Do not use it for a status update or discussion; " + `use dispatch_message instead. Question is at most 800 characters and has at most 8 options. ${OWNER_REFERENCE}`,
29822
+ description: "Open a durable, answerable decision on an issue or project document. Do not use it for a status update or discussion; " + "use dispatch_message instead. Anchor a document question, thread reply_to/reply_to_ask, or cite a dispatch:// " + `reference \u2014 it must be answerable from its own text and anchor alone, never "see above". Question is at most 800 ` + `characters and has at most 8 options. ${OWNER_REFERENCE}`,
29819
29823
  arguments: (z) => ({
29820
29824
  issue: z.string().describe(ISSUE_REFERENCE).optional(),
29821
29825
  project: z.string().describe("Project key owning the document.").optional(),
@@ -29896,15 +29900,16 @@ var dispatchToolSpecs = [
29896
29900
  },
29897
29901
  {
29898
29902
  name: "dispatch_message",
29899
- description: "Post a plain issue update. Do not use it for a decision or line-specific review; use dispatch_ask " + `or dispatch_comment instead. Body is at most 2,000 characters. ${ISSUE_REFERENCE}`,
29903
+ description: "Post a note humans must read now: a reply to a human's message, a deliverable that landed, or a blocker only " + "they can clear. Never progress or status updates - Dispatch is a high-signal record, not a log. Not a decision " + `(dispatch_ask) or document feedback (dispatch_comment). Body is at most 2,000 characters. ${ISSUE_REFERENCE}`,
29900
29904
  arguments: (z) => ({
29901
29905
  issue: z.string().describe(ISSUE_REFERENCE),
29902
- body: z.string({ max: 2000 }).describe("Update text, at most 2,000 characters.")
29906
+ body: z.string({ max: 2000 }).describe("Update text, at most 2,000 characters."),
29907
+ 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.").optional()
29903
29908
  })
29904
29909
  },
29905
29910
  {
29906
29911
  name: "dispatch_doc_edit",
29907
- description: "Apply deterministic text edits to an issue or project document. Do not use it for review feedback or for reading; use " + `dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. ${OWNER_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
29912
+ description: "Apply deterministic text edits to an issue or project document. Do not use it for review feedback or for reading; use " + "dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. The spec (or any document) holds requirements, " + `design, and decisions - never progress, status, or timestamps. ${OWNER_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
29908
29913
  arguments: (z) => ({
29909
29914
  issue: z.string().describe(ISSUE_REFERENCE).optional(),
29910
29915
  project: z.string().describe("Project key owning the document.").optional(),
@@ -31473,6 +31478,12 @@ function dispatchAskQuestion(event) {
31473
31478
  const parsed = CommentEventPayloadSchema.safeParse(event.payload);
31474
31479
  return parsed.success && parsed.data.ask_question !== "" ? parsed.data.ask_question : undefined;
31475
31480
  }
31481
+ function dispatchMessageReplyPreview(event) {
31482
+ if (event.type !== "message.created")
31483
+ return;
31484
+ const parsed = MessageEventPayloadSchema.safeParse(event.payload);
31485
+ return parsed.success && parsed.data.reply_body !== undefined && parsed.data.reply_body !== "" ? parsed.data.reply_body : undefined;
31486
+ }
31476
31487
  function parseDispatchFrame(rawPayload) {
31477
31488
  let value;
31478
31489
  try {
@@ -31523,6 +31534,7 @@ function renderInbound(raw, sessionID, subject) {
31523
31534
  let dispatchEvent;
31524
31535
  let dispatchIssue;
31525
31536
  let askQuestion;
31537
+ let messageReplyPreview;
31526
31538
  const dispatchRendered = envelope.source === "dispatch" && envelope.payload !== undefined;
31527
31539
  if (envelope.source === "dispatch") {
31528
31540
  if (envelope.payload === undefined) {
@@ -31537,6 +31549,7 @@ function renderInbound(raw, sessionID, subject) {
31537
31549
  return { skip: true, content: "", envelope };
31538
31550
  }
31539
31551
  askQuestion = dispatchAskQuestion(frame.event);
31552
+ messageReplyPreview = dispatchMessageReplyPreview(frame.event);
31540
31553
  dispatchEvent = {
31541
31554
  owner: dispatchOwner(frame.event, subject ?? envelope.topic),
31542
31555
  ...frame.event.issue_key === null ? {
@@ -31590,7 +31603,7 @@ ${envelope.payload ?? ""}`;
31590
31603
  ...envelope.expires_at === undefined ? {} : { by: inboundTimestamp(envelope.expires_at) },
31591
31604
  ...envelope.urgency === undefined ? {} : { urgency: envelope.urgency },
31592
31605
  ...envelope.expects_reply === undefined ? {} : { expects_reply: envelope.expects_reply },
31593
- ...envelope.in_reply_to === undefined ? {} : { re: askQuestion ?? envelope.in_reply_to },
31606
+ ...envelope.in_reply_to === undefined ? {} : { re: askQuestion ?? messageReplyPreview ?? envelope.in_reply_to },
31594
31607
  ...envelope.supersedes === undefined ? {} : { supersedes: envelope.supersedes },
31595
31608
  ...reply === undefined ? {} : { reply_with: reply },
31596
31609
  ...role === undefined ? {} : {
@@ -31894,6 +31907,16 @@ class DispatchClient {
31894
31907
  async message(issue, input) {
31895
31908
  return this.#json("POST", ["api", "v1", "issues", await this.#resolveIssue(issue), "messages"], input);
31896
31909
  }
31910
+ async getMessage(issue, id) {
31911
+ return this.#json("GET", [
31912
+ "api",
31913
+ "v1",
31914
+ "issues",
31915
+ await this.#resolveIssue(issue),
31916
+ "messages",
31917
+ id
31918
+ ]);
31919
+ }
31897
31920
  async artifact(issue, input) {
31898
31921
  const artifactPath = ["api", "v1", "issues", await this.#resolveIssue(issue), "artifacts"];
31899
31922
  if ("content" in input)
@@ -32158,10 +32181,10 @@ function parseDispatchRef(ref) {
32158
32181
  id: targetID
32159
32182
  };
32160
32183
  }
32161
- const issueReference = ref.match(/^dispatch:\/\/([A-Z][A-Z0-9]{1,9}-[1-9][0-9]*)(?:\/(spec)|\/(log)|\/(children)|\/artifact\/([^/@]+)(?:@v(\d+))?|\/ask\/([^/]+)|\/comment\/([^/]+))?$/);
32184
+ const issueReference = ref.match(/^dispatch:\/\/([A-Z][A-Z0-9]{1,9}-[1-9][0-9]*)(?:\/(spec)|\/(log)|\/(children)|\/artifact\/([^/@]+)(?:@v(\d+))?|\/ask\/([^/]+)|\/comment\/([^/]+)|\/message\/([^/]+))?$/);
32162
32185
  if (!issueReference)
32163
32186
  return null;
32164
- const [, issue, spec, log, children, artifact, version, ask, comment] = issueReference;
32187
+ const [, issue, spec, log, children, artifact, version, ask, comment, message] = issueReference;
32165
32188
  if (!issue || version !== undefined && Number(version) < 1)
32166
32189
  return null;
32167
32190
  const owner = { kind: "issue", issue };
@@ -32183,6 +32206,8 @@ function parseDispatchRef(ref) {
32183
32206
  return { owner, kind: "ask", id: ask };
32184
32207
  if (comment)
32185
32208
  return { owner, kind: "comment", id: comment };
32209
+ if (message)
32210
+ return { owner, kind: "message", id: message };
32186
32211
  return { owner, kind: "issue", id: issue };
32187
32212
  }
32188
32213
  function askId(args) {
@@ -32195,6 +32220,16 @@ function askId(args) {
32195
32220
  }
32196
32221
  return reference.id;
32197
32222
  }
32223
+ function messageReplyTo(args) {
32224
+ const replyTo = optionalString(args, "reply_to");
32225
+ if (replyTo === undefined || !replyTo.startsWith("dispatch://"))
32226
+ return replyTo;
32227
+ const reference = parseDispatchRef(replyTo);
32228
+ if (reference?.kind !== "message") {
32229
+ throw new Error("reply_to must be a bare message id or a dispatch://.../message/<id> reference");
32230
+ }
32231
+ return reference.id;
32232
+ }
32198
32233
  function toolSchema(tool) {
32199
32234
  const spec = dispatchToolSpecs.find((candidate) => candidate.name === tool);
32200
32235
  if (!spec)
@@ -32206,7 +32241,7 @@ async function resolveOwnerArguments(tool, args, cwd, env, exec) {
32206
32241
  return { args, ref: null, owner: null };
32207
32242
  const refArgument = args.ref;
32208
32243
  const ref = typeof refArgument === "string" ? parseDispatchRef(refArgument) ?? (() => {
32209
- throw new Error("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/artifact/<slug>, or dispatch://PROJECT/artifact/<slug>");
32244
+ throw new Error("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>, or " + "dispatch://PROJECT/artifact/<slug>");
32210
32245
  })() : null;
32211
32246
  const issueArgument = args.issue;
32212
32247
  const projectArgument = args.project;
@@ -32286,8 +32321,12 @@ async function resolveArtifact(client, owner, artifactReference) {
32286
32321
  } catch (error) {
32287
32322
  if (!(error instanceof DispatchServiceError) || error.status !== 404)
32288
32323
  throw error;
32289
- const artifacts = await client.listProjectArtifacts(owner.project);
32290
- const artifact = artifacts.find((candidate) => candidate.name === artifactReference);
32324
+ const artifacts = await client.listProjectArtifacts(owner.project, true);
32325
+ const matches = artifacts.filter((candidate) => candidate.name === artifactReference);
32326
+ if (matches.length > 1) {
32327
+ throw new Error(`artifact name ${artifactReference} is ambiguous in project ${owner.project}; ` + `${matches.length} documents share it \u2014 use its slug instead`);
32328
+ }
32329
+ const artifact = matches[0];
32291
32330
  if (!artifact)
32292
32331
  throw error;
32293
32332
  return { owner, artifact };
@@ -32393,6 +32432,18 @@ function commentSummary({ comment, replies }) {
32393
32432
  return ["Comment:", ...root, "Reply chain:", ...chain.length === 0 ? ["- none"] : chain].join(`
32394
32433
  `);
32395
32434
  }
32435
+ function messageSummary({ message, replies }) {
32436
+ const root = [
32437
+ `${message.id} \xB7 ${message.author.kind} ${message.author.id}`,
32438
+ `Body: ${message.body}`
32439
+ ];
32440
+ const chain = replies.flatMap((reply) => [
32441
+ `${reply.id} \xB7 ${reply.author.kind} ${reply.author.id}`,
32442
+ `Body: ${reply.body}`
32443
+ ]);
32444
+ return ["Message:", ...root, "Reply chain:", ...chain.length === 0 ? ["- none"] : chain].join(`
32445
+ `);
32446
+ }
32396
32447
  async function openArtifactMarks(client, resolved) {
32397
32448
  const asks = resolved.owner.kind === "project" ? await client.getArtifactAsks(resolved.artifact.id) : resolved.issue?.open_asks ?? [];
32398
32449
  const marks = asks.filter((ask) => ask.state === "open" && ask.anchor?.artifact_id === resolved.artifact.id).map((ask) => `ask ${ask.id}`);
@@ -32600,9 +32651,15 @@ async function executeDispatchTool(input) {
32600
32651
  };
32601
32652
  }
32602
32653
  case "dispatch_message": {
32603
- const message = await client.message(issue(), { body: stringArg(args, "body"), actor });
32654
+ const replyTo = messageReplyTo(args);
32655
+ const message = await client.message(issue(), {
32656
+ body: stringArg(args, "body"),
32657
+ ...replyTo === undefined ? {} : { reply_to: replyTo },
32658
+ actor
32659
+ });
32660
+ const messageRef = `dispatch://${message.issue_key}/message/${message.id}`;
32604
32661
  return {
32605
- text: `Posted message ${message.id}`,
32662
+ text: `Posted message ${message.id} (${messageRef})`,
32606
32663
  details: {
32607
32664
  issue: message.issue_key,
32608
32665
  topic: dispatchIssueSubject(message.issue_key, ">"),
@@ -32689,6 +32746,16 @@ Open anchored asks/comments: ${marks.join(", ")}`,
32689
32746
  details: ownerArguments.ref.owner.kind === "project" ? { project: ownerArguments.ref.owner.project } : { issue: comment.comment.issue_key }
32690
32747
  };
32691
32748
  }
32749
+ if (ownerArguments.ref?.kind === "message") {
32750
+ if (ownerArguments.ref.owner.kind !== "issue") {
32751
+ throw new Error("message references are issue-scoped");
32752
+ }
32753
+ const messageRead = await client.getMessage(ownerArguments.ref.owner.issue, ownerArguments.ref.id);
32754
+ return {
32755
+ text: messageSummary(messageRead),
32756
+ details: { issue: messageRead.message.issue_key }
32757
+ };
32758
+ }
32692
32759
  if (documentOwner().kind === "project") {
32693
32760
  const resolved = await resolveArtifact(client, documentOwner(), stringArg(args, "artifact"));
32694
32761
  return {
@@ -5,17 +5,18 @@ description: "Use when asking Sami a question, updating the spec, commenting on
5
5
 
6
6
  # Dispatch
7
7
 
8
- Dispatch is your issue's or project document's living spec, asks, comments, and artifacts. The
9
- transcript is your scratch pad. Anything meant for a human goes through a `dispatch_*` tool.
8
+ Dispatch is your issue's or project document's living spec, asks, comments, and artifacts — a high-signal record for the humans who
9
+ decide, never a log of your work. The transcript is your scratch pad; progress and status stay there. Anything meant for a human
10
+ goes through a `dispatch_*` tool.
10
11
 
11
- The server enforces high signal: an ask question is at most 800 characters with at most eight
12
- options; comment and message bodies are at most 2,000 characters; an artifact is at most 25 MiB.
13
- It refuses over-limit input; it never truncates it. GitHub threads and markers no longer exist.
12
+ The server enforces high signal: an ask question is at most 800 characters with at most eight options; comment and message bodies are at
13
+ most 2,000 characters; an artifact is at most 25 MiB. It refuses over-limit input; it never truncates it. GitHub threads and markers no
14
+ longer exist.
14
15
 
15
16
  ## Writing a spec
16
17
 
17
- A spec is a decision record for the human who decides and the implementer who builds, not a
18
- transcript of your thinking. Use exactly these document headings in this order.
18
+ A spec is a decision record for the human who decides and the implementer who builds, not a transcript of your thinking. Use exactly
19
+ these document headings in this order.
19
20
 
20
21
  | Section | Required content | Form |
21
22
  | --- | --- | --- |
@@ -35,32 +36,23 @@ transcript of your thinking. Use exactly these document headings in this order.
35
36
  - Do not hedge with “might” or “could consider.”
36
37
  - Do not use TBD, TODO, or placeholders; an open item is a Decision needed.
37
38
  - Keep each section to one screen; work that exceeds one screen per section is two specs.
38
- - Update the spec in place as decisions land. The spec is the record; comments are the discussion.
39
-
40
- ### Self-review
41
-
42
- - [ ] No placeholders remain.
43
- - [ ] No sections conflict.
44
- - [ ] The spec covers one implementation plan's worth of work.
45
- - [ ] Every requirement has exactly one reading.
39
+ - Update the spec in place as decisions land: the spec is the record, comments are the discussion.
40
+ - Before sending it: no sections conflict, and every requirement has exactly one reading.
46
41
 
47
42
  ## Your owner
48
43
 
49
- Every session works on an issue or project document. Legion pre-fills `issue` from
50
- `LEGION_ISSUE`: use a native issue key such as `LEGION-3`, an external `owner/repo#n`
51
- reference, or a bare positive number (resolved against the cwd repository). Otherwise pass
52
- exactly one owner to every owner-scoped tool: `issue` for an issue, or `project` and
53
- `artifact` for an unlinked project document. A project key such as `CORE` with artifact
54
- `design-notes` identifies `dispatch://CORE/artifact/design-notes`. On first use, an external
55
- issue reference creates its native issue in the project configured for that repository in
56
- Dispatch Settings, then falls back to `DISPATCH_DEFAULT_PROJECT`.
44
+ Every session works on an issue or project document. Legion pre-fills `issue` from `LEGION_ISSUE`: use a native issue key such as
45
+ `LEGION-3`, an external `owner/repo#n` reference, or a bare positive number (resolved against the cwd repository). Otherwise pass
46
+ exactly one owner to every owner-scoped tool: `issue` for an issue, or `project` and `artifact` for an unlinked project document (see
47
+ [References](#references) for the resulting ref shape). On first use, an external issue reference creates its native issue in the
48
+ project configured for that repository in Dispatch Settings, then falls back to `DISPATCH_DEFAULT_PROJECT`.
57
49
 
58
50
  Architects create newly tracked child work with:
59
51
  ```ts
60
52
  dispatch_issue({ project, title, parent?, external?, spec?, force? })
61
53
  ```
62
- It returns `details` `{ issue, topic }`. Use `dispatch_issue` only to create an issue; never use
63
- it to park a question. When `spec` is supplied, follow [Writing a spec](#writing-a-spec).
54
+ It returns `details` `{ issue, topic }`. Use `dispatch_issue` only to create an issue; never use it to park a question. When `spec` is
55
+ supplied, follow [Writing a spec](#writing-a-spec).
64
56
 
65
57
  ## Search first
66
58
 
@@ -68,13 +60,12 @@ Before you create an issue or start a design document, search:
68
60
  ```ts
69
61
  dispatch_search({ query, project?, limit? })
70
62
  ```
71
- It returns every issue, document, comment, ask, and message that contains the words, with the
72
- issue key and a link. Cite the hit you build on (`dispatch://KEY` or the document reference), or
73
- state "no prior issue" in the spec. Websearch syntax applies: `"merge queue"`, `-daemon`, `OR`.
63
+ It returns every issue, document, comment, ask, and message that contains the words, with the issue key and a link. Cite the hit you
64
+ build on (`dispatch://KEY` or the document reference), or state "no prior issue" in the spec. Websearch syntax applies: `"merge queue"`,
65
+ `-daemon`, `OR`.
74
66
 
75
- `dispatch_issue` refuses a title that near-duplicates an issue in the same project and returns
76
- the candidates (`POSSIBLE_DUPLICATE`). Read them; reference the existing issue, or repeat the
77
- call with `force: true` when it is genuinely new work.
67
+ `dispatch_issue` refuses a title that near-duplicates an issue in the same project and returns the candidates (`POSSIBLE_DUPLICATE`).
68
+ Read them; reference the existing issue, or repeat the call with `force: true` when it is genuinely new work.
78
69
 
79
70
  ## Asking
80
71
 
@@ -91,14 +82,14 @@ dispatch_ask({
91
82
  anchor?: { artifact, quote, occurrence? },
92
83
  })
93
84
  ```
94
- It returns `details` `{ issue, topic, ask }` for an issue or `{ project, artifact, document,
95
- topic, ask }` for a project document. Options are buttons: never enumerate choices in
96
- prose. Put the recommendation in `question`, and put each selectable choice in `options`.
97
- Anchor a document question with `anchor: { artifact, quote, occurrence? }`; `occurrence` is
98
- zero-based and selects a repeated quote. The server writes the resulting mark. The HTTP API also
99
- accepts `{ artifact, mark_id }` from a browser that has already written its mark; Dispatch tools
100
- use the quote form. An anchor whose quote disappears becomes orphaned but remains readable against
101
- its original document version.
85
+ It returns `details` `{ issue, topic, ask }` for an issue or `{ project, artifact, document, topic, ask }` for a project document.
86
+ Options are buttons: never enumerate choices in prose. Put the recommendation in `question`, and put each selectable choice in
87
+ `options`. Anchor a document question with `anchor: { artifact, quote, occurrence? }`; `occurrence` is zero-based and selects a repeated
88
+ quote, and an anchor whose quote later disappears becomes orphaned but stays readable against its original document version.
89
+
90
+ An ask must be answerable from its own text and its anchor alone. Anchor a question about a document passage with `anchor`; thread one
91
+ about a comment with `reply_to`; thread a follow-up on your own ask with `reply_to_ask`; cite anything else with a `dispatch://`
92
+ reference (see [References](#references)). Never write "see above", "the message above", or "as attached".
102
93
 
103
94
  Correct or refine an open ask in place instead of opening a second question:
104
95
  ```ts
@@ -110,12 +101,11 @@ dispatch_edit_ask({
110
101
  urgency?,
111
102
  })
112
103
  ```
113
- At least one field besides `ask` is required. Use this only while the same decision remains
114
- open: it keeps the prior text in the event log. An answered or resolved ask cannot be edited.
115
- If the decision is moot or superseded, retract the old ask and open a new one.
104
+ At least one field besides `ask` is required. Use this only while the same decision remains open: it keeps the prior text in the event
105
+ log. An answered or resolved ask cannot be edited. If the decision is moot or superseded, retract the old ask and open a new one.
116
106
 
117
- An ask stays open until a human answers, unless its question no longer needs that answer. Retract a
118
- moot or superseded question, or self-resolve one after finding the answer:
107
+ An ask stays open until a human answers, unless its question no longer needs that answer. Retract a moot or superseded question, or
108
+ self-resolve one after finding the answer:
119
109
  ```ts
120
110
  dispatch_resolve_ask({
121
111
  ask,
@@ -123,32 +113,31 @@ dispatch_resolve_ask({
123
113
  reason: "A newer ask supersedes this question.",
124
114
  })
125
115
  ```
126
- Use `retracted` when the question is obsolete and `resolved` when you found the answer. Include the
127
- reason because the question remains in its Conversation card and reply thread. Resolution is not an
128
- answer: it never records a human decision, and an answered ask cannot be resolved.
116
+ Use `retracted` when the question is obsolete and `resolved` when you found the answer. Include the reason because the question remains
117
+ in its Conversation card and reply thread. Resolution is not an answer: it never records a human decision, and an answered ask cannot be
118
+ resolved. A human may reply to an open or answered ask; so may you, e.g. after finding the answer — use `reply_to_ask` on
119
+ `dispatch_comment` (mutually exclusive with `reply_to`).
129
120
 
130
- An ask is a thread, not a dead end: a human can reply to it before or after answering, and you
131
- (the asker) can reply too — e.g. acknowledging a clarifying question, or following up after the
132
- answer. Use `reply_to_ask` on `dispatch_comment` to reply under your own ask; it is mutually
133
- exclusive with `reply_to`.
121
+ ## The Spec
134
122
 
135
- ## The spec is where narrative goes
136
- Write and update the issue specification according to [Writing a spec](#writing-a-spec).
123
+ The spec holds requirements, design, acceptance, decisions, and rejected alternatives, structured per [Writing a spec](#writing-a-spec).
124
+ It changes only when a decision or requirement changes, and every version that records one is named with `summary`. Never write
125
+ progress, status, timestamps, an "Update HH:MMZ" section, a PR list, or handoff notes into the spec. Progress is not a
126
+ Dispatch object at all: it lives in your transcript and your pull request (see [Messages](#messages)).
137
127
 
138
128
  Read the current document before changing it:
139
129
 
140
130
  ```ts
141
131
  dispatch_doc_read({ issue?, project?, artifact?, version?, ref? })
142
132
  ```
143
- It returns live or versioned markdown with open marks. `issue` with an omitted `artifact`
144
- reads the issue specification; a project needs `artifact`; and a
145
- `dispatch://PROJECT/artifact/<slug>` ref supplies both. Then write narrative with:
133
+ It returns live or versioned markdown with open marks. `issue` with an omitted `artifact` reads the issue specification; a project needs
134
+ `artifact`; and a `dispatch://PROJECT/artifact/<slug>` ref supplies both. Then write with:
146
135
 
147
136
  ```ts
148
137
  dispatch_doc_edit({ issue?, project?, artifact, ops, summary? })
149
138
  ```
150
- It returns issue or project-document owner details plus `applied`, optional `version`, and its
151
- write `topic`. `ops` is an array of this exact `EditOp` shape:
139
+ It returns issue or project-document owner details plus `applied`, optional `version`, and its write `topic`. `ops` is an array of this
140
+ exact `EditOp` shape:
152
141
 
153
142
  ```ts
154
143
  type EditOp = {
@@ -162,23 +151,18 @@ type EditOp = {
162
151
  };
163
152
  ```
164
153
 
165
- Target `replace` and `delete` by the document's plain text: inline-code and link text match
166
- without Markdown syntax, and a table-cell anchor is its cell text. Quote code-block contents
167
- without their Markdown fences. A quote must stay within one textblock; split changes that
168
- span separate blocks into separate operations.
154
+ Target `replace` and `delete` by the document's plain text: inline-code and link text match without Markdown syntax, and a table-cell
155
+ anchor is its cell text. Quote code-block contents without their Markdown fences. A quote must stay within one textblock; split changes
156
+ that span separate blocks into separate operations.
169
157
 
170
- `replace` requires `find` and `with`; `delete` requires `find`; `insert` requires `markdown`
171
- and exactly one of `after` or `before`. An insert anchor is a quote, `"start"`, `"end"`, or
172
- `"heading:Title"`. Ordinary inserts create a sibling block before or after the quote or
173
- heading's enclosing document block; `"start"` and `"end"` select the document edges.
158
+ `replace` requires `find` and `with`; `delete` requires `find`; `insert` requires `markdown` and exactly one of `after` or `before`. An
159
+ insert anchor is a quote, `"start"`, `"end"`, or `"heading:Title"`. Ordinary inserts create a sibling block before or after the quote or
160
+ heading's enclosing document block; `"start"` and `"end"` select the document edges. At a table-cell quote, a body-row fragment (no
161
+ header or delimiter rows) extends that table before or after the matched row instead; short rows are padded, wider rows are rejected,
162
+ and deleting a cell's quoted text removes only that text.
174
163
 
175
- At a table-cell quote, pipe-table body-row fragments extend that table before or after the
176
- matched row; omit table header and delimiter rows. Short rows are padded to the table width;
177
- rows wider than the table are rejected.
178
- Deleting a cell's quoted text removes that text, not the surrounding row or table.
179
- Use `replace` for inline continuation. Use zero-based `occurrence` for a repeated target;
180
- re-read a missing or ambiguous target before retrying. Pass `summary` to name the version
181
- when recording a decision. Never paste progress into a message.
164
+ Use `replace` for inline continuation. Use zero-based `occurrence` for a repeated target; re-read a missing or ambiguous target before
165
+ retrying. Pass `summary` to name the version when recording a decision.
182
166
 
183
167
  ## Comments and suggestions
184
168
 
@@ -188,14 +172,11 @@ Add feedback with:
188
172
  dispatch_comment({ issue?, project?, artifact?, quote?, occurrence?, body, reply_to?, reply_to_ask? })
189
173
  ```
190
174
 
191
- It returns issue or project-document owner details plus `comment` and, for writes, `topic`.
192
- `quote` requires `artifact`; omit both for a floating issue comment. A reply
193
- (`reply_to`/`reply_to_ask`) takes no `quote`; it belongs to its parent's anchor. Use `reply_to`
194
- to continue a comment thread at its root; a reply to a resolved thread reopens it. Use
195
- `reply_to_ask` to reply directly under a question asked with `dispatch_ask`. The two are
196
- mutually exclusive. Comments are edited only by their author from the dashboard. A delivered
197
- `comment.created` event carries the comment `id`; reply to it with
198
- `dispatch_comment({ reply_to: <id> })`.
175
+ It returns issue or project-document owner details plus `comment` and, for writes, `topic`. `quote` requires `artifact`; omit both for a
176
+ floating issue comment. A reply (`reply_to`/`reply_to_ask`) takes no `quote`; it belongs to its parent's anchor. Use `reply_to` to
177
+ continue a comment thread at its root; a reply to a resolved thread reopens it. Use `reply_to_ask` to reply directly under a question
178
+ asked with `dispatch_ask`. Comments are edited only by their author from the dashboard. A delivered `comment.created` event carries the
179
+ comment `id`; reply to it with `dispatch_comment({ reply_to: <id> })`.
199
180
 
200
181
  Propose an exact replacement instead of describing it:
201
182
 
@@ -203,15 +184,9 @@ Propose an exact replacement instead of describing it:
203
184
  dispatch_suggest({ issue?, project?, artifact, quote, replace_with, body?, occurrence? })
204
185
  ```
205
186
 
206
- It returns issue or project-document owner details plus `comment` and its write `topic`. A
207
- human accepts or rejects a suggestion. On
208
- `TARGET_AMBIGUOUS`, add zero-based `occurrence`. On `TARGET_NOT_FOUND`, re-read the document
209
- before retrying. `INVALID_ANCHOR` requires exactly one nonempty anchor `quote` or `mark_id`;
210
- `ANCHOR_MISSING` means a browser mark was not observed in the live tree, and
211
- `ANCHOR_ORPHANED` means its marked text no longer exists. `INVALID_MARKDOWN` and `DOC_SCHEMA`
212
- reject Markdown or a live tree outside the Proof schema. `INVALID_OP` names a malformed edit;
213
- `CAP_EXCEEDED` never truncates; `ISSUE_CLOSED` rejects a write. `ACTOR_KIND` and `ROUTE_INVALID`
214
- reject an invalid actor or route.
187
+ It returns issue or project-document owner details plus `comment` and its write `topic`. A human accepts or rejects a suggestion.
188
+ Errors: `TARGET_AMBIGUOUS` (add `occurrence`), `TARGET_NOT_FOUND` (re-read first), `INVALID_ANCHOR`/`ANCHOR_MISSING`/`ANCHOR_ORPHANED`
189
+ (bad, unwritten, or stale quote), `INVALID_MARKDOWN`/`DOC_SCHEMA` (malformed content), `CAP_EXCEEDED`, `ISSUE_CLOSED`.
215
190
 
216
191
  ## Artifacts
217
192
 
@@ -227,40 +202,39 @@ Or, when the text is already in the call, post a Markdown document directly:
227
202
  dispatch_artifact({ issue?, project?, name: "spec.md", content: "# Design\n..." })
228
203
  ```
229
204
 
230
- Exactly one of `issue` and `project` is required. A project upload creates an unlinked project
231
- document; it must not include `artifact`. Exactly one of `path` and `content` is required. The
232
- inline form sends JSON with `Content-Type: application/json`. It returns issue or
233
- project-document owner details plus `artifact`, `version`, and its write `topic`. Uploading the
234
- same `name` creates its next version. Use `content` when the text is already in the call.
235
- Address an existing artifact by the slug shown in the upload result or by its filename.
205
+ Exactly one of `issue` and `project` is required. A project upload creates an unlinked project document; it must not include `artifact`.
206
+ Exactly one of `path` and `content` is required. It returns issue or project-document owner details plus `artifact`, `version`, and its
207
+ write `topic`. Uploading the same `name` creates its next version. Address an existing artifact by the slug shown in the upload result
208
+ or by its filename; the slug also arrives on `artifact.created` events.
236
209
 
237
210
  ## Messages
238
211
 
239
- Use the escape valve only for a note that fits nowhere else:
212
+ Dispatch is a high-signal record for humans, not a log of what you are doing. A message is a reply to a human's message, or a
213
+ change a human must know about now: a deliverable landed, a blocker only they can clear. Nothing else — no progress updates, no
214
+ "starting X", no "still working", no restating the spec, no status on a timer. Your transcript is where work is narrated; the
215
+ pull request is where it is summarised. One message that a human reads beats ten that train them to skip you.
240
216
 
241
217
  ```ts
242
218
  dispatch_message({ issue, body })
243
219
  ```
244
220
 
245
- It returns `details` `{ issue, topic, message }`. `body` is capped at 2,000 characters. Your
246
- message does not wake anyone. Do not use it for status, a decision, or document feedback.
221
+ It returns `details` `{ issue, topic, message }`. `body` is capped at 2,000 characters. A message is not a decision
222
+ (`dispatch_ask`) or document feedback (`dispatch_comment`), and it does not wake anyone unless the issue is routed.
247
223
 
248
224
  ## What comes back
249
225
 
250
- A write result's `details.topic` subscribes the host to its owner. Issue writes use
251
- `notifications.dispatch.issue.<KEY>.>`; project-document writes use
252
- `notifications.dispatch.document.<PROJECT>.<SLUG>.>`. The owner topic carries every Dispatch
253
- event; `notify` only controls agent wake and routed delivery. After a restart, catch up with:
226
+ A write result's `details.topic` subscribes the host to its owner. Issue writes use `notifications.dispatch.issue.<KEY>.>`;
227
+ project-document writes use `notifications.dispatch.document.<PROJECT>.<SLUG>.>`. The owner topic carries every Dispatch event; `notify`
228
+ only controls agent wake and routed delivery. After a restart, catch up with:
254
229
 
255
230
  ```ts
256
231
  dispatch_read({ issue?, project?, artifact?, ref? })
257
232
  ```
258
233
 
259
- With an issue ref, it returns the issue summary, open asks, references, and recent events with
260
- `details` `{ issue }`. With a project document owner or ref, it returns a document summary with
261
- `details` `{ project, document }`. With an ask ref, it returns that ask's question, options,
262
- state, answer, and its reply thread. With a comment ref, it returns that comment and its quoted
263
- reply chain. Reads do not subscribe; use `dispatch_doc_read` for document contents.
234
+ With an issue ref, it returns the issue summary, open asks, references, and recent events with `details` `{ issue }`. With a project
235
+ document owner or ref, it returns a document summary with `details` `{ project, document }`. With an ask ref, it returns that ask's
236
+ question, options, state, answer, and its reply thread. With a comment ref, it returns that comment and its quoted reply chain. With a
237
+ message ref, it returns that message and its reply chain. Reads do not subscribe; use `dispatch_doc_read` for document contents.
264
238
 
265
239
  ## References
266
240
 
@@ -272,11 +246,14 @@ dispatch://KEY/spec
272
246
  dispatch://KEY/artifact/<slug>[@vN]
273
247
  dispatch://KEY/ask/<id>
274
248
  dispatch://KEY/comment/<id>
249
+ dispatch://KEY/message/<id>
275
250
  dispatch://PROJECT/artifact/<slug>[@vN]
276
251
  dispatch://PROJECT/artifact/<slug>/ask/<id>
277
252
  dispatch://PROJECT/artifact/<slug>/comment/<id>
278
253
  ```
279
254
 
255
+ A bare UUID or `KEY#seq` is not a reference; the `dispatch://` form is what Dispatch links and records.
256
+
280
257
  ## Before / after
281
258
 
282
259
  Before — a wall of text hides the decision and makes the choices unclickable:
@@ -294,48 +271,28 @@ After — anchor the decision and make each option a button:
294
271
  dispatch_ask({
295
272
  issue: "LEGION-815",
296
273
  question:
297
- "Choose the release gate. Recommendation: ship after release-note review because the tested deployment is otherwise ready.",
274
+ "Choose the release gate. Recommendation: ship after release-note review, since the tested deployment is otherwise ready.",
298
275
  options: [
299
- {
300
- label: "Review notes, then ship",
301
- description: "Keeps the tested release intact and publishes reviewed instructions.",
302
- },
303
- {
304
- label: "Ship now",
305
- description: "Meets the demo deadline; release notes follow separately.",
306
- },
307
- {
308
- label: "Remove dashboard changes",
309
- description: "Narrows the release but requires another deployment test.",
310
- },
276
+ { label: "Review notes, then ship", description: "Keeps the release intact and reviewed." },
277
+ { label: "Ship now", description: "Meets the demo deadline; release notes follow later." },
311
278
  ],
312
279
  urgency: "high",
313
- anchor: {
314
- artifact: "spec",
315
- quote: "Release requires reviewed operator instructions before deployment.",
316
- },
280
+ anchor: { artifact: "spec", quote: "Release requires reviewed operator instructions before deployment." },
317
281
  })
318
282
  ```
319
283
 
320
- Before — a status message loses the durable outcome:
284
+ Before — a progress note that nobody needs, posted where humans look for decisions:
321
285
 
322
- ```text
323
- Done, PR merged.
286
+ ```ts
287
+ dispatch_message({ issue: "LEGION-815", body: "Merged the release PR, moving to docs next." })
324
288
  ```
325
289
 
326
- After — record the result in the spec and name its version:
290
+ After — nothing. The merge is visible on the pull request; the docs work shows up as its own deliverable. Post a message only when
291
+ a human must act or a deliverable is theirs to use:
327
292
 
328
293
  ```ts
329
- dispatch_doc_edit({
294
+ dispatch_message({
330
295
  issue: "LEGION-815",
331
- artifact: "spec",
332
- ops: [
333
- {
334
- op: "replace",
335
- find: "Release pending.",
336
- with: "Release merged and ready for deployment.",
337
- },
338
- ],
339
- summary: "Recorded merged release",
296
+ body: "Release 1.4 is live on the devbox (dispatch://LEGION-815/artifact/release-notes). Nothing needed from you.",
340
297
  })
341
298
  ```
@@ -71,6 +71,9 @@ exercise a criterion end to end, building that path is a child issue of this tre
71
71
  inert until released.
72
72
 
73
73
  Specifications written into Dispatch follow [`skills/dispatch`'s Writing a spec](../dispatch/SKILL.md#writing-a-spec).
74
+ Wave releases, child closures, and your own status are visible from the issue tree and the
75
+ handoffs; do not narrate them into the spec or a `dispatch_message`. A blocker only Sami can
76
+ clear is a `dispatch_ask`.
74
77
 
75
78
  Write one root specification containing the accepted scope, adoption/decomposition,
76
79
  waves, acceptance criteria, and integration test. When the config-armed root design gate
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-legion-envoy",
3
- "version": "0.41.1",
3
+ "version": "0.42.0",
4
4
  "type": "module",
5
5
  "omp": {
6
6
  "extensions": [