@sjawhar/pi-legion-envoy 1.35.1 → 1.36.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
@@ -29903,7 +29903,7 @@ var dispatchToolSpecs = [
29903
29903
  options: z.array(z.object({
29904
29904
  label: z.string().describe("Selectable option label."),
29905
29905
  description: z.string().describe("Optional option context.").optional()
29906
- }), { max: 8 }).describe("Optional choices, at most 8.").optional(),
29906
+ }), { max: 8 }).describe("Up to 8 choices, each an object { label, description? } (never a bare string).").optional(),
29907
29907
  multiple: z.boolean().describe("Whether multiple choices may be selected.").optional(),
29908
29908
  urgency: z.enum(ASK_URGENCIES).describe("Optional decision urgency.").optional(),
29909
29909
  anchor: z.object({
@@ -29955,7 +29955,7 @@ var dispatchToolSpecs = [
29955
29955
  quote: z.string().describe("Optional exact quoted document text.").optional(),
29956
29956
  occurrence: z.number({ int: true, min: 0 }).describe("Optional zero-based occurrence of quote.").optional(),
29957
29957
  body: z.string({ max: 2000 }).describe("Review comment, at most 2,000 characters."),
29958
- reply_to: z.string().describe("Full id of a comment to reply to; replying to any comment in a thread continues that " + "thread (an ask's clarification thread included).").optional(),
29958
+ 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(),
29959
29959
  reply_to_ask: z.string().describe("Optional ask id to reply to, threading this comment under that question. Mutually " + "exclusive with reply_to.").optional(),
29960
29960
  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()
29961
29961
  }),
@@ -29987,7 +29987,7 @@ var dispatchToolSpecs = [
29987
29987
  },
29988
29988
  {
29989
29989
  name: "dispatch_doc_edit",
29990
- description: "Apply deterministic document edits, including retyping an identified paragraph into a schema-declared typed block. " + "Do not use it for review feedback or for reading; use dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. " + "For replace, delete, and quote insert anchors, find text exactly as rendered: omit Markdown markers such as backticks or asterisks. " + `The spec (or any document) holds requirements, design, and decisions - never progress, status, or timestamps. ${OWNER_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
29990
+ description: "Apply deterministic document edits, including retyping an identified paragraph into a schema-declared typed block. " + "Do not use it for review feedback or for reading; use dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. " + 'For replace, delete, and quote insert anchors, find text as rendered: inline Markdown (**bold**, `code`) is tolerated; a leading \'# \' matches a heading. Insert anchors also accept "start", "end", and "heading:<exact heading text>". ' + `The spec (or any document) holds requirements, design, and decisions - never progress, status, or timestamps. ${OWNER_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
29991
29991
  arguments: (z) => ({
29992
29992
  issue: z.string().describe(ISSUE_REFERENCE).optional(),
29993
29993
  project: z.string().describe("Project key owning the document.").optional(),
@@ -29995,12 +29995,12 @@ var dispatchToolSpecs = [
29995
29995
  ref: z.string().describe("Optional dispatch:// issue or document reference.").optional(),
29996
29996
  ops: z.array(z.object({
29997
29997
  op: z.enum(DOC_EDIT_OPS).describe("Edit operation."),
29998
- find: z.string().describe("Text to find for replace or delete.").optional(),
29998
+ find: z.string().describe("Text of the target block as rendered, for replace or delete; inline markdown (**bold**, `code`) is tolerated; a leading '# ' matches a heading.").optional(),
29999
29999
  with: z.string().describe("Replacement text for replace.").optional(),
30000
30000
  occurrence: z.number({ int: true, min: 0 }).describe("Optional zero-based match occurrence.").optional(),
30001
30001
  markdown: z.string().describe("Markdown to insert.").optional(),
30002
- after: z.string().describe("Anchor after which to insert.").optional(),
30003
- before: z.string().describe("Anchor before which to insert.").optional(),
30002
+ after: z.string().describe(`Insert after this anchor: a quote of the neighbouring block's text, or one of "start", "end", "heading:<exact heading text>".`).optional(),
30003
+ before: z.string().describe(`Insert before this anchor: a quote of the neighbouring block's text, or one of "start", "end", "heading:<exact heading text>".`).optional(),
30004
30004
  block: z.string().describe("Block id to retype.").optional(),
30005
30005
  type: z.string().describe("Typed block name for retype.").optional(),
30006
30006
  attributes: z.unknown().describe("Typed block attributes for retype.").optional()
@@ -30504,6 +30504,9 @@ function canonicalRepo(owner, repo) {
30504
30504
  return `${owner.trim().toLowerCase()}/${repo.trim().toLowerCase().replace(/\.git$/, "")}`;
30505
30505
  }
30506
30506
  // ../contracts/src/tool-schema.ts
30507
+ function overCapMessage(length, max) {
30508
+ return `is ${length - max} characters over the ${max}-character limit (${length}/${max})`;
30509
+ }
30507
30510
  function zodSchemaApi(zod) {
30508
30511
  const api = zod;
30509
30512
  return {
@@ -30511,8 +30514,12 @@ function zodSchemaApi(zod) {
30511
30514
  let schema = api.string();
30512
30515
  if (opts.min !== undefined)
30513
30516
  schema = schema.min(opts.min);
30514
- if (opts.max !== undefined)
30515
- schema = schema.max(opts.max);
30517
+ if (opts.max !== undefined) {
30518
+ const max = opts.max;
30519
+ schema = schema.max(max, {
30520
+ error: (issue) => overCapMessage(typeof issue.input === "string" ? issue.input.length : max + 1, max)
30521
+ });
30522
+ }
30516
30523
  return schema;
30517
30524
  },
30518
30525
  number: (opts = {}) => {
@@ -31091,6 +31098,24 @@ function resolveOptions(options) {
31091
31098
  };
31092
31099
  }
31093
31100
 
31101
+ // ../envoy-client/src/ask-answer.ts
31102
+ var HEAD_LENGTH = 120;
31103
+ function textHead(text, limit = HEAD_LENGTH) {
31104
+ const flat = text.replace(/\s+/g, " ").trim();
31105
+ return flat.length > limit ? `${flat.slice(0, limit)}\u2026` : flat;
31106
+ }
31107
+ function askAnswerText(answer) {
31108
+ if (answer === null || answer === undefined)
31109
+ return "";
31110
+ const selected = (answer.selected ?? []).join(", ");
31111
+ const text = answer.text ?? "";
31112
+ if (text === "")
31113
+ return selected;
31114
+ if (selected === "")
31115
+ return text;
31116
+ return `${selected} - ${text}`;
31117
+ }
31118
+
31094
31119
  // ../envoy-client/src/delivery.ts
31095
31120
  var KNOWN_SOURCES = EnvelopeSchema.shape.source.enum;
31096
31121
  var FOREIGN_SESSION_ID = /\b01a0[0-9a-f]{4}-[0-9a-f]{4}-7[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}\b/g;
@@ -31147,7 +31172,14 @@ function senderLabel(envelope) {
31147
31172
  function replyWith(envelope) {
31148
31173
  if (envelope.source !== "agent" || envelope.source_session === undefined)
31149
31174
  return;
31150
- return `envoy_send(session_id="${envelope.source_session}", message="...")`;
31175
+ return {
31176
+ tool: "envoy_send",
31177
+ args: {
31178
+ session_id: envelope.source_session,
31179
+ ...envelope.event_id === undefined ? {} : { in_reply_to: envelope.event_id },
31180
+ message: "..."
31181
+ }
31182
+ };
31151
31183
  }
31152
31184
  var DISPATCH_PAYLOAD_SCHEMAS = {
31153
31185
  "issue.created": IssueEventPayloadSchema,
@@ -31170,15 +31202,18 @@ var DISPATCH_PAYLOAD_SCHEMAS = {
31170
31202
  "message.answered": MessageEventPayloadSchema,
31171
31203
  "child.status": ChildStatusEventPayloadSchema
31172
31204
  };
31205
+ function topicDocument(topic) {
31206
+ if (topic?.startsWith(DISPATCH_DOCUMENT_TOPIC_PREFIX) !== true)
31207
+ return;
31208
+ const [project, slug] = topic.slice(DISPATCH_DOCUMENT_TOPIC_PREFIX.length).split(".", 3);
31209
+ return project !== undefined && project !== "" && slug !== undefined && slug !== "" ? { project, slug } : undefined;
31210
+ }
31173
31211
  function dispatchOwner(event, topic) {
31174
31212
  if (event.issue_key !== null)
31175
31213
  return event.issue_key;
31176
- if (topic?.startsWith(DISPATCH_DOCUMENT_TOPIC_PREFIX) === true) {
31177
- const [project, slug] = topic.slice(DISPATCH_DOCUMENT_TOPIC_PREFIX.length).split(".", 3);
31178
- if (project !== undefined && project !== "" && slug !== undefined && slug !== "") {
31179
- return `${project} / ${slug}`;
31180
- }
31181
- }
31214
+ const document = topicDocument(topic);
31215
+ if (document !== undefined)
31216
+ return `${document.project} / ${document.slug}`;
31182
31217
  return event.artifact_id ?? "unknown";
31183
31218
  }
31184
31219
  function dispatchCommentReplyWith(event, topic) {
@@ -31189,9 +31224,12 @@ function dispatchCommentReplyWith(event, topic) {
31189
31224
  return;
31190
31225
  const reply = comment.data.ask_id;
31191
31226
  const thread = reply === undefined || reply === null || reply === "" ? "reply_to" : "reply_to_ask";
31192
- const threadID = thread === "reply_to" ? comment.data.id : reply;
31227
+ const threadID = thread === "reply_to" ? comment.data.id : String(reply);
31193
31228
  if (event.issue_key !== null) {
31194
- return `dispatch_comment(issue="${event.issue_key}", ${thread}="${threadID}", body="...")`;
31229
+ return {
31230
+ tool: "dispatch_comment",
31231
+ args: { issue: event.issue_key, [thread]: threadID, body: "..." }
31232
+ };
31195
31233
  }
31196
31234
  let project = comment.data.project_key;
31197
31235
  let artifact = comment.data.artifact_slug;
@@ -31200,7 +31238,21 @@ function dispatchCommentReplyWith(event, topic) {
31200
31238
  }
31201
31239
  if (project === undefined || project === "" || artifact === undefined || artifact === "")
31202
31240
  return;
31203
- return `dispatch_comment(project="${project}", artifact="${artifact}", ${thread}="${threadID}", body="...")`;
31241
+ return {
31242
+ tool: "dispatch_comment",
31243
+ args: { project, artifact, [thread]: threadID, body: "..." }
31244
+ };
31245
+ }
31246
+ function dispatchAskAnswer(event) {
31247
+ if (event.type !== "ask.answered")
31248
+ return;
31249
+ const ask = AskEventPayloadSchema.safeParse(event.payload);
31250
+ if (!ask.success)
31251
+ return;
31252
+ return {
31253
+ question: textHead(ask.data.question ?? ""),
31254
+ answer: askAnswerText(ask.data.answer)
31255
+ };
31204
31256
  }
31205
31257
  function dispatchPayload(event) {
31206
31258
  const schema = DISPATCH_PAYLOAD_SCHEMAS[event.type];
@@ -31213,13 +31265,33 @@ function dispatchAskQuestion(event) {
31213
31265
  if (event.type !== "comment.created")
31214
31266
  return;
31215
31267
  const parsed = CommentEventPayloadSchema.safeParse(event.payload);
31216
- return parsed.success && parsed.data.ask_question !== "" ? parsed.data.ask_question : undefined;
31217
- }
31218
- function dispatchMessageReplyPreview(event) {
31219
- if (event.type !== "message.created")
31220
- return;
31221
- const parsed = MessageEventPayloadSchema.safeParse(event.payload);
31222
- return parsed.success && parsed.data.reply_body !== undefined && parsed.data.reply_body !== "" ? parsed.data.reply_body : undefined;
31268
+ return parsed.success && parsed.data.ask_question !== undefined && parsed.data.ask_question !== "" ? textHead(parsed.data.ask_question) : undefined;
31269
+ }
31270
+ function dispatchReplyRef(event, topic, inReplyTo) {
31271
+ let kind;
31272
+ let document = topicDocument(topic);
31273
+ if (event.type.startsWith("ask.")) {
31274
+ kind = "ask";
31275
+ } else if (event.type.startsWith("message.")) {
31276
+ kind = "message";
31277
+ } else if (event.type === "comment.created") {
31278
+ const comment = CommentEventPayloadSchema.safeParse(event.payload);
31279
+ if (!comment.success || comment.data.ask_id === undefined || comment.data.ask_id === null) {
31280
+ return inReplyTo;
31281
+ }
31282
+ kind = "ask";
31283
+ const { project_key: project, artifact_slug: slug } = comment.data;
31284
+ if (project !== undefined && project !== "" && slug !== undefined && slug !== "") {
31285
+ document = { project, slug };
31286
+ }
31287
+ } else {
31288
+ return inReplyTo;
31289
+ }
31290
+ if (event.issue_key !== null)
31291
+ return `dispatch://${event.issue_key}/${kind}/${inReplyTo}`;
31292
+ if (document === undefined)
31293
+ return inReplyTo;
31294
+ return `dispatch://${document.project}/artifact/${document.slug}/${kind}/${inReplyTo}`;
31223
31295
  }
31224
31296
  function isObject2(value) {
31225
31297
  return typeof value === "object" && value !== null;
@@ -31308,8 +31380,7 @@ function renderInbound(raw, sessionID, subject) {
31308
31380
  let delivery;
31309
31381
  let rejectedDelivery;
31310
31382
  let malformedDelivery = false;
31311
- let askQuestion;
31312
- let messageReplyPreview;
31383
+ let inReplyTo = envelope.in_reply_to;
31313
31384
  const dispatchRendered = envelope.source === "dispatch" && envelope.payload !== undefined;
31314
31385
  if (envelope.source === "dispatch") {
31315
31386
  if (envelope.payload === undefined) {
@@ -31335,8 +31406,11 @@ function renderInbound(raw, sessionID, subject) {
31335
31406
  envelope
31336
31407
  };
31337
31408
  }
31338
- askQuestion = dispatchAskQuestion(frame.event);
31339
- messageReplyPreview = dispatchMessageReplyPreview(frame.event);
31409
+ const answered = dispatchAskAnswer(frame.event);
31410
+ const question = answered?.question ?? dispatchAskQuestion(frame.event);
31411
+ if (inReplyTo !== undefined) {
31412
+ inReplyTo = dispatchReplyRef(frame.event, subject ?? envelope.topic, inReplyTo);
31413
+ }
31340
31414
  dispatchReply = dispatchCommentReplyWith(frame.event, subject ?? envelope.topic);
31341
31415
  if (frame.event.type === "message.created") {
31342
31416
  const message = MessageEventPayloadSchema.safeParse(frame.event.payload);
@@ -31350,7 +31424,10 @@ function renderInbound(raw, sessionID, subject) {
31350
31424
  body: message.data.body ?? envelope.payload_summary ?? ""
31351
31425
  };
31352
31426
  if (frame.event.issue_key !== null) {
31353
- dispatchReply = `dispatch_message(issue="${frame.event.issue_key}", in_reply_to="${message.data.id}", body="...")`;
31427
+ dispatchReply = {
31428
+ tool: "dispatch_message",
31429
+ args: { issue: frame.event.issue_key, in_reply_to: message.data.id, body: "..." }
31430
+ };
31354
31431
  }
31355
31432
  }
31356
31433
  }
@@ -31364,6 +31441,8 @@ function renderInbound(raw, sessionID, subject) {
31364
31441
  } : { issue_key: frame.event.issue_key },
31365
31442
  type: frame.event.type,
31366
31443
  actor: frame.event.actor,
31444
+ ...question === undefined ? {} : { question },
31445
+ ...answered === undefined ? {} : { answer: answered.answer },
31367
31446
  payload: dispatchPayload(frame.event)
31368
31447
  };
31369
31448
  dispatchActor = frame.event.actor;
@@ -31410,11 +31489,14 @@ ${envelope.payload ?? ""}`;
31410
31489
  ...envelope.expires_at === undefined ? {} : { by: inboundTimestamp(envelope.expires_at) },
31411
31490
  ...envelope.urgency === undefined ? {} : { urgency: envelope.urgency },
31412
31491
  ...envelope.expects_reply === undefined ? {} : { expects_reply: envelope.expects_reply },
31413
- ...envelope.in_reply_to === undefined ? {} : { re: askQuestion ?? messageReplyPreview ?? envelope.in_reply_to },
31492
+ ...inReplyTo === undefined ? {} : { re: inReplyTo },
31414
31493
  ...envelope.supersedes === undefined ? {} : { supersedes: envelope.supersedes },
31415
31494
  ...reply === undefined ? {} : { reply_with: reply },
31416
31495
  ...role === undefined ? {} : {
31417
- reply_role: `envoy_publish(topic="notifications.role.${role}", message="...")`
31496
+ reply_role: {
31497
+ tool: "envoy_publish",
31498
+ args: { topic: `notifications.role.${role}`, message: "..." }
31499
+ }
31418
31500
  },
31419
31501
  ...dispatchRendered ? { dispatch: dispatchEvent } : {
31420
31502
  ...summaryIsHead ? {} : { summary: payloadSummary },
@@ -31760,6 +31842,9 @@ class DispatchClient {
31760
31842
  async comment(issue, input) {
31761
31843
  return this.#json("POST", ["api", "v1", "issues", await this.#resolveIssue(issue), "comments"], input);
31762
31844
  }
31845
+ async listIssueAsks(issue, state) {
31846
+ return this.#json("GET", ["api", "v1", "issues", await this.#resolveIssue(issue), "asks"], undefined, state === undefined ? undefined : { state });
31847
+ }
31763
31848
  async getArtifactAsks(id, state) {
31764
31849
  return this.#json("GET", ["api", "v1", "artifacts", id, "asks"], undefined, state === undefined ? undefined : { state });
31765
31850
  }
@@ -31958,6 +32043,116 @@ class DispatchClient {
31958
32043
  }
31959
32044
  }
31960
32045
 
32046
+ // ../envoy-client/src/tool-input-errors.ts
32047
+ class ToolInputError extends Error {
32048
+ tool;
32049
+ problems;
32050
+ constructor(tool, problems) {
32051
+ const count = problems.length;
32052
+ super([
32053
+ `${tool} was not called: ${count} problem${count === 1 ? "" : "s"}`,
32054
+ ...problems.map((problem) => `- ${problem}`)
32055
+ ].join(`
32056
+ `));
32057
+ this.name = "ToolInputError";
32058
+ this.tool = tool;
32059
+ this.problems = problems;
32060
+ }
32061
+ }
32062
+ function unwrap(schema) {
32063
+ let current = schema;
32064
+ while (current !== undefined) {
32065
+ const def = current.def;
32066
+ if (!("innerType" in def) || !(def.type === "optional" || def.type === "nullable" || def.type === "default"))
32067
+ break;
32068
+ current = def.innerType;
32069
+ }
32070
+ return current;
32071
+ }
32072
+ function shapeOf(schema) {
32073
+ const unwrapped = unwrap(schema);
32074
+ if (unwrapped === undefined)
32075
+ return;
32076
+ const def = unwrapped.def;
32077
+ return def.type === "object" && "shape" in def ? def.shape : undefined;
32078
+ }
32079
+ function schemaAt(schema, path) {
32080
+ let current = schema;
32081
+ for (const key of path) {
32082
+ const unwrapped = unwrap(current);
32083
+ if (unwrapped === undefined)
32084
+ return;
32085
+ const def = unwrapped.def;
32086
+ if (def.type === "object" && "shape" in def) {
32087
+ current = shapeOf(unwrapped)?.[String(key)];
32088
+ } else if (def.type === "array" && "element" in def) {
32089
+ current = def.element;
32090
+ } else {
32091
+ return;
32092
+ }
32093
+ }
32094
+ return unwrap(current);
32095
+ }
32096
+ function describeInput(value) {
32097
+ if (value === null)
32098
+ return "null";
32099
+ if (Array.isArray(value))
32100
+ return "an array";
32101
+ switch (typeof value) {
32102
+ case "number":
32103
+ case "boolean":
32104
+ return String(value);
32105
+ case "string":
32106
+ return "a string";
32107
+ case "object":
32108
+ return "an object";
32109
+ default:
32110
+ return typeof value;
32111
+ }
32112
+ }
32113
+ function describeExpected(expected, schema) {
32114
+ switch (expected) {
32115
+ case "int":
32116
+ return "an integer";
32117
+ case "object": {
32118
+ const shape = shapeOf(schema);
32119
+ const keys = shape === undefined ? [] : Object.entries(shape).map(([key, field]) => `${key}${field.def.type === "optional" ? "?" : ""}`);
32120
+ return keys.length === 0 ? "an object" : `an object {${keys.join(", ")}}`;
32121
+ }
32122
+ case "array":
32123
+ return "an array";
32124
+ default:
32125
+ return `a ${expected}`;
32126
+ }
32127
+ }
32128
+ function formatZodIssues(issues, schema) {
32129
+ const allowed = Object.keys(shapeOf(schema) ?? {}).join(", ");
32130
+ return issues.flatMap((issue) => {
32131
+ const path = issue.path.map(String).join(".");
32132
+ switch (issue.code) {
32133
+ case "invalid_type":
32134
+ return issue.input === undefined ? [`${path} is required (${issue.expected})`] : [
32135
+ `${path} must be ${describeExpected(issue.expected, schemaAt(schema, issue.path))}, not ${describeInput(issue.input)}`
32136
+ ];
32137
+ case "unrecognized_keys":
32138
+ return issue.keys.map((key) => `unknown field "${key}"; allowed: ${allowed}`);
32139
+ case "invalid_value":
32140
+ return [
32141
+ `${path} must be one of ${issue.values.map(String).join("|")}; got ${JSON.stringify(issue.input)}`
32142
+ ];
32143
+ case "too_big":
32144
+ if (issue.origin === "array" && Array.isArray(issue.input)) {
32145
+ return [`${path} has ${issue.input.length} items; the limit is ${issue.maximum}`];
32146
+ }
32147
+ return [`${path} ${issue.message}`];
32148
+ case "custom":
32149
+ return [issue.message];
32150
+ default:
32151
+ return [path === "" ? issue.message : `${path}: ${issue.message}`];
32152
+ }
32153
+ });
32154
+ }
32155
+
31961
32156
  // ../envoy-client/src/dispatch-execute.ts
31962
32157
  function documentResultDetails(artifact) {
31963
32158
  return {
@@ -32052,23 +32247,19 @@ function askUrgency(args) {
32052
32247
  return ASK_URGENCIES.find((urgency) => urgency === value);
32053
32248
  }
32054
32249
  function askKind(args) {
32055
- const value = optionalString(args, "kind");
32056
- if (value === undefined || value === "action")
32057
- return value;
32058
- throw new Error("kind must be action");
32250
+ return optionalString(args, "kind") === "action" ? "action" : undefined;
32059
32251
  }
32060
- function askQuestionWithRef(args) {
32061
- const question = stringArg(args, "question");
32062
- const ref = optionalString(args, "ref");
32063
- if (ref === undefined || question.includes(ref))
32064
- return question;
32065
- const withRef = `${question}
32252
+ var maxAskQuestion16 = 800;
32253
+ function questionWithRef(question, ref) {
32254
+ return ref === undefined || question.includes(ref) ? question : `${question}
32066
32255
 
32067
32256
  Ref: ${ref}`;
32068
- if (withRef.length > 800) {
32069
- throw new Error("question plus ref must be at most 800 characters");
32070
- }
32071
- return withRef;
32257
+ }
32258
+ function askQuestionWithRef(args) {
32259
+ return questionWithRef(stringArg(args, "question"), optionalString(args, "ref"));
32260
+ }
32261
+ function askQuestionProblem(withRef) {
32262
+ return withRef.length > maxAskQuestion16 ? `question plus ref ${overCapMessage(withRef.length, maxAskQuestion16)}; shorten the question or drop the ref` : undefined;
32072
32263
  }
32073
32264
  function parseDispatchRef(ref) {
32074
32265
  const projectDocument = ref.match(/^dispatch:\/\/([A-Z][A-Z0-9]{1,9})\/artifact\/([^/@]+)(?:@v(\d+))?(?:\/(ask|comment)\/([^/]+))?$/);
@@ -32090,7 +32281,8 @@ function parseDispatchRef(ref) {
32090
32281
  return {
32091
32282
  owner: { kind: "project", project },
32092
32283
  kind: targetKind,
32093
- id: targetID
32284
+ id: targetID,
32285
+ artifact
32094
32286
  };
32095
32287
  }
32096
32288
  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\/([^/]+))?$/);
@@ -32122,60 +32314,171 @@ function parseDispatchRef(ref) {
32122
32314
  return { owner, kind: "message", id: message };
32123
32315
  return { owner, kind: "issue", id: issue };
32124
32316
  }
32317
+ var uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
32318
+ var askIdProblem = "ask must be a bare ask id or a dispatch://.../ask/<id> reference";
32319
+ var messageIdProblem = "in_reply_to must be a full message id (uuid) or a dispatch://KEY/message/<id> reference";
32125
32320
  function askId(args) {
32126
32321
  const ask = stringArg(args, "ask");
32127
- if (!ask.startsWith("dispatch://"))
32128
- return ask;
32129
- const reference = parseDispatchRef(ask);
32130
- if (reference?.kind !== "ask") {
32131
- throw new Error("ask must be a bare ask id or a dispatch://.../ask/<id> reference");
32322
+ return ask.startsWith("dispatch://") ? parseDispatchRef(ask)?.id ?? ask : ask;
32323
+ }
32324
+ function messageIdOf(value) {
32325
+ const id = value.startsWith("dispatch://") ? (() => {
32326
+ const reference = parseDispatchRef(value);
32327
+ return reference?.kind === "message" ? reference.id : undefined;
32328
+ })() : value;
32329
+ return id !== undefined && uuidPattern.test(id) ? id : undefined;
32330
+ }
32331
+ var idPrefixPattern = /^[0-9a-f][0-9a-f-]{7,}$/i;
32332
+ async function resolveIdPrefix(tool, kind, ref, list) {
32333
+ if (uuidPattern.test(ref.id))
32334
+ return ref.id;
32335
+ const ownerName = ref.owner.kind === "issue" ? ref.owner.issue : `${ref.owner.project}/${ref.artifact}`;
32336
+ if (!idPrefixPattern.test(ref.id)) {
32337
+ throw new ToolInputError(tool, [
32338
+ `${kind} id ${ref.id} must be a full uuid or a prefix of at least 8 hex characters`
32339
+ ]);
32132
32340
  }
32133
- return reference.id;
32341
+ const prefix = ref.id.toLowerCase();
32342
+ const matches = (await list()).filter((item) => item.id.toLowerCase().startsWith(prefix));
32343
+ if (matches.length === 1 && matches[0] !== undefined)
32344
+ return matches[0].id;
32345
+ throw new ToolInputError(tool, [
32346
+ matches.length === 0 ? `${kind} id ${ref.id} matches none of the ${kind}s on ${ownerName}; use the full id` : `${kind} id ${ref.id} matches ${matches.length} ${kind}s on ${ownerName}; use the full id`
32347
+ ]);
32134
32348
  }
32135
32349
  function messageInReplyTo(args) {
32136
32350
  const inReplyTo = optionalString(args, "in_reply_to");
32137
- if (inReplyTo === undefined || !inReplyTo.startsWith("dispatch://"))
32138
- return inReplyTo;
32139
- const reference = parseDispatchRef(inReplyTo);
32140
- if (reference?.kind !== "message") {
32141
- throw new Error("in_reply_to must be a bare message id or a dispatch://.../message/<id> reference");
32351
+ return inReplyTo === undefined ? undefined : messageIdOf(inReplyTo);
32352
+ }
32353
+ function argumentProblems(tool, args) {
32354
+ const problems = [];
32355
+ switch (tool) {
32356
+ case "dispatch_ask": {
32357
+ const question = optionalString(args, "question");
32358
+ const ref = optionalString(args, "ref");
32359
+ if (question !== undefined && ref !== undefined && question.length <= maxAskQuestion16) {
32360
+ const problem = askQuestionProblem(questionWithRef(question, ref));
32361
+ if (problem !== undefined)
32362
+ problems.push(problem);
32363
+ }
32364
+ break;
32365
+ }
32366
+ case "dispatch_edit_ask":
32367
+ case "dispatch_resolve_ask": {
32368
+ const ask = optionalString(args, "ask");
32369
+ if (ask?.startsWith("dispatch://") && parseDispatchRef(ask)?.kind !== "ask") {
32370
+ problems.push(askIdProblem);
32371
+ }
32372
+ break;
32373
+ }
32374
+ case "dispatch_comment": {
32375
+ if (optionalString(args, "quote") !== undefined && optionalString(args, "artifact") === undefined) {
32376
+ problems.push("artifact is required when quote is supplied");
32377
+ }
32378
+ if (optionalString(args, "reply_to") !== undefined && optionalString(args, "reply_to_ask") !== undefined) {
32379
+ problems.push("reply_to and reply_to_ask cannot both be set");
32380
+ }
32381
+ break;
32382
+ }
32383
+ case "dispatch_message": {
32384
+ const inReplyTo = optionalString(args, "in_reply_to");
32385
+ if (inReplyTo !== undefined && messageIdOf(inReplyTo) === undefined) {
32386
+ problems.push(messageIdProblem);
32387
+ }
32388
+ break;
32389
+ }
32142
32390
  }
32143
- return reference.id;
32391
+ return problems;
32144
32392
  }
32393
+ function dispatchRefFromUrl(value, serverUrl) {
32394
+ let url;
32395
+ let origin;
32396
+ try {
32397
+ url = new URL(value);
32398
+ origin = new URL(serverUrl).origin;
32399
+ } catch {
32400
+ return;
32401
+ }
32402
+ if (url.origin !== origin)
32403
+ return;
32404
+ const versionOf = (name) => {
32405
+ const value = url.searchParams.get(name);
32406
+ return value !== null && /^[1-9][0-9]*$/.test(value) ? `@v${value}` : "";
32407
+ };
32408
+ const issuePage = url.pathname.match(/^\/issues\/([A-Z][A-Z0-9]{1,9}-[1-9][0-9]*)(?:\/(spec|log|conversation|children)|\/artifacts\/([^/]+)|\/asks\/([^/]+)|\/comments\/([^/]+)|\/messages\/([^/]+))?\/?$/);
32409
+ if (issuePage) {
32410
+ const [, key, page, artifact, ask, comment, message] = issuePage;
32411
+ const version = versionOf("v");
32412
+ if (page === "spec" && version !== "")
32413
+ return `dispatch://${key}/artifact/spec${version}`;
32414
+ if (page !== undefined)
32415
+ return `dispatch://${key}/${page === "conversation" ? "log" : page}`;
32416
+ if (artifact !== undefined) {
32417
+ return `dispatch://${key}/artifact/${decodeURIComponent(artifact)}${version}`;
32418
+ }
32419
+ if (ask !== undefined)
32420
+ return `dispatch://${key}/ask/${ask}`;
32421
+ if (comment !== undefined)
32422
+ return `dispatch://${key}/comment/${comment}`;
32423
+ if (message !== undefined)
32424
+ return `dispatch://${key}/message/${message}`;
32425
+ return `dispatch://${key}`;
32426
+ }
32427
+ const documentPage = url.pathname.match(/^\/projects\/([A-Z][A-Z0-9]{1,9})\/documents\/([^/]+)\/?$/);
32428
+ if (!documentPage)
32429
+ return;
32430
+ const [, project, slug] = documentPage;
32431
+ const document = `dispatch://${project}/artifact/${decodeURIComponent(slug ?? "")}${versionOf("version")}`;
32432
+ const ask = url.searchParams.get("ask");
32433
+ if (ask !== null)
32434
+ return `${document}/ask/${ask}`;
32435
+ const comment = url.searchParams.get("comment");
32436
+ if (comment !== null)
32437
+ return `${document}/comment/${comment}`;
32438
+ return document;
32439
+ }
32440
+ 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>, or " + "dispatch://PROJECT/artifact/<document-ref> (an artifact id, slug, or filename); " + "a dashboard URL on this Dispatch server is accepted too";
32441
+ var ownerRequiredProblem = "issue is required; supply issue or set LEGION_ISSUE";
32145
32442
  function toolSchema(tool) {
32146
32443
  const spec = dispatchToolSpecs.find((candidate) => candidate.name === tool);
32147
32444
  if (!spec)
32148
32445
  throw new Error(`Unknown Dispatch tool: ${tool}`);
32149
32446
  return dispatchToolSchema(spec, zodSchemaApi(exports_external), { strict: true });
32150
32447
  }
32151
- async function resolveOwnerArguments(tool, args, cwd, env, exec) {
32448
+ async function resolveOwnerArguments(tool, input, cwd, env, exec, serverUrl, problems) {
32152
32449
  if (issueFreeTools[tool] === true)
32153
- return { args, ref: null, owner: null };
32154
- const refArgument = args.ref;
32155
- if (tool === "dispatch_ask" && typeof refArgument === "string" && !refArgument.startsWith("dispatch://")) {
32156
- throw new Error("ref must be a dispatch:// reference");
32157
- }
32158
- const ref = typeof refArgument === "string" ? parseDispatchRef(refArgument) ?? (() => {
32159
- 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/<document-ref> (an artifact id, slug, or filename)");
32160
- })() : null;
32450
+ return { args: input, ref: null, owner: null };
32451
+ const refArgument = input.ref;
32452
+ let ref = null;
32453
+ let args = input;
32454
+ if (typeof refArgument === "string") {
32455
+ const refText = dispatchRefFromUrl(refArgument, serverUrl) ?? refArgument;
32456
+ if (refText !== refArgument)
32457
+ args = { ...input, ref: refText };
32458
+ ref = parseDispatchRef(refText);
32459
+ if (ref === null) {
32460
+ problems.push(tool === "dispatch_ask" && !refText.startsWith("dispatch://") ? "ref must be a dispatch:// reference" : refGrammarProblem);
32461
+ }
32462
+ }
32161
32463
  const issueArgument = args.issue;
32162
32464
  const projectArgument = args.project;
32163
32465
  const artifactArgument = args.artifact;
32164
32466
  const versionArgument = args.version;
32165
32467
  if (issueArgument !== undefined && projectArgument !== undefined) {
32166
- throw new Error("exactly one of issue and project is required");
32468
+ problems.push("exactly one of issue and project is required");
32167
32469
  }
32168
32470
  if (typeof projectArgument === "string") {
32169
32471
  if (!/^[A-Z][A-Z0-9]{1,9}$/.test(projectArgument)) {
32170
- throw new Error("project must be a project key such as CORE");
32472
+ problems.push("project must be a project key such as CORE");
32171
32473
  }
32172
- if (ref?.owner.kind === "project" && (ref.owner.project !== projectArgument || artifactArgument !== undefined && artifactArgument !== ref.id)) {
32173
- throw new Error("project and ref must name the same document");
32474
+ const refDocument = ref?.owner.kind === "project" ? ref.artifact ?? ref.id : undefined;
32475
+ if (ref?.owner.kind === "project" && (ref.owner.project !== projectArgument || artifactArgument !== undefined && artifactArgument !== refDocument)) {
32476
+ problems.push("project and ref must name the same document");
32174
32477
  }
32175
32478
  return {
32176
32479
  args: {
32177
32480
  ...args,
32178
- ...artifactArgument === undefined && ref?.owner.kind === "project" ? { artifact: ref.id } : {},
32481
+ ...artifactArgument === undefined && refDocument !== undefined ? { artifact: refDocument } : {},
32179
32482
  ...versionArgument === undefined && ref?.version !== undefined ? { version: ref.version } : {}
32180
32483
  },
32181
32484
  ref,
@@ -32187,7 +32490,7 @@ async function resolveOwnerArguments(tool, args, cwd, env, exec) {
32187
32490
  args: {
32188
32491
  ...args,
32189
32492
  project: ref.owner.project,
32190
- ...artifactArgument === undefined ? { artifact: ref.id } : {},
32493
+ ...artifactArgument === undefined ? { artifact: ref.artifact ?? ref.id } : {},
32191
32494
  ...versionArgument === undefined && ref.version !== undefined ? { version: ref.version } : {}
32192
32495
  },
32193
32496
  ref,
@@ -32208,20 +32511,25 @@ async function resolveOwnerArguments(tool, args, cwd, env, exec) {
32208
32511
  };
32209
32512
  }
32210
32513
  const legionIssue = env.LEGION_ISSUE;
32211
- if (!legionIssue)
32212
- throw new Error("issue is required; supply issue or set LEGION_ISSUE");
32514
+ if (!legionIssue) {
32515
+ problems.push(ownerRequiredProblem);
32516
+ return { args, ref, owner: null };
32517
+ }
32213
32518
  if (nativeIssueKeyPattern.test(legionIssue) || externalIssueRefPattern.test(legionIssue)) {
32214
32519
  const issue = canonicalExternalIssueRef(legionIssue);
32215
- return { args: { ...args, issue }, ref: null, owner: { kind: "issue", issue } };
32520
+ return { args: { ...args, issue }, ref, owner: { kind: "issue", issue } };
32216
32521
  }
32217
32522
  if (!bareIssueNumberPattern.test(legionIssue)) {
32218
- throw new Error("LEGION_ISSUE must be a native issue key (e.g. LEGION-3), an external owner/repo#n reference, or a bare positive issue number");
32523
+ problems.push("LEGION_ISSUE must be a native issue key (e.g. LEGION-3), an external owner/repo#n reference, or a bare positive issue number");
32524
+ return { args, ref, owner: null };
32219
32525
  }
32220
32526
  const repo = await resolveCwdRepo(cwd, exec);
32221
- if (!repo)
32222
- throw new Error("issue is required; LEGION_ISSUE needs a GitHub repository in cwd");
32527
+ if (!repo) {
32528
+ problems.push("issue is required; LEGION_ISSUE needs a GitHub repository in cwd");
32529
+ return { args, ref, owner: null };
32530
+ }
32223
32531
  const issue = `${repo}#${legionIssue}`;
32224
- return { args: { ...args, issue }, ref: null, owner: { kind: "issue", issue } };
32532
+ return { args: { ...args, issue }, ref, owner: { kind: "issue", issue } };
32225
32533
  }
32226
32534
  async function resolveArtifact(client, owner, artifactReference) {
32227
32535
  if (owner.kind === "project") {
@@ -32252,7 +32560,8 @@ async function resolveArtifact(client, owner, artifactReference) {
32252
32560
  const issue = await client.getIssue(owner.issue);
32253
32561
  const artifact = artifactReference === undefined || artifactReference === "spec" ? issue.artifacts.find((candidate) => candidate.primary || candidate.id === issue.primary_artifact_id) : issue.artifacts.find((candidate) => candidate.id === artifactReference || candidate.slug === artifactReference || candidate.name === artifactReference);
32254
32562
  if (!artifact) {
32255
- throw new Error(`artifact ${artifactReference ?? "spec"} was not found on issue ${issue.key}`);
32563
+ const slugs = issue.artifacts.map((candidate) => `${candidate.slug}${candidate.primary || candidate.id === issue.primary_artifact_id ? " (primary)" : ""}`);
32564
+ throw new Error(`artifact "${artifactReference ?? "spec"}" was not found on issue ${issue.key}; artifacts: ${slugs.length === 0 ? "none" : slugs.join(", ")}`);
32256
32565
  }
32257
32566
  return { owner, issue, artifact };
32258
32567
  }
@@ -32309,7 +32618,7 @@ function issueSummary(issue, events, references) {
32309
32618
  ...typeof references === "string" ? [`- ${references}`] : references.members.length === 0 ? ["- none"] : references.members.map(({ artifact, depth, via }) => `- ${artifact.project}/${artifact.slug} \xB7 depth ${depth} via ${via.kind} ${via.id}`),
32310
32619
  ...typeof references === "string" || !references.truncated ? [] : ["- more references beyond 8 hops"],
32311
32620
  "Events:",
32312
- ...events.length === 0 ? ["- none"] : events.map((event) => `- #${event.seq} ${event.type} \xB7 ${event.actor.kind} ${event.actor.id} \xB7 ${event.created_at}`)
32621
+ ...events.length === 0 ? ["- none"] : events.map(eventLine)
32313
32622
  ].join(`
32314
32623
  `);
32315
32624
  }
@@ -32317,10 +32626,41 @@ function logSummary(issue, events) {
32317
32626
  return [
32318
32627
  `Key: ${issue.key}`,
32319
32628
  "Events:",
32320
- ...events.length === 0 ? ["- none"] : events.map((event) => `- #${event.seq} ${event.type} \xB7 ${event.actor.kind} ${event.actor.id} \xB7 ${event.created_at}`)
32629
+ ...events.length === 0 ? ["- none"] : events.map(eventLine)
32321
32630
  ].join(`
32322
32631
  `);
32323
32632
  }
32633
+ function eventHead(event) {
32634
+ switch (event.type) {
32635
+ case "ask.opened":
32636
+ case "ask.edited":
32637
+ case "ask.resolved":
32638
+ return textHead(event.payload.question);
32639
+ case "ask.answered":
32640
+ return `${textHead(event.payload.question)} -> ${textHead(askAnswerText(event.payload.answer))}`;
32641
+ case "comment.created":
32642
+ case "comment.edited":
32643
+ case "comment.resolved":
32644
+ case "comment.reopened":
32645
+ case "suggestion.accepted":
32646
+ case "suggestion.rejected":
32647
+ case "message.created":
32648
+ case "message.answered":
32649
+ return textHead(event.payload.body);
32650
+ case "artifact.version":
32651
+ return textHead(`${event.payload.name} v${event.payload.version.number}${event.payload.version.summary ? `: ${event.payload.version.summary}` : ""}`);
32652
+ case "issue.created":
32653
+ case "issue.updated":
32654
+ case "issue.closed":
32655
+ return `status ${event.payload.status}`;
32656
+ default:
32657
+ return;
32658
+ }
32659
+ }
32660
+ function eventLine(event) {
32661
+ const head = eventHead(event);
32662
+ return `- #${event.seq} ${event.type} \xB7 ${event.actor.kind} ${event.actor.id} \xB7 ${event.created_at}${head === undefined || head === "" ? "" : ` \xB7 ${head}`}`;
32663
+ }
32324
32664
  function childrenSummary(issue) {
32325
32665
  return [
32326
32666
  `Key: ${issue.key}`,
@@ -32455,19 +32795,29 @@ async function executeDispatchTool(input) {
32455
32795
  throw error;
32456
32796
  }
32457
32797
  }, { preconnect: baseFetch.preconnect });
32798
+ const env = input.env ?? process.env;
32799
+ const exec = input.exec ?? defaultExec;
32800
+ const problems = [];
32801
+ const ownerArguments = await resolveOwnerArguments(input.tool, input.args, input.cwd, env, exec, configUrl, problems);
32802
+ const ownerMissing = issueFreeTools[input.tool] !== true && ownerArguments.owner === null;
32803
+ const schema = toolSchema(input.tool);
32804
+ const parsed = schema.safeParse(ownerArguments.args, { reportInput: true });
32805
+ if (!parsed.success) {
32806
+ const issues = parsed.error.issues.filter((issue) => !ownerMissing || !(issue.code === "invalid_type" && issue.path.length === 1 && issue.path[0] === "issue" && issue.input === undefined || issue.code === "custom" && issue.path.length === 0 && issue.message.startsWith("Exactly one of issue and project is required")));
32807
+ problems.push(...formatZodIssues(issues, schema));
32808
+ }
32809
+ problems.push(...argumentProblems(input.tool, ownerArguments.args));
32810
+ if (problems.length > 0)
32811
+ throw new ToolInputError(input.tool, problems);
32458
32812
  if (input.tool === "dispatch_open_asks") {
32459
32813
  const sessionId = input.sessionId?.trim();
32460
32814
  if (!sessionId)
32461
32815
  throw new Error("host session id is required for dispatch_open_asks");
32462
- toolSchema(input.tool).parse(input.args);
32463
32816
  const client = new DispatchClient(configUrl, configToken, fetchImpl, input.signal);
32464
32817
  const response = await client.openAsks(sessionId);
32465
32818
  return { text: formatOpenAsksSummary(response, configUrl), details: { ...response } };
32466
32819
  }
32467
- const env = input.env ?? process.env;
32468
- const exec = input.exec ?? defaultExec;
32469
- const ownerArguments = await resolveOwnerArguments(input.tool, input.args, input.cwd, env, exec);
32470
- const args = toolSchema(input.tool).parse(ownerArguments.args);
32820
+ const args = parsed.success ? parsed.data : ownerArguments.args;
32471
32821
  const actor = toolActor(await resolveOrigin(env, exec, input.cwd), input);
32472
32822
  const client = new DispatchClient(configUrl, configToken, fetchImpl, input.signal);
32473
32823
  const owner = ownerArguments.owner?.kind === "issue" ? {
@@ -32550,8 +32900,6 @@ async function executeDispatchTool(input) {
32550
32900
  }
32551
32901
  case "dispatch_resolve_ask": {
32552
32902
  const kind = stringArg(args, "kind");
32553
- if (kind !== "retracted" && kind !== "resolved")
32554
- throw new Error("kind must be retracted or resolved");
32555
32903
  const ask = await client.resolveAsk(stringArg(args, "ask"), {
32556
32904
  kind,
32557
32905
  reason: stringArg(args, "reason"),
@@ -32608,24 +32956,12 @@ async function executeDispatchTool(input) {
32608
32956
  }
32609
32957
  case "dispatch_comment": {
32610
32958
  const artifactReference = optionalString(args, "artifact");
32611
- if (optionalString(args, "quote") !== undefined && artifactReference === undefined) {
32612
- throw new Error("artifact is required when quote is supplied");
32613
- }
32614
32959
  const owner = documentOwner();
32615
32960
  const resolved = owner.kind === "project" || artifactReference === undefined ? owner.kind === "project" ? await resolveArtifact(client, owner, artifactReference) : undefined : await resolveArtifact(client, owner, artifactReference);
32616
32961
  const anchored = resolved ? anchor(resolved.artifact, args) : undefined;
32617
32962
  const replyTo = optionalString(args, "reply_to");
32618
32963
  const replyToAsk = optionalString(args, "reply_to_ask");
32619
- if (replyTo !== undefined && replyToAsk !== undefined) {
32620
- throw new Error("reply_to and reply_to_ask cannot both be set");
32621
- }
32622
32964
  const requestedTurn = optionalString(args, "turn");
32623
- if (requestedTurn !== undefined && replyToAsk === undefined) {
32624
- throw new Error("turn requires reply_to_ask");
32625
- }
32626
- if (requestedTurn !== undefined && requestedTurn !== "agent" && requestedTurn !== "human") {
32627
- throw new Error("turn must be agent or human");
32628
- }
32629
32965
  const commentInput = {
32630
32966
  body: stringArg(args, "body"),
32631
32967
  ...anchored === undefined ? {} : { anchor: anchored },
@@ -32779,17 +33115,21 @@ ${trailer.join(`
32779
33115
  }
32780
33116
  case "dispatch_read": {
32781
33117
  if (ownerArguments.ref?.kind === "ask") {
32782
- const askRead = await client.getAsk(ownerArguments.ref.id);
33118
+ const ref = ownerArguments.ref;
33119
+ const id = await resolveIdPrefix(input.tool, "ask", ref, async () => ref.owner.kind === "issue" ? client.listIssueAsks(ref.owner.issue) : client.getArtifactAsks((await resolveArtifact(client, ref.owner, ref.artifact)).artifact.id, "all"));
33120
+ const askRead = await client.getAsk(id);
32783
33121
  return {
32784
33122
  text: askSummary(askRead),
32785
- details: ownerArguments.ref.owner.kind === "project" ? { project: ownerArguments.ref.owner.project } : { issue: ownerArguments.ref.owner.issue }
33123
+ details: ref.owner.kind === "project" ? { project: ref.owner.project } : { issue: ref.owner.issue }
32786
33124
  };
32787
33125
  }
32788
33126
  if (ownerArguments.ref?.kind === "comment") {
32789
- const comment = await client.getComment(ownerArguments.ref.id);
33127
+ const ref = ownerArguments.ref;
33128
+ const id = await resolveIdPrefix(input.tool, "comment", ref, async () => ref.owner.kind === "issue" ? client.getComments(ref.owner.issue) : client.getArtifactComments((await resolveArtifact(client, ref.owner, ref.artifact)).artifact.id));
33129
+ const comment = await client.getComment(id);
32790
33130
  return {
32791
33131
  text: commentSummary(comment),
32792
- details: ownerArguments.ref.owner.kind === "project" ? { project: ownerArguments.ref.owner.project } : { issue: comment.comment.issue_key }
33132
+ details: ref.owner.kind === "project" ? { project: ref.owner.project } : { issue: comment.comment.issue_key }
32793
33133
  };
32794
33134
  }
32795
33135
  if (ownerArguments.ref?.kind === "message") {
@@ -32918,8 +33258,8 @@ function messageMetadataShape(schema) {
32918
33258
  return {
32919
33259
  in_reply_to: schema.string().optional(),
32920
33260
  supersedes: schema.string().optional(),
32921
- urgency: schema.enum(URGENCY_VALUES).optional(),
32922
- expects_reply: schema.enum(EXPECTS_REPLY_VALUES).optional(),
33261
+ urgency: schema.enum(URGENCY_VALUES).describe("low | med | high | blocking").optional(),
33262
+ expects_reply: schema.enum(EXPECTS_REPLY_VALUES).describe("none | optional | required").optional(),
32923
33263
  expires_at: schema.number({ int: true }).optional()
32924
33264
  };
32925
33265
  }
@@ -33029,6 +33369,17 @@ var envoyToolSpecs = [
33029
33369
  requiresSubscriptionCapability: false
33030
33370
  }
33031
33371
  ];
33372
+ function parseEnvoyToolArguments(operation, parameters) {
33373
+ const spec = envoyToolSpecs.find((candidate) => candidate.operation === operation);
33374
+ if (spec === undefined)
33375
+ throw new Error(`missing Envoy tool specification for ${operation}`);
33376
+ const schema = object(spec.arguments(zodSchemaApi(exports_external))).strict();
33377
+ const parsed = schema.safeParse(parameters, { reportInput: true });
33378
+ if (!parsed.success) {
33379
+ throw new ToolInputError(spec.name, formatZodIssues(parsed.error.issues, schema));
33380
+ }
33381
+ return parsed.data;
33382
+ }
33032
33383
 
33033
33384
  // ../envoy-client/src/transport.ts
33034
33385
  var DEFAULT_TIMEOUT_MS = 5000;
@@ -33912,6 +34263,7 @@ function envoyExtension(pi) {
33912
34263
  label: spec.name,
33913
34264
  description: spec.description,
33914
34265
  parameters: schemaFor(pi, spec.operation),
34266
+ lenientArgValidation: true,
33915
34267
  execute: async (_id, parameters) => execute(spec.operation, parameters)
33916
34268
  });
33917
34269
  }
@@ -33922,6 +34274,7 @@ function envoyExtension(pi) {
33922
34274
  label: spec.name,
33923
34275
  description: spec.description,
33924
34276
  parameters: dispatchToolSchema(spec, zodSchemaApi(pi.zod)),
34277
+ lenientArgValidation: true,
33925
34278
  execute: async (_id, params, signal, _onUpdate, context) => {
33926
34279
  try {
33927
34280
  const result = await executeDispatchTool({
@@ -33994,8 +34347,9 @@ ${formatOpenAsksSummary(snapshot, currentDispatchConfig().url)}`,
33994
34347
  activeSessionContext?.ui.notify(`envoy: dispatch reply auto-subscribe failed (${messageFor(error)}); run envoy_subscribe ${topic}`, "warning");
33995
34348
  }
33996
34349
  });
33997
- async function execute(operation, parameters) {
34350
+ async function execute(operation, rawParameters) {
33998
34351
  try {
34352
+ const parameters = parseEnvoyToolArguments(operation, rawParameters);
33999
34353
  switch (operation) {
34000
34354
  case EnvoyToolOperation.subscribe: {
34001
34355
  const added = [];
package/dist/legion.js CHANGED
@@ -29177,7 +29177,7 @@ var dispatchToolSpecs = [
29177
29177
  options: z.array(z.object({
29178
29178
  label: z.string().describe("Selectable option label."),
29179
29179
  description: z.string().describe("Optional option context.").optional()
29180
- }), { max: 8 }).describe("Optional choices, at most 8.").optional(),
29180
+ }), { max: 8 }).describe("Up to 8 choices, each an object { label, description? } (never a bare string).").optional(),
29181
29181
  multiple: z.boolean().describe("Whether multiple choices may be selected.").optional(),
29182
29182
  urgency: z.enum(ASK_URGENCIES).describe("Optional decision urgency.").optional(),
29183
29183
  anchor: z.object({
@@ -29229,7 +29229,7 @@ var dispatchToolSpecs = [
29229
29229
  quote: z.string().describe("Optional exact quoted document text.").optional(),
29230
29230
  occurrence: z.number({ int: true, min: 0 }).describe("Optional zero-based occurrence of quote.").optional(),
29231
29231
  body: z.string({ max: 2000 }).describe("Review comment, at most 2,000 characters."),
29232
- reply_to: z.string().describe("Full id of a comment to reply to; replying to any comment in a thread continues that " + "thread (an ask's clarification thread included).").optional(),
29232
+ 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(),
29233
29233
  reply_to_ask: z.string().describe("Optional ask id to reply to, threading this comment under that question. Mutually " + "exclusive with reply_to.").optional(),
29234
29234
  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()
29235
29235
  }),
@@ -29261,7 +29261,7 @@ var dispatchToolSpecs = [
29261
29261
  },
29262
29262
  {
29263
29263
  name: "dispatch_doc_edit",
29264
- description: "Apply deterministic document edits, including retyping an identified paragraph into a schema-declared typed block. " + "Do not use it for review feedback or for reading; use dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. " + "For replace, delete, and quote insert anchors, find text exactly as rendered: omit Markdown markers such as backticks or asterisks. " + `The spec (or any document) holds requirements, design, and decisions - never progress, status, or timestamps. ${OWNER_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
29264
+ description: "Apply deterministic document edits, including retyping an identified paragraph into a schema-declared typed block. " + "Do not use it for review feedback or for reading; use dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. " + 'For replace, delete, and quote insert anchors, find text as rendered: inline Markdown (**bold**, `code`) is tolerated; a leading \'# \' matches a heading. Insert anchors also accept "start", "end", and "heading:<exact heading text>". ' + `The spec (or any document) holds requirements, design, and decisions - never progress, status, or timestamps. ${OWNER_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
29265
29265
  arguments: (z) => ({
29266
29266
  issue: z.string().describe(ISSUE_REFERENCE).optional(),
29267
29267
  project: z.string().describe("Project key owning the document.").optional(),
@@ -29269,12 +29269,12 @@ var dispatchToolSpecs = [
29269
29269
  ref: z.string().describe("Optional dispatch:// issue or document reference.").optional(),
29270
29270
  ops: z.array(z.object({
29271
29271
  op: z.enum(DOC_EDIT_OPS).describe("Edit operation."),
29272
- find: z.string().describe("Text to find for replace or delete.").optional(),
29272
+ find: z.string().describe("Text of the target block as rendered, for replace or delete; inline markdown (**bold**, `code`) is tolerated; a leading '# ' matches a heading.").optional(),
29273
29273
  with: z.string().describe("Replacement text for replace.").optional(),
29274
29274
  occurrence: z.number({ int: true, min: 0 }).describe("Optional zero-based match occurrence.").optional(),
29275
29275
  markdown: z.string().describe("Markdown to insert.").optional(),
29276
- after: z.string().describe("Anchor after which to insert.").optional(),
29277
- before: z.string().describe("Anchor before which to insert.").optional(),
29276
+ after: z.string().describe(`Insert after this anchor: a quote of the neighbouring block's text, or one of "start", "end", "heading:<exact heading text>".`).optional(),
29277
+ before: z.string().describe(`Insert before this anchor: a quote of the neighbouring block's text, or one of "start", "end", "heading:<exact heading text>".`).optional(),
29278
29278
  block: z.string().describe("Block id to retype.").optional(),
29279
29279
  type: z.string().describe("Typed block name for retype.").optional(),
29280
29280
  attributes: z.unknown().describe("Typed block attributes for retype.").optional()
@@ -29778,6 +29778,9 @@ function canonicalRepo(owner, repo) {
29778
29778
  return `${owner.trim().toLowerCase()}/${repo.trim().toLowerCase().replace(/\.git$/, "")}`;
29779
29779
  }
29780
29780
  // ../contracts/src/tool-schema.ts
29781
+ function overCapMessage(length, max) {
29782
+ return `is ${length - max} characters over the ${max}-character limit (${length}/${max})`;
29783
+ }
29781
29784
  function zodSchemaApi(zod) {
29782
29785
  const api = zod;
29783
29786
  return {
@@ -29785,8 +29788,12 @@ function zodSchemaApi(zod) {
29785
29788
  let schema = api.string();
29786
29789
  if (opts.min !== undefined)
29787
29790
  schema = schema.min(opts.min);
29788
- if (opts.max !== undefined)
29789
- schema = schema.max(opts.max);
29791
+ if (opts.max !== undefined) {
29792
+ const max = opts.max;
29793
+ schema = schema.max(max, {
29794
+ error: (issue) => overCapMessage(typeof issue.input === "string" ? issue.input.length : max + 1, max)
29795
+ });
29796
+ }
29790
29797
  return schema;
29791
29798
  },
29792
29799
  number: (opts = {}) => {
@@ -10,7 +10,9 @@ decide, never a log of your work. The transcript is your scratch pad; progress a
10
10
  goes through a `dispatch_*` tool.
11
11
 
12
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
13
+ most 2,000 characters; an artifact is at most 25 MiB. It refuses over-limit input with the number to trim (`question is 50 characters over
14
+ the 800-character limit (850/800)`); it never truncates it. A tool call with several problems is refused once, every problem listed
15
+ (`<tool> was not called: N problems`), so one corrected call lands. GitHub threads and markers no
14
16
  longer exist.
15
17
 
16
18
  ## Writing for the human
@@ -273,7 +275,8 @@ within one textblock; split changes that span separate blocks into separate oper
273
275
  insert anchor is a quote, `"start"`, `"end"`, or `"heading:Title"`. Ordinary inserts create a sibling block before or after the quote or
274
276
  heading's enclosing document block; `"start"` and `"end"` select the document edges. At a table-cell quote, a body-row fragment (no
275
277
  header or delimiter rows) extends that table before or after the matched row instead; short rows are padded, wider rows are rejected,
276
- and deleting a cell's quoted text removes only that text.
278
+ and deleting a cell's quoted text removes only that text. A `find` or quote anchor tolerates inline Markdown (`**bold**`, `` `code` ``)
279
+ and a leading `# ` selects a heading by its text; a miss names the three nearest blocks so the next quote lands.
277
280
 
278
281
  Use `replace` for inline continuation. Use zero-based `occurrence` for a repeated target; re-read a missing or ambiguous target before
279
282
  retrying. Pass `summary` to name the version when recording a decision.
@@ -400,7 +403,7 @@ It returns `details` `{ issue, topic, message }`. `body` is capped at 2,000 char
400
403
 
401
404
  A human — or any bearer caller over HTTP, such as a test rig — can target the issue message at a
402
405
  live Envoy session or role as **BTW**, **Aside**, or **Steer**. The incoming Dispatch frame names
403
- the issue and includes a `reply_with` instruction; reply on the same open issue with the existing
406
+ the issue and includes a `reply_with` hint (`{ tool, args }`, ready to issue on any host); reply on the same open issue with the existing
404
407
  tool, never a new targeted send:
405
408
 
406
409
  ```ts
@@ -458,7 +461,11 @@ dispatch://PROJECT/artifact/<document-ref>/ask/<id>
458
461
  dispatch://PROJECT/artifact/<document-ref>/comment/<id>
459
462
  ```
460
463
 
461
- A bare UUID or `KEY#seq` is not a reference; the `dispatch://` form is what Dispatch links and records.
464
+ A bare UUID or `KEY#seq` is not a reference; the `dispatch://` form is what Dispatch links and records. `dispatch_read` also
465
+ accepts the dashboard URL of an issue, spec, artifact, ask, comment, or project document on the configured server (it maps to the
466
+ `dispatch://` form above), and an ask or comment id may be a unique prefix of at least 8 hex characters; a message id is always the
467
+ full uuid. A non-uuid id on `GET /asks/{id}`, `/comments/{id}`, or `/issues/{key}/messages/{id}` is a 400 `ASK_ID_INPUT` /
468
+ `COMMENT_ID_INPUT` / `MESSAGE_ID_INPUT`, never a 500.
462
469
 
463
470
  ## Before / after
464
471
 
@@ -47,8 +47,17 @@ envoy:
47
47
  expects_reply: required
48
48
  re: agent-message-1
49
49
  supersedes: agent-message-0
50
- reply_with: "envoy_send(session_id=\"01a0bbbb-cccc-7ddd-eeee-0123456789ab\", message=\"...\")"
51
- reply_role: "envoy_publish(topic=\"notifications.role.legion-reviewer\", message=\"...\")"
50
+ reply_with:
51
+ tool: envoy_send
52
+ args:
53
+ session_id: 01a0bbbb-cccc-7ddd-eeee-0123456789ab
54
+ in_reply_to: agent-message-2
55
+ message: ...
56
+ reply_role:
57
+ tool: envoy_publish
58
+ args:
59
+ topic: notifications.role.legion-reviewer
60
+ message: ...
52
61
  summary: Deployment needs confirmation.
53
62
  message: "Confirm the listener health check passed.\n\nThen publish the release."
54
63
  note: body names session 01a0cccc-dddd-7eee-ffff-0123456789ab; the sender is 01a0bbbb-cccc-7ddd-eeee-0123456789ab
@@ -62,7 +71,10 @@ envoy:
62
71
  - `by` is the expiry deadline, when the sender supplied one.
63
72
  - `urgency` is the sender's priority classification.
64
73
  - `expects_reply` states whether a reply is `none`, `optional`, or `required`.
65
- - `re` names the delivery this message replies to.
74
+ - `re` names the delivery this message replies to. A Dispatch frame names it as a `dispatch://`
75
+ ref — the ask (`dispatch://KEY/ask/<id>`, or `dispatch://PROJECT/artifact/<slug>/ask/<id>` for a
76
+ document ask) or the message (`dispatch://KEY/message/<id>`) — never the quoted text; the
77
+ question head, when there is one, is `dispatch.question`.
66
78
  - `supersedes` names an earlier delivery this one replaces.
67
79
  - `reply_with` is the direct-reply call for the sender.
68
80
  - `reply_role` is the role-publish reply call when the sender has a role.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-legion-envoy",
3
- "version": "1.35.1",
3
+ "version": "1.36.0",
4
4
  "type": "module",
5
5
  "omp": {
6
6
  "extensions": [