@sjawhar/pi-legion-envoy 1.48.0 → 1.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/envoy.js CHANGED
@@ -29863,6 +29863,14 @@ var commentValidation = (() => {
29863
29863
  message: `${owner.message} turn requires reply_to_ask.`
29864
29864
  };
29865
29865
  })();
29866
+ var ISSUE_COMPONENTS_MODES = ["inherit", "explicit", "none"];
29867
+ function componentsArgument(z) {
29868
+ return z.object({
29869
+ 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."),
29870
+ 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(),
29871
+ reason: z.string({ min: 1 }).describe("For mode none: why this issue is not architectural (process, hiring, ops).").optional()
29872
+ }).describe("Attach the issue to architecture components. Attach the root before decomposing it; children inherit unless they choose.");
29873
+ }
29866
29874
  var SPEC_SECTIONS = [
29867
29875
  "Summary",
29868
29876
  "Decisions needed",
@@ -29901,12 +29909,13 @@ var dispatchToolSpecs = [
29901
29909
  spec: z.string().describe(`Optional initial primary-document markdown. ${SPEC_WRITING_GUIDANCE}`).optional(),
29902
29910
  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(),
29903
29911
  priority: z.number({ int: true, min: 0, max: 3 }).describe("Optional coarse priority: P0 is highest and P3 is lowest.").optional(),
29904
- 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()
29912
+ 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(),
29913
+ components: componentsArgument(z).optional()
29905
29914
  })
29906
29915
  },
29907
29916
  {
29908
29917
  name: "dispatch_issue_update",
29909
- 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}`,
29918
+ 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}`,
29910
29919
  arguments: (z) => ({
29911
29920
  issue: z.string().describe(ISSUE_REFERENCE),
29912
29921
  status: z.enum(ISSUE_STATUSES).describe("New lifecycle status.").optional(),
@@ -29914,14 +29923,15 @@ var dispatchToolSpecs = [
29914
29923
  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(),
29915
29924
  external_links: z.array(z.string({ min: 1 })).describe("URLs to link; merged into the issue's existing external links by URL.").optional(),
29916
29925
  route: z.string().describe("Route the issue to role:<name> or session:<id>; an empty string clears it.").optional(),
29917
- parent: z.string().describe("Parent issue key in the same project; an empty string clears the parent.").optional()
29926
+ parent: z.string().describe("Parent issue key in the same project; an empty string clears the parent.").optional(),
29927
+ components: componentsArgument(z).optional()
29918
29928
  }),
29919
29929
  validation: {
29920
29930
  check: (value) => {
29921
29931
  const input = value;
29922
- 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";
29932
+ 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;
29923
29933
  },
29924
- message: "Issue update requires at least one field besides issue: status, title, labels, external_links, route, or parent."
29934
+ message: "Issue update requires at least one field besides issue: status, title, labels, external_links, route, parent, or components."
29925
29935
  },
29926
29936
  strict: true
29927
29937
  },
@@ -32390,6 +32400,27 @@ function optionalNumber(args, name) {
32390
32400
  const value = args[name];
32391
32401
  return typeof value === "number" ? value : undefined;
32392
32402
  }
32403
+ function optionalComponents(args, name) {
32404
+ const value = args[name];
32405
+ if (typeof value !== "object" || value === null)
32406
+ return;
32407
+ const input = value;
32408
+ return {
32409
+ mode: input.mode,
32410
+ ...input.ids === undefined ? {} : { ids: input.ids },
32411
+ ...input.reason === undefined ? {} : { reason: input.reason }
32412
+ };
32413
+ }
32414
+ function componentsChange(input, after) {
32415
+ switch (input.mode) {
32416
+ case "explicit":
32417
+ return `components -> explicit [${after.ids.join(", ")}]`;
32418
+ case "none":
32419
+ return `components -> none (${after.reason ?? input.reason ?? ""})`;
32420
+ case "inherit":
32421
+ return "components -> inherit";
32422
+ }
32423
+ }
32393
32424
  function isDuplicateCandidate(value) {
32394
32425
  if (typeof value !== "object" || value === null)
32395
32426
  return false;
@@ -32773,6 +32804,18 @@ function approvalLine(artifact) {
32773
32804
  return `Approval: changes requested on v${approval.version} by ${approval.by?.id ?? "unknown"}: ${approval.reason ?? ""}`;
32774
32805
  }
32775
32806
  }
