@sjawhar/opencode-legion-envoy 8.6.0 → 8.7.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/dispatch.js CHANGED
@@ -14033,7 +14033,8 @@ var dispatchToolSpecs = [
14033
14033
  labels: z2.array(z2.string({ min: 1, max: 40 }), { max: 20 }).describe("Optional initial labels, at most 20 labels of up to 40 characters.").optional(),
14034
14034
  priority: z2.number({ int: true, min: 0, max: 3 }).describe("Optional coarse priority: P0 is highest and P3 is lowest.").optional(),
14035
14035
  assignee: z2.string().describe("Email of the person who answers this issue's asks (someone who has signed in to Dispatch); defaults to your owner when you act for a person, else the parent's assignee, else unassigned.").optional(),
14036
- components: componentsArgument(z2).optional()
14036
+ components: componentsArgument(z2).optional(),
14037
+ repo: z2.string().describe("The repository the issue targets, owner/name, mapped to the issue's project in Settings; a child inherits its parent's when unset, and an issue with none on its parent chain takes the project's default.").optional()
14037
14038
  })
14038
14039
  },
14039
14040
  {
@@ -14043,7 +14044,7 @@ var dispatchToolSpecs = [
14043
14044
  status: "done",
14044
14045
  reason: "Shipped in owner/repo#7; verified on the production dashboard."
14045
14046
  },
14046
- description: "Update an existing issue: move its lifecycle status, retitle it, replace its labels, set " + "its priority, link a URL (the pull request that delivers it, a run, a document), set its " + "route, set or clear its parent, replace the issues it is blocked by, 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. A non-empty external_links is " + "merged into the issue's existing links by URL, so linking the pull request you just opened " + "keeps every earlier link; an empty external_links removes every link instead. components " + "replaces the issue's own attachment. Closing an issue " + "(status done) requires reason, the note that says why: it is posted on the issue as a " + "message, then the issue closes, because a closed issue refuses messages, comments, and " + "artifacts; reason goes only with status done. A closed issue takes only rank, components, " + "a reopening status (any status but done), and an external_links write that adds no new " + "link (so a link can move to the issue that replaced it); everything else, priority " + "included, waits for the reopen. " + "priority is yours to set and a human overrides it; rank, the board's own order, is not " + "settable here. At least one " + `field besides issue is required. ${ISSUE_REFERENCE}`,
14047
+ description: "Update an existing issue: move its lifecycle status, retitle it, replace its labels, set " + "its priority, link a URL (the pull request that delivers it, a run, a document), set its " + "route, set or clear its parent, replace the issues it is blocked by, attach it to " + "architecture components, or set the repository it targets. Status is one of " + `${ISSUE_STATUSES.join(", ")}; outside Legion, move it yourself as the work advances; inside ` + "Legion the daemon moves it. A non-empty external_links is " + "merged into the issue's existing links by URL, so linking the pull request you just opened " + "keeps every earlier link; an empty external_links removes every link instead. components " + "replaces the issue's own attachment. Closing an issue " + "(status done) requires reason, the note that says why: it is posted on the issue as a " + "message, then the issue closes, because a closed issue refuses messages, comments, and " + "artifacts; reason goes only with status done. A closed issue takes only rank, components, " + "a reopening status (any status but done), and an external_links write that adds no new " + "link (so a link can move to the issue that replaced it); everything else, priority " + "included, waits for the reopen. " + "priority is yours to set and a human overrides it; rank, the board's own order, is not " + "settable here. At least one " + `field besides issue is required. ${ISSUE_REFERENCE}`,
14047
14048
  arguments: (z2) => ({
14048
14049
  issue: z2.string().describe(ISSUE_REFERENCE),
14049
14050
  status: z2.enum(ISSUE_STATUSES).describe("New lifecycle status.").optional(),
@@ -14055,15 +14056,16 @@ var dispatchToolSpecs = [
14055
14056
  route: z2.string().describe("Route the issue to role:<name> or session:<id>; an empty string clears it.").optional(),
14056
14057
  parent: z2.string().describe("Parent issue key in the same project; an empty string clears the parent.").optional(),
14057
14058
  blocked_by: z2.array(z2.string({ min: 1 }), { max: MAX_ISSUE_BLOCKERS }).describe("Replacement list of issue keys in the same project this issue waits on; [] clears it. Dispatch refuses a dependency cycle.").optional(),
14058
- components: componentsArgument(z2).optional()
14059
+ components: componentsArgument(z2).optional(),
14060
+ repo: z2.string().describe("The repository the issue targets, owner/name, mapped to the issue's project in Settings; an empty string clears it, so the issue inherits its parent's or the project's default again.").optional()
14059
14061
  }),
14060
14062
  validation: {
14061
14063
  check: (value) => {
14062
14064
  const input = value;
14063
14065
  const reasonFits = input.status === "done" ? typeof input.reason === "string" && input.reason.trim() !== "" : input.reason === undefined;
14064
- return reasonFits && (typeof input.status === "string" || typeof input.title === "string" || Array.isArray(input.labels) || typeof input.priority === "number" || input.priority === null || Array.isArray(input.external_links) || typeof input.route === "string" || typeof input.parent === "string" || Array.isArray(input.blocked_by) || typeof input.components === "object" && input.components !== null);
14066
+ return reasonFits && (typeof input.status === "string" || typeof input.title === "string" || Array.isArray(input.labels) || typeof input.priority === "number" || input.priority === null || Array.isArray(input.external_links) || typeof input.route === "string" || typeof input.parent === "string" || Array.isArray(input.blocked_by) || typeof input.components === "object" && input.components !== null || typeof input.repo === "string");
14065
14067
  },
14066
- message: "Issue update requires at least one field besides issue: status, title, labels, priority, external_links, route, parent, blocked_by, or components. " + "status done requires reason, a non-empty note saying why the issue is closing, posted on the issue before it closes because a closed issue refuses messages, comments, and artifacts; reason goes only with status done."
14068
+ message: "Issue update requires at least one field besides issue: status, title, labels, priority, external_links, route, parent, blocked_by, components, or repo. " + "status done requires reason, a non-empty note saying why the issue is closing, posted on the issue before it closes because a closed issue refuses messages, comments, and artifacts; reason goes only with status done."
14067
14069
  },
14068
14070
  strict: true
14069
14071
  },
@@ -16846,6 +16848,31 @@ function progressText(progress, sep) {
16846
16848
  }
16847
16849
  return parts.join(sep);
16848
16850
  }
