@sjawhar/opencode-legion-envoy 0.40.0 → 0.41.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.
@@ -13673,6 +13673,18 @@ function dispatchToolSchema(spec, z, opts) {
13673
13673
  return spec.validation === undefined ? z.object(shape, opts) : z.refineObject(shape, spec.validation.check, spec.validation.message, opts);
13674
13674
  }
13675
13675
  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).";
13676
+ 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.";
13677
+ function documentOwnerValidation(requireArtifact) {
13678
+ return {
13679
+ check: (value) => {
13680
+ const input = value;
13681
+ const hasIssue = typeof input.issue === "string";
13682
+ const hasProject = typeof input.project === "string";
13683
+ return (hasIssue !== hasProject || !hasIssue && !hasProject && typeof input.ref === "string") && (!hasProject || !requireArtifact || typeof input.artifact === "string");
13684
+ },
13685
+ message: "Exactly one of issue and project is required; with project, artifact names the document."
13686
+ };
13687
+ }
13676
13688
  var SPEC_SECTIONS = [
13677
13689
  "Decisions needed",
13678
13690
  "Acceptance",
@@ -13700,9 +13712,11 @@ var dispatchToolSpecs = [
13700
13712
  },
13701
13713
  {
13702
13714
  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}`,
13715
+ 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
13716
  arguments: (z) => ({
13705
- issue: z.string().describe(ISSUE_REFERENCE),
13717
+ issue: z.string().describe(ISSUE_REFERENCE).optional(),
13718
+ project: z.string().describe("Project key owning the document.").optional(),
13719
+ artifact: z.string().describe("Project document slug or id.").optional(),
13706
13720
  question: z.string({ max: 800 }).describe("Decision question, at most 800 characters."),
13707
13721
  options: z.array(z.object({
13708
13722
  label: z.string().describe("Selectable option label."),
@@ -13715,7 +13729,8 @@ var dispatchToolSpecs = [
13715
13729
  quote: z.string().describe("Exact text the decision concerns."),
13716
13730
  occurrence: z.number({ int: true, min: 0 }).describe("Zero-based occurrence of the quote.").optional()
13717
13731
  }).describe("Optional document location for the question.").optional()
13718
- })
13732
+ }),
13733
+ validation: documentOwnerValidation(true)
13719
13734
  },
13720
13735
  {
13721
13736
  name: "dispatch_edit_ask",
@@ -13749,28 +13764,32 @@ var dispatchToolSpecs = [
13749
13764
  },
13750
13765
  {
13751
13766
  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}`,
13767
+ 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
13768
  arguments: (z) => ({
13754
- issue: z.string().describe(ISSUE_REFERENCE),
13769
+ issue: z.string().describe(ISSUE_REFERENCE).optional(),
13770
+ project: z.string().describe("Project key owning the document.").optional(),
13755
13771
  artifact: z.string().describe("Artifact slug or id required when quote is given.").optional(),
13756
13772
  quote: z.string().describe("Optional exact quoted document text.").optional(),
13757
13773
  occurrence: z.number({ int: true, min: 0 }).describe("Optional zero-based occurrence of quote.").optional(),
13758
13774
  body: z.string({ max: 2000 }).describe("Review comment, at most 2,000 characters."),
13759
13775
  reply_to: z.string().describe("Optional comment id to reply to.").optional(),
13760
13776
  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
- })
13777
+ }),
13778
+ validation: documentOwnerValidation(true)
13762
13779
  },
13763
13780
  {
13764
13781
  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}`,
13782
+ 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
13783
  arguments: (z) => ({
13767
- issue: z.string().describe(ISSUE_REFERENCE),
13784
+ issue: z.string().describe(ISSUE_REFERENCE).optional(),
13785
+ project: z.string().describe("Project key owning the document.").optional(),
13768
13786
  artifact: z.string().describe("Artifact slug or id containing the quoted text."),
13769
13787
  quote: z.string().describe("Exact document text to replace."),
13770
13788
  replace_with: z.string().describe("Replacement text."),
13771
13789
  body: z.string({ max: 2000 }).describe("Optional rationale, at most 2,000 characters.").optional(),
13772
13790
  occurrence: z.number({ int: true, min: 0 }).describe("Optional zero-based occurrence of quote.").optional()
13773
- })
13791
+ }),
13792
+ validation: documentOwnerValidation(true)
13774
13793
  },
13775
13794
  {
13776
13795
  name: "dispatch_message",
@@ -13782,9 +13801,10 @@ var dispatchToolSpecs = [
13782
13801
  },
13783
13802
  {
13784
13803
  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}`,
13804
+ 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
13805
  arguments: (z) => ({
13787
- issue: z.string().describe(ISSUE_REFERENCE),
13806
+ issue: z.string().describe(ISSUE_REFERENCE).optional(),
13807
+ project: z.string().describe("Project key owning the document.").optional(),
13788
13808
  artifact: z.string().describe("Artifact slug or id for the document."),
13789
13809
  ops: z.array(z.object({
13790
13810
  op: z.enum(DOC_EDIT_OPS).describe("Edit operation."),
@@ -13796,23 +13816,27 @@ var dispatchToolSpecs = [
13796
13816
  before: z.string().describe("Anchor before which to insert.").optional()
13797
13817
  })).describe("Flat tagged edits; the server validates fields required for each operation."),
13798
13818
  summary: z.string().describe("Optional named-version summary.").optional()
13799
- })
13819
+ }),
13820
+ validation: documentOwnerValidation(true)
13800
13821
  },
13801
13822
  {
13802
13823
  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}`,
13824
+ 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
13825
  arguments: (z) => ({
13805
13826
  issue: z.string().describe(ISSUE_REFERENCE).optional(),
13806
- artifact: z.string().describe("Optional artifact slug or id; primary document by default.").optional(),
13827
+ project: z.string().describe("Project key owning the document.").optional(),
13828
+ artifact: z.string().describe("Optional artifact slug or id; primary document by default for an issue.").optional(),
13807
13829
  version: z.number({ int: true, min: 1 }).describe("Optional version number.").optional(),
13808
13830
  ref: z.string().describe("Optional dispatch:// document reference.").optional()
13809
- })
13831
+ }),
13832
+ validation: documentOwnerValidation(true)
13810
13833
  },
13811
13834
  {
13812
13835
  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}`,