32807
+ function componentsLine(components) {
32808
+ const inherited = components.inherited_from === null ? "" : ` (inherited from ${components.inherited_from})`;
32809
+ const retired = components.unknown.length === 0 ? "" : ` (retired: ${components.unknown.join(", ")})`;
32810
+ switch (components.mode) {
32811
+ case "explicit":
32812
+ return `Components: ${components.ids.length === 0 ? "none live" : components.ids.join(", ")}${inherited}${retired}`;
32813
+ case "none":
32814
+ return `Components: none \u2014 ${components.reason ?? ""}${inherited}`;
32815
+ case "inherit":
32816
+ return "Components: unassigned";
32817
+ }
32818
+ }
32776
32819
  function issueSummary(issue, events, references, graph) {
32777
32820
  const asks = issue.open_asks;
32778
32821
  const spec = issue.artifacts?.find((artifact) => artifact.primary);
@@ -32781,6 +32824,8 @@ function issueSummary(issue, events, references, graph) {
32781
32824
  throw new Error("Dispatch issue is missing priority");
32782
32825
  if (issue.assignee === undefined)
32783
32826
  throw new Error("Dispatch issue is missing assignee");
32827
+ if (issue.components === undefined)
32828
+ throw new Error("Dispatch issue is missing components");
32784
32829
  return [
32785
32830
  `Title: ${issue.title}`,
32786
32831
  `Key: ${issue.key}`,
@@ -32788,6 +32833,7 @@ function issueSummary(issue, events, references, graph) {
32788
32833
  `Assignee: ${issue.assignee ?? "unassigned"}`,
32789
32834
  ...issue.priority === null ? [] : [`Priority: P${issue.priority}`],
32790
32835
  `Labels: ${issue.labels.length === 0 ? "none" : issue.labels.join(", ")}`,
32836
+ componentsLine(issue.components),
32791
32837
  `Route: ${issue.route ?? "none"}`,
32792
32838
  ...specApproval === undefined ? [] : [`Spec ${specApproval.replace(/^Approval/, "approval")}`],
32793
32839
  "Open asks:",
@@ -33077,6 +33123,7 @@ async function executeDispatchTool(input) {
33077
33123
  const spec = optionalString(args, "spec");
33078
33124
  const priority = optionalNumber(args, "priority");
33079
33125
  const assignee = optionalString(args, "assignee");
33126
+ const components = optionalComponents(args, "components");
33080
33127
  const labels = args.labels;
33081
33128
  try {
33082
33129
  const created = await client.issue({
@@ -33088,6 +33135,7 @@ async function executeDispatchTool(input) {
33088
33135
  ...spec === undefined ? {} : { spec },
33089
33136
  ...priority === undefined ? {} : { priority },
33090
33137
  ...assignee === undefined ? {} : { assignee },
33138
+ ...components === undefined ? {} : { components },
33091
33139
  ...Array.isArray(labels) ? { labels } : {},
33092
33140
  actor
33093
33141
  });
@@ -33120,6 +33168,7 @@ async function executeDispatchTool(input) {
33120
33168
  const title = optionalString(args, "title");
33121
33169
  const route = optionalString(args, "route");
33122
33170
  const parent = optionalString(args, "parent");
33171
+ const components = optionalComponents(args, "components");
33123
33172
  const labels = Array.isArray(args.labels) ? args.labels : undefined;
33124
33173
  const requestedLinks = Array.isArray(args.external_links) ? [...new Set(args.external_links)] : undefined;
33125
33174
  let newLinks = [];
@@ -33133,6 +33182,7 @@ async function executeDispatchTool(input) {
33133
33182
  ...labels === undefined ? {} : { labels },
33134
33183
  ...route === undefined ? {} : { route },
33135
33184
  ...parent === undefined ? {} : { parent: parent === "" ? null : parent },
33185
+ ...components === undefined ? {} : { components },
33136
33186
  ...requestedLinks === undefined ? {} : { external_links: [...before.external_links, ...newLinks.map((url) => ({ url }))] },
33137
33187
  actor
33138
33188
  });
@@ -33145,7 +33195,8 @@ async function executeDispatchTool(input) {
33145
33195
  newLinks.length === 0 ? `already linked ${requestedLinks.join(", ")} ${linkCount}` : `linked ${newLinks.join(", ")} ${linkCount}`
33146
33196
  ],
33147
33197
  ...route === undefined ? [] : [after.route === null ? "route cleared" : `route ${after.route}`],
33148
- ...parent === undefined ? [] : [after.parent === null ? "parent cleared" : `parent -> ${after.parent}`]
33198
+ ...parent === undefined ? [] : [after.parent === null ? "parent cleared" : `parent -> ${after.parent}`],
33199
+ ...components === undefined ? [] : [componentsChange(components, after.components)]
33149
33200
  ];
33150
33201
  return {
33151
33202
  text: `${after.key}: ${changes.join("; ")} ${notSubscribed(issueTopic(after.key))}`,
package/dist/legion.js CHANGED
@@ -29137,6 +29137,14 @@ var commentValidation = (() => {
29137
29137
  message: `${owner.message} turn requires reply_to_ask.`
29138
29138
  };
29139
29139
  })();
29140
+ var ISSUE_COMPONENTS_MODES = ["inherit", "explicit", "none"];
29141
+ function componentsArgument(z) {
29142
+ return z.object({
29143
+ 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."),
29144
+ 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(),
29145
+ reason: z.string({ min: 1 }).describe("For mode none: why this issue is not architectural (process, hiring, ops).").optional()
29146
+ }).describe("Attach the issue to architecture components. Attach the root before decomposing it; children inherit unless they choose.");
29147
+ }
29140
29148
  var SPEC_SECTIONS = [
29141
29149
  "Summary",
29142
29150
  "Decisions needed",
@@ -29175,12 +29183,13 @@ var dispatchToolSpecs = [
29175
29183
  spec: z.string().describe(`Optional initial primary-document markdown. ${SPEC_WRITING_GUIDANCE}`).optional(),
29176
29184
  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(),
29177
29185
  priority: z.number({ int: true, min: 0, max: 3 }).describe("Optional coarse priority: P0 is highest and P3 is lowest.").optional(),
29178
- 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()
29186
+ 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(),
29187
+ components: componentsArgument(z).optional()
29179
29188
  })
29180
29189
  },
29181
29190
  {
29182
29191
  name: "dispatch_issue_update",
29183
- 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}`,
29192
+ 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}`,
29184
29193
  arguments: (z) => ({
29185
29194
  issue: z.string().describe(ISSUE_REFERENCE),
29186
29195
  status: z.enum(ISSUE_STATUSES).describe("New lifecycle status.").optional(),
@@ -29188,14 +29197,15 @@ var dispatchToolSpecs = [
29188
29197
  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(),
29189
29198
  external_links: z.array(z.string({ min: 1 })).describe("URLs to link; merged into the issue's existing external links by URL.").optional(),
29190
29199
  route: z.string().describe("Route the issue to role:<name> or session:<id>; an empty string clears it.").optional(),
29191
- parent: z.string().describe("Parent issue key in the same project; an empty string clears the parent.").optional()
29200
+ parent: z.string().describe("Parent issue key in the same project; an empty string clears the parent.").optional(),
29201
+ components: componentsArgument(z).optional()
29192
29202
  }),
29193
29203
  validation: {
29194
29204
  check: (value) => {
29195
29205
  const input = value;
29196
- 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";
29206
+ 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;
29197
29207
  },
29198
- message: "Issue update requires at least one field besides issue: status, title, labels, external_links, route, or parent."
29208
+ message: "Issue update requires at least one field besides issue: status, title, labels, external_links, route, parent, or components."
29199
29209
  },
29200
29210
  strict: true
29201
29211
  },
@@ -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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-legion-envoy",
3
- "version": "1.48.0",
3
+ "version": "1.49.0",
4
4
  "type": "module",
5
5
  "omp": {
6
6
  "extensions": [