16851
+ function repositoryLine(issue2) {
16852
+ if (issue2.resolved_repo === undefined) {
16853
+ return "Repository: unknown (this Dispatch server predates repository fields; upgrade it)";
16854
+ }
16855
+ if (issue2.resolved_repo === null)
16856
+ return "Repository: none";
16857
+ if (issue2.repo !== null && issue2.repo !== undefined)
16858
+ return `Repository: ${issue2.repo} (own)`;
16859
+ return `Repository: ${issue2.resolved_repo} (inherited or project default)`;
16860
+ }
16861
+ function repositoryFieldsWarnings(issue2) {
16862
+ if (issue2.resolved_repo !== undefined)
16863
+ return {};
16864
+ return {
16865
+ warnings: [
16866
+ `dispatch: warning: Dispatch issue is missing resolved_repo: this Dispatch server predates repository fields, so ${issue2.key}'s repository is unknown; upgrade the server`
16867
+ ]
16868
+ };
16869
+ }
16870
+ function refuseUnsavedRepository(verb, issue2, repo2) {
16871
+ if (repo2 === undefined || issue2.repo !== undefined)
16872
+ return;
16873
+ const lost = repo2 === "" ? "the repository was not cleared" : `${repo2} was not saved`;
16874
+ throw new Error(`Dispatch ${verb} ${issue2.key} without its repository: this Dispatch server predates repository fields, so ${lost}; upgrade the server`);
16875
+ }
16849
16876
  function issueSummary(issue2, events, references, graph, linkBase, titles) {
16850
16877
  const asks = issue2.open_asks;
16851
16878
  const spec = issue2.artifacts?.find((artifact) => artifact.primary);
@@ -16872,6 +16899,7 @@ function issueSummary(issue2, events, references, graph, linkBase, titles) {
16872
16899
  `Labels: ${issue2.labels.length === 0 ? "none" : issue2.labels.join(", ")}`,
16873
16900
  componentsLine(issue2.components),
16874
16901
  `Route: ${routeText(issue2, titles)}`,
16902
+ repositoryLine(issue2),
16875
16903
  `Progress: ${progressText(issue2.progress, ", ") || "none"}`,
16876
16904
  ...specApproval === undefined ? [] : [`Spec ${specApproval.replace(/^Approval/, "approval")}`],
16877
16905
  "External links:",
@@ -17427,6 +17455,7 @@ async function executeDispatchTool(input) {
17427
17455
  const priority = optionalPriority(args, "priority");
17428
17456
  const assignee = optionalString(args, "assignee");
17429
17457
  const components = optionalComponents(args, "components");
17458
+ const repo2 = optionalString(args, "repo");
17430
17459
  const labels = args.labels;
17431
17460
  const blockedBy = args.blocked_by;
17432
17461
  try {
@@ -17441,9 +17470,11 @@ async function executeDispatchTool(input) {
17441
17470
  ...priority === undefined ? {} : { priority },
17442
17471
  ...assignee === undefined ? {} : { assignee },
17443
17472
  ...components === undefined ? {} : { components },
17473
+ ...repo2 === undefined ? {} : { repo: repo2 },
17444
17474
  ...Array.isArray(labels) ? { labels } : {},
17445
17475
  actor
17446
17476
  });
17477
+ refuseUnsavedRepository("created", created, repo2);
17447
17478
  const componentGuidance = await architectureGuidance(client, created.project, created.components);
17448
17479
  const adviceLines = [
17449
17480
  ...renderAdvice(input.tool, created.key, created.advice, {
@@ -17492,6 +17523,7 @@ async function executeDispatchTool(input) {
17492
17523
  const route = optionalString(args, "route");
17493
17524
  const parent = optionalString(args, "parent");
17494
17525
  const components = optionalComponents(args, "components");
17526
+ const repo2 = optionalString(args, "repo");
17495
17527
  const priority = optionalPriority(args, "priority");
17496
17528
  const labels = Array.isArray(args.labels) ? args.labels : undefined;
17497
17529
  const blockedBy = Array.isArray(args.blocked_by) ? args.blocked_by : undefined;
@@ -17531,6 +17563,7 @@ async function executeDispatchTool(input) {
17531
17563
  ...parent === undefined ? {} : { parent: parent === "" ? null : parent },
17532
17564
  ...blockedBy === undefined ? {} : { blocked_by: blockedBy },
17533
17565
  ...components === undefined ? {} : { components },
17566
+ ...repo2 === undefined ? {} : { repo: repo2 === "" ? null : repo2 },
17534
17567
  ...sentLinks === undefined ? {} : { external_links: sentLinks },
17535
17568
  actor
17536
17569
  });
@@ -17545,6 +17578,7 @@ async function executeDispatchTool(input) {
17545
17578
  throw refusalWithCode(error48, taken, landed);
17546
17579
  throw withAccount(error48, landed);
17547
17580
  }
17581
+ refuseUnsavedRepository("updated", after, repo2);
17548
17582
  const linkCount = `(${after.external_links.length} ${after.external_links.length === 1 ? "link" : "links"})`;
17549
17583
  const changes = [
17550
17584
  ...closingNote === undefined ? [] : [`reason posted as message ${closingNote.id} (${closingNote.ref})`],
@@ -17560,7 +17594,10 @@ async function executeDispatchTool(input) {
17560
17594
  ...blockedBy === undefined ? [] : [
17561
17595
  after.blocked_by.length === 0 ? "blocked_by cleared" : `blocked by ${after.blocked_by.join(", ")}`
17562
17596
  ],
17563
- ...components === undefined ? [] : [componentsChange(components, after.components)]
17597
+ ...components === undefined ? [] : [componentsChange(components, after.components)],
17598
+ ...repo2 === undefined ? [] : [
17599
+ after.repo === null ? `repo cleared (resolves to ${after.resolved_repo ?? "none"})` : `repo -> ${after.repo}`
17600
+ ]
17564
17601
  ];
17565
17602
  const adviceLines = renderAdvice(input.tool, after.key, after.advice, {
17566
17603
  setsStatus: status !== undefined
@@ -18255,7 +18292,8 @@ ${trailer.join(`
18255
18292
  return {
18256
18293
  text: issueSummary(read.issue, read.events, references, [...pictures2.lines, ...graph], linkBase, titles),
18257
18294
  details: { issue: read.issue.key },
18258
- ...showImages(pictures2)
18295
+ ...showImages(pictures2),
18296
+ ...repositoryFieldsWarnings(read.issue)
18259
18297
  };
18260
18298
  }
18261
18299
  default:
@@ -19170,6 +19208,10 @@ async function runDispatchCli(argv, rawEnv, io) {
19170
19208
  ...io.fetchImpl === undefined ? {} : { fetchImpl: io.fetchImpl }
19171
19209
  });
19172
19210
  text = result.text;
19211
+ for (const warning of result.warnings ?? []) {
19212
+ io.stderr(`${warning}
19213
+ `);
19214
+ }
19173
19215
  try {
19174
19216
  for (const image of result.images ?? []) {
19175
19217
  const picture = writePicture(dir, image);
@@ -19211,6 +19253,7 @@ async function runDispatchCli(argv, rawEnv, io) {
19211
19253
  var readText = (path2) => readFileSync4(path2 === "-" ? 0 : path2, "utf-8");
19212
19254
  process.exit(await runDispatchCli(Bun.argv.slice(2), process.env, {
19213
19255
  stdout: (text) => process.stdout.write(text),
19256
+ stderr: (text) => process.stderr.write(text),
19214
19257
  readText,
19215
19258
  cwd: process.cwd()
19216
19259
  }));
@@ -13890,7 +13890,8 @@ var dispatchToolSpecs = [
13890
13890
  labels: z2.array(z2.string({ min: 1, max: 40 }), { max: 20 }).describe("Optional initial labels, at most 20 labels of up to 40 characters.").optional(),
13891
13891
  priority: z2.number({ int: true, min: 0, max: 3 }).describe("Optional coarse priority: P0 is highest and P3 is lowest.").optional(),
13892
13892
  assignee: z2.string().describe("Email of the person who answers this issue's asks (someone who has signed in to Dispatch); defaults to your owner when you act for a person, else the parent's assignee, else unassigned.").optional(),
13893
- components: componentsArgument(z2).optional()
13893
+ components: componentsArgument(z2).optional(),
13894
+ repo: z2.string().describe("The repository the issue targets, owner/name, mapped to the issue's project in Settings; a child inherits its parent's when unset, and an issue with none on its parent chain takes the project's default.").optional()
13894
13895
  })
13895
13896
  },
13896
13897
  {
@@ -13900,7 +13901,7 @@ var dispatchToolSpecs = [
13900
13901
  status: "done",
13901
13902
  reason: "Shipped in owner/repo#7; verified on the production dashboard."
13902
13903
  },
13903
- description: "Update an existing issue: move its lifecycle status, retitle it, replace its labels, set " + "its priority, link a URL (the pull request that delivers it, a run, a document), set its " + "route, set or clear its parent, replace the issues it is blocked by, 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. A non-empty external_links is " + "merged into the issue's existing links by URL, so linking the pull request you just opened " + "keeps every earlier link; an empty external_links removes every link instead. components " + "replaces the issue's own attachment. Closing an issue " + "(status done) requires reason, the note that says why: it is posted on the issue as a " + "message, then the issue closes, because a closed issue refuses messages, comments, and " + "artifacts; reason goes only with status done. A closed issue takes only rank, components, " + "a reopening status (any status but done), and an external_links write that adds no new " + "link (so a link can move to the issue that replaced it); everything else, priority " + "included, waits for the reopen. " + "priority is yours to set and a human overrides it; rank, the board's own order, is not " + "settable here. At least one " + `field besides issue is required. ${ISSUE_REFERENCE}`,
13904
+ description: "Update an existing issue: move its lifecycle status, retitle it, replace its labels, set " + "its priority, link a URL (the pull request that delivers it, a run, a document), set its " + "route, set or clear its parent, replace the issues it is blocked by, attach it to " + "architecture components, or set the repository it targets. Status is one of " + `${ISSUE_STATUSES.join(", ")}; outside Legion, move it yourself as the work advances; inside ` + "Legion the daemon moves it. A non-empty external_links is " + "merged into the issue's existing links by URL, so linking the pull request you just opened " + "keeps every earlier link; an empty external_links removes every link instead. components " + "replaces the issue's own attachment. Closing an issue " + "(status done) requires reason, the note that says why: it is posted on the issue as a " + "message, then the issue closes, because a closed issue refuses messages, comments, and " + "artifacts; reason goes only with status done. A closed issue takes only rank, components, " + "a reopening status (any status but done), and an external_links write that adds no new " + "link (so a link can move to the issue that replaced it); everything else, priority " + "included, waits for the reopen. " + "priority is yours to set and a human overrides it; rank, the board's own order, is not " + "settable here. At least one " + `field besides issue is required. ${ISSUE_REFERENCE}`,
13904
13905
  arguments: (z2) => ({
13905
13906
  issue: z2.string().describe(ISSUE_REFERENCE),
13906
13907
  status: z2.enum(ISSUE_STATUSES).describe("New lifecycle status.").optional(),
@@ -13912,15 +13913,16 @@ var dispatchToolSpecs = [
13912
13913
  route: z2.string().describe("Route the issue to role:<name> or session:<id>; an empty string clears it.").optional(),
13913
13914
  parent: z2.string().describe("Parent issue key in the same project; an empty string clears the parent.").optional(),
13914
13915
  blocked_by: z2.array(z2.string({ min: 1 }), { max: MAX_ISSUE_BLOCKERS }).describe("Replacement list of issue keys in the same project this issue waits on; [] clears it. Dispatch refuses a dependency cycle.").optional(),
13915
- components: componentsArgument(z2).optional()
13916
+ components: componentsArgument(z2).optional(),
13917
+ repo: z2.string().describe("The repository the issue targets, owner/name, mapped to the issue's project in Settings; an empty string clears it, so the issue inherits its parent's or the project's default again.").optional()
13916
13918
  }),
13917
13919
  validation: {
13918
13920
  check: (value) => {
13919
13921
  const input = value;
13920
13922
  const reasonFits = input.status === "done" ? typeof input.reason === "string" && input.reason.trim() !== "" : input.reason === undefined;
13921
- return reasonFits && (typeof input.status === "string" || typeof input.title === "string" || Array.isArray(input.labels) || typeof input.priority === "number" || input.priority === null || Array.isArray(input.external_links) || typeof input.route === "string" || typeof input.parent === "string" || Array.isArray(input.blocked_by) || typeof input.components === "object" && input.components !== null);
13923
+ return reasonFits && (typeof input.status === "string" || typeof input.title === "string" || Array.isArray(input.labels) || typeof input.priority === "number" || input.priority === null || Array.isArray(input.external_links) || typeof input.route === "string" || typeof input.parent === "string" || Array.isArray(input.blocked_by) || typeof input.components === "object" && input.components !== null || typeof input.repo === "string");
13922
13924
  },
13923
- message: "Issue update requires at least one field besides issue: status, title, labels, priority, external_links, route, parent, blocked_by, or components. " + "status done requires reason, a non-empty note saying why the issue is closing, posted on the issue before it closes because a closed issue refuses messages, comments, and artifacts; reason goes only with status done."
13925
+ message: "Issue update requires at least one field besides issue: status, title, labels, priority, external_links, route, parent, blocked_by, components, or repo. " + "status done requires reason, a non-empty note saying why the issue is closing, posted on the issue before it closes because a closed issue refuses messages, comments, and artifacts; reason goes only with status done."
13924
13926
  },
13925
13927
  strict: true
13926
13928
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/opencode-legion-envoy",
3
- "version": "8.6.0",
3
+ "version": "8.7.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "dist/src/server.js",
@@ -153,7 +153,7 @@ that GitHub issue or pull request. Only `dispatch issue` with `--external` creat
153
153
  `dispatch issue --external owner/repo#n --project <project> --title '<title>'` before addressing it.
154
154
 
155
155
  Create an issue for newly tracked work with `dispatch issue --project <KEY> --title '<title>'` (`dispatch issue --help` lists `--parent`, `--spec-file`, `--label`, `--priority` and the rest).
156
- Labels are optional: Dispatch trims them, preserves their case, and removes case-insensitive duplicates. `--priority` is yours on creation too — see [Priority is yours to set](skill://dispatch/references/issues.md). Set `--assignee` (the email of a person who has signed in to Dispatch) only when the human said who owns the work; otherwise the default above applies, so a child inherits its parent's assignee. It prints
156
+ Labels are optional: Dispatch trims them, preserves their case, and removes case-insensitive duplicates. `--priority` is yours on creation too — see [Priority is yours to set](skill://dispatch/references/issues.md). Set `--assignee` (the email of a person who has signed in to Dispatch) only when the human said who owns the work; otherwise the default above applies, so a child inherits its parent's assignee. Set `--repo <owner/name>` when the issue targets a repository other than the one it would inherit: its parent's, else the project's default; the repository must be mapped to the project in Settings, and `dispatch read` shows the one the issue resolves to on its `Repository:` line. It prints
157
157
  the new key; creating an issue does not subscribe you to it (see [Following](#following)). Use `dispatch issue` only to create an issue; never use it to park a question. When you pass a spec,
158
158
  follow [Writing a spec](#writing-a-spec).
159
159
 
@@ -85,12 +85,13 @@ issues you own as well as the root. An issue left at `triage` while work is unde
85
85
  Waiting for the deploy lane is not a status and is never announced.
86
86
 
87
87
  ```bash
88
- # PATCH /api/v1/issues/{key} — status, title, labels, priority, external links (merged by URL, or cleared by an empty list), route, parent, blocked_by
88
+ # PATCH /api/v1/issues/{key} — status, title, labels, priority, external links (merged by URL, or cleared by an empty list), route, parent, blocked_by, repo
89
89
  dispatch issue-update --issue PROJ-175 --status testing
90
90
  dispatch issue-update --issue PROJ-175 --status done --reason 'Shipped in owner/repo#7; verified on the production dashboard.'
91
91
  dispatch issue-update --issue PROJ-175 --priority 1 # 0–3; see Priority is yours to set
92
92
  dispatch issue-update --issue PROJ-175 --external-link https://github.com/owner/repo/pull/7
93
93
  dispatch issue-update --issue PROJ-175 --parent PROJ-170 # same-project key; --parent '' clears it
94
+ dispatch issue-update --issue PROJ-175 --repo acme/widgets # mapped to the project; --repo '' clears it
94
95
  ```
95
96
 
96
97
  Closing takes a `--reason`, and the command refuses `--status done` without one: it posts the reason on
@@ -100,9 +101,16 @@ error names the posted message; after a timeout or a server error it also says t
100
101
  landed, so read the issue's status first. A retry points its reason at the posted message rather
101
102
  than repeating it.
102
103
 
103
- The three clears differ: `--clear-priority` sends null, `--parent ''` and `--route ''` clear with an
104
- empty string, and `--clear-external-links` / `--clear-blocked-by` send an empty list. Guessing the
105
- wrong shape is a refusal either way.
104
+ The three clears differ: `--clear-priority` sends null, `--parent ''`, `--route ''` and
105
+ `--repo ''` clear with an empty string, and `--clear-external-links` / `--clear-blocked-by` send an
106
+ empty list. Guessing the wrong shape is a refusal either way.
107
+
108
+ An issue targets one repository: its own `--repo`, else its nearest ancestor's, else the project's
109
+ default (set on the Settings page), else none. A repository not mapped to the issue's project is
110
+ refused `REPO_NOT_MAPPED`, naming it. `dispatch read` prints the result as
111
+ `Repository: owner/name (own)`, `Repository: owner/name (inherited or project default)`, or
112
+ `Repository: none`. `Repository: unknown (…)` means the Dispatch server is older than repository
113
+ fields: the read still works, and a `--repo` write there fails rather than drop the repository.
106
114
 
107
115
  Link the pull request that delivers the issue with `--external-link` when you open it; the issue page
108
116
  renders its state and checks from that link, and `dispatch read` lists it under `External links:`.
@@ -309,10 +309,11 @@ The daemon keeps this order from its fixed table; you start none of its steps:
309
309
  one final commit, the removal of `.legion/<issue>/`, so no merge carries a handoff onto the
310
310
  default branch (dispatch://LEGION-605); those commits do not void the approval and never return
311
311
  the tree to the tester or reviewer;
312
- 4. the merger verifies the current head is the reviewer-approved head plus only retro's commits,
313
- those that change `docs/solutions/` and that removal (`jj diff --from <approved-sha> --to
314
- <tip-sha> --summary`, quoted in READY) and sends the READY packet with its completion, which
315
- refuses a head that still carries `.legion/<issue>/`; the daemon posts
312
+ 4. the merger quotes what sits above the reviewer-approved head (`jj diff --from <approved-sha> --to
313
+ <tip-sha> --summary`) in READY and sends the READY packet with its completion, which refuses a
314
+ head carrying anything above the approved head but retro's commits, those that change
315
+ `docs/solutions/` and that removal (`READY_UNREVIEWED_CHANGE`), and a
316
+ head that still carries `.legion/<issue>/`; the daemon posts
316
317
  `READY #<n> at <current sha> (approved at <approved sha>) for <KEY> (<pr url>)` on the Dispatch
317
318
  issue and publishes it to the project's merge queue role when one is set. The moment the daemon
318
319
  accepts READY, it submits the merge of that head itself, as the implement App and with GitHub's
@@ -324,8 +325,11 @@ The daemon keeps this order from its fixed table; you start none of its steps:
324
325
  design gate refused is submitted by the daemon once that spec version is approved; nobody is
325
326
  told to submit it. If the merger reports a failed verification,
326
327
  treat it like `pr-blocked`: the merger holds the phase, so tell it to move the issue back with
327
- `request_backward_move`, naming what failed; never bypass. A READY refused because the head
328
- still carries `.legion/<issue>/` goes back to `retro`, so the implementer's retro removes it.
328
+ `request_backward_move`, naming what failed; never bypass. A READY refused
329
+ `READY_UNREVIEWED_CHANGE`, or a merger task with no approved head (`READY_NOT_APPROVED`), goes
330
+ back to `reviewing`, the move's reason naming the code and what it found, so the reviewer runs a
331
+ new round. A READY refused because the head still carries `.legion/<issue>/` goes back to
332
+ `retro`, so the implementer's retro removes it.
329
333
  5. the merge lands under the repository's rules; the daemon then starts the **implementer** once
330
334
  more, on the production check.
331
335
  It drives the changed path in production through the user's own access path and records
@@ -403,7 +407,7 @@ active phase worker.
403
407
  | `design-approved` | Its `version` is the approved one. A human approved the root spec document at its current version; the gate is open. Proceed to section 2 — except on a root whose catch-up said `handed_over`: its workers already run their phases, so start nothing; the approval releases any READY the gate refused. One whose `version` is the version a human's own answer to a decision block wrote, after an earlier approval (Dispatch carried that approval to it, section 1), opens nothing new: carry on, and release no wave because of it. |
404
408
  | `design-changes-requested` | Its `version` and `reason`. A human asked for changes to the root spec at `version`, for `reason`. Revise the spec as the reason asks and request approval again as section 1 says; stay parked; the gate is closed. |
405
409
  | `phase-finished` | The daemon has already moved the issue to its next phase by its fixed table and started that phase's role; you start nothing. Read the committed handoff for the finishing phase; if it shows unresolved gaps, tell the role now working the issue (`envoy_publish` to its role topic). A `planner` notice that names a departure from the spec's design defers to section 1's full condition: when the approved Summary, Acceptance, scope and settled decisions still hold, the plan is the record; otherwise change the root spec and request approval again as section 1 says. A `reviewer` notice whose GitHub review is `CHANGES_REQUESTED` needs nothing from you: the daemon has returned the issue to `implementing` (Dispatch `in_progress`) and started the **implementer**, whose correction goes through the tester and the reviewer again, never straight to retro. A `reviewer` notice with an `APPROVED` review means the daemon has started retro (step 5). An `implementer` notice for `production_check` is its production report: read the record on the pull request and the issue, then run step 7. A `tester` notice with `verdict: "fail"` — its handoff carries `implementerProof.verdict: "rejected"`, or a failure naming the production-like proof — has gone back to the **implementer** by the daemon's table; never supply the proof from another role. A worker that reports no surface reaches the changed path sends that report instead of completing its phase, so the daemon starts nothing more on that issue and the worker stays idle in its session, not suspended: file a child issue in this tree to build the surface (infrastructure, tooling, or a skill), and when that child's `child-closed` arrives, tell the waiting worker to continue with `envoy_publish` to its role topic. That report is never a reason to advance the phase. |
406
- | `pr-blocked` | Its `reason` names the pull request and the `max_fix_attempts` it reached, or reads `Issue reached review_round_cap=<N>.` (the end of this row). The count is of heads pushed onto a red verdict that changed something outside `.legion/` — handoff-only pushes (`.legion/` paths only) never count, a push by the review App (a planner's, tester's, reviewer's or architect's) never counts, and the head after a red the tester's red tests earned (a review-App push that changed a path outside `.legion/`, however many handoff-only pushes follow it) does not count either — so after the tester's handoff-only push onto the implementer's red, the implementer's next push does count; a push the daemon cannot classify (a listener without `changed_paths`, a list the listener stopped at 100 paths or 32,768 runes of text, a push listing no commits) does. Published once per exhausted count, not on every later red verdict for that count. Read the failed CI evidence and recovery attempts. The notice moves nothing, and no later CI red within the attempt that ran the count out (the counted head, and every head pushed on top of it until a push lands on a head CI judged green, the tester's red tests and handoff onto the held red included) sends the work back from `testing`, `reviewing`, `retro` or `merging` either: the issue stays in its phase until you move it, or a worker's own decision does (a tester's `fail`, a reviewer's request for changes). A head pushed onto green CI leaves that attempt, so its red still comes as a `checks-red`. In `implementing`, give the implementer the failing checks (`envoy_publish` to its role topic). In any later phase a worker holds, tell that worker (`envoy_publish` to its role topic) to move the issue back to `implementing` with `request_backward_move`, naming the failing checks, and the daemon starts the implementer; or file a corrective child. In `awaiting_merge`, where no worker holds a phase and a backward move is refused, open a `dispatch ask` naming the failing checks for the human who merges, as section 6 does for a conflict there. Do not treat the blocked PR as final. A `review_round_cap` reason is the implementer's round count instead: every move back to an earlier phase counts one, and the notice comes again with each round past the cap. It moves nothing either, since the round that reached the cap has already started its worker. Tell the rounds apart by what announced each: a tester's fail by its `phase-finished` with `verdict: "fail"`; a request for changes by the reviewer's `phase-finished` over a `CHANGES_REQUESTED` review; the daemon's own move by a `checks-red` notice, whose reason names a conflict (`the head conflicts with <base>`), the required checks that failed (`CI is red at <head>`), or the merge queue's failed merge and its failing run (`the merge queue took pull request #<n> out at READY's head …`); a worker's `request_backward_move` by a `phase-finished` from the role that asked, whose reason reaches only the task of the worker the move started, so ask that worker. Judge the cap by the rounds that are not conflicts. Conflict returns need nothing from you beyond the next round. Tester fails, requests for changes and CI-red returns (a `checks-red` notice is raised for CI only when a required check fails on code the branch pushed, or when the merge queue fails the merge) are the work going in circles: narrow it, re-plan it with the responsible worker, or ask a human with `dispatch ask`. Judge a worker's backward move by the reason that worker gives. A move you ordered for a conflict found before `awaiting_merge` (section 6) is a conflict return. The merger's move back to `reviewing` because its task names no approved head is not the work going in circles either. A move that sends back code found wrong counts with the tester fails. |
410
+ | `pr-blocked` | Its `reason` names the pull request and the `max_fix_attempts` it reached, or reads `Issue reached review_round_cap=<N>.` (the end of this row). The count is of heads pushed onto a red verdict that changed something outside `.legion/` — handoff-only pushes (`.legion/` paths only) never count, a push by the review App (a planner's, tester's, reviewer's or architect's) never counts, and the head after a red the tester's red tests earned (a review-App push that changed a path outside `.legion/`, however many handoff-only pushes follow it) does not count either — so after the tester's handoff-only push onto the implementer's red, the implementer's next push does count; a push the daemon cannot classify (a listener without `changed_paths`, a list the listener stopped at 100 paths or 32,768 runes of text, a push listing no commits) does. Published once per exhausted count, not on every later red verdict for that count. Read the failed CI evidence and recovery attempts. The notice moves nothing, and no later CI red within the attempt that ran the count out (the counted head, and every head pushed on top of it until a push lands on a head CI judged green, the tester's red tests and handoff onto the held red included) sends the work back from `testing`, `reviewing`, `retro` or `merging` either: the issue stays in its phase until you move it, or a worker's own decision does (a tester's `fail`, a reviewer's request for changes). A head pushed onto green CI leaves that attempt, so its red still comes as a `checks-red`. In `implementing`, give the implementer the failing checks (`envoy_publish` to its role topic). In any later phase a worker holds, tell that worker (`envoy_publish` to its role topic) to move the issue back to `implementing` with `request_backward_move`, naming the failing checks, and the daemon starts the implementer; or file a corrective child. In `awaiting_merge`, where no worker holds a phase and a backward move is refused, open a `dispatch ask` naming the failing checks for the human who merges, as section 6 does for a conflict there. Do not treat the blocked PR as final. A `review_round_cap` reason is the implementer's round count instead: every move back to an earlier phase counts one, and the notice comes again with each round past the cap. It moves nothing either, since the round that reached the cap has already started its worker. Tell the rounds apart by what announced each: a tester's fail by its `phase-finished` with `verdict: "fail"`; a request for changes by the reviewer's `phase-finished` over a `CHANGES_REQUESTED` review; the daemon's own move by a `checks-red` notice, whose reason names a conflict (`the head conflicts with <base>`), the required checks that failed (`CI is red at <head>`), or the merge queue's failed merge and its failing run (`the merge queue took pull request #<n> out at READY's head …`); a worker's `request_backward_move` by a `phase-finished` from the role that asked, whose reason reaches only the task of the worker the move started, so ask that worker. Judge the cap by the rounds that are not conflicts. Conflict returns need nothing from you beyond the next round. Tester fails, requests for changes and CI-red returns (a `checks-red` notice is raised for CI only when a required check fails on code the branch pushed, or when the merge queue fails the merge) are the work going in circles: narrow it, re-plan it with the responsible worker, or ask a human with `dispatch ask`. Judge a worker's backward move by the reason that worker gives. A move you ordered for a conflict found before `awaiting_merge` (section 6) is a conflict return. The merger's move back to `reviewing` for a READY refusal (`READY_UNREVIEWED_CHANGE`, or `READY_NOT_APPROVED` when its task names no approved head), and the reviewer's no-fix request for changes that follows it so the tester runs the head, are not the work going in circles either. A move that sends back code found wrong counts with the tester fails. |
407
411
  | `pr-merged` | Its `reason` names the pull request. The PR merged, under the repository's rules, before the issue reached `awaiting_merge`. The workflow runs on and asks no one to merge it: the daemon starts the **implementer** on the production check once the issue gets there, and you start nothing. Its `phase-finished` for `production_check` is what brings you to step 7: verify the record on the pull request and this issue first, then sign off naming it with `sign_off`. A merge is not the close. |
408
412
  | `pr-closed-unmerged` | Decide from current scope whether the work is reopened, started over (`park_child` then `rerun_child`, for a child), or ended with a reason. Delegate the repository action to the responsible phase worker and keep ownership. |
409
413
  | `merge-refused` | Its `reason` names the pull request, READY's head, what the daemon asked GitHub to do (merge, enqueue, or enable auto-merge for it), and GitHub's refusal, which a retry would meet again (squash merges disabled, a permission the implement App lacks). The daemon has stopped submitting that merge; the issue stays in `awaiting_merge`. The cause is a repository setting or an App permission, a person's to change: ask one with `dispatch ask`, naming the refusal and the pull request at READY's head, which that person merges under the repository's rules once it is fixed. The merge moves the issue on to its production check. Never bypass a rule. |
@@ -26,9 +26,9 @@ retrospective's durable output.
26
26
  issue. Retro writes **no handoff**. The removal exists because a squash merge commits the head
27
27
  merged into the base, and nothing on the base reads a handoff; the handoffs stay on the issue
28
28
  branch below that commit.
29
- 4. The merger verifies the tip is the approved head plus retro's commits, quotes their
30
- `jj diff --from <approved-sha> --to <tip-sha> --summary` in READY, and sends the READY packet
31
- with its completion, which refuses a head that still carries `.legion/<issue>/`; the daemon
29
+ 4. The merger quotes retro's commits (`jj diff --from <approved-sha> --to <tip-sha> --summary`) in
30
+ READY, and sends the READY packet with its completion, which refuses a head with anything above
31
+ the approved head but those commits, or still carrying `.legion/<issue>/`; the daemon
32
32
  posts it on the Dispatch issue and publishes it to the project's merge-queue role when one is
33
33
  configured. On READY's acceptance the daemon submits the merge, and it lands under the
34
34
  repository's rules: its required reviews and checks, and its merge queue when it enables one.
@@ -40,12 +40,15 @@ retrospective's durable output.
40
40
  merge commit. A defect the production check finds becomes a corrective child issue of the same tree,
41
41
  owned by the architect and implemented by the same implementer; the parent stays open until it lands.
42
42
 
43
- Retro's commits sit above the reviewer's approved head and the approval stands: commits that
44
- change only `docs/solutions/`, and the one that removes `.legion/<issue>/`, do not void it, and the
45
- tree goes from retro to the merger — never back to the tester or reviewer. A conflict-forced
46
- rebase after retro moves these commits with the branch. Any round after retro (a conflict round, a
47
- round a red CI sends back, a re-review) writes `.legion/<issue>/` again, and every approval moves
48
- the issue to retro once more: each retro ends with the removal again, or READY is refused.
43
+ Retro's commits sit above the reviewer's approved head and the approval stands: commits that change
44
+ only `docs/solutions/`, and the one that removes `.legion/<issue>/`, do not void it, and the tree
45
+ goes from retro to the merger — never back to the tester or reviewer. A conflict merge pushed above
46
+ the approved head with no review round after it brings the base's paths above that head, so READY
47
+ refuses it (`READY_UNREVIEWED_CHANGE`) and the reviewer reviews the new head as a new round; one the
48
+ conflict procedure sends back through `implementing` is confirmed before READY. Any round after
49
+ retro (a conflict round, a round a red CI sends back, a re-review) writes `.legion/<issue>/` again,
50
+ and every approval moves the issue to retro once more: each retro ends with the removal again, or
51
+ READY is refused.
49
52
 
50
53
  Do not start retro before step 2 or skip it because the change seems mechanical; the merger's
51
54
  `READY` comes only after step 3. The design gate is not a substitute for review and retro.
@@ -204,7 +207,7 @@ workspace stands on, the removal commit your push made, so push before you compl
204
207
  Before returning, verify all of the following:
205
208
 
206
209
  - The reviewer-approved head remains below the retro commits, and the reviewer's approval of that
207
- head stands: the merger accepts the approved head plus the documentation commit and the removal.
210
+ head stands: READY accepts the approved head plus the documentation commit and the removal.
208
211
  - The learning documents and the Dispatch message both exist (never a GitHub issue comment).
209
212
  - The PR body carries what the repository's instructions derive from the pull request's changed
210
213
  paths at the retro head, written back before that head's push, and each line that affirms work
@@ -93,9 +93,9 @@ fingerprint=$(set -o pipefail; jj -R "$LEGION_WORKSPACE" diff --from "fork_point
93
93
 
94
94
  Where each role gets its two heads: the implementer — the tip before and after its own rebase;
95
95
  the tester — the head its `E2E` line names and the new head; the reviewer — the `commit_id` of
96
- its last submitted review (`gh api repos/{owner}/{repo}/pulls/{n}/reviews --jq '.[] | {commit_id, state, user: .user.login}'`)
97
- and the new head; the merger never computes a fingerprint — it uses the `--summary` check in
98
- `skill://legion-worker/references/merge-gate.md`.
96
+ its last submitted review (`gh api repos/{owner}/{repo}/pulls/{n}/reviews --jq '.[] | {commit_id, state, user: .user.login, body}'`, whose body tells a no-fix request that answered a READY refusal from any other)
97
+ and the new head; the merger never computes a fingerprint — it quotes the `--summary` lines
98
+ in READY, and the daemon holds the head to retro's commits.
99
99
 
100
100
  ## Rewriting pushed commits
101
101
 
@@ -12,8 +12,8 @@ the default branch and nothing there reads a handoff, and READY refuses a head t
12
12
  carries them. After the approval, only retro's commits leave it standing on their own: those that
13
13
  change only `docs/solutions/`, and that removal (*Retro*, below). A conflict-forced merge goes back
14
14
  to the reviewer for a confirmation or a new round, as the fingerprint decides (*The reviewer*,
15
- below, and `skill://legion-worker/references/conflicts-and-rewrites.md`), and any other change to
16
- the head voids it.
15
+ below, and `skill://legion-worker/references/conflicts-and-rewrites.md`), unless READY refused
16
+ it, and any other change to the head voids it.
17
17
 
18
18
  ## The reviewer
19
19
 
@@ -71,7 +71,10 @@ the head voids it.
71
71
  and the fingerprint — a confirmation, not a round; no thermo pass, no thread pass. Equal and
72
72
  that review was `COMMENT` or `REQUEST_CHANGES`: continue that round against the new head;
73
73
  nothing restarts. Different, or either run printed no fingerprint: a new round — thermo again,
74
- one review.
74
+ one review. A return whose task's `Reason:` names a READY refusal is never a confirmation, even
75
+ after a rebase, and a no-fix `REQUEST_CHANGES` that answered one is never continued: the round
76
+ after it reviews the refused change in full from the last `APPROVE` (*Retro's commits do not
77
+ void the reviewer's approval* below).
75
78
  - Answer every thread you opened, and every thread a bot opened that is none of Legion's role
76
79
  Apps, as `skill://legion-worker/references/review-threads.md` says; the same reference says
77
80
  when every thread is settled enough to approve, and resolving one never gates your approval.
@@ -95,9 +98,14 @@ reviewing until you finish.
95
98
  head, its `docs/solutions/` learnings and its last commit removing `.legion/<issue>/`
96
99
  (`skill://legion-retro` gives the steps), leave the approval standing, and the tree goes to the
97
100
  merger — never back to the tester or reviewer. Anything else above the approved head does void
98
- it, and the merger tells the architect the head must return to review instead of completing. A
99
- conflict-forced rebase after retro moves those commits with the branch. Every approval moves the
100
- issue to retro again, so each retro ends with the removal again.
101
+ it: READY is refused `READY_UNREVIEWED_CHANGE`, naming the paths, and the merger tells the
102
+ architect, who has it send the head back to review instead of completing, the move's reason
103
+ naming the refusal's code and what it found. A conflict merge pushed above the approved head
104
+ with no review round after it brings the base's paths above that head, so READY refuses it the
105
+ same way; one sent back through `implementing` by the conflict procedure gets its confirmation
106
+ before READY runs. A READY refusal's return reaches the reviewer as a new round, never a
107
+ fingerprint confirmation. Every approval moves the issue to
108
+ retro again, so each retro ends with the removal again.
101
109
  - **Retro brings the PR body's path-derived content up to date before its push.** Whatever the
102
110
  repository's instructions derive from the pull request's changed paths (a checklist named for
103
111
  each class of path, read by a required check), retro recomputes for the whole diff at its head
@@ -108,11 +116,12 @@ reviewing until you finish.
108
116
  ## The merger
109
117
 
110
118
  - The merger resolves no review thread: the reviewer's approval already judged them, and READY
111
- requires no thread state beyond it. It proves the head is the approved head plus retro's commits
112
- with two commands. First `cd -- "$LEGION_WORKSPACE" && jj -R "$LEGION_WORKSPACE" git fetch && jj -R
113
- "$LEGION_WORKSPACE" diff --from <approved-sha> --to <tip-sha> --summary`, whose output is quoted
114
- in READY; then the same with the one fileset `'~(docs/solutions | .legion/<issue>)'` appended,
115
- which must print nothing (jj unions separate path arguments, so two of them leave nothing out).
119
+ requires no thread state beyond it. It quotes what sits above the approved head with
120
+ `cd -- "$LEGION_WORKSPACE" && jj -R "$LEGION_WORKSPACE" git fetch && jj -R
121
+ "$LEGION_WORKSPACE" diff --from <approved-sha> --to <tip-sha> --summary` in READY; the daemon,
122
+ not the merger, holds those commits to retro's: READY reads GitHub's compare of the round's
123
+ approved head with READY's head and refuses any other path (`READY_UNREVIEWED_CHANGE`), and a
124
+ round with no recorded approval (`READY_NOT_APPROVED`).
116
125
  *The READY packet* is
117
126
  `READY #<n> at <current sha> (approved at <approved sha>) for <KEY> (<pr url>)`
118
127
  (the shape `packages/daemon/internal/prompts/roles/merger.md` defines), then the PR body's