@sjawhar/opencode-legion-envoy 3.8.2 → 3.10.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.
@@ -14255,13 +14255,14 @@ var dispatchToolSpecs = [
14255
14255
  },
14256
14256
  {
14257
14257
  name: "dispatch_issues",
14258
- example: { project: "AGENTC" },
14259
- description: "List a project's issues for a roadmap or backlog pass: every issue in one project, each carrying " + "its status, priority, parent, labels, and open-ask count, so you can see backlog shape without " + "opening every issue. Optionally filter by status, parent, label, or how recently it changed. Do " + "not use it to search by keyword or phrase; dispatch_search remains the keyword surface. Rows are " + "capped at limit (default 50, max 250), applied to the response here, not by the server.",
14258
+ example: { project: "AGENTC", priority: [0, 1] },
14259
+ description: "List a project's issues for a roadmap or backlog pass: every issue in one project, each carrying " + "its status, priority, parent, labels, and open-ask count, so you can see backlog shape without " + "opening every issue. Optionally filter by status, parent, label, priority, or how recently it " + "changed; priority takes one or more of 0-3 (P0-P3) and null for an issue with no priority, so " + "an owner's P0/P1 audit is priority [0, 1]. Do not use it to search by keyword or phrase; " + "dispatch_search remains the keyword surface. Rows are capped at limit (default 50, max 250), " + "applied to the response here, not by the server.",
14260
14260
  arguments: (z2) => ({
14261
14261
  project: z2.string().describe("Project key to list issues from."),
14262
14262
  status: z2.enum(ISSUE_STATUSES).describe("Optional lifecycle status filter.").optional(),
14263
14263
  parent: z2.string().describe("Optional parent issue key filter.").optional(),
14264
14264
  label: z2.string().describe("Optional label filter.").optional(),
14265
+ priority: z2.array(z2.number({ int: true, min: 0, max: 3 }).nullable(), { min: 1, max: 5 }).describe("Optional priority filter: one or more of 0 (P0, highest) through 3 (P3, lowest), and null " + "for an issue with no priority; an issue matching any listed value is returned.").optional(),
14265
14266
  updated_since: z2.string().describe("Optional RFC3339 timestamp; only issues updated at or after it.").optional(),
14266
14267
  limit: z2.number({ int: true, min: 1, max: 250 }).describe("Maximum rows, 1-250; default 50.").optional()
14267
14268
  })
@@ -15323,6 +15324,9 @@ class DispatchClient {
15323
15324
  for (const [name, value] of Object.entries(query)) {
15324
15325
  if (typeof value === "string" || typeof value === "number") {
15325
15326
  url2.searchParams.set(name, String(value));
15327
+ } else if (Array.isArray(value)) {
15328
+ for (const item of value)
15329
+ url2.searchParams.append(name, String(item));
15326
15330
  }
15327
15331
  }
15328
15332
  }
@@ -15623,6 +15627,10 @@ function optionalPriority(args, name) {
15623
15627
  return null;
15624
15628
  return typeof value === "number" ? value : undefined;
15625
15629
  }
15630
+ function optionalPriorityFilter(args, name) {
15631
+ const value = args[name];
15632
+ return Array.isArray(value) ? value.map((item) => item ?? "none") : undefined;
15633
+ }
15626
15634
  function optionalComponents(args, name) {
15627
15635
  const value = args[name];
15628
15636
  if (typeof value !== "object" || value === null)
@@ -16033,13 +16041,23 @@ async function resolveOwnerArguments(tool, input, cwd, env, exec, serverUrl, pro
16033
16041
  const issue2 = `${repo2}#${legionIssue}`;
16034
16042
  return { args: { ...args, issue: issue2 }, ref, owner: { kind: "issue", issue: issue2 } };
16035
16043
  }
16036
- function artifactByReference(artifacts, artifactReference, owner) {
16037
- const byIdOrSlug = artifacts.find((candidate) => candidate.id === artifactReference) ?? artifacts.find((candidate) => candidate.slug === artifactReference);
16038
- if (byIdOrSlug !== undefined)
16039
- return byIdOrSlug;
16044
+ function artifactByReference(artifacts, artifactReference, owner, canonical) {
16045
+ const bySlug = artifacts.find((candidate) => candidate.slug === artifactReference);
16046
+ if (canonical && bySlug !== undefined)
16047
+ return bySlug;
16048
+ const byId = artifacts.find((candidate) => candidate.id === artifactReference);
16049
+ if (byId !== undefined)
16050
+ return byId;
16040
16051
  const byName = artifacts.filter((candidate) => candidate.name === artifactReference);
16052
+ if (bySlug !== undefined) {
16053
+ const named = byName.filter((candidate) => candidate.id !== bySlug.id && candidate.kind === "doc");
16054
+ if (named.length > 0) {
16055
+ throw new Error(documentReferenceProblem(artifactReference, [bySlug, ...named], owner, "id"));
16056
+ }
16057
+ return bySlug;
16058
+ }
16041
16059
  if (byName.length > 1) {
16042
- throw new Error(documentReferenceProblem(artifactReference, byName, owner, true));
16060
+ throw new Error(documentReferenceProblem(artifactReference, byName, owner, "slug"));
16043
16061
  }
16044
16062
  const [artifact] = byName;
16045
16063
  if (artifact === undefined) {
@@ -16047,32 +16065,31 @@ function artifactByReference(artifacts, artifactReference, owner) {
16047
16065
  }
16048
16066
  return artifact;
16049
16067
  }
16050
- async function resolveArtifact(client, owner, artifactReference) {
16068
+ async function resolveArtifact(client, owner, artifactReference, { canonical = false } = {}) {
16051
16069
  if (owner.kind === "project") {
16052
16070
  if (artifactReference === undefined) {
16053
16071
  throw new Error("artifact is required for a project document");
16054
16072
  }
16055
- try {
16056
- return {
16057
- owner,
16058
- artifact: await client.getProjectArtifact(owner.project, artifactReference)
16059
- };
16060
- } catch (error48) {
16073
+ const routed = await client.getProjectArtifact(owner.project, artifactReference).catch((error48) => {
16061
16074
  if (!(error48 instanceof DispatchServiceError) || error48.status !== 404)
16062
16075
  throw error48;
16063
- const artifacts = await client.listProjectArtifacts(owner.project, true);
16064
- return {
16065
- owner,
16066
- artifact: artifactByReference(artifacts, artifactReference, "project")
16067
- };
16068
- }
16076
+ return;
16077
+ });
16078
+ if (canonical && routed !== undefined)
16079
+ return { owner, artifact: routed };
16080
+ const unlinked = await client.listProjectArtifacts(owner.project, true);
16081
+ const artifacts = routed === undefined ? unlinked : [routed, ...unlinked.filter((candidate) => candidate.id !== routed.id)];
16082
+ return {
16083
+ owner,
16084
+ artifact: artifactByReference(artifacts, artifactReference, "project", canonical)
16085
+ };
16069
16086
  }
16070
16087
  const issue2 = await client.getIssue(owner.issue);
16071
16088
  let artifact;
16072
16089
  if (artifactReference === undefined || artifactReference === "spec") {
16073
16090
  artifact = issue2.artifacts.find((candidate) => candidate.primary || candidate.id === issue2.primary_artifact_id);
16074
16091
  } else {
16075
- artifact = artifactByReference(issue2.artifacts, artifactReference, "issue");
16092
+ artifact = artifactByReference(issue2.artifacts, artifactReference, "issue", canonical);
16076
16093
  }
16077
16094
  if (!artifact) {
16078
16095
  throw new Error(documentReferenceProblem(artifactReference ?? "spec", issue2.artifacts, "issue"));
@@ -16080,10 +16097,10 @@ async function resolveArtifact(client, owner, artifactReference) {
16080
16097
  return { owner, issue: issue2, artifact };
16081
16098
  }
16082
16099
  var documentHintLimit = 8;
16083
- function documentReferenceProblem(reference, documents, owner, ambiguous = false) {
16084
- const hints = documents.slice(0, documentHintLimit).map((document) => `${document.slug} (${document.name})`);
16100
+ function documentReferenceProblem(reference, documents, owner, use) {
16101
+ const hints = documents.slice(0, documentHintLimit).map((document) => use === "id" ? `${document.id} (${document.slug}, ${document.name})` : `${document.slug} (${document.name})`);
16085
16102
  const list = hints.length === 0 ? "none" : hints.join(", ");
16086
- return ambiguous ? `"${reference}" names ${documents.length} documents on this ${owner}; use a slug: ${list}` : `document "${reference}" not found by slug; this ${owner}'s documents: ${list}`;
16103
+ return use === undefined ? `document "${reference}" not found by slug; this ${owner}'s documents: ${list}` : `"${reference}" names ${documents.length} documents on this ${owner}; use ${use === "id" ? "the id" : "a slug"}: ${list}`;
16087
16104
  }
16088
16105
  var askHintLimit = 8;
16089
16106
  function askIDInputProblem(asks, scope) {
@@ -16473,6 +16490,10 @@ async function executeDispatchTool(input) {
16473
16490
  const args = parsed.success ? parsed.data : ownerArguments.args;
16474
16491
  const actor = toolActor(await resolveOrigin(env, exec, input.cwd), input);
16475
16492
  const client = dispatchClient();
16493
+ const refDocument = ownerArguments.ref?.kind === "artifact" ? ownerArguments.ref.id : ownerArguments.ref?.artifact;
16494
+ const resolveDocument = (documentOwner2, reference) => resolveArtifact(client, documentOwner2, reference, {
16495
+ canonical: reference !== undefined && reference === refDocument
16496
+ });
16476
16497
  const owner = ownerArguments.owner?.kind === "issue" ? {
16477
16498
  kind: "issue",
16478
16499
  issue: await resolveExistingIssue(client, ownerArguments.owner.issue)
@@ -16688,6 +16709,7 @@ async function executeDispatchTool(input) {
16688
16709
  const status = optionalString(args, "status");
16689
16710
  const parent = optionalString(args, "parent");
16690
16711
  const label = optionalString(args, "label");
16712
+ const priority = optionalPriorityFilter(args, "priority");
16691
16713
  const updatedSince = optionalString(args, "updated_since");
16692
16714
  const limit = Math.min(Math.max(optionalNumber(args, "limit") ?? 50, 1), 250);
16693
16715
  const issues = await client.listIssues({
@@ -16695,6 +16717,7 @@ async function executeDispatchTool(input) {
16695
16717
  ...status === undefined ? {} : { status },
16696
16718
  ...parent === undefined ? {} : { parent },
16697
16719
  ...label === undefined ? {} : { label },
16720
+ ...priority === undefined ? {} : { priority },
16698
16721
  ...updatedSince === undefined ? {} : { updated_since: updatedSince }
16699
16722
  });
16700
16723
  const rows = issues.slice(0, limit).map((row) => ({
@@ -16761,7 +16784,7 @@ async function executeDispatchTool(input) {
16761
16784
  const issueKey = ref.owner.issue;
16762
16785
  id = await resolveIdPrefix(input.tool, "comment", ref.id, refOwnerName(ref), () => client.getComments(issueKey));
16763
16786
  } else if (ref !== null) {
16764
- const artifact = (await resolveArtifact(client, ref.owner, ref.artifact)).artifact;
16787
+ const artifact = (await resolveArtifact(client, ref.owner, ref.artifact, { canonical: true })).artifact;
16765
16788
  document = artifact;
16766
16789
  id = await resolveIdPrefix(input.tool, "comment", ref.id, refOwnerName(ref), () => client.getArtifactComments(artifact.id));
16767
16790
  }
@@ -16773,7 +16796,7 @@ async function executeDispatchTool(input) {
16773
16796
  const anchorArgs = asObject(args.anchor);
16774
16797
  const owner2 = documentOwner();
16775
16798
  const artifactReference = optionalString(args, "artifact") ?? (anchorArgs === null ? undefined : optionalString(anchorArgs, "artifact"));
16776
- const resolved = owner2.kind === "project" || artifactReference === undefined ? owner2.kind === "project" ? await resolveArtifact(client, owner2, artifactReference) : undefined : await resolveArtifact(client, owner2, artifactReference);
16799
+ const resolved = owner2.kind === "project" || artifactReference === undefined ? owner2.kind === "project" ? await resolveDocument(owner2, artifactReference) : undefined : await resolveDocument(owner2, artifactReference);
16777
16800
  const options = args.options;
16778
16801
  const multiple = optionalBoolean(args, "multiple");
16779
16802
  const urgency = askUrgency(args);
@@ -16823,7 +16846,7 @@ ${followsAsk(askOwner)}`,
16823
16846
  case "dispatch_comment": {
16824
16847
  const artifactReference = optionalString(args, "artifact");
16825
16848
  const owner2 = documentOwner();
16826
- const resolved = owner2.kind === "project" || artifactReference === undefined ? owner2.kind === "project" ? await resolveArtifact(client, owner2, artifactReference) : undefined : await resolveArtifact(client, owner2, artifactReference);
16849
+ const resolved = owner2.kind === "project" || artifactReference === undefined ? owner2.kind === "project" ? await resolveDocument(owner2, artifactReference) : undefined : await resolveDocument(owner2, artifactReference);
16827
16850
  const anchored = resolved ? anchor(resolved.artifact, args) : undefined;
16828
16851
  const replyTo = optionalString(args, "reply_to");
16829
16852
  const replyToAskReference = optionalString(args, "reply_to_ask");
@@ -16876,7 +16899,7 @@ ${followsAsk(askOwner)}`,
16876
16899
  };
16877
16900
  }
16878
16901
  case "dispatch_suggest": {
16879
- const resolved = await resolveArtifact(client, documentOwner(), stringArg(args, "artifact"));
16902
+ const resolved = await resolveDocument(documentOwner(), stringArg(args, "artifact"));
16880
16903
  const anchored = anchor(resolved.artifact, args);
16881
16904
  if (anchored === undefined)
16882
16905
  throw new Error("quote is required");
@@ -16931,7 +16954,7 @@ ${followsAsk(askOwner)}`,
16931
16954
  };
16932
16955
  }
16933
16956
  case "dispatch_doc_edit": {
16934
- const resolved = await resolveArtifact(client, documentOwner(), stringArg(args, "artifact"));
16957
+ const resolved = await resolveDocument(documentOwner(), stringArg(args, "artifact"));
16935
16958
  const ops = args.ops;
16936
16959
  const summary = optionalString(args, "summary");
16937
16960
  const { precondition: rawPrecondition } = args;
@@ -16973,7 +16996,7 @@ ${followsAsk(askOwner)}`,
16973
16996
  }
16974
16997
  case "dispatch_doc_read": {
16975
16998
  const artifactReference = optionalString(args, "artifact") ?? (ownerArguments.ref?.kind === "spec" || ownerArguments.ref?.kind === "artifact" ? ownerArguments.ref.id : undefined);
16976
- const resolved = await resolveArtifact(client, documentOwner(), artifactReference);
16999
+ const resolved = await resolveDocument(documentOwner(), artifactReference);
16977
17000
  const version2 = optionalNumber(args, "version") ?? ownerArguments.ref?.version;
16978
17001
  const documentPromise = client.docRead(resolved.artifact.id, version2);
16979
17002
  const marksPromise = openArtifactMarks(client, resolved);
@@ -17002,7 +17025,7 @@ ${trailer.join(`
17002
17025
  }
17003
17026
  case "dispatch_request_approval": {
17004
17027
  const artifactReference = optionalString(args, "artifact") ?? (ownerArguments.ref?.kind === "spec" || ownerArguments.ref?.kind === "artifact" ? ownerArguments.ref.id : undefined);
17005
- const resolved = await resolveArtifact(client, documentOwner(), artifactReference);
17028
+ const resolved = await resolveDocument(documentOwner(), artifactReference);
17006
17029
  const result = await client.requestApproval(resolved.artifact.id, { actor });
17007
17030
  if (result.ask === null) {
17008
17031
  return {
@@ -17080,7 +17103,7 @@ ${trailer.join(`
17080
17103
  case "dispatch_read": {
17081
17104
  if (ownerArguments.ref?.kind === "ask") {
17082
17105
  const ref = ownerArguments.ref;
17083
- const id = await resolveIdPrefix(input.tool, "ask", ref.id, refOwnerName(ref), async () => ref.owner.kind === "issue" ? client.listIssueAsks(ref.owner.issue) : client.getArtifactAsks((await resolveArtifact(client, ref.owner, ref.artifact)).artifact.id, "all"));
17106
+ const id = await resolveIdPrefix(input.tool, "ask", ref.id, refOwnerName(ref), async () => ref.owner.kind === "issue" ? client.listIssueAsks(ref.owner.issue) : client.getArtifactAsks((await resolveDocument(ref.owner, ref.artifact)).artifact.id, "all"));
17084
17107
  const askRead = await client.getAsk(id);
17085
17108
  const askRef = refTarget(ref, "ask", id);
17086
17109
  return {
@@ -17090,7 +17113,7 @@ ${trailer.join(`
17090
17113
  }
17091
17114
  if (ownerArguments.ref?.kind === "comment") {
17092
17115
  const ref = ownerArguments.ref;
17093
- const id = await resolveIdPrefix(input.tool, "comment", ref.id, refOwnerName(ref), async () => ref.owner.kind === "issue" ? client.getComments(ref.owner.issue) : client.getArtifactComments((await resolveArtifact(client, ref.owner, ref.artifact)).artifact.id));
17116
+ const id = await resolveIdPrefix(input.tool, "comment", ref.id, refOwnerName(ref), async () => ref.owner.kind === "issue" ? client.getComments(ref.owner.issue) : client.getArtifactComments((await resolveDocument(ref.owner, ref.artifact)).artifact.id));
17094
17117
  const comment = await client.getComment(id);
17095
17118
  const commentRef = refTarget(ref, "comment", id);
17096
17119
  return {
@@ -17110,7 +17133,7 @@ ${trailer.join(`
17110
17133
  };
17111
17134
  }
17112
17135
  if (documentOwner().kind === "project") {
17113
- const resolved = await resolveArtifact(client, documentOwner(), stringArg(args, "artifact"));
17136
+ const resolved = await resolveDocument(documentOwner(), stringArg(args, "artifact"));
17114
17137
  const documentRef = dispatchDocumentRef(resolved.artifact.project, resolved.artifact.slug);
17115
17138
  return {
17116
17139
  text: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/opencode-legion-envoy",
3
- "version": "3.8.2",
3
+ "version": "3.10.0",
4
4
  "type": "module",
5
5
  "main": "dist/src/server.js",
6
6
  "exports": {
@@ -286,16 +286,54 @@ distinction from the named issue, citing it (`dispatch://KEY`), for whoever read
286
286
 
287
287
  To see the shape of a project rather than find a phrase, list its issues:
288
288
  ```ts
289
- dispatch_issues({ project, status?, parent?, label?, updated_since?, limit? })
289
+ dispatch_issues({ project, status?, parent?, label?, priority?, updated_since?, limit? })
290
290
  ```
291
291
  Each row carries the issue key, title, status, priority, parent, labels, its open-ask count, and
292
292
  when it last changed — a roadmap or backlog pass without opening every issue. Filter with `status`
293
- (a lifecycle status), `parent` (one issue's children), `label`, or `updated_since` (an RFC3339
293
+ (a lifecycle status), `parent` (one issue's children), `label`, `priority` (a list of `0`–`3`, with
294
+ `null` for an issue with no priority: `[0, 1]` is every P0 and P1), or `updated_since` (an RFC3339
294
295
  timestamp, for "what moved this week"). `limit` caps the rows at 50 by default and 250 at most.
295
296
 
296
297
  This is not search: it matches no text. Use `dispatch_search` for a keyword or phrase, and
297
298
  `dispatch_issues` when you want every issue in a project and its current state.
298
299
 
300
+ ### The owner audit
301
+
302
+ As the owner of a surface, list your area's P0 and P1 issues and staff or close each one nobody
303
+ has started:
304
+ ```ts
305
+ dispatch_issues({ project, priority: [0, 1], limit: 250 })
306
+ ```
307
+ Every unclaimed row in `triage`, `icebox`, `backlog` or `todo` is a decision: someone takes it and
308
+ builds it, or it closes. A row in `in_progress`, `testing`, `needs_review` or `retro`, or one that
309
+ carries a claim, is work under way ([Issue status is yours to move](#issue-status-is-yours-to-move))
310
+ and is not re-staffed. A todo with a finished spec reads as queued work that nobody is doing
311
+ (LEGION-173 sat in todo for two weeks with a complete spec; AGENTC-1010's v4 plan sat in backlog
312
+ with nobody building it).
313
+
314
+ The audit finds three shapes:
315
+
316
+ - **Unstaffed work.** A plan or measurement exists, and no one is building it.
317
+ - **Unrecorded delivery.** An issue not yet in `testing` or `done`, claimed or not, has a merged PR
318
+ naming it. Check the change live, then move the issue (AGENTC-1033 sat at `triage` after its fix,
319
+ agent-c #20367, merged).
320
+ - **Unrecorded practice.** Someone does the issue's work by hand, more than once, while the issue
321
+ sits in backlog (OPS-132, done by hand on every migration merge). It leaves no plan and no PR to
322
+ find; the tell is your own messages. Doing something by hand more than once means an issue is
323
+ wearing the wrong status.
324
+
325
+ Run the audit as a step of a coordinator's loop, at each checkpoint, not as a habit: these shapes
326
+ are found by running the check, not by noticing them.
327
+
328
+ ### Symptom versus cause
329
+
330
+ When a symptom and its cause sit on different issues, the work accrues to the cause's issue, and
331
+ the symptom's issue carries a pointer to it. Before posting a measurement or finding, search
332
+ Dispatch for the failing identity's or component's name, and post on the issue whose title names
333
+ the fix, not the one naming the symptom. A symptom issue gathering messages with no human response
334
+ is the tell. (The production freeze was iterated on AGENTC-546, the failing gate, while its cause
335
+ and answer sat on AGENTC-1010.)
336
+
299
337
  ## Asking
300
338
 
301
339
  ### Before you ask
@@ -565,7 +603,8 @@ dispatch_doc_read({ issue?, project?, artifact?, version?, ref? })
565
603
  ```
566
604
  It returns live or versioned markdown with open marks. A live read ends with a document token; `issue` with an
567
605
  omitted `artifact` reads the issue specification; a project needs `artifact`; and a
568
- `dispatch://PROJECT/artifact/<document-ref>` ref supplies both, where `document-ref` is the id, slug, or filename.
606
+ `dispatch://PROJECT/artifact/<document-ref>` ref supplies both, where `document-ref` is the slug (an id or a
607
+ filename resolves when no document has that slug).
569
608
 
570
609
  Editing one is [Editing a document](references/document-edits.md): the shape of `dispatch_doc_edit`,
571
610
  how to quote the text you mean, one `replace` per paragraph, preconditions against a stale edit, and
@@ -703,7 +742,9 @@ Exactly one of `path` and `content` is required. It returns issue or project-doc
703
742
  Uploading the same `name` creates its next version — so uploading `spec.md` **replaces the issue's own specification**
704
743
  with your text. Never do that: the spec is edited in place with `dispatch_doc_edit` (see [Editing a document](references/document-edits.md)). Address an existing
705
744
  artifact by the slug shown in the upload result or by its filename, and a project document by its artifact id, slug, or filename; the
706
- slug also arrives on `artifact.created` events.
745
+ slug also arrives on `artifact.created` events. Dispatch suffixes a slug two documents would share, so one document's
746
+ filename can be another's slug (`plan v2` takes `plan-v2`, then a document named `plan-v2` takes `plan-v2-2`): a bare
747
+ `artifact` that names both is refused with each one's id, while a `dispatch://` reference's document part is always the slug.
707
748
 
708
749
  **Where a deliverable goes.** Text the human must read to decide — a draft message, a proposal,
709
750
  a summary — goes in the spec as a section: the spec is the one document they open. A separate