@sjawhar/opencode-legion-envoy 1.37.0 → 1.38.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.
@@ -13766,6 +13766,14 @@ var commentValidation = (() => {
13766
13766
  message: `${owner.message} turn requires reply_to_ask.`
13767
13767
  };
13768
13768
  })();
13769
+ var ISSUE_COMPONENTS_MODES = ["inherit", "explicit", "none"];
13770
+ function componentsArgument(z) {
13771
+ return z.object({
13772
+ mode: z.enum(ISSUE_COMPONENTS_MODES).describe("inherit: take the parent chain's attachment (the default; deletes this issue's own). explicit: attach to ids. none: not architectural work, with reason."),
13773
+ ids: z.array(z.string({ min: 1 }), { min: 1, max: 50 }).describe("For mode explicit: bare component ids from the project's architecture model (web, dispatch-server), not external ones.").optional(),
13774
+ reason: z.string({ min: 1 }).describe("For mode none: why this issue is not architectural (process, hiring, ops).").optional()
13775
+ }).describe("Attach the issue to architecture components. Attach the root before decomposing it; children inherit unless they choose.");
13776
+ }
13769
13777
  var SPEC_SECTIONS = [
13770
13778
  "Summary",
13771
13779
  "Decisions needed",
@@ -13804,12 +13812,13 @@ var dispatchToolSpecs = [
13804
13812
  spec: z.string().describe(`Optional initial primary-document markdown. ${SPEC_WRITING_GUIDANCE}`).optional(),
13805
13813
  labels: z.array(z.string({ min: 1, max: 40 }), { max: 20 }).describe("Optional initial labels, at most 20 labels of up to 40 characters.").optional(),
13806
13814
  priority: z.number({ int: true, min: 0, max: 3 }).describe("Optional coarse priority: P0 is highest and P3 is lowest.").optional(),
13807
- assignee: z.string().describe("GitHub login of the human who answers this issue's asks; defaults to your owner when you act for a person, else the parent's assignee, else unassigned.").optional()
13815
+ assignee: z.string().describe("GitHub login of the human who answers this issue's asks; defaults to your owner when you act for a person, else the parent's assignee, else unassigned.").optional(),
13816
+ components: componentsArgument(z).optional()
13808
13817
  })
13809
13818
  },