13836
+ 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
13837
  arguments: (z) => ({
13815
- issue: z.string().describe(ISSUE_REFERENCE),
13838
+ issue: z.string().describe(ISSUE_REFERENCE).optional(),
13839
+ project: z.string().describe("Project key for an unlinked document.").optional(),
13816
13840
  name: z.string().describe("Artifact filename shown in Dispatch."),
13817
13841
  path: z.string().describe("Local path to the file to upload.").optional(),
13818
13842
  content: z.string().describe("Inline text to store as a Markdown document.").optional(),
@@ -13821,18 +13845,21 @@ var dispatchToolSpecs = [
13821
13845
  validation: {
13822
13846
  check: (value) => {
13823
13847
  const input = value;
13824
- return typeof input.path === "string" !== (typeof input.content === "string");
13848
+ return documentOwnerValidation(false).check(value) && typeof input.path === "string" !== (typeof input.content === "string");
13825
13849
  },
13826
- message: "Exactly one of path or content is required."
13850
+ message: "Exactly one of path or content is required. Exactly one of issue and project is required; with project, artifact names the document."
13827
13851
  }
13828
13852
  },
13829
13853
  {
13830
13854
  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,
13855
+ 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
13856
  arguments: (z) => ({
13833
13857
  issue: z.string().describe(ISSUE_REFERENCE).optional(),
13834
- ref: z.string().describe("Optional dispatch:// issue reference.").optional()
13835
- })
13858
+ project: z.string().describe("Project key owning the document.").optional(),
13859
+ artifact: z.string().describe("Project document slug or id.").optional(),
13860
+ ref: z.string().describe("Optional dispatch:// issue or document reference.").optional()
13861
+ }),
13862
+ validation: documentOwnerValidation(true)
13836
13863
  },
13837
13864
  {
13838
13865
  name: "dispatch_search",
@@ -13960,10 +13987,15 @@ var handoffMessageSchema = object({
13960
13987
  });
13961
13988
  // ../contracts/src/subject.ts
13962
13989
  var AGENT_TOPIC_PREFIX = "notifications.agent.";
13963
- var DISPATCH_ISSUE_TOPIC_PREFIX = "notifications.dispatch.issue.";
13990
+ var DISPATCH_TOPIC_PREFIX = "notifications.dispatch.";
13991
+ var DISPATCH_ISSUE_TOPIC_PREFIX = `${DISPATCH_TOPIC_PREFIX}issue.`;
13992
+ var DISPATCH_DOCUMENT_TOPIC_PREFIX = `${DISPATCH_TOPIC_PREFIX}document.`;
13964
13993
  function dispatchIssueSubject(issueKey, type) {
13965
13994
  return `${DISPATCH_ISSUE_TOPIC_PREFIX}${issueKey}.${type}`;
13966
13995
  }
13996
+ function dispatchDocumentSubject(project, slug, type) {
13997
+ return `${DISPATCH_DOCUMENT_TOPIC_PREFIX}${project}.${slug}.${type}`;
13998
+ }
13967
13999
  function agentSubject(session) {
13968
14000
  return `${AGENT_TOPIC_PREFIX}${session}`;
13969
14001
  }
@@ -14482,6 +14514,25 @@ class DispatchClient {
14482
14514
  async listIssues(options = {}) {
14483
14515
  return this.#json("GET", ["api", "v1", "issues"], undefined, options);
14484
14516
  }
14517
+ async listProjectArtifacts(project, unlinked = false) {
14518
+ return this.#json("GET", ["api", "v1", "projects", project, "artifacts"], undefined, unlinked ? { unlinked: "true" } : undefined);
14519
+ }
14520
+ async projectArtifact(project, input) {
14521
+ const artifactPath = ["api", "v1", "projects", project, "artifacts"];
14522
+ if ("content" in input)
14523
+ return this.#json("POST", artifactPath, input);
14524
+ const form = new FormData;
14525
+ form.set("name", input.name);
14526
+ if (input.summary !== undefined)
14527
+ form.set("summary", input.summary);
14528
+ if (input.actor !== undefined)
14529
+ form.set("actor", JSON.stringify(input.actor));
14530
+ form.set("file", input.file, input.name);
14531
+ return this.#form("POST", artifactPath, form);
14532
+ }
14533
+ async getProjectArtifact(project, slug) {
14534
+ return this.#json("GET", ["api", "v1", "projects", project, "artifacts", slug]);
14535
+ }
14485
14536
  async search(query, options = {}) {
14486
14537
  return this.#json("GET", ["api", "v1", "search"], undefined, { q: query, ...options });
14487
14538
  }
@@ -14509,6 +14560,12 @@ class DispatchClient {
14509
14560
  async comment(issue, input) {
14510
14561
  return this.#json("POST", ["api", "v1", "issues", await this.#resolveIssue(issue), "comments"], input);
14511
14562
  }
14563
+ async getArtifactAsks(id, state) {
14564
+ return this.#json("GET", ["api", "v1", "artifacts", id, "asks"], undefined, state === undefined ? undefined : { state });
14565
+ }
14566
+ async artifactAsk(id, input) {
14567
+ return this.#json("POST", ["api", "v1", "artifacts", id, "asks"], input);
14568
+ }
14512
14569
  async suggest(issue, input) {
14513
14570
  const { replace_with, ...comment } = input;
14514
14571
  return this.#json("POST", ["api", "v1", "issues", await this.#resolveIssue(issue), "comments"], {
@@ -14516,6 +14573,19 @@ class DispatchClient {
14516
14573
  suggestion: { replace_with }
14517
14574
  });
14518
14575
  }
14576
+ async getArtifactComments(id) {
14577
+ return this.#json("GET", ["api", "v1", "artifacts", id, "comments"]);
14578
+ }
14579
+ async artifactComment(id, input) {
14580
+ return this.#json("POST", ["api", "v1", "artifacts", id, "comments"], input);
14581
+ }
14582
+ async artifactSuggest(id, input) {
14583
+ const { replace_with, ...comment } = input;
14584
+ return this.#json("POST", ["api", "v1", "artifacts", id, "comments"], {
14585
+ ...comment,
14586
+ suggestion: { replace_with }
14587
+ });
14588
+ }
14519
14589
  async message(issue, input) {
14520
14590
  return this.#json("POST", ["api", "v1", "issues", await this.#resolveIssue(issue), "messages"], input);
14521
14591
  }
