@sjawhar/opencode-legion-envoy 0.40.0 → 0.41.1

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.
@@ -13603,15 +13603,22 @@ var AskEditedEventPayloadSchema = object({
13603
13603
  edited_by: object({ kind: string2(), id: string2() }).passthrough()
13604
13604
  });
13605
13605
  var CommentEventPayloadSchema = object({
13606
+ id: string2().optional(),
13606
13607
  artifact_name: string2().optional(),
13607
13608
  body: string2().optional(),
13608
13609
  reply_to: string2().nullish(),
13609
13610
  ask_id: string2().nullish(),
13610
13611
  ask_question: string2().optional(),
13611
13612
  anchor: object({ quote: string2().optional() }).nullish(),
13612
- suggestion: object({ replace_with: string2().optional() }).nullish()
13613
+ suggestion: object({ replace_with: string2().optional() }).nullish(),
13614
+ author: object({ kind: string2(), id: string2().optional() }).passthrough().optional(),
13615
+ created_at: string2().optional()
13616
+ });
13617
+ var MessageEventPayloadSchema = object({
13618
+ id: string2().optional(),
13619
+ body: string2().optional(),
13620
+ author: object({ kind: string2(), id: string2().optional() }).passthrough().optional()
13613
13621
  });
13614
- var MessageEventPayloadSchema = object({ body: string2().optional() });
13615
13622
  var ChildStatusEventPayloadSchema = object({
13616
13623
  child_key: string2().optional(),
13617
13624
  from: string2().optional(),
@@ -13673,6 +13680,18 @@ function dispatchToolSchema(spec, z, opts) {
13673
13680
  return spec.validation === undefined ? z.object(shape, opts) : z.refineObject(shape, spec.validation.check, spec.validation.message, opts);
13674
13681
  }
13675
13682
  var ISSUE_REFERENCE = "An issue is a native KEY or external owner/repo#n reference; an external reference creates its native issue in the repository's dashboard-configured project or, failing that, the default project (DISPATCH_DEFAULT_PROJECT).";
13683
+ var OWNER_REFERENCE = "Exactly one of issue and project is required. An issue is a native KEY or external owner/repo#n reference; a project is a project key such as CORE and addresses an unlinked project document named by artifact.";
13684
+ function documentOwnerValidation(requireArtifact) {
13685
+ return {
13686
+ check: (value) => {
13687
+ const input = value;
13688
+ const hasIssue = typeof input.issue === "string";
13689
+ const hasProject = typeof input.project === "string";
13690
+ return (hasIssue !== hasProject || !hasIssue && !hasProject && typeof input.ref === "string") && (!hasProject || !requireArtifact || typeof input.artifact === "string");
13691
+ },
13692
+ message: "Exactly one of issue and project is required; with project, artifact names the document."
13693
+ };
13694
+ }
13676
13695
  var SPEC_SECTIONS = [
13677
13696
  "Decisions needed",
13678
13697
  "Acceptance",
@@ -13700,9 +13719,11 @@ var dispatchToolSpecs = [
13700
13719
  },
13701
13720
  {
13702
13721
  name: "dispatch_ask",
13703
- description: "Open a durable, answerable decision on an issue. 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. ${ISSUE_REFERENCE}`,
13722
+ 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}`,
13704
13723
  arguments: (z) => ({
13705
- issue: z.string().describe(ISSUE_REFERENCE),
13724
+ issue: z.string().describe(ISSUE_REFERENCE).optional(),
13725
+ project: z.string().describe("Project key owning the document.").optional(),
13726
+ artifact: z.string().describe("Project document slug or id.").optional(),
13706
13727
  question: z.string({ max: 800 }).describe("Decision question, at most 800 characters."),
13707
13728
  options: z.array(z.object({
13708
13729
  label: z.string().describe("Selectable option label."),
@@ -13715,7 +13736,8 @@ var dispatchToolSpecs = [
13715
13736
  quote: z.string().describe("Exact text the decision concerns."),
13716
13737
  occurrence: z.number({ int: true, min: 0 }).describe("Zero-based occurrence of the quote.").optional()
13717
13738
  }).describe("Optional document location for the question.").optional()
13718
- })
13739
+ }),
13740
+ validation: documentOwnerValidation(true)
13719
13741
  },
13720
13742
  {
13721
13743
  name: "dispatch_edit_ask",
@@ -13749,28 +13771,32 @@ var dispatchToolSpecs = [
13749
13771
  },
13750
13772
  {
13751
13773
  name: "dispatch_comment",
13752
- description: "Add review feedback to an issue or document quote, or reply to a question asked with dispatch_ask. " + "Do not use it for an exact replacement; use " + `dispatch_suggest instead. Body is at most 2,000 characters. ${ISSUE_REFERENCE}`,
13774
+ description: "Add review feedback to an issue or project document quote, or reply to a question asked with dispatch_ask. " + "Do not use it for an exact replacement; use " + `dispatch_suggest instead. Body is at most 2,000 characters. ${OWNER_REFERENCE}`,
13753
13775
  arguments: (z) => ({
13754
- issue: z.string().describe(ISSUE_REFERENCE),
13776
+ issue: z.string().describe(ISSUE_REFERENCE).optional(),
13777
+ project: z.string().describe("Project key owning the document.").optional(),
13755
13778
  artifact: z.string().describe("Artifact slug or id required when quote is given.").optional(),
13756
13779
  quote: z.string().describe("Optional exact quoted document text.").optional(),
13757
13780
  occurrence: z.number({ int: true, min: 0 }).describe("Optional zero-based occurrence of quote.").optional(),
13758
13781
  body: z.string({ max: 2000 }).describe("Review comment, at most 2,000 characters."),
13759
13782
  reply_to: z.string().describe("Optional comment id to reply to.").optional(),
13760
13783
  reply_to_ask: z.string().describe("Optional ask id to reply to, threading this comment under that question. Mutually " + "exclusive with reply_to.").optional()
13761
- })
13784
+ }),
13785
+ validation: documentOwnerValidation(true)
13762
13786
  },
13763
13787
  {
13764
13788
  name: "dispatch_suggest",
13765
- description: "Propose an exact replacement for quoted document text. Do not use it for general feedback; use " + `dispatch_comment instead. Optional explanation is at most 2,000 characters. ${ISSUE_REFERENCE}`,
13789
+ description: "Propose an exact replacement for quoted document text. Do not use it for general feedback; use " + `dispatch_comment instead. Optional explanation is at most 2,000 characters. ${OWNER_REFERENCE}`,
13766
13790
  arguments: (z) => ({
13767
- issue: z.string().describe(ISSUE_REFERENCE),
13791
+ issue: z.string().describe(ISSUE_REFERENCE).optional(),
13792
+ project: z.string().describe("Project key owning the document.").optional(),
13768
13793
  artifact: z.string().describe("Artifact slug or id containing the quoted text."),
13769
13794
  quote: z.string().describe("Exact document text to replace."),
13770
13795
  replace_with: z.string().describe("Replacement text."),
13771
13796
  body: z.string({ max: 2000 }).describe("Optional rationale, at most 2,000 characters.").optional(),
13772
13797
  occurrence: z.number({ int: true, min: 0 }).describe("Optional zero-based occurrence of quote.").optional()
13773
- })
13798
+ }),
13799
+ validation: documentOwnerValidation(true)
13774
13800
  },
13775
13801
  {
13776
13802
  name: "dispatch_message",
@@ -13782,9 +13808,10 @@ var dispatchToolSpecs = [
13782
13808
  },
13783
13809
  {
13784
13810
  name: "dispatch_doc_edit",
13785
- description: "Apply deterministic text edits to a document. Do not use it for review feedback or for reading; use " + `dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. ${ISSUE_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
13811
+ 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}`,
13786
13812
  arguments: (z) => ({
13787
- issue: z.string().describe(ISSUE_REFERENCE),
13813
+ issue: z.string().describe(ISSUE_REFERENCE).optional(),
13814
+ project: z.string().describe("Project key owning the document.").optional(),
13788
13815
  artifact: z.string().describe("Artifact slug or id for the document."),
13789
13816
  ops: z.array(z.object({
13790
13817
  op: z.enum(DOC_EDIT_OPS).describe("Edit operation."),
@@ -13796,23 +13823,27 @@ var dispatchToolSpecs = [
13796
13823
  before: z.string().describe("Anchor before which to insert.").optional()
13797
13824
  })).describe("Flat tagged edits; the server validates fields required for each operation."),
13798
13825
  summary: z.string().describe("Optional named-version summary.").optional()
13799
- })
13826
+ }),
13827
+ validation: documentOwnerValidation(true)
13800
13828
  },
13801
13829
  {
13802
13830
  name: "dispatch_doc_read",
13803
- description: "Read a live document or a named document version. Do not use it for issue status, asks, or events; " + "use dispatch_read instead. Supply ref or issue; issue plus an omitted artifact reads the primary document. " + `${ISSUE_REFERENCE}`,
13831
+ description: "Read a live document or a named document version. Do not use it for issue status, asks, or events; " + "use dispatch_read instead. Supply ref, issue, or project plus artifact; issue plus an omitted artifact reads the primary document. " + OWNER_REFERENCE,
13804
13832
  arguments: (z) => ({
13805
13833
  issue: z.string().describe(ISSUE_REFERENCE).optional(),
13806
- artifact: z.string().describe("Optional artifact slug or id; primary document by default.").optional(),
13834
+ project: z.string().describe("Project key owning the document.").optional(),
13835
+ artifact: z.string().describe("Optional artifact slug or id; primary document by default for an issue.").optional(),
13807
13836
  version: z.number({ int: true, min: 1 }).describe("Optional version number.").optional(),
13808
13837
  ref: z.string().describe("Optional dispatch:// document reference.").optional()
13809
- })
13838
+ }),
13839
+ validation: documentOwnerValidation(true)
13810
13840
  },
13811
13841
  {
13812
13842
  name: "dispatch_artifact",
13813
- description: "Attach a local file or inline text as an issue artifact. Do not use it to edit a live document; use " + `dispatch_doc_edit instead. Exactly one of path or content is required; artifacts are limited to 25 MiB. ${ISSUE_REFERENCE}`,
13843
+ description: "Attach a local file or inline text as an issue artifact or project document. Do not use it to edit a live document; use " + `dispatch_doc_edit instead. Exactly one of path or content is required; artifacts are limited to 25 MiB. ${OWNER_REFERENCE}`,
13814
13844
  arguments: (z) => ({
13815
- issue: z.string().describe(ISSUE_REFERENCE),
13845
+ issue: z.string().describe(ISSUE_REFERENCE).optional(),
13846
+ project: z.string().describe("Project key for an unlinked document.").optional(),
13816
13847
  name: z.string().describe("Artifact filename shown in Dispatch."),
13817
13848
  path: z.string().describe("Local path to the file to upload.").optional(),
13818
13849
  content: z.string().describe("Inline text to store as a Markdown document.").optional(),
@@ -13821,18 +13852,21 @@ var dispatchToolSpecs = [
13821
13852
  validation: {
13822
13853
  check: (value) => {
13823
13854
  const input = value;
13824
- return typeof input.path === "string" !== (typeof input.content === "string");
13855
+ return documentOwnerValidation(false).check(value) && typeof input.path === "string" !== (typeof input.content === "string");
13825
13856
  },
13826
- message: "Exactly one of path or content is required."
13857
+ message: "Exactly one of path or content is required. Exactly one of issue and project is required; with project, artifact names the document."
13827
13858
  }
13828
13859
  },
13829
13860
  {
13830
13861
  name: "dispatch_read",
13831
- description: "Read an issue summary, targeted ask, or targeted comment reply chain. Do not use it for document " + "contents; use dispatch_doc_read instead. Supply issue or ref. " + ISSUE_REFERENCE,
13862
+ description: "Read an issue or project-document summary, targeted ask, or targeted comment reply chain. Do not use it for document " + "contents; use dispatch_doc_read instead. Supply ref, issue, or project plus artifact. " + OWNER_REFERENCE,
13832
13863
  arguments: (z) => ({
13833
13864
  issue: z.string().describe(ISSUE_REFERENCE).optional(),
13834
- ref: z.string().describe("Optional dispatch:// issue reference.").optional()
13835
- })
13865
+ project: z.string().describe("Project key owning the document.").optional(),
13866
+ artifact: z.string().describe("Project document slug or id.").optional(),
13867
+ ref: z.string().describe("Optional dispatch:// issue or document reference.").optional()
13868
+ }),
13869
+ validation: documentOwnerValidation(true)
13836
13870
  },
13837
13871
  {
13838
13872
  name: "dispatch_search",
@@ -13960,10 +13994,15 @@ var handoffMessageSchema = object({
13960
13994
  });
13961
13995
  // ../contracts/src/subject.ts
13962
13996
  var AGENT_TOPIC_PREFIX = "notifications.agent.";
13963
- var DISPATCH_ISSUE_TOPIC_PREFIX = "notifications.dispatch.issue.";
13997
+ var DISPATCH_TOPIC_PREFIX = "notifications.dispatch.";
13998
+ var DISPATCH_ISSUE_TOPIC_PREFIX = `${DISPATCH_TOPIC_PREFIX}issue.`;
13999
+ var DISPATCH_DOCUMENT_TOPIC_PREFIX = `${DISPATCH_TOPIC_PREFIX}document.`;
13964
14000
  function dispatchIssueSubject(issueKey, type) {
13965
14001
  return `${DISPATCH_ISSUE_TOPIC_PREFIX}${issueKey}.${type}`;
13966
14002
  }
14003
+ function dispatchDocumentSubject(project, slug, type) {
14004
+ return `${DISPATCH_DOCUMENT_TOPIC_PREFIX}${project}.${slug}.${type}`;
14005
+ }
13967
14006
  function agentSubject(session) {
13968
14007
  return `${AGENT_TOPIC_PREFIX}${session}`;
13969
14008
  }
@@ -14482,6 +14521,25 @@ class DispatchClient {
14482
14521
  async listIssues(options = {}) {
14483
14522
  return this.#json("GET", ["api", "v1", "issues"], undefined, options);
14484
14523
  }
14524
+ async listProjectArtifacts(project, unlinked = false) {
14525
+ return this.#json("GET", ["api", "v1", "projects", project, "artifacts"], undefined, unlinked ? { unlinked: "true" } : undefined);
14526
+ }
14527
+ async projectArtifact(project, input) {
14528
+ const artifactPath = ["api", "v1", "projects", project, "artifacts"];
14529
+ if ("content" in input)
14530
+ return this.#json("POST", artifactPath, input);
14531
+ const form = new FormData;
14532
+ form.set("name", input.name);
14533
+ if (input.summary !== undefined)
14534
+ form.set("summary", input.summary);
14535
+ if (input.actor !== undefined)
14536
+ form.set("actor", JSON.stringify(input.actor));
14537
+ form.set("file", input.file, input.name);
14538
+ return this.#form("POST", artifactPath, form);
14539
+ }
14540
+ async getProjectArtifact(project, slug) {
14541
+ return this.#json("GET", ["api", "v1", "projects", project, "artifacts", slug]);
14542
+ }
14485
14543
  async search(query, options = {}) {
14486
14544
  return this.#json("GET", ["api", "v1", "search"], undefined, { q: query, ...options });
14487
14545
  }
@@ -14509,6 +14567,12 @@ class DispatchClient {
14509
14567
  async comment(issue, input) {
14510
14568
  return this.#json("POST", ["api", "v1", "issues", await this.#resolveIssue(issue), "comments"], input);
14511
14569
  }
14570
+ async getArtifactAsks(id, state) {
14571
+ return this.#json("GET", ["api", "v1", "artifacts", id, "asks"], undefined, state === undefined ? undefined : { state });
14572
+ }
14573
+ async artifactAsk(id, input) {
14574
+ return this.#json("POST", ["api", "v1", "artifacts", id, "asks"], input);
14575
+ }
14512
14576
  async suggest(issue, input) {
14513
14577
  const { replace_with, ...comment } = input;
14514
14578
  return this.#json("POST", ["api", "v1", "issues", await this.#resolveIssue(issue), "comments"], {
@@ -14516,6 +14580,19 @@ class DispatchClient {
14516
14580
  suggestion: { replace_with }
14517
14581
  });
14518
14582
  }
14583
+ async getArtifactComments(id) {
14584
+ return this.#json("GET", ["api", "v1", "artifacts", id, "comments"]);
14585
+ }
14586
+ async artifactComment(id, input) {
14587
+ return this.#json("POST", ["api", "v1", "artifacts", id, "comments"], input);
14588
+ }
14589
+ async artifactSuggest(id, input) {
14590
+ const { replace_with, ...comment } = input;
14591
+ return this.#json("POST", ["api", "v1", "artifacts", id, "comments"], {
14592
+ ...comment,
14593
+ suggestion: { replace_with }
14594
+ });
14595
+ }
14519
14596
  async message(issue, input) {
14520
14597
  return this.#json("POST", ["api", "v1", "issues", await this.#resolveIssue(issue), "messages"], input);
14521
14598
  }
@@ -14544,6 +14621,12 @@ class DispatchClient {
14544
14621
  async nameArtifactVersion(id, input) {
14545
14622
  return this.#json("POST", ["api", "v1", "artifacts", id, "versions"], input);
14546
14623
  }
14624
+ async getArtifactEvents(id, after = 0, limit = 200) {
14625
+ return this.#json("GET", ["api", "v1", "artifacts", id, "events"], undefined, { after, limit });
14626
+ }
14627
+ async getArtifactReferences(id) {
14628
+ return this.#json("GET", ["api", "v1", "artifacts", id, "references"]);
14629
+ }
14547
14630
  async getAsk(id) {
14548
14631
  return this.#json("GET", ["api", "v1", "asks", id]);
14549
14632
  }
@@ -14553,6 +14636,15 @@ class DispatchClient {
14553
14636
  async getComments(issue, artifact) {
14554
14637
  return this.#json("GET", ["api", "v1", "issues", await this.#resolveIssue(issue), "comments"], undefined, artifact ? { artifact } : undefined);
14555
14638
  }
14639
+ async getIssueReferences(issue) {
14640
+ return this.#json("GET", [
14641
+ "api",
14642
+ "v1",
14643
+ "issues",
14644
+ await this.#resolveIssue(issue),
14645
+ "references"
14646
+ ]);
14647
+ }
14556
14648
  async ensureIssue(issueReference, actor) {
14557
14649
  if (!issueReference.includes("#"))
14558
14650
  return issueReference;
@@ -14659,16 +14751,27 @@ class DispatchClient {
14659
14751
  }
14660
14752
 
14661
14753
  // ../envoy-client/src/dispatch-execute.ts
14662
- var nativeIssueKeyPattern = /^[A-Z][A-Z0-9]{1,9}-[0-9]+$/;
14663
- var externalIssueRefPattern = /^([^/\s]+)\/([^/\s#]+)#([1-9][0-9]*)$/;
14664
- var bareIssueNumberPattern = /^[1-9][0-9]*$/;
14665
- var issueFreeTools = new Set([
14666
- "dispatch_issue",
14667
- "dispatch_edit_ask",
14668
- "dispatch_resolve_ask",
14669
- "dispatch_search"
14670
- ]);
14671
- async function askResultDetails(client, ask) {
14754
+ function documentResultDetails(artifact) {
14755
+ return {
14756
+ project: artifact.project,
14757
+ artifact: artifact.id,
14758
+ document: `${artifact.project}/${artifact.slug}`,
14759
+ topic: dispatchDocumentSubject(artifact.project, artifact.slug, ">")
14760
+ };
14761
+ }
14762
+ function writeResultDetails(resolved, fields) {
14763
+ if (resolved.owner.kind === "project") {
14764
+ return { ...documentResultDetails(resolved.artifact), ...fields };
14765
+ }
14766
+ if (resolved.issue === undefined)
14767
+ throw new Error("issue document is missing its issue");
14768
+ return {
14769
+ issue: resolved.issue.key,
14770
+ topic: dispatchIssueSubject(resolved.issue.key, ">"),
14771
+ ...fields
14772
+ };
14773
+ }
14774
+ async function askResultDetails(client, ask, resolved) {
14672
14775
  if (ask.issue_key !== null) {
14673
14776
  return {
14674
14777
  issue: ask.issue_key,
@@ -14679,12 +14782,18 @@ async function askResultDetails(client, ask) {
14679
14782
  if (ask.artifact_id === undefined || ask.artifact_id === null) {
14680
14783
  throw new Error("document ask is missing its artifact ID");
14681
14784
  }
14682
- const artifact = await client.getArtifact(ask.artifact_id);
14683
- return {
14684
- topic: `notifications.dispatch.document.${artifact.project}.${artifact.slug}.>`,
14685
- ask: ask.id
14686
- };
14785
+ const artifact = resolved?.artifact ?? await client.getArtifact(ask.artifact_id);
14786
+ return { ...documentResultDetails(artifact), ask: ask.id };
14687
14787
  }
14788
+ var nativeIssueKeyPattern = /^[A-Z][A-Z0-9]{1,9}-[0-9]+$/;
14789
+ var externalIssueRefPattern = /^([^/\s]+)\/([^/\s#]+)#([1-9][0-9]*)$/;
14790
+ var bareIssueNumberPattern = /^[1-9][0-9]*$/;
14791
+ var issueFreeTools = {
14792
+ dispatch_issue: true,
14793
+ dispatch_edit_ask: true,
14794
+ dispatch_resolve_ask: true,
14795
+ dispatch_search: true
14796
+ };
14688
14797
  function canonicalExternalIssueRef(value) {
14689
14798
  const match = value.trim().match(externalIssueRefPattern);
14690
14799
  return match ? `${canonicalRepo(match[1] ?? "", match[2] ?? "")}#${match[3]}` : value;
@@ -14729,31 +14838,54 @@ function askUrgency(args) {
14729
14838
  return ASK_URGENCIES.find((urgency) => urgency === value);
14730
14839
  }
14731
14840
  function parseDispatchRef(ref) {
14732
- const match = ref.match(/^dispatch:\/\/([A-Z][A-Z0-9]{1,9}-[1-9][0-9]*)(?:\/(spec)|\/(log)|\/(children)|\/artifact\/([^/@]+)(?:@v(\d+))?|\/ask\/([^/]+)|\/comment\/([^/]+))?$/);
14733
- if (!match)
14841
+ const projectDocument = ref.match(/^dispatch:\/\/([A-Z][A-Z0-9]{1,9})\/artifact\/([^/@]+)(?:@v(\d+))?(?:\/(ask|comment)\/([^/]+))?$/);
14842
+ if (projectDocument) {
14843
+ const [, project, artifact, version, targetKind, targetID] = projectDocument;
14844
+ if (project === undefined || artifact === undefined || version !== undefined && Number(version) < 1) {
14845
+ return null;
14846
+ }
14847
+ if (targetKind === undefined) {
14848
+ return {
14849
+ owner: { kind: "project", project },
14850
+ kind: "artifact",
14851
+ id: artifact,
14852
+ ...version === undefined ? {} : { version: Number(version) }
14853
+ };
14854
+ }
14855
+ if (targetID === undefined || targetKind !== "ask" && targetKind !== "comment")
14856
+ return null;
14857
+ return {
14858
+ owner: { kind: "project", project },
14859
+ kind: targetKind,
14860
+ id: targetID
14861
+ };
14862
+ }
14863
+ const issueReference = ref.match(/^dispatch:\/\/([A-Z][A-Z0-9]{1,9}-[1-9][0-9]*)(?:\/(spec)|\/(log)|\/(children)|\/artifact\/([^/@]+)(?:@v(\d+))?|\/ask\/([^/]+)|\/comment\/([^/]+))?$/);
14864
+ if (!issueReference)
14734
14865
  return null;
14735
- const [, issue, spec, log, children, artifact, version, ask, comment] = match;
14866
+ const [, issue, spec, log, children, artifact, version, ask, comment] = issueReference;
14736
14867
  if (!issue || version !== undefined && Number(version) < 1)
14737
14868
  return null;
14869
+ const owner = { kind: "issue", issue };
14738
14870
  if (spec)
14739
- return { issue, kind: "spec", id: spec };
14871
+ return { owner, kind: "spec", id: spec };
14740
14872
  if (log)
14741
- return { issue, kind: "log", id: log };
14873
+ return { owner, kind: "log", id: log };
14742
14874
  if (children)
14743
- return { issue, kind: "children", id: children };
14875
+ return { owner, kind: "children", id: children };
14744
14876
  if (artifact) {
14745
14877
  return {
14746
- issue,
14878
+ owner,
14747
14879
  kind: "artifact",
14748
14880
  id: artifact,
14749
14881
  ...version === undefined ? {} : { version: Number(version) }
14750
14882
  };
14751
14883
  }
14752
14884
  if (ask)
14753
- return { issue, kind: "ask", id: ask };
14885
+ return { owner, kind: "ask", id: ask };
14754
14886
  if (comment)
14755
- return { issue, kind: "comment", id: comment };
14756
- return { issue, kind: "issue", id: issue };
14887
+ return { owner, kind: "comment", id: comment };
14888
+ return { owner, kind: "issue", id: issue };
14757
14889
  }
14758
14890
  function askId(args) {
14759
14891
  const ask = stringArg(args, "ask");
@@ -14771,32 +14903,68 @@ function toolSchema(tool) {
14771
14903
  throw new Error(`Unknown Dispatch tool: ${tool}`);
14772
14904
  return dispatchToolSchema(spec, zodSchemaApi(exports_external), { strict: true });
14773
14905
  }
14774
- async function resolveIssueArguments(tool, args, cwd, env, exec) {
14775
- if (issueFreeTools.has(tool))
14776
- return { args, ref: null };
14906
+ async function resolveOwnerArguments(tool, args, cwd, env, exec) {
14907
+ if (issueFreeTools[tool] === true)
14908
+ return { args, ref: null, owner: null };
14777
14909
  const refArgument = args.ref;
14778
14910
  const ref = typeof refArgument === "string" ? parseDispatchRef(refArgument) ?? (() => {
14779
- throw new Error("ref must be a valid dispatch:// reference");
14911
+ 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>");
14780
14912
  })() : null;
14781
14913
  const issueArgument = args.issue;
14914
+ const projectArgument = args.project;
14782
14915
  const artifactArgument = args.artifact;
14783
14916
  const versionArgument = args.version;
14917
+ if (issueArgument !== undefined && projectArgument !== undefined) {
14918
+ throw new Error("exactly one of issue and project is required");
14919
+ }
14920
+ if (typeof projectArgument === "string") {
14921
+ if (!/^[A-Z][A-Z0-9]{1,9}$/.test(projectArgument)) {
14922
+ throw new Error("project must be a project key such as CORE");
14923
+ }
14924
+ if (ref?.owner.kind === "project" && (ref.owner.project !== projectArgument || artifactArgument !== undefined && artifactArgument !== ref.id)) {
14925
+ throw new Error("project and ref must name the same document");
14926
+ }
14927
+ return {
14928
+ args: {
14929
+ ...args,
14930
+ ...artifactArgument === undefined && ref?.owner.kind === "project" ? { artifact: ref.id } : {},
14931
+ ...versionArgument === undefined && ref?.version !== undefined ? { version: ref.version } : {}
14932
+ },
14933
+ ref,
14934
+ owner: { kind: "project", project: projectArgument }
14935
+ };
14936
+ }
14937
+ if (ref?.owner.kind === "project") {
14938
+ return {
14939
+ args: {
14940
+ ...args,
14941
+ project: ref.owner.project,
14942
+ ...artifactArgument === undefined ? { artifact: ref.id } : {},
14943
+ ...versionArgument === undefined && ref.version !== undefined ? { version: ref.version } : {}
14944
+ },
14945
+ ref,
14946
+ owner: ref.owner
14947
+ };
14948
+ }
14784
14949
  if (issueArgument !== undefined || ref !== null) {
14950
+ const issue = typeof issueArgument === "string" ? canonicalExternalIssueRef(issueArgument) : ref?.owner.kind === "issue" ? ref.owner.issue : undefined;
14785
14951
  return {
14786
14952
  args: {
14787
14953
  ...args,
14788
- ...typeof issueArgument === "string" ? { issue: canonicalExternalIssueRef(issueArgument) } : issueArgument === undefined && ref?.issue !== undefined ? { issue: ref.issue } : {},
14954
+ ...issue === undefined ? {} : { issue },
14789
14955
  ...artifactArgument === undefined && (ref?.kind === "spec" || ref?.kind === "artifact") ? { artifact: ref.id } : {},
14790
14956
  ...versionArgument === undefined && ref?.version !== undefined ? { version: ref.version } : {}
14791
14957
  },
14792
- ref
14958
+ ref,
14959
+ owner: issue === undefined ? null : { kind: "issue", issue }
14793
14960
  };
14794
14961
  }
14795
14962
  const legionIssue = env.LEGION_ISSUE;
14796
14963
  if (!legionIssue)
14797
14964
  throw new Error("issue is required; supply issue or set LEGION_ISSUE");
14798
14965
  if (nativeIssueKeyPattern.test(legionIssue) || externalIssueRefPattern.test(legionIssue)) {
14799
- return { args: { ...args, issue: canonicalExternalIssueRef(legionIssue) }, ref: null };
14966
+ const issue = canonicalExternalIssueRef(legionIssue);
14967
+ return { args: { ...args, issue }, ref: null, owner: { kind: "issue", issue } };
14800
14968
  }
14801
14969
  if (!bareIssueNumberPattern.test(legionIssue)) {
14802
14970
  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");
@@ -14804,15 +14972,35 @@ async function resolveIssueArguments(tool, args, cwd, env, exec) {
14804
14972
  const repo = await resolveCwdRepo(cwd, exec);
14805
14973
  if (!repo)
14806
14974
  throw new Error("issue is required; LEGION_ISSUE needs a GitHub repository in cwd");
14807
- return { args: { ...args, issue: `${repo}#${legionIssue}` }, ref: null };
14975
+ const issue = `${repo}#${legionIssue}`;
14976
+ return { args: { ...args, issue }, ref: null, owner: { kind: "issue", issue } };
14808
14977
  }
14809
- async function resolveArtifact(client, issueReference, artifactReference) {
14810
- const issue = await client.getIssue(issueReference);
14811
- 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);
14978
+ async function resolveArtifact(client, owner, artifactReference) {
14979
+ if (owner.kind === "project") {
14980
+ if (artifactReference === undefined) {
14981
+ throw new Error("artifact is required for a project document");
14982
+ }
14983
+ try {
14984
+ return {
14985
+ owner,
14986
+ artifact: await client.getProjectArtifact(owner.project, artifactReference)
14987
+ };
14988
+ } catch (error) {
14989
+ if (!(error instanceof DispatchServiceError) || error.status !== 404)
14990
+ throw error;
14991
+ const artifacts = await client.listProjectArtifacts(owner.project);
14992
+ const artifact = artifacts.find((candidate) => candidate.name === artifactReference);
14993
+ if (!artifact)
14994
+ throw error;
14995
+ return { owner, artifact };
14996
+ }
14997
+ }
14998
+ const issue = await client.getIssue(owner.issue);
14999
+ 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);
14812
15000
  if (!artifact) {
14813
15001
  throw new Error(`artifact ${artifactReference ?? "spec"} was not found on issue ${issue.key}`);
14814
15002
  }
14815
- return { issue, artifact };
15003
+ return { owner, issue, artifact };
14816
15004
  }
14817
15005
  function anchor(artifact, args) {
14818
15006
  const quote = optionalString(args, "quote");
@@ -14832,7 +15020,7 @@ function toolActor(origin, input) {
14832
15020
  }
14833
15021
  };
14834
15022
  }
14835
- function issueSummary(issue, events) {
15023
+ function issueSummary(issue, events, references) {
14836
15024
  const asks = issue.open_asks;
14837
15025
  return [
14838
15026
  `Title: ${issue.title}`,
@@ -14841,6 +15029,9 @@ function issueSummary(issue, events) {
14841
15029
  `Route: ${issue.route ?? "none"}`,
14842
15030
  "Open asks:",
14843
15031
  ...asks.length === 0 ? ["- none"] : asks.map((ask) => `- ${ask.id}: ${ask.question}`),
15032
+ "References:",
15033
+ ...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}`),
15034
+ ...typeof references === "string" || !references.truncated ? [] : ["- more references beyond 8 hops"],
14844
15035
  "Events:",
14845
15036
  ...events.length === 0 ? ["- none"] : events.map((event) => `- #${event.seq} ${event.type} \xB7 ${event.actor.kind} ${event.actor.id} \xB7 ${event.created_at}`)
14846
15037
  ].join(`
@@ -14905,10 +15096,11 @@ function commentSummary({ comment, replies }) {
14905
15096
  `);
14906
15097
  }
14907
15098
  async function openArtifactMarks(client, resolved) {
14908
- const marks = resolved.issue.open_asks.filter((ask) => ask.state === "open" && ask.anchor?.artifact_id === resolved.artifact.id).map((ask) => `ask ${ask.id}`);
15099
+ const asks = resolved.owner.kind === "project" ? await client.getArtifactAsks(resolved.artifact.id) : resolved.issue?.open_asks ?? [];
15100
+ const marks = asks.filter((ask) => ask.state === "open" && ask.anchor?.artifact_id === resolved.artifact.id).map((ask) => `ask ${ask.id}`);
14909
15101
  let comments;
14910
15102
  try {
14911
- comments = await client.getComments(resolved.issue.key, resolved.artifact.id);
15103
+ comments = resolved.owner.kind === "project" ? await client.getArtifactComments(resolved.artifact.id) : await client.getComments(resolved.issue?.key ?? "", resolved.artifact.id);
14912
15104
  } catch (error) {
14913
15105
  if (error instanceof DispatchServiceError && error.status === 404)
14914
15106
  return marks;
@@ -14927,15 +15119,23 @@ async function executeDispatchTool(input) {
14927
15119
  }
14928
15120
  const env = input.env ?? process.env;
14929
15121
  const exec = input.exec ?? defaultExec;
14930
- const issueArguments = await resolveIssueArguments(input.tool, input.args, input.cwd, env, exec);
14931
- const args = toolSchema(input.tool).parse(issueArguments.args);
15122
+ const ownerArguments = await resolveOwnerArguments(input.tool, input.args, input.cwd, env, exec);
15123
+ const args = toolSchema(input.tool).parse(ownerArguments.args);
14932
15124
  const actor = toolActor(await resolveOrigin(env, exec, input.cwd), input);
14933
15125
  const client = new DispatchClient(configUrl, configToken, input.fetchImpl);
14934
- const issueKey = issueFreeTools.has(input.tool) ? null : await ensureIssue(client, stringArg(args, "issue"), actor);
15126
+ const owner = ownerArguments.owner?.kind === "issue" ? {
15127
+ kind: "issue",
15128
+ issue: await ensureIssue(client, ownerArguments.owner.issue, actor)
15129
+ } : ownerArguments.owner;
14935
15130
  const issue = () => {
14936
- if (issueKey === null)
15131
+ if (owner?.kind !== "issue")
14937
15132
  throw new Error("issue is required");
14938
- return issueKey;
15133
+ return owner.issue;
15134
+ };
15135
+ const documentOwner = () => {
15136
+ if (owner === null)
15137
+ throw new Error("issue or project is required");
15138
+ return owner;
14939
15139
  };
14940
15140
  switch (input.tool) {
14941
15141
  case "dispatch_issue": {
@@ -15015,22 +15215,25 @@ async function executeDispatchTool(input) {
15015
15215
  }
15016
15216
  case "dispatch_ask": {
15017
15217
  const anchorArgs = asObject(args.anchor);
15018
- const resolved = anchorArgs ? await resolveArtifact(client, issue(), stringArg(anchorArgs, "artifact")) : undefined;
15218
+ const owner = documentOwner();
15219
+ const artifactReference = optionalString(args, "artifact") ?? (anchorArgs === null ? undefined : optionalString(anchorArgs, "artifact"));
15220
+ const resolved = owner.kind === "project" || artifactReference === undefined ? owner.kind === "project" ? await resolveArtifact(client, owner, artifactReference) : undefined : await resolveArtifact(client, owner, artifactReference);
15019
15221
  const options = args.options;
15020
15222
  const multiple = optionalBoolean(args, "multiple");
15021
15223
  const urgency = askUrgency(args);
15022
15224
  const anchored = anchorArgs && resolved ? anchor(resolved.artifact, anchorArgs) : undefined;
15023
- const ask = await client.ask(issue(), {
15225
+ const askInput = {
15024
15226
  question: stringArg(args, "question"),
15025
15227
  ...Array.isArray(options) ? { options } : {},
15026
15228
  ...multiple === undefined ? {} : { multiple },
15027
15229
  ...urgency === undefined ? {} : { urgency },
15028
15230
  ...anchored === undefined ? {} : { anchor: anchored },
15029
15231
  actor
15030
- });
15232
+ };
15233
+ const ask = resolved?.owner.kind === "project" ? await client.artifactAsk(resolved.artifact.id, askInput) : await client.ask(issue(), askInput);
15031
15234
  return {
15032
15235
  text: `Opened ask ${ask.id}: ${ask.question}`,
15033
- details: await askResultDetails(client, ask)
15236
+ details: await askResultDetails(client, ask, resolved)
15034
15237
  };
15035
15238
  }
15036
15239
  case "dispatch_edit_ask": {
@@ -15055,48 +15258,47 @@ async function executeDispatchTool(input) {
15055
15258
  if (optionalString(args, "quote") !== undefined && artifactReference === undefined) {
15056
15259
  throw new Error("artifact is required when quote is supplied");
15057
15260
  }
15058
- const resolved = artifactReference ? await resolveArtifact(client, issue(), artifactReference) : undefined;
15261
+ const owner = documentOwner();
15262
+ const resolved = owner.kind === "project" || artifactReference === undefined ? owner.kind === "project" ? await resolveArtifact(client, owner, artifactReference) : undefined : await resolveArtifact(client, owner, artifactReference);
15059
15263
  const anchored = resolved ? anchor(resolved.artifact, args) : undefined;
15060
15264
  const replyTo = optionalString(args, "reply_to");
15061
15265
  const replyToAsk = optionalString(args, "reply_to_ask");
15062
15266
  if (replyTo !== undefined && replyToAsk !== undefined) {
15063
15267
  throw new Error("reply_to and reply_to_ask cannot both be set");
15064
15268
  }
15065
- const comment = await client.comment(issue(), {
15269
+ const commentInput = {
15066
15270
  body: stringArg(args, "body"),
15067
15271
  ...anchored === undefined ? {} : { anchor: anchored },
15068
15272
  ...replyTo === undefined ? {} : { reply_to: replyTo },
15069
15273
  ...replyToAsk === undefined ? {} : { ask_id: replyToAsk },
15070
15274
  actor
15071
- });
15275
+ };
15276
+ const comment = resolved?.owner.kind === "project" ? await client.artifactComment(resolved.artifact.id, commentInput) : await client.comment(issue(), commentInput);
15072
15277
  return {
15073
15278
  text: `Posted comment ${comment.id}`,
15074
- details: {
15279
+ details: resolved === undefined ? {
15075
15280
  issue: comment.issue_key,
15076
- topic: dispatchIssueSubject(comment.issue_key, ">"),
15281
+ topic: dispatchIssueSubject(issue(), ">"),
15077
15282
  comment: comment.id
15078
- }
15283
+ } : writeResultDetails(resolved, { comment: comment.id })
15079
15284
  };
15080
15285
  }
15081
15286
  case "dispatch_suggest": {
15082
- const resolved = await resolveArtifact(client, issue(), stringArg(args, "artifact"));
15287
+ const resolved = await resolveArtifact(client, documentOwner(), stringArg(args, "artifact"));
15083
15288
  const anchored = anchor(resolved.artifact, args);
15084
15289
  if (anchored === undefined)
15085
15290
  throw new Error("quote is required");
15086
15291
  const body = optionalString(args, "body");
15087
- const comment = await client.suggest(issue(), {
15292
+ const suggestionInput = {
15088
15293
  ...body === undefined ? {} : { body },
15089
15294
  anchor: anchored,
15090
15295
  replace_with: stringArg(args, "replace_with"),
15091
15296
  actor
15092
- });
15297
+ };
15298
+ const comment = resolved.owner.kind === "project" ? await client.artifactSuggest(resolved.artifact.id, suggestionInput) : await client.suggest(issue(), suggestionInput);
15093
15299
  return {
15094
15300
  text: `Posted suggestion ${comment.id}`,
15095
- details: {
15096
- issue: comment.issue_key,
15097
- topic: dispatchIssueSubject(comment.issue_key, ">"),
15098
- comment: comment.id
15099
- }
15301
+ details: writeResultDetails(resolved, { comment: comment.id })
15100
15302
  };
15101
15303
  }
15102
15304
  case "dispatch_message": {
@@ -15111,7 +15313,7 @@ async function executeDispatchTool(input) {
15111
15313
  };
15112
15314
  }
15113
15315
  case "dispatch_doc_edit": {
15114
- const resolved = await resolveArtifact(client, issue(), stringArg(args, "artifact"));
15316
+ const resolved = await resolveArtifact(client, documentOwner(), stringArg(args, "artifact"));
15115
15317
  const ops = args.ops;
15116
15318
  const summary = optionalString(args, "summary");
15117
15319
  const edited = await client.docEdit(resolved.artifact.id, {
@@ -15121,32 +15323,33 @@ async function executeDispatchTool(input) {
15121
15323
  });
15122
15324
  return {
15123
15325
  text: edited.version === null ? `Applied ${edited.applied} ops (no new version)` : `Applied ${edited.applied} ops (version ${edited.version.number})`,
15124
- details: {
15125
- issue: resolved.issue.key,
15126
- topic: dispatchIssueSubject(resolved.issue.key, ">"),
15326
+ details: writeResultDetails(resolved, {
15127
15327
  applied: edited.applied,
15128
15328
  ...edited.version === null ? {} : { version: edited.version.number }
15129
- }
15329
+ })
15130
15330
  };
15131
15331
  }
15132
15332
  case "dispatch_doc_read": {
15133
- const artifactReference = optionalString(args, "artifact") ?? (issueArguments.ref?.kind === "spec" || issueArguments.ref?.kind === "artifact" ? issueArguments.ref.id : undefined);
15134
- const resolved = await resolveArtifact(client, issue(), artifactReference);
15135
- const version = optionalNumber(args, "version") ?? issueArguments.ref?.version;
15333
+ const artifactReference = optionalString(args, "artifact") ?? (ownerArguments.ref?.kind === "spec" || ownerArguments.ref?.kind === "artifact" ? ownerArguments.ref.id : undefined);
15334
+ const resolved = await resolveArtifact(client, documentOwner(), artifactReference);
15335
+ const version = optionalNumber(args, "version") ?? ownerArguments.ref?.version;
15136
15336
  const document = await client.docRead(resolved.artifact.id, version);
15137
15337
  const marks = await openArtifactMarks(client, resolved);
15138
15338
  return {
15139
15339
  text: marks.length === 0 ? document.markdown : `${document.markdown}
15140
15340
 
15141
15341
  Open anchored asks/comments: ${marks.join(", ")}`,
15142
- details: { issue: resolved.issue.key }
15342
+ details: resolved.owner.kind === "project" ? {
15343
+ project: resolved.artifact.project,
15344
+ document: `${resolved.artifact.project}/${resolved.artifact.slug}`
15345
+ } : { issue: resolved.issue?.key }
15143
15346
  };
15144
15347
  }
15145
15348
  case "dispatch_artifact": {
15146
15349
  const summary = optionalString(args, "summary");
15147
15350
  const name = stringArg(args, "name");
15148
15351
  const content = optionalString(args, "content");
15149
- const result = await client.artifact(issue(), content === undefined ? {
15352
+ const artifactInput = content === undefined ? {
15150
15353
  name,
15151
15354
  file: Bun.file(resolvePath(input.cwd, stringArg(args, "path"))),
15152
15355
  ...summary === undefined ? {} : { summary },
@@ -15156,47 +15359,74 @@ Open anchored asks/comments: ${marks.join(", ")}`,
15156
15359
  content,
15157
15360
  ...summary === undefined ? {} : { summary },
15158
15361
  actor
15159
- });
15362
+ };
15363
+ const artifactOwner = documentOwner();
15364
+ const result = artifactOwner.kind === "project" ? await client.projectArtifact(artifactOwner.project, artifactInput) : await client.artifact(issue(), artifactInput);
15365
+ const artifactRef = artifactOwner.kind === "project" ? `dispatch://${artifactOwner.project}/artifact/${result.artifact.slug}` : `dispatch://${issue()}/artifact/${result.artifact.slug}`;
15160
15366
  return {
15161
- text: `Uploaded ${result.artifact.name} as version ${result.version.number}`,
15162
- details: {
15163
- issue: result.artifact.issue_key,
15164
- topic: dispatchIssueSubject(result.artifact.issue_key, ">"),
15367
+ text: `Uploaded ${result.artifact.name} as version ${result.version.number} (artifact slug ${result.artifact.slug}; ${artifactRef})`,
15368
+ details: artifactOwner.kind === "project" ? {
15369
+ ...documentResultDetails(result.artifact),
15370
+ version: result.version.number
15371
+ } : {
15372
+ issue: issue(),
15373
+ topic: dispatchIssueSubject(issue(), ">"),
15165
15374
  artifact: result.artifact.id,
15166
15375
  version: result.version.number
15167
15376
  }
15168
15377
  };
15169
15378
  }
15170
15379
  case "dispatch_read": {
15171
- if (issueArguments.ref?.kind === "ask") {
15172
- const askRead = await client.getAsk(issueArguments.ref.id);
15380
+ if (ownerArguments.ref?.kind === "ask") {
15381
+ const askRead = await client.getAsk(ownerArguments.ref.id);
15173
15382
  return {
15174
15383
  text: askSummary(askRead),
15175
- details: { issue: issueArguments.ref.issue }
15384
+ details: ownerArguments.ref.owner.kind === "project" ? { project: ownerArguments.ref.owner.project } : { issue: ownerArguments.ref.owner.issue }
15176
15385
  };
15177
15386
  }
15178
- if (issueArguments.ref?.kind === "comment") {
15179
- const comment = await client.getComment(issueArguments.ref.id);
15387
+ if (ownerArguments.ref?.kind === "comment") {
15388
+ const comment = await client.getComment(ownerArguments.ref.id);
15180
15389
  return {
15181
15390
  text: commentSummary(comment),
15182
- details: { issue: comment.comment.issue_key }
15391
+ details: ownerArguments.ref.owner.kind === "project" ? { project: ownerArguments.ref.owner.project } : { issue: comment.comment.issue_key }
15392
+ };
15393
+ }
15394
+ if (documentOwner().kind === "project") {
15395
+ const resolved = await resolveArtifact(client, documentOwner(), stringArg(args, "artifact"));
15396
+ return {
15397
+ text: [
15398
+ `Document: ${resolved.artifact.project} / ${resolved.artifact.name}`,
15399
+ `Reference: dispatch://${resolved.artifact.project}/artifact/${resolved.artifact.slug}`,
15400
+ `Versions: ${resolved.artifact.versions.length}`
15401
+ ].join(`
15402
+ `),
15403
+ details: {
15404
+ project: resolved.artifact.project,
15405
+ document: `${resolved.artifact.project}/${resolved.artifact.slug}`
15406
+ }
15183
15407
  };
15184
15408
  }
15185
15409
  const read = await client.read(issue());
15186
- if (issueArguments.ref?.kind === "log") {
15410
+ if (ownerArguments.ref?.kind === "log") {
15187
15411
  return {
15188
15412
  text: logSummary(read.issue, read.events),
15189
15413
  details: { issue: read.issue.key }
15190
15414
  };
15191
15415
  }
15192
- if (issueArguments.ref?.kind === "children") {
15416
+ if (ownerArguments.ref?.kind === "children") {
15193
15417
  return {
15194
15418
  text: childrenSummary(read.issue),
15195
15419
  details: { issue: read.issue.key }
15196
15420
  };
15197
15421
  }
15422
+ let references;
15423
+ try {
15424
+ references = await client.getIssueReferences(read.issue.key);
15425
+ } catch (error) {
15426
+ references = error instanceof DispatchServiceError && error.status === 404 ? "unavailable" : `unavailable: ${error instanceof Error ? error.message : String(error)}`;
15427
+ }
15198
15428
  return {
15199
- text: issueSummary(read.issue, read.events),
15429
+ text: issueSummary(read.issue, read.events, references),
15200
15430
  details: { issue: read.issue.key }
15201
15431
  };
15202
15432
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/opencode-legion-envoy",
3
- "version": "0.40.0",
3
+ "version": "0.41.1",
4
4
  "type": "module",
5
5
  "main": "dist/src/server.js",
6
6
  "exports": {
@@ -5,8 +5,8 @@ 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 living spec, asks, comments, and artifacts. The transcript is your
9
- 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. The
9
+ transcript is your scratch pad. Anything meant for a human goes through a `dispatch_*` tool.
10
10
 
11
11
  The server enforces high signal: an ask question is at most 800 characters with at most eight
12
12
  options; comment and message bodies are at most 2,000 characters; an artifact is at most 25 MiB.
@@ -44,13 +44,16 @@ transcript of your thinking. Use exactly these document headings in this order.
44
44
  - [ ] The spec covers one implementation plan's worth of work.
45
45
  - [ ] Every requirement has exactly one reading.
46
46
 
47
- ## Your issue
47
+ ## Your owner
48
48
 
49
- Every session works on an issue. Legion pre-fills `issue` from `LEGION_ISSUE`: use a native issue key
50
- such as `LEGION-3`, an external `owner/repo#n` reference, or a bare positive number (resolved
51
- against the cwd repository). Otherwise pass the issue to every issue-scoped tool as its native key
52
- or an external `owner/repo#n` reference. On first use, an external reference creates its native issue in the
53
- project configured for that repository in Dispatch Settings, then falls back to `DISPATCH_DEFAULT_PROJECT`.
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`.
54
57
 
55
58
  Architects create newly tracked child work with:
56
59
  ```ts
@@ -78,7 +81,9 @@ call with `force: true` when it is genuinely new work.
78
81
  Open a decision with:
79
82
  ```ts
80
83
  dispatch_ask({
81
- issue,
84
+ issue?,
85
+ project?,
86
+ artifact?,
82
87
  question,
83
88
  options?: { label, description? }[],
84
89
  multiple?,
@@ -86,7 +91,8 @@ dispatch_ask({
86
91
  anchor?: { artifact, quote, occurrence? },
87
92
  })
88
93
  ```
89
- It returns `details` `{ issue, topic, ask }`. Options are buttons: never enumerate choices in
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
90
96
  prose. Put the recommendation in `question`, and put each selectable choice in `options`.
91
97
  Anchor a document question with `anchor: { artifact, quote, occurrence? }`; `occurrence` is
92
98
  zero-based and selects a repeated quote. The server writes the resulting mark. The HTTP API also
@@ -132,16 +138,17 @@ Write and update the issue specification according to [Writing a spec](#writing-
132
138
  Read the current document before changing it:
133
139
 
134
140
  ```ts
135
- dispatch_doc_read({ issue?, artifact?, version?, ref? })
141
+ dispatch_doc_read({ issue?, project?, artifact?, version?, ref? })
136
142
  ```
137
- It returns live or versioned markdown with open marks and `details` `{ issue }`; omit `artifact`
138
- with `issue` to read the issue specification. Then write narrative with:
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:
139
146
 
140
147
  ```ts
141
- dispatch_doc_edit({ issue, artifact, ops, summary? })
148
+ dispatch_doc_edit({ issue?, project?, artifact, ops, summary? })
142
149
  ```
143
- It returns `details` `{ issue, topic, applied, version? }`. `ops` is an array of this exact
144
- `EditOp` shape:
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:
145
152
 
146
153
  ```ts
147
154
  type EditOp = {
@@ -156,38 +163,48 @@ type EditOp = {
156
163
  ```
157
164
 
158
165
  Target `replace` and `delete` by the document's plain text: inline-code and link text match
159
- without Markdown syntax, and a table-cell anchor is its cell text. `replace` requires `find` and
160
- `with`; `delete` requires `find`; `insert` requires `markdown` and exactly one of `after` or
161
- `before`. An insert anchor is a quote, `"start"`, `"end"`, or `"heading:Title"`. Every insert
162
- creates a sibling block before or after the quote or heading's enclosing document block; `"start"`
163
- and `"end"` select the document edges. At a table-cell quote, pipe-table body-row fragments extend
164
- that table before or after the matched row; omit table header and delimiter rows, and do not exceed
165
- the table width. Use `replace` for inline continuation. Use zero-based `occurrence` for a repeated
166
- target. When a decision lands, pass `summary` to name the resulting version. Never paste progress
167
- into a message.
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.
169
+
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.
174
+
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.
168
182
 
169
183
  ## Comments and suggestions
170
184
 
171
185
  Add feedback with:
172
186
 
173
187
  ```ts
174
- dispatch_comment({ issue, artifact?, quote?, occurrence?, body, reply_to?, reply_to_ask? })
188
+ dispatch_comment({ issue?, project?, artifact?, quote?, occurrence?, body, reply_to?, reply_to_ask? })
175
189
  ```
176
190
 
177
- It returns `details` `{ issue, topic, comment }`. `quote` requires `artifact`; omit both for a
178
- floating issue comment. A reply (`reply_to`/`reply_to_ask`) takes no `quote`; it belongs to its
179
- parent's anchor. Use `reply_to` to continue a comment thread at its root; a reply to a resolved
180
- thread reopens it. Use `reply_to_ask` to reply directly under a question asked with
181
- `dispatch_ask`. The two are mutually exclusive. Comments are edited only by their author from the
182
- dashboard.
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> })`.
183
199
 
184
200
  Propose an exact replacement instead of describing it:
185
201
 
186
202
  ```ts
187
- dispatch_suggest({ issue, artifact, quote, replace_with, body?, occurrence? })
203
+ dispatch_suggest({ issue?, project?, artifact, quote, replace_with, body?, occurrence? })
188
204
  ```
189
205
 
190
- It returns `details` `{ issue, topic, comment }`. A human accepts or rejects a suggestion. On
206
+ It returns issue or project-document owner details plus `comment` and its write `topic`. A
207
+ human accepts or rejects a suggestion. On
191
208
  `TARGET_AMBIGUOUS`, add zero-based `occurrence`. On `TARGET_NOT_FOUND`, re-read the document
192
209
  before retrying. `INVALID_ANCHOR` requires exactly one nonempty anchor `quote` or `mark_id`;
193
210
  `ANCHOR_MISSING` means a browser mark was not observed in the live tree, and
@@ -201,19 +218,21 @@ reject an invalid actor or route.
201
218
  Attach an image, diagram, or local file with:
202
219
 
203
220
  ```ts
204
- dispatch_artifact({ issue, name, path, summary? })
221
+ dispatch_artifact({ issue?, project?, name, path, summary? })
205
222
  ```
206
223
 
207
224
  Or, when the text is already in the call, post a Markdown document directly:
208
225
 
209
226
  ```ts
210
- dispatch_artifact({ issue, name: "spec.md", content: "# Design\n..." })
227
+ dispatch_artifact({ issue?, project?, name: "spec.md", content: "# Design\n..." })
211
228
  ```
212
229
 
213
- Exactly one of `path` and `content` is required. The inline form sends JSON with
214
- `Content-Type: application/json`. It returns `details` `{ issue, topic, artifact, version }`.
215
- Uploading the same `name` creates its next version. Use `content` when the text is already in
216
- the call.
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.
217
236
 
218
237
  ## Messages
219
238
 
@@ -228,23 +247,20 @@ message does not wake anyone. Do not use it for status, a decision, or document
228
247
 
229
248
  ## What comes back
230
249
 
231
- A write result's `details.topic` subscribes the host to the issue. Events render as:
232
-
233
- ```text
234
- dispatch <KEY> · <type> · by <actor>
235
- ```
236
-
237
- The issue topic carries every Dispatch event; `notify` only controls agent wake and routed delivery.
238
- After a restart, catch up with:
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:
239
254
 
240
255
  ```ts
241
- dispatch_read({ issue?, ref? })
256
+ dispatch_read({ issue?, project?, artifact?, ref? })
242
257
  ```
243
258
 
244
- With an issue ref, it returns the issue summary, open asks, and recent events with `details`
245
- `{ issue }`. With an ask ref, it returns that ask's question, options, state, answer, and its
246
- reply thread. With a comment ref, it returns that comment and its quoted reply chain. Use
247
- `dispatch_doc_read` for document contents.
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.
248
264
 
249
265
  ## References
250
266
 
@@ -256,6 +272,9 @@ dispatch://KEY/spec
256
272
  dispatch://KEY/artifact/<slug>[@vN]
257
273
  dispatch://KEY/ask/<id>
258
274
  dispatch://KEY/comment/<id>
275
+ dispatch://PROJECT/artifact/<slug>[@vN]
276
+ dispatch://PROJECT/artifact/<slug>/ask/<id>
277
+ dispatch://PROJECT/artifact/<slug>/comment/<id>
259
278
  ```
260
279
 
261
280
  ## Before / after
@@ -313,8 +332,8 @@ dispatch_doc_edit({
313
332
  ops: [
314
333
  {
315
334
  op: "replace",
316
- find: "## Delivery\n\nRelease pending.",
317
- with: "## Delivery\n\nRelease merged and ready for deployment.",
335
+ find: "Release pending.",
336
+ with: "Release merged and ready for deployment.",
318
337
  },
319
338
  ],
320
339
  summary: "Recorded merged release",