13810
13819
  {
13811
13820
  name: "dispatch_issue_update",
13812
- description: "Update an existing issue: move its lifecycle status, retitle it, replace its labels, link a URL " + "(the pull request that delivers it, a run, a document), set its route, or set or clear its parent. " + "Status is one of " + `${ISSUE_STATUSES.join(", ")}; outside Legion, move it yourself as the work advances; inside ` + "Legion the daemon moves it. external_links are " + "merged into the issue's existing links by URL, so linking the pull request you just opened " + "keeps every earlier link. Priority is the human's and is not settable here. At least one " + `field besides issue is required. ${ISSUE_REFERENCE}`,
13821
+ description: "Update an existing issue: move its lifecycle status, retitle it, replace its labels, link a URL " + "(the pull request that delivers it, a run, a document), set its route, set or clear its parent, " + "or attach it to architecture components. Status is one of " + `${ISSUE_STATUSES.join(", ")}; outside Legion, move it yourself as the work advances; inside ` + "Legion the daemon moves it. external_links are " + "merged into the issue's existing links by URL, so linking the pull request you just opened " + "keeps every earlier link. components replaces the issue's own attachment and is allowed on a " + "closed issue. Priority is the human's and is not settable here. At least one " + `field besides issue is required. ${ISSUE_REFERENCE}`,
13813
13822
  arguments: (z) => ({
13814
13823
  issue: z.string().describe(ISSUE_REFERENCE),
13815
13824
  status: z.enum(ISSUE_STATUSES).describe("New lifecycle status.").optional(),
@@ -13817,14 +13826,15 @@ var dispatchToolSpecs = [
13817
13826
  labels: z.array(z.string({ min: 1, max: 40 }), { max: 20 }).describe("Replacement label set, at most 20 labels of up to 40 characters; replaces every existing label.").optional(),
13818
13827
  external_links: z.array(z.string({ min: 1 })).describe("URLs to link; merged into the issue's existing external links by URL.").optional(),
13819
13828
  route: z.string().describe("Route the issue to role:<name> or session:<id>; an empty string clears it.").optional(),
13820
- parent: z.string().describe("Parent issue key in the same project; an empty string clears the parent.").optional()
13829
+ parent: z.string().describe("Parent issue key in the same project; an empty string clears the parent.").optional(),
13830
+ components: componentsArgument(z).optional()
13821
13831
  }),
13822
13832
  validation: {
13823
13833
  check: (value) => {
13824
13834
  const input = value;
13825
- return typeof input.status === "string" || typeof input.title === "string" || Array.isArray(input.labels) || Array.isArray(input.external_links) || typeof input.route === "string" || typeof input.parent === "string";
13835
+ return typeof input.status === "string" || typeof input.title === "string" || Array.isArray(input.labels) || Array.isArray(input.external_links) || typeof input.route === "string" || typeof input.parent === "string" || typeof input.components === "object" && input.components !== null;
13826
13836
  },
13827
- message: "Issue update requires at least one field besides issue: status, title, labels, external_links, route, or parent."
13837
+ message: "Issue update requires at least one field besides issue: status, title, labels, external_links, route, parent, or components."
13828
13838
  },
13829
13839
  strict: true
13830
13840
  },
@@ -15327,6 +15337,27 @@ function optionalNumber(args, name) {
15327
15337
  const value = args[name];
15328
15338
  return typeof value === "number" ? value : undefined;
15329
15339
  }
15340
+ function optionalComponents(args, name) {
15341
+ const value = args[name];
15342
+ if (typeof value !== "object" || value === null)
15343
+ return;
15344
+ const input = value;
15345
+ return {
15346
+ mode: input.mode,
15347
+ ...input.ids === undefined ? {} : { ids: input.ids },
15348
+ ...input.reason === undefined ? {} : { reason: input.reason }
15349
+ };
15350
+ }
15351
+ function componentsChange(input, after) {
15352
+ switch (input.mode) {
15353
+ case "explicit":
15354
+ return `components -> explicit [${after.ids.join(", ")}]`;
15355
+ case "none":
15356
+ return `components -> none (${after.reason ?? input.reason ?? ""})`;
15357
+ case "inherit":
15358
+ return "components -> inherit";
15359
+ }
15360
+ }
15330
15361
  function isDuplicateCandidate(value) {
15331
15362
  if (typeof value !== "object" || value === null)
15332
15363
  return false;
@@ -15710,6 +15741,18 @@ function approvalLine(artifact) {
15710
15741
  return `Approval: changes requested on v${approval.version} by ${approval.by?.id ?? "unknown"}: ${approval.reason ?? ""}`;
15711
15742
  }
15712
15743
  }
15744
+ function componentsLine(components) {
15745
+ const inherited = components.inherited_from === null ? "" : ` (inherited from ${components.inherited_from})`;
15746
+ const retired = components.unknown.length === 0 ? "" : ` (retired: ${components.unknown.join(", ")})`;
15747
+ switch (components.mode) {
15748
+ case "explicit":
15749
+ return `Components: ${components.ids.length === 0 ? "none live" : components.ids.join(", ")}${inherited}${retired}`;
15750
+ case "none":
15751
+ return `Components: none \u2014 ${components.reason ?? ""}${inherited}`;
15752
+ case "inherit":
15753
+ return "Components: unassigned";
15754
+ }
15755
+ }
15713
15756
  function issueSummary(issue, events, references, graph) {
15714
15757
  const asks = issue.open_asks;
15715
15758
  const spec = issue.artifacts?.find((artifact) => artifact.primary);
@@ -15718,6 +15761,8 @@ function issueSummary(issue, events, references, graph) {
15718
15761
  throw new Error("Dispatch issue is missing priority");
15719
15762
  if (issue.assignee === undefined)
15720
15763
  throw new Error("Dispatch issue is missing assignee");
15764
+ if (issue.components === undefined)
15765
+ throw new Error("Dispatch issue is missing components");
15721
15766
  return [
15722
15767
  `Title: ${issue.title}`,
15723
15768
  `Key: ${issue.key}`,
@@ -15725,6 +15770,7 @@ function issueSummary(issue, events, references, graph) {
15725
15770
  `Assignee: ${issue.assignee ?? "unassigned"}`,
15726
15771
  ...issue.priority === null ? [] : [`Priority: P${issue.priority}`],
15727
15772
  `Labels: ${issue.labels.length === 0 ? "none" : issue.labels.join(", ")}`,
15773
+ componentsLine(issue.components),
15728
15774
  `Route: ${issue.route ?? "none"}`,
15729
15775
  ...specApproval === undefined ? [] : [`Spec ${specApproval.replace(/^Approval/, "approval")}`],
15730
15776
  "Open asks:",
@@ -16014,6 +16060,7 @@ async function executeDispatchTool(input) {
16014
16060
  const spec = optionalString(args, "spec");
16015
16061
  const priority = optionalNumber(args, "priority");
16016
16062
  const assignee = optionalString(args, "assignee");
16063
+ const components = optionalComponents(args, "components");
16017
16064
  const labels = args.labels;
16018
16065
  try {
16019
16066
  const created = await client.issue({
@@ -16025,6 +16072,7 @@ async function executeDispatchTool(input) {
16025
16072
  ...spec === undefined ? {} : { spec },
16026
16073
  ...priority === undefined ? {} : { priority },
16027
16074
  ...assignee === undefined ? {} : { assignee },
16075
+ ...components === undefined ? {} : { components },
16028
16076
  ...Array.isArray(labels) ? { labels } : {},
16029
16077
  actor
16030
16078
  });
@@ -16057,6 +16105,7 @@ async function executeDispatchTool(input) {
16057
16105
  const title = optionalString(args, "title");
16058
16106
  const route = optionalString(args, "route");
16059
16107
  const parent = optionalString(args, "parent");
16108
+ const components = optionalComponents(args, "components");
16060
16109
  const labels = Array.isArray(args.labels) ? args.labels : undefined;
16061
16110
  const requestedLinks = Array.isArray(args.external_links) ? [...new Set(args.external_links)] : undefined;
16062
16111
  let newLinks = [];
@@ -16070,6 +16119,7 @@ async function executeDispatchTool(input) {
16070
16119
  ...labels === undefined ? {} : { labels },
16071
16120
  ...route === undefined ? {} : { route },
16072
16121
  ...parent === undefined ? {} : { parent: parent === "" ? null : parent },
16122
+ ...components === undefined ? {} : { components },
16073
16123
  ...requestedLinks === undefined ? {} : { external_links: [...before.external_links, ...newLinks.map((url) => ({ url }))] },
16074
16124
  actor
16075
16125
  });
@@ -16082,7 +16132,8 @@ async function executeDispatchTool(input) {
16082
16132
  newLinks.length === 0 ? `already linked ${requestedLinks.join(", ")} ${linkCount}` : `linked ${newLinks.join(", ")} ${linkCount}`
16083
16133
  ],
16084
16134
  ...route === undefined ? [] : [after.route === null ? "route cleared" : `route ${after.route}`],
16085
- ...parent === undefined ? [] : [after.parent === null ? "parent cleared" : `parent -> ${after.parent}`]
16135
+ ...parent === undefined ? [] : [after.parent === null ? "parent cleared" : `parent -> ${after.parent}`],
16136
+ ...components === undefined ? [] : [componentsChange(components, after.components)]
16086
16137
  ];
16087
16138
  return {
16088
16139
  text: `${after.key}: ${changes.join("; ")} ${notSubscribed(issueTopic(after.key))}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/opencode-legion-envoy",
3
- "version": "1.37.0",
3
+ "version": "1.38.0",
4
4
  "type": "module",
5
5
  "main": "dist/src/server.js",
6
6
  "exports": {
@@ -248,6 +248,17 @@ that follow from it:
248
248
  the question carries `dispatch://KEY/artifact/<slug>` (or `ref`), never just its filename. Text
249
249
  they must read to decide belongs in the spec in the first place — see [Artifacts](#artifacts).
250
250
 
251
+ Attach an issue to the architecture components it changes, before decomposing it — children inherit the parent's attachment unless they choose their own, so attaching the root once classifies the whole tree:
252
+
253
+ ```
254
+ dispatch_issue({ project: "CORE", title: "...", components: { mode: "explicit", ids: ["dispatch-server", "web"] } })
255
+ dispatch_issue_update({ issue: "CORE-12", components: { mode: "explicit", ids: ["web"] } }) // this issue's own set, replacing what it inherited
256
+ dispatch_issue_update({ issue: "CORE-13", components: { mode: "none", reason: "hiring, not code" } })
257
+ dispatch_issue_update({ issue: "CORE-14", components: { mode: "inherit" } }) // back to the parent chain's attachment
258
+ ```
259
+
260
+ Component ids are the file names under the repository's `.dispatch/architecture/` (`web.md` → `web`); an id the model lacks, or an `external` component, is refused with `COMPONENTS_INPUT`. A closed issue can be classified without reopening. Two rules: declare and attach before decomposing work, and change code and its architecture description (`.dispatch/architecture/<id>.md`) in the same review — the tree at `GET /api/v1/projects/{key}/architecture` counts every issue whose effective set names a component or anything it contains.
261
+
251
262
  Import a project's architecture model from its configured source repository now (a human configures the source in Settings):
252
263
  ```ts
253
264
  dispatch_architecture_sync({ project: "CORE" })
@@ -331,3 +331,7 @@ capacity, and cross-tree conflict. Use the Legion escalation operation for those
331
331
  everything else in the tree, or use `dispatch_ask` for a human question; workers may reach
332
332
  Sami directly with `dispatch_ask` the same way. Do not create a wait loop for any wake
333
333
  source.
334
+
335
+ ## Architecture components
336
+
337
+ Bootstrap on a root whose project has an architecture source: in the root's first PR (the implement worker pushes it), write `.dispatch/architecture/<id>.md` files for the planned components — front matter `title`, `parent`, `depends_on`, `external`; no `paths` yet. The importer reads the source branch's head, so the sync and the attach below work only once that PR has merged to the source branch: until then leave the root on `inherit` (the attach would answer `400 COMPONENTS_INPUT`, the component does not exist yet). After the merge, `dispatch_architecture_sync({ project })` and attach the root with `dispatch_issue_update({ issue, components: { mode: "explicit", ids: [...] } })`. Children inherit the root's attachment; give a child its own `components` only when it changes a narrower set, and `{ mode: "none", reason }` when it is not architectural work. Attach before decomposing, and require every implementer to change the component file beside the code it describes in the same review.