@@ -14544,6 +14614,12 @@ class DispatchClient {
14544
14614
  async nameArtifactVersion(id, input) {
14545
14615
  return this.#json("POST", ["api", "v1", "artifacts", id, "versions"], input);
14546
14616
  }
14617
+ async getArtifactEvents(id, after = 0, limit = 200) {
14618
+ return this.#json("GET", ["api", "v1", "artifacts", id, "events"], undefined, { after, limit });
14619
+ }
14620
+ async getArtifactReferences(id) {
14621
+ return this.#json("GET", ["api", "v1", "artifacts", id, "references"]);
14622
+ }
14547
14623
  async getAsk(id) {
14548
14624
  return this.#json("GET", ["api", "v1", "asks", id]);
14549
14625
  }
@@ -14553,6 +14629,15 @@ class DispatchClient {
14553
14629
  async getComments(issue, artifact) {
14554
14630
  return this.#json("GET", ["api", "v1", "issues", await this.#resolveIssue(issue), "comments"], undefined, artifact ? { artifact } : undefined);
14555
14631
  }
14632
+ async getIssueReferences(issue) {
14633
+ return this.#json("GET", [
14634
+ "api",
14635
+ "v1",
14636
+ "issues",
14637
+ await this.#resolveIssue(issue),
14638
+ "references"
14639
+ ]);
14640
+ }
14556
14641
  async ensureIssue(issueReference, actor) {
14557
14642
  if (!issueReference.includes("#"))
14558
14643
  return issueReference;
@@ -14659,16 +14744,27 @@ class DispatchClient {
14659
14744
  }
14660
14745
 
14661
14746
  // ../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) {
14747
+ function documentResultDetails(artifact) {
14748
+ return {
14749
+ project: artifact.project,
14750
+ artifact: artifact.id,
14751
+ document: `${artifact.project}/${artifact.slug}`,
14752
+ topic: dispatchDocumentSubject(artifact.project, artifact.slug, ">")
14753
+ };
14754
+ }
14755
+ function writeResultDetails(resolved, fields) {
14756
+ if (resolved.owner.kind === "project") {
14757
+ return { ...documentResultDetails(resolved.artifact), ...fields };
14758
+ }
14759
+ if (resolved.issue === undefined)
14760
+ throw new Error("issue document is missing its issue");
14761
+ return {
14762
+ issue: resolved.issue.key,
14763
+ topic: dispatchIssueSubject(resolved.issue.key, ">"),
14764
+ ...fields
14765
+ };
14766
+ }
14767
+ async function askResultDetails(client, ask, resolved) {
14672
14768
  if (ask.issue_key !== null) {
14673
14769
  return {
14674
14770
  issue: ask.issue_key,
@@ -14679,12 +14775,18 @@ async function askResultDetails(client, ask) {
14679
14775
  if (ask.artifact_id === undefined || ask.artifact_id === null) {
14680
14776
  throw new Error("document ask is missing its artifact ID");
14681
14777
  }
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
- };
14778
+ const artifact = resolved?.artifact ?? await client.getArtifact(ask.artifact_id);
14779
+ return { ...documentResultDetails(artifact), ask: ask.id };
14687
14780
  }
14781
+ var nativeIssueKeyPattern = /^[A-Z][A-Z0-9]{1,9}-[0-9]+$/;
14782
+ var externalIssueRefPattern = /^([^/\s]+)\/([^/\s#]+)#([1-9][0-9]*)$/;
14783
+ var bareIssueNumberPattern = /^[1-9][0-9]*$/;
14784
+ var issueFreeTools = {
14785
+ dispatch_issue: true,
14786
+ dispatch_edit_ask: true,
14787
+ dispatch_resolve_ask: true,
14788
+ dispatch_search: true
14789
+ };
14688
14790
  function canonicalExternalIssueRef(value) {
14689
14791
  const match = value.trim().match(externalIssueRefPattern);
14690
14792
  return match ? `${canonicalRepo(match[1] ?? "", match[2] ?? "")}#${match[3]}` : value;
@@ -14729,31 +14831,54 @@ function askUrgency(args) {
14729
14831
  return ASK_URGENCIES.find((urgency) => urgency === value);
14730
14832
  }
14731
14833
  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)
14834
+ const projectDocument = ref.match(/^dispatch:\/\/([A-Z][A-Z0-9]{1,9})\/artifact\/([^/@]+)(?:@v(\d+))?(?:\/(ask|comment)\/([^/]+))?$/);
14835
+ if (projectDocument) {
14836
+ const [, project, artifact, version, targetKind, targetID] = projectDocument;
14837
+ if (project === undefined || artifact === undefined || version !== undefined && Number(version) < 1) {
14838
+ return null;
14839
+ }
14840
+ if (targetKind === undefined) {
14841
+ return {
14842
+ owner: { kind: "project", project },
14843
+ kind: "artifact",
14844
+ id: artifact,
14845
+ ...version === undefined ? {} : { version: Number(version) }
14846
+ };
14847
+ }
14848
+ if (targetID === undefined || targetKind !== "ask" && targetKind !== "comment")
14849
+ return null;
14850
+ return {
14851
+ owner: { kind: "project", project },
14852
+ kind: targetKind,
14853
+ id: targetID
14854
+ };
14855
+ }
14856
+ const issueReference = ref.match(/^dispatch:\/\/([A-Z][A-Z0-9]{1,9}-[1-9][0-9]*)(?:\/(spec)|\/(log)|\/(children)|\/artifact\/([^/@]+)(?:@v(\d+))?|\/ask\/([^/]+)|\/comment\/([^/]+))?$/);
14857
+ if (!issueReference)
14734
14858
  return null;
14735
- const [, issue, spec, log, children, artifact, version, ask, comment] = match;
14859
+ const [, issue, spec, log, children, artifact, version, ask, comment] = issueReference;
14736
14860
  if (!issue || version !== undefined && Number(version) < 1)
14737
14861
  return null;
14862
+ const owner = { kind: "issue", issue };
14738
14863
  if (spec)
14739
- return { issue, kind: "spec", id: spec };
14864
+ return { owner, kind: "spec", id: spec };
14740
14865
  if (log)
14741
- return { issue, kind: "log", id: log };
14866
+ return { owner, kind: "log", id: log };
14742
14867
  if (children)
14743
- return { issue, kind: "children", id: children };
14868
+ return { owner, kind: "children", id: children };
14744
14869
  if (artifact) {
14745
14870
  return {
14746
- issue,
14871
+ owner,
14747
14872
  kind: "artifact",
14748
14873
  id: artifact,
14749
14874
  ...version === undefined ? {} : { version: Number(version) }
14750
14875
  };
14751
14876
  }
14752
14877
  if (ask)
14753
- return { issue, kind: "ask", id: ask };
14878
+ return { owner, kind: "ask", id: ask };
14754
14879
  if (comment)
14755
- return { issue, kind: "comment", id: comment };
14756
- return { issue, kind: "issue", id: issue };
14880
+ return { owner, kind: "comment", id: comment };
14881
+ return { owner, kind: "issue", id: issue };
14757
14882
  }
14758
14883
  function askId(args) {
14759
14884
  const ask = stringArg(args, "ask");
@@ -14771,32 +14896,68 @@ function toolSchema(tool) {
14771
14896
  throw new Error(`Unknown Dispatch tool: ${tool}`);
14772
14897
  return dispatchToolSchema(spec, zodSchemaApi(exports_external), { strict: true });
14773
14898
  }
14774
- async function resolveIssueArguments(tool, args, cwd, env, exec) {
14775
- if (issueFreeTools.has(tool))
14776
- return { args, ref: null };
14899
+ async function resolveOwnerArguments(tool, args, cwd, env, exec) {
14900
+ if (issueFreeTools[tool] === true)
14901
+ return { args, ref: null, owner: null };
14777
14902
  const refArgument = args.ref;
14778
14903
  const ref = typeof refArgument === "string" ? parseDispatchRef(refArgument) ?? (() => {
14779
14904
  throw new Error("ref must be a valid dispatch:// reference");
14780
14905
  })() : null;
14781
14906
  const issueArgument = args.issue;
14907
+ const projectArgument = args.project;
14782
14908
  const artifactArgument = args.artifact;
14783
14909
  const versionArgument = args.version;
14910
+ if (issueArgument !== undefined && projectArgument !== undefined) {
14911
+ throw new Error("exactly one of issue and project is required");
14912
+ }
14913
+ if (typeof projectArgument === "string") {
14914
+ if (!/^[A-Z][A-Z0-9]{1,9}$/.test(projectArgument)) {
14915
+ throw new Error("project must be a project key such as CORE");
14916
+ }
14917
+ if (ref?.owner.kind === "project" && (ref.owner.project !== projectArgument || artifactArgument !== undefined && artifactArgument !== ref.id)) {
14918
+ throw new Error("project and ref must name the same document");
14919
+ }
14920
+ return {
14921
+ args: {
14922
+ ...args,
14923
+ ...artifactArgument === undefined && ref?.owner.kind === "project" ? { artifact: ref.id } : {},
14924
+ ...versionArgument === undefined && ref?.version !== undefined ? { version: ref.version } : {}
14925
+ },
14926
+ ref,
14927
+ owner: { kind: "project", project: projectArgument }
14928
+ };
14929
+ }
14930
+ if (ref?.owner.kind === "project") {
14931
+ return {
14932
+ args: {
14933
+ ...args,
14934
+ project: ref.owner.project,
14935
+ ...artifactArgument === undefined ? { artifact: ref.id } : {},
14936
+ ...versionArgument === undefined && ref.version !== undefined ? { version: ref.version } : {}
14937
+ },
14938
+ ref,
14939
+ owner: ref.owner
14940
+ };
14941
+ }
14784
14942
  if (issueArgument !== undefined || ref !== null) {
14943
+ const issue = typeof issueArgument === "string" ? canonicalExternalIssueRef(issueArgument) : ref?.owner.kind === "issue" ? ref.owner.issue : undefined;
14785
14944
  return {
14786
14945
  args: {
14787
14946
  ...args,
14788
- ...typeof issueArgument === "string" ? { issue: canonicalExternalIssueRef(issueArgument) } : issueArgument === undefined && ref?.issue !== undefined ? { issue: ref.issue } : {},
14947
+ ...issue === undefined ? {} : { issue },
14789
14948
  ...artifactArgument === undefined && (ref?.kind === "spec" || ref?.kind === "artifact") ? { artifact: ref.id } : {},
14790
14949
  ...versionArgument === undefined && ref?.version !== undefined ? { version: ref.version } : {}
14791
14950
  },
14792
- ref
14951
+ ref,
14952
+ owner: issue === undefined ? null : { kind: "issue", issue }
14793
14953
  };
14794
14954
  }
14795
14955
  const legionIssue = env.LEGION_ISSUE;
14796
14956
  if (!legionIssue)
14797
14957
  throw new Error("issue is required; supply issue or set LEGION_ISSUE");
14798
14958
  if (nativeIssueKeyPattern.test(legionIssue) || externalIssueRefPattern.test(legionIssue)) {
14799
- return { args: { ...args, issue: canonicalExternalIssueRef(legionIssue) }, ref: null };
14959
+ const issue = canonicalExternalIssueRef(legionIssue);
14960
+ return { args: { ...args, issue }, ref: null, owner: { kind: "issue", issue } };
14800
14961
  }
14801
14962
  if (!bareIssueNumberPattern.test(legionIssue)) {
14802
14963
  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 +14965,25 @@ async function resolveIssueArguments(tool, args, cwd, env, exec) {
14804
14965
  const repo = await resolveCwdRepo(cwd, exec);
14805
14966
  if (!repo)
14806
14967
  throw new Error("issue is required; LEGION_ISSUE needs a GitHub repository in cwd");
14807
- return { args: { ...args, issue: `${repo}#${legionIssue}` }, ref: null };
14968
+ const issue = `${repo}#${legionIssue}`;
14969
+ return { args: { ...args, issue }, ref: null, owner: { kind: "issue", issue } };
14808
14970
  }
14809
- async function resolveArtifact(client, issueReference, artifactReference) {
14810
- const issue = await client.getIssue(issueReference);
14971
+ async function resolveArtifact(client, owner, artifactReference) {
14972
+ if (owner.kind === "project") {
14973
+ if (artifactReference === undefined) {
14974
+ throw new Error("artifact is required for a project document");
14975
+ }
14976
+ return {
14977
+ owner,
14978
+ artifact: await client.getProjectArtifact(owner.project, artifactReference)
14979
+ };
14980
+ }
14981
+ const issue = await client.getIssue(owner.issue);
14811
14982
  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);
14812
14983
  if (!artifact) {
14813
14984
  throw new Error(`artifact ${artifactReference ?? "spec"} was not found on issue ${issue.key}`);
14814
14985
  }
14815
- return { issue, artifact };
14986
+ return { owner, issue, artifact };
14816
14987
  }
14817
14988
  function anchor(artifact, args) {
14818
14989
  const quote = optionalString(args, "quote");
@@ -14832,7 +15003,7 @@ function toolActor(origin, input) {
14832
15003
  }
14833
15004
  };
14834
15005
  }
14835
- function issueSummary(issue, events) {
15006
+ function issueSummary(issue, events, references) {
14836
15007
  const asks = issue.open_asks;
14837
15008
  return [
14838
15009
  `Title: ${issue.title}`,
@@ -14841,6 +15012,9 @@ function issueSummary(issue, events) {
14841
15012
  `Route: ${issue.route ?? "none"}`,
14842
15013
  "Open asks:",
14843
15014
  ...asks.length === 0 ? ["- none"] : asks.map((ask) => `- ${ask.id}: ${ask.question}`),
15015
+ "References:",
15016
+ ...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}`),
15017
+ ...typeof references === "string" || !references.truncated ? [] : ["- more references beyond 8 hops"],
14844
15018
  "Events:",
14845
15019
  ...events.length === 0 ? ["- none"] : events.map((event) => `- #${event.seq} ${event.type} \xB7 ${event.actor.kind} ${event.actor.id} \xB7 ${event.created_at}`)
14846
15020
  ].join(`
@@ -14905,10 +15079,11 @@ function commentSummary({ comment, replies }) {
14905
15079
  `);
14906
15080
  }
14907
15081
  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}`);
15082
+ const asks = resolved.owner.kind === "project" ? await client.getArtifactAsks(resolved.artifact.id) : resolved.issue?.open_asks ?? [];
15083
+ const marks = asks.filter((ask) => ask.state === "open" && ask.anchor?.artifact_id === resolved.artifact.id).map((ask) => `ask ${ask.id}`);
14909
15084
  let comments;
14910
15085
  try {
14911
- comments = await client.getComments(resolved.issue.key, resolved.artifact.id);
15086
+ comments = resolved.owner.kind === "project" ? await client.getArtifactComments(resolved.artifact.id) : await client.getComments(resolved.issue?.key ?? "", resolved.artifact.id);
14912
15087
  } catch (error) {
14913
15088
  if (error instanceof DispatchServiceError && error.status === 404)
14914
15089
  return marks;
@@ -14927,15 +15102,23 @@ async function executeDispatchTool(input) {
14927
15102
  }
14928
15103
  const env = input.env ?? process.env;
14929
15104
  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);
15105
+ const ownerArguments = await resolveOwnerArguments(input.tool, input.args, input.cwd, env, exec);
15106
+ const args = toolSchema(input.tool).parse(ownerArguments.args);
14932
15107
  const actor = toolActor(await resolveOrigin(env, exec, input.cwd), input);
14933
15108
  const client = new DispatchClient(configUrl, configToken, input.fetchImpl);
14934
- const issueKey = issueFreeTools.has(input.tool) ? null : await ensureIssue(client, stringArg(args, "issue"), actor);
15109
+ const owner = ownerArguments.owner?.kind === "issue" ? {
15110
+ kind: "issue",
15111
+ issue: await ensureIssue(client, ownerArguments.owner.issue, actor)
15112
+ } : ownerArguments.owner;
14935
15113
  const issue = () => {
14936
- if (issueKey === null)
15114
+ if (owner?.kind !== "issue")
14937
15115
  throw new Error("issue is required");
14938
- return issueKey;
15116
+ return owner.issue;
15117
+ };
15118
+ const documentOwner = () => {
15119
+ if (owner === null)
15120
+ throw new Error("issue or project is required");
15121
+ return owner;
14939
15122
  };
14940
15123
  switch (input.tool) {
14941
15124
  case "dispatch_issue": {
@@ -15015,22 +15198,25 @@ async function executeDispatchTool(input) {
15015
15198
  }
15016
15199
  case "dispatch_ask": {
15017
15200
  const anchorArgs = asObject(args.anchor);
15018
- const resolved = anchorArgs ? await resolveArtifact(client, issue(), stringArg(anchorArgs, "artifact")) : undefined;
15201
+ const owner = documentOwner();
15202
+ const artifactReference = optionalString(args, "artifact") ?? (anchorArgs === null ? undefined : optionalString(anchorArgs, "artifact"));
15203
+ const resolved = owner.kind === "project" || artifactReference === undefined ? owner.kind === "project" ? await resolveArtifact(client, owner, artifactReference) : undefined : await resolveArtifact(client, owner, artifactReference);
15019
15204
  const options = args.options;
15020
15205
  const multiple = optionalBoolean(args, "multiple");
15021
15206
  const urgency = askUrgency(args);
15022
15207
  const anchored = anchorArgs && resolved ? anchor(resolved.artifact, anchorArgs) : undefined;
15023
- const ask = await client.ask(issue(), {
15208
+ const askInput = {
15024
15209
  question: stringArg(args, "question"),
15025
15210
  ...Array.isArray(options) ? { options } : {},
15026
15211
  ...multiple === undefined ? {} : { multiple },
15027
15212
  ...urgency === undefined ? {} : { urgency },
15028
15213
  ...anchored === undefined ? {} : { anchor: anchored },
15029
15214
  actor
15030
- });
15215
+ };
15216
+ const ask = resolved?.owner.kind === "project" ? await client.artifactAsk(resolved.artifact.id, askInput) : await client.ask(issue(), askInput);
15031
15217
  return {
15032
15218
  text: `Opened ask ${ask.id}: ${ask.question}`,
15033
- details: await askResultDetails(client, ask)
15219
+ details: await askResultDetails(client, ask, resolved)
15034
15220
  };
15035
15221
  }
15036
15222
  case "dispatch_edit_ask": {
@@ -15055,48 +15241,47 @@ async function executeDispatchTool(input) {
15055
15241
  if (optionalString(args, "quote") !== undefined && artifactReference === undefined) {
15056
15242
  throw new Error("artifact is required when quote is supplied");
15057
15243
  }
15058
- const resolved = artifactReference ? await resolveArtifact(client, issue(), artifactReference) : undefined;
15244
+ const owner = documentOwner();
15245
+ const resolved = owner.kind === "project" || artifactReference === undefined ? owner.kind === "project" ? await resolveArtifact(client, owner, artifactReference) : undefined : await resolveArtifact(client, owner, artifactReference);
15059
15246
  const anchored = resolved ? anchor(resolved.artifact, args) : undefined;
15060
15247
  const replyTo = optionalString(args, "reply_to");
15061
15248
  const replyToAsk = optionalString(args, "reply_to_ask");
15062
15249
  if (replyTo !== undefined && replyToAsk !== undefined) {
15063
15250
  throw new Error("reply_to and reply_to_ask cannot both be set");
15064
15251
  }
15065
- const comment = await client.comment(issue(), {
15252
+ const commentInput = {
15066
15253
  body: stringArg(args, "body"),
15067
15254
  ...anchored === undefined ? {} : { anchor: anchored },
15068
15255
  ...replyTo === undefined ? {} : { reply_to: replyTo },
15069
15256
  ...replyToAsk === undefined ? {} : { ask_id: replyToAsk },
15070
15257
  actor
15071
- });
15258
+ };
15259
+ const comment = resolved?.owner.kind === "project" ? await client.artifactComment(resolved.artifact.id, commentInput) : await client.comment(issue(), commentInput);
15072
15260
  return {
15073
15261
  text: `Posted comment ${comment.id}`,
15074
- details: {
15262
+ details: resolved === undefined ? {
15075
15263
  issue: comment.issue_key,
15076
- topic: dispatchIssueSubject(comment.issue_key, ">"),
15264
+ topic: dispatchIssueSubject(issue(), ">"),
15077
15265
  comment: comment.id
15078
- }
15266
+ } : writeResultDetails(resolved, { comment: comment.id })
15079
15267
  };
15080
15268
  }
15081
15269
  case "dispatch_suggest": {
15082
- const resolved = await resolveArtifact(client, issue(), stringArg(args, "artifact"));
15270
+ const resolved = await resolveArtifact(client, documentOwner(), stringArg(args, "artifact"));
15083
15271
  const anchored = anchor(resolved.artifact, args);
15084
15272
  if (anchored === undefined)
15085
15273
  throw new Error("quote is required");
15086
15274
  const body = optionalString(args, "body");
15087
- const comment = await client.suggest(issue(), {
15275
+ const suggestionInput = {
15088
15276
  ...body === undefined ? {} : { body },
15089
15277
  anchor: anchored,
15090
15278
  replace_with: stringArg(args, "replace_with"),
15091
15279
  actor
15092
- });
15280
+ };
15281
+ const comment = resolved.owner.kind === "project" ? await client.artifactSuggest(resolved.artifact.id, suggestionInput) : await client.suggest(issue(), suggestionInput);
15093
15282
  return {
15094
15283
  text: `Posted suggestion ${comment.id}`,
15095
- details: {
15096
- issue: comment.issue_key,
15097
- topic: dispatchIssueSubject(comment.issue_key, ">"),
15098
- comment: comment.id
15099
- }
15284
+ details: writeResultDetails(resolved, { comment: comment.id })
15100
15285
  };
15101
15286
  }
15102
15287
  case "dispatch_message": {
@@ -15111,7 +15296,7 @@ async function executeDispatchTool(input) {
15111
15296
  };
15112
15297
  }
15113
15298
  case "dispatch_doc_edit": {
15114
- const resolved = await resolveArtifact(client, issue(), stringArg(args, "artifact"));
15299
+ const resolved = await resolveArtifact(client, documentOwner(), stringArg(args, "artifact"));
15115
15300
  const ops = args.ops;
15116
15301
  const summary = optionalString(args, "summary");
15117
15302
  const edited = await client.docEdit(resolved.artifact.id, {
@@ -15121,32 +15306,33 @@ async function executeDispatchTool(input) {
15121
15306
  });
15122
15307
  return {
15123
15308
  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, ">"),
15309
+ details: writeResultDetails(resolved, {
15127
15310
  applied: edited.applied,
15128
15311
  ...edited.version === null ? {} : { version: edited.version.number }
15129
- }
15312
+ })
15130
15313
  };
15131
15314
  }
15132
15315
  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;
15316
+ const artifactReference = optionalString(args, "artifact") ?? (ownerArguments.ref?.kind === "spec" || ownerArguments.ref?.kind === "artifact" ? ownerArguments.ref.id : undefined);
15317
+ const resolved = await resolveArtifact(client, documentOwner(), artifactReference);
15318
+ const version = optionalNumber(args, "version") ?? ownerArguments.ref?.version;
15136
15319
  const document = await client.docRead(resolved.artifact.id, version);
15137
15320
  const marks = await openArtifactMarks(client, resolved);
15138
15321
  return {
15139
15322
  text: marks.length === 0 ? document.markdown : `${document.markdown}
15140
15323
 
15141
15324
  Open anchored asks/comments: ${marks.join(", ")}`,
15142
- details: { issue: resolved.issue.key }
15325
+ details: resolved.owner.kind === "project" ? {
15326
+ project: resolved.artifact.project,
15327
+ document: `${resolved.artifact.project}/${resolved.artifact.slug}`
15328
+ } : { issue: resolved.issue?.key }
15143
15329
  };
15144
15330
  }
15145
15331
  case "dispatch_artifact": {
15146
15332
  const summary = optionalString(args, "summary");
15147
15333
  const name = stringArg(args, "name");
15148
15334
  const content = optionalString(args, "content");
15149
- const result = await client.artifact(issue(), content === undefined ? {
15335
+ const artifactInput = content === undefined ? {
15150
15336
  name,
15151
15337
  file: Bun.file(resolvePath(input.cwd, stringArg(args, "path"))),
15152
15338
  ...summary === undefined ? {} : { summary },
@@ -15156,47 +15342,73 @@ Open anchored asks/comments: ${marks.join(", ")}`,
15156
15342
  content,
15157
15343
  ...summary === undefined ? {} : { summary },
15158
15344
  actor
15159
- });
15345
+ };
15346
+ const artifactOwner = documentOwner();
15347
+ const result = artifactOwner.kind === "project" ? await client.projectArtifact(artifactOwner.project, artifactInput) : await client.artifact(issue(), artifactInput);
15160
15348
  return {
15161
15349
  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, ">"),
15350
+ details: artifactOwner.kind === "project" ? {
15351
+ ...documentResultDetails(result.artifact),
15352
+ version: result.version.number
15353
+ } : {
15354
+ issue: issue(),
15355
+ topic: dispatchIssueSubject(issue(), ">"),
15165
15356
  artifact: result.artifact.id,
15166
15357
  version: result.version.number
15167
15358
  }
15168
15359
  };
15169
15360
  }
15170
15361
  case "dispatch_read": {
15171
- if (issueArguments.ref?.kind === "ask") {
15172
- const askRead = await client.getAsk(issueArguments.ref.id);
15362
+ if (ownerArguments.ref?.kind === "ask") {
15363
+ const askRead = await client.getAsk(ownerArguments.ref.id);
15173
15364
  return {
15174
15365
  text: askSummary(askRead),
15175
- details: { issue: issueArguments.ref.issue }
15366
+ details: ownerArguments.ref.owner.kind === "project" ? { project: ownerArguments.ref.owner.project } : { issue: ownerArguments.ref.owner.issue }
15176
15367
  };
15177
15368
  }
15178
- if (issueArguments.ref?.kind === "comment") {
15179
- const comment = await client.getComment(issueArguments.ref.id);
15369
+ if (ownerArguments.ref?.kind === "comment") {
15370
+ const comment = await client.getComment(ownerArguments.ref.id);
15180
15371
  return {
15181
15372
  text: commentSummary(comment),
15182
- details: { issue: comment.comment.issue_key }
15373
+ details: ownerArguments.ref.owner.kind === "project" ? { project: ownerArguments.ref.owner.project } : { issue: comment.comment.issue_key }
15374
+ };
15375
+ }
15376
+ if (documentOwner().kind === "project") {
15377
+ const resolved = await resolveArtifact(client, documentOwner(), stringArg(args, "artifact"));
15378
+ return {
15379
+ text: [
15380
+ `Document: ${resolved.artifact.project} / ${resolved.artifact.name}`,
15381
+ `Reference: dispatch://${resolved.artifact.project}/artifact/${resolved.artifact.slug}`,
15382
+ `Versions: ${resolved.artifact.versions.length}`
15383
+ ].join(`
15384
+ `),
15385
+ details: {
15386
+ project: resolved.artifact.project,
15387
+ document: `${resolved.artifact.project}/${resolved.artifact.slug}`
15388
+ }
15183
15389
  };
15184
15390
  }
15185
15391
  const read = await client.read(issue());
15186
- if (issueArguments.ref?.kind === "log") {
15392
+ if (ownerArguments.ref?.kind === "log") {
15187
15393
  return {
15188
15394
  text: logSummary(read.issue, read.events),
15189
15395
  details: { issue: read.issue.key }
15190
15396
  };
15191
15397
  }
15192
- if (issueArguments.ref?.kind === "children") {
15398
+ if (ownerArguments.ref?.kind === "children") {
15193
15399
  return {
15194
15400
  text: childrenSummary(read.issue),
15195
15401
  details: { issue: read.issue.key }
15196
15402
  };
15197
15403
  }
15404
+ let references;
15405
+ try {
15406
+ references = await client.getIssueReferences(read.issue.key);
15407
+ } catch (error) {
15408
+ references = error instanceof DispatchServiceError && error.status === 404 ? "unavailable" : `unavailable: ${error instanceof Error ? error.message : String(error)}`;
15409
+ }
15198
15410
  return {
15199
- text: issueSummary(read.issue, read.events),
15411
+ text: issueSummary(read.issue, read.events, references),
15200
15412
  details: { issue: read.issue.key }
15201
15413
  };
15202
15414
  }
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.0",
4
4
  "type": "module",
5
5
  "main": "dist/src/server.js",
6
6
  "exports": {
@@ -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,16 @@ 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:
139
-
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:
140
146
  ```ts
141
- dispatch_doc_edit({ issue, artifact, ops, summary? })
147
+ dispatch_doc_edit({ issue?, project?, artifact, ops, summary? })
142
148
  ```
143
- It returns `details` `{ issue, topic, applied, version? }`. `ops` is an array of this exact
144
- `EditOp` shape:
149
+ It returns issue or project-document owner details plus `applied`, optional `version`, and its
150
+ write `topic`. `ops` is an array of this exact `EditOp` shape:
145
151
 
146
152
  ```ts
147
153
  type EditOp = {
@@ -171,23 +177,24 @@ into a message.
171
177
  Add feedback with:
172
178
 
173
179
  ```ts
174
- dispatch_comment({ issue, artifact?, quote?, occurrence?, body, reply_to?, reply_to_ask? })
180
+ dispatch_comment({ issue?, project?, artifact?, quote?, occurrence?, body, reply_to?, reply_to_ask? })
175
181
  ```
176
182
 
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.
183
+ It returns issue or project-document owner details plus `comment` and, for writes, `topic`.
184
+ `quote` requires `artifact`; omit both for a floating issue comment. A reply
185
+ (`reply_to`/`reply_to_ask`) takes no `quote`; it belongs to its parent's anchor. Use `reply_to`
186
+ to continue a comment thread at its root; a reply to a resolved thread reopens it. Use
187
+ `reply_to_ask` to reply directly under a question asked with `dispatch_ask`. The two are
188
+ mutually exclusive. Comments are edited only by their author from the dashboard.
183
189
 
184
190
  Propose an exact replacement instead of describing it:
185
191
 
186
192
  ```ts
187
- dispatch_suggest({ issue, artifact, quote, replace_with, body?, occurrence? })
193
+ dispatch_suggest({ issue?, project?, artifact, quote, replace_with, body?, occurrence? })
188
194
  ```
189
195
 
190
- It returns `details` `{ issue, topic, comment }`. A human accepts or rejects a suggestion. On
196
+ It returns issue or project-document owner details plus `comment` and its write `topic`. A
197
+ human accepts or rejects a suggestion. On
191
198
  `TARGET_AMBIGUOUS`, add zero-based `occurrence`. On `TARGET_NOT_FOUND`, re-read the document
192
199
  before retrying. `INVALID_ANCHOR` requires exactly one nonempty anchor `quote` or `mark_id`;
193
200
  `ANCHOR_MISSING` means a browser mark was not observed in the live tree, and
@@ -201,19 +208,20 @@ reject an invalid actor or route.
201
208
  Attach an image, diagram, or local file with:
202
209
 
203
210
  ```ts
204
- dispatch_artifact({ issue, name, path, summary? })
211
+ dispatch_artifact({ issue?, project?, name, path, summary? })
205
212
  ```
206
213
 
207
214
  Or, when the text is already in the call, post a Markdown document directly:
208
215
 
209
216
  ```ts
210
- dispatch_artifact({ issue, name: "spec.md", content: "# Design\n..." })
217
+ dispatch_artifact({ issue?, project?, name: "spec.md", content: "# Design\n..." })
211
218
  ```
212
219
 
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.
220
+ Exactly one of `issue` and `project` is required. A project upload creates an unlinked project
221
+ document; it must not include `artifact`. Exactly one of `path` and `content` is required. The
222
+ inline form sends JSON with `Content-Type: application/json`. It returns issue or
223
+ project-document owner details plus `artifact`, `version`, and its write `topic`. Uploading the
224
+ same `name` creates its next version. Use `content` when the text is already in the call.
217
225
 
218
226
  ## Messages
219
227
 
@@ -228,23 +236,20 @@ message does not wake anyone. Do not use it for status, a decision, or document
228
236
 
229
237
  ## What comes back
230
238
 
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:
239
+ A write result's `details.topic` subscribes the host to its owner. Issue writes use
240
+ `notifications.dispatch.issue.<KEY>.>`; project-document writes use
241
+ `notifications.dispatch.document.<PROJECT>.<SLUG>.>`. The owner topic carries every Dispatch
242
+ event; `notify` only controls agent wake and routed delivery. After a restart, catch up with:
239
243
 
240
244
  ```ts
241
- dispatch_read({ issue?, ref? })
245
+ dispatch_read({ issue?, project?, artifact?, ref? })
242
246
  ```
243
247
 
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.
248
+ With an issue ref, it returns the issue summary, open asks, references, and recent events with
249
+ `details` `{ issue }`. With a project document owner or ref, it returns a document summary with
250
+ `details` `{ project, document }`. With an ask ref, it returns that ask's question, options,
251
+ state, answer, and its reply thread. With a comment ref, it returns that comment and its quoted
252
+ reply chain. Reads do not subscribe; use `dispatch_doc_read` for document contents.
248
253
 
249
254
  ## References
250
255
 
@@ -256,6 +261,9 @@ dispatch://KEY/spec
256
261
  dispatch://KEY/artifact/<slug>[@vN]
257
262
  dispatch://KEY/ask/<id>
258
263
  dispatch://KEY/comment/<id>
264
+ dispatch://PROJECT/artifact/<slug>[@vN]
265
+ dispatch://PROJECT/artifact/<slug>/ask/<id>
266
+ dispatch://PROJECT/artifact/<slug>/comment/<id>
259
267
  ```
260
268
 
261
269
  ## Before / after