@sjawhar/opencode-legion-envoy 8.20.13 → 8.21.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
@@ -13930,6 +13930,7 @@ function dispatchToolSchema(spec, z2, opts) {
13930
13930
  return spec.validation === undefined ? z2.object(shape, schemaOptions) : z2.refineObject(shape, spec.validation.check, spec.validation.refusal ?? spec.validation.message, schemaOptions);
13931
13931
  }
13932
13932
  var ISSUE_REFERENCE = "An issue is a native KEY or external owner/repo#n reference. An external reference addresses an existing Dispatch issue, including one linked to that GitHub pull request; only dispatch issue with --external creates a native issue.";
13933
+ var BOARD_NEIGHBOUR_REFUSAL = "The server refuses a key that does not exist, one in another project, this issue itself, an empty value, and a pair the board does not hold in that order.";
13933
13934
  var OWNER_REFERENCE = "Exactly one of issue and project is required. An issue is a native KEY or external owner/repo#n reference; a project is a project key such as CORE and addresses an unlinked project document named by artifact.";
13934
13935
  var ASK_QUESTION_CONTRACT = "The question carries the problem the reader recognises and why it matters now, what constrains " + "the answer, and the recommendation with its reason. It asks how to solve the problem or which " + "outcome is wanted; never enumerate choices in the question.";
13935
13936
  var ASK_OPTIONS_CONTRACT = "Options carry the genuinely different approaches. Each option has a label, and its description " + "says what that approach costs.";
@@ -13969,7 +13970,7 @@ var commentValidation = {
13969
13970
  },
13970
13971
  message: `${commentOwner.message} turn requires reply_to_ask.`
13971
13972
  };
13972
- var ISSUE_UPDATE_FIELDS_RULE = "Issue update requires at least one field besides issue: status, title, labels, priority, external_links, route, parent, blocked_by, components, or repo.";
13973
+ var ISSUE_UPDATE_FIELDS_RULE = "Issue update requires at least one field besides issue: status, title, labels, priority, external_links, route, parent, blocked_by, components, repo, before, or after.";
13973
13974
  var ISSUE_UPDATE_DONE_RULE = "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";
13974
13975
  var ISSUE_UPDATE_REASON_RULE = "reason goes only with status done";
13975
13976
  var ISSUE_UPDATE_RULE = `${ISSUE_UPDATE_FIELDS_RULE} ${ISSUE_UPDATE_DONE_RULE}; ${ISSUE_UPDATE_REASON_RULE}.`;
@@ -13983,7 +13984,7 @@ function issueUpdateProblem(value) {
13983
13984
  const status = typeof input.status === "string" ? `status ${input.status}` : "an update without status done";
13984
13985
  return `${ISSUE_UPDATE_REASON_RULE}: it is the note posted on the issue as it closes, and ${status} closes nothing. Drop reason, or set status done to close the issue.`;
13985
13986
  }
13986
- const changesAField = 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";
13987
+ const changesAField = 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" || typeof input.before === "string" || typeof input.after === "string";
13987
13988
  return changesAField ? undefined : ISSUE_UPDATE_FIELDS_RULE;
13988
13989
  }
13989
13990
  var messageValidation = {
@@ -14068,10 +14069,10 @@ var dispatchToolSpecs = [
14068
14069
  name: "dispatch_issue_update",
14069
14070
  example: {
14070
14071
  issue: "DSP-1",
14071
- status: "done",
14072
- reason: "Shipped in owner/repo#7; verified on the production dashboard."
14072
+ priority: 1,
14073
+ before: "DSP-2"
14073
14074
  },
14074
- 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}`,
14075
+ 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, set the repository it targets, or move it on its project's " + "board. 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 a board move " + "(before, after), 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. before and after move the issue on " + "its project's board: before puts it above that issue, after below, and both together " + "land it between them. A project's board order is one agent's to keep; in a Legion " + "deployment that is the controller. At least one " + `field besides issue is required. ${ISSUE_REFERENCE}`,
14075
14076
  arguments: (z2) => ({
14076
14077
  issue: z2.string().describe(ISSUE_REFERENCE),
14077
14078
  status: z2.enum(ISSUE_STATUSES).describe("New lifecycle status.").optional(),
@@ -14084,7 +14085,9 @@ var dispatchToolSpecs = [
14084
14085
  parent: z2.string().describe("Parent issue key in the same project; an empty string clears the parent.").optional(),
14085
14086
  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(),
14086
14087
  components: componentsArgument(z2).optional(),
14087
- 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()
14088
+ 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(),
14089
+ before: z2.string().describe(`Move the issue above this issue on its project's board: an issue key in the same project. With after, the issue lands between the two. ${BOARD_NEIGHBOUR_REFUSAL}`).optional(),
14090
+ after: z2.string().describe(`Move the issue below this issue on its project's board: an issue key in the same project. With before, the issue lands between the two. ${BOARD_NEIGHBOUR_REFUSAL}`).optional()
14088
14091
  }),
14089
14092
  validation: {
14090
14093
  check: (value) => issueUpdateProblem(value) === undefined,
@@ -17805,6 +17808,8 @@ async function executeDispatchTool(input) {
17805
17808
  const parent = optionalString(args, "parent");
17806
17809
  const components = optionalComponents(args, "components");
17807
17810
  const repo2 = optionalString(args, "repo");
17811
+ const rankBefore = optionalString(args, "before");
17812
+ const rankAfter = optionalString(args, "after");
17808
17813
  const priority = optionalPriority(args, "priority");
17809
17814
  const labels = Array.isArray(args.labels) ? args.labels : undefined;
17810
17815
  const blockedBy = Array.isArray(args.blocked_by) ? args.blocked_by : undefined;
@@ -17833,6 +17838,10 @@ async function executeDispatchTool(input) {
17833
17838
  const linked = before.external_links.map((link) => link.url);
17834
17839
  const newLinks = requestedLinks?.filter((url2) => !linked.includes(url2)) ?? [];
17835
17840
  const sentLinks = requestedLinks === undefined ? undefined : requestedLinks.length === 0 ? [] : [...before.external_links, ...newLinks.map((url2) => ({ url: url2 }))];
17841
+ const rank = rankBefore === undefined && rankAfter === undefined ? undefined : {
17842
+ ...rankBefore === undefined ? {} : { before: rankBefore },
17843
+ ...rankAfter === undefined ? {} : { after: rankAfter }
17844
+ };
17836
17845
  let after;
17837
17846
  try {
17838
17847
  after = await client.updateIssue(issueKey, {
@@ -17846,6 +17855,7 @@ async function executeDispatchTool(input) {
17846
17855
  ...components === undefined ? {} : { components },
17847
17856
  ...repo2 === undefined ? {} : { repo: repo2 === "" ? null : repo2 },
17848
17857
  ...sentLinks === undefined ? {} : { external_links: sentLinks },
17858
+ ...rank === undefined ? {} : { rank },
17849
17859
  actor
17850
17860
  });
17851
17861
  } catch (error48) {
@@ -17867,6 +17877,12 @@ async function executeDispatchTool(input) {
17867
17877
  ...title === undefined ? [] : [`title "${after.title}"`],
17868
17878
  ...labels === undefined ? [] : [after.labels.length === 0 ? "labels cleared" : `labels ${after.labels.join(", ")}`],
17869
17879
  ...priority === undefined ? [] : [after.priority === null ? "priority cleared" : `priority -> P${after.priority}`],
17880
+ ...rank === undefined ? [] : [
17881
+ `rank -> ${[
17882
+ ...rankAfter === undefined ? [] : [`after ${rankAfter}`],
17883
+ ...rankBefore === undefined ? [] : [`before ${rankBefore}`]
17884
+ ].join(", ")}`
17885
+ ],
17870
17886
  ...requestedLinks === undefined ? [] : [
17871
17887
  requestedLinks.length === 0 ? "external links cleared" : newLinks.length === 0 ? `already linked ${requestedLinks.join(", ")} ${linkCount}` : `linked ${newLinks.join(", ")} ${linkCount}`
17872
17888
  ],
@@ -13787,6 +13787,7 @@ var SESSION_ID_PATTERN = "[^/?#\\s\\p{Z}\\p{Cc}<>\"'`\\[\\]|]+";
13787
13787
  var sessionIdOnly = new RegExp(`^${SESSION_ID_PATTERN}$`, "u");
13788
13788
  // ../contracts/src/dispatch-tools.ts
13789
13789
  var ISSUE_REFERENCE = "An issue is a native KEY or external owner/repo#n reference. An external reference addresses an existing Dispatch issue, including one linked to that GitHub pull request; only dispatch issue with --external creates a native issue.";
13790
+ var BOARD_NEIGHBOUR_REFUSAL = "The server refuses a key that does not exist, one in another project, this issue itself, an empty value, and a pair the board does not hold in that order.";
13790
13791
  var OWNER_REFERENCE = "Exactly one of issue and project is required. An issue is a native KEY or external owner/repo#n reference; a project is a project key such as CORE and addresses an unlinked project document named by artifact.";
13791
13792
  var ASK_QUESTION_CONTRACT = "The question carries the problem the reader recognises and why it matters now, what constrains " + "the answer, and the recommendation with its reason. It asks how to solve the problem or which " + "outcome is wanted; never enumerate choices in the question.";
13792
13793
  var ASK_OPTIONS_CONTRACT = "Options carry the genuinely different approaches. Each option has a label, and its description " + "says what that approach costs.";
@@ -13826,7 +13827,7 @@ var commentValidation = {
13826
13827
  },
13827
13828
  message: `${commentOwner.message} turn requires reply_to_ask.`
13828
13829
  };
13829
- var ISSUE_UPDATE_FIELDS_RULE = "Issue update requires at least one field besides issue: status, title, labels, priority, external_links, route, parent, blocked_by, components, or repo.";
13830
+ var ISSUE_UPDATE_FIELDS_RULE = "Issue update requires at least one field besides issue: status, title, labels, priority, external_links, route, parent, blocked_by, components, repo, before, or after.";
13830
13831
  var ISSUE_UPDATE_DONE_RULE = "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";
13831
13832
  var ISSUE_UPDATE_REASON_RULE = "reason goes only with status done";
13832
13833
  var ISSUE_UPDATE_RULE = `${ISSUE_UPDATE_FIELDS_RULE} ${ISSUE_UPDATE_DONE_RULE}; ${ISSUE_UPDATE_REASON_RULE}.`;
@@ -13840,7 +13841,7 @@ function issueUpdateProblem(value) {
13840
13841
  const status = typeof input.status === "string" ? `status ${input.status}` : "an update without status done";
13841
13842
  return `${ISSUE_UPDATE_REASON_RULE}: it is the note posted on the issue as it closes, and ${status} closes nothing. Drop reason, or set status done to close the issue.`;
13842
13843
  }
13843
- const changesAField = 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";
13844
+ const changesAField = 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" || typeof input.before === "string" || typeof input.after === "string";
13844
13845
  return changesAField ? undefined : ISSUE_UPDATE_FIELDS_RULE;
13845
13846
  }
13846
13847
  var messageValidation = {
@@ -13925,10 +13926,10 @@ var dispatchToolSpecs = [
13925
13926
  name: "dispatch_issue_update",
13926
13927
  example: {
13927
13928
  issue: "DSP-1",
13928
- status: "done",
13929
- reason: "Shipped in owner/repo#7; verified on the production dashboard."
13929
+ priority: 1,
13930
+ before: "DSP-2"
13930
13931
  },
13931
- 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}`,
13932
+ 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, set the repository it targets, or move it on its project's " + "board. 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 a board move " + "(before, after), 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. before and after move the issue on " + "its project's board: before puts it above that issue, after below, and both together " + "land it between them. A project's board order is one agent's to keep; in a Legion " + "deployment that is the controller. At least one " + `field besides issue is required. ${ISSUE_REFERENCE}`,
13932
13933
  arguments: (z2) => ({
13933
13934
  issue: z2.string().describe(ISSUE_REFERENCE),
13934
13935
  status: z2.enum(ISSUE_STATUSES).describe("New lifecycle status.").optional(),
@@ -13941,7 +13942,9 @@ var dispatchToolSpecs = [
13941
13942
  parent: z2.string().describe("Parent issue key in the same project; an empty string clears the parent.").optional(),
13942
13943
  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(),
13943
13944
  components: componentsArgument(z2).optional(),
13944
- 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()
13945
+ 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(),
13946
+ before: z2.string().describe(`Move the issue above this issue on its project's board: an issue key in the same project. With after, the issue lands between the two. ${BOARD_NEIGHBOUR_REFUSAL}`).optional(),
13947
+ after: z2.string().describe(`Move the issue below this issue on its project's board: an issue key in the same project. With before, the issue lands between the two. ${BOARD_NEIGHBOUR_REFUSAL}`).optional()
13945
13948
  }),
13946
13949
  validation: {
13947
13950
  check: (value) => issueUpdateProblem(value) === undefined,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/opencode-legion-envoy",
3
- "version": "8.20.13",
3
+ "version": "8.21.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "dist/src/server.js",
@@ -9,7 +9,8 @@ Read this to choose, claim or release an issue, or to handle `409 ISSUE_CLAIMED`
9
9
  - Hold at most three issues in flight (`in_progress`, `testing`, `needs_review`, `retro`), of any kind, per agent. A child counts on its own. Priorities decide only what you pull next. Past three: push unfinished work, say where in one comment, move it to `backlog` and clear its route.
10
10
  - An in-flight issue with no owner goes back to `backlog`. No owner means no claim or route held by a live session, no Dispatch activity in a day, and no PR moving on GitHub. The order keeper sweeps those.
11
11
  - Never write the status of an issue with the `legion` label or under one. A status you write on a running Legion root is set back and its architect is told. Only a person in the dashboard, the controller's `set_status`, or the operator stops that tree. On a child, a status out of the flow parks it and stops its workers.
12
- - Set priority yourself. Never reorder the board: one agent keeps the order. When the top looks wrong, or a priority's next step is not a ready issue, publish to `notifications.role.backlog-order`.
12
+ - Set priority yourself. Never reorder the board: one agent keeps each project's order, and in a Legion deployment that agent is the controller, at `notifications.role.legion-<project>-controller`. When the top looks wrong, or a priority's next step is not a ready issue, publish it there, and say it on the issue where no Legion controller runs.
13
+ - The keeper moves a card with `dispatch issue-update --issue <KEY> --before <OTHER>` or `--after <OTHER>`.
13
14
 
14
15
  ## Claim the issue before you work it
15
16
 
@@ -36,13 +37,13 @@ dispatch claim --issue LEGION-234 --release # I have stopped; it is free
36
37
  ## Priority is yours to set
37
38
 
38
39
  - `0` (P0, highest) to `3` (P3), `null` clears. Set it on creation and on grooming passes over issues with none. Say what you set and why. The human overrides from the dashboard.
39
- - A closed issue takes only `rank`, `components`, a reopening status, and an external-links write that adds no link. Reopen first, then set priority, in two commands (`409 ISSUE_CLOSED`).
40
+ - A closed issue takes only a board move (`--before` / `--after`), `components`, a reopening status, and an external-links write that adds no link. Reopen first, then set priority, in two commands (`409 ISSUE_CLOSED`).
40
41
  - Labels are optional. Dispatch trims them, keeps their case, and drops case-insensitive duplicates.
41
42
 
42
- ## Board rank, priority, and assignee on a read
43
+ ## Board rank, priority, and assignee
43
44
 
44
45
  - Reads carry `rank`, nullable priority, and `assignee`: a lowercase email, or `null`. `dispatch read` prints `Assignee: unassigned` for `null`.
45
- - Reorder with `PATCH /api/v1/issues/{key}` and `{"rank": {"before": "<key>", "after": "<key>"}}` (either optional, same project). A bearer also sends `"actor": {"kind": "session", "id": "<your session id>"}`, or gets `ACTOR_KIND`.
46
+ - Move a card with `dispatch issue-update --issue <KEY> --before <OTHER>`, which puts it above that issue, `--after <OTHER>`, which puts it below, or both together, which land it between them. The neighbour is an issue of the same project and never the issue itself, and the server refuses a pair the board does not hold in that order. The move rides along with any other change in the same call, and a closed issue takes it.
46
47
 
47
48
  ## Reading a project's backlog
48
49
 
@@ -1,9 +1,9 @@
1
1
  # Backlog stewardship
2
2
 
3
3
  The controller keeps the Dispatch backlog's facts aligned with the deployment's current weekly
4
- plan. The plan is the source of the work order and its one-owner map; Dispatch priority and board
5
- rank are the executable form of that order. The deployment instructions name the weekly-planning
6
- issue and its `plan-week-*` artifact.
4
+ plan. The plan is the source of the work order and its one-owner map; Dispatch board rank is the
5
+ executable form of that order. The deployment instructions name the weekly-planning issue and its
6
+ `plan-week-*` artifact.
7
7
 
8
8
  ## Controller routing address
9
9
 
@@ -18,12 +18,13 @@ Run this pass on the first turn of every UTC day and after every ruling from a p
18
18
 
19
19
  1. Read the weekly-planning issue and select its newest `plan-week-*` artifact by its latest
20
20
  `artifact.version` event. Read that artifact before changing any issue.
21
- 2. For every current plan line in this deployment's project, make Dispatch's sort order exactly
22
- match the line order: set the coarse priority and board rank together so the admission rule
23
- (highest priority, then board rank) produces the plan's order. Keep each line's own stated
24
- sequence. Do not invent a second order from a title, a stale route, or an instruction.
25
- Reorder through Dispatch's board-rank API described in
26
- `skill://dispatch/references/issues.md#board-rank-priority-and-assignee-on-a-read`.
21
+ 2. For every current plan line in this deployment's project, make the project's board order match
22
+ the line order exactly: Legion admits waiting roots by board rank alone — unlike the ready order
23
+ an agent takes its own next issue by, which is priority, then board rank — so board rank is what
24
+ carries the plan's order. Set the coarse priority too, as the signal people and filters read; it
25
+ changes no order. Keep each line's own stated sequence. Do not invent a second order from a
26
+ title, a stale route, or an instruction. Reorder with
27
+ `dispatch issue-update --issue <KEY> --before <OTHER>` or `--after <OTHER>`.
27
28
  3. Reconcile the plan's one-owner map. For each listed owner, verify the named session is live.
28
29
  For unowned work, find a suitable live owner with `envoy_sessions` filtered by `title` or `dir`,
29
30
  route the issue to that exact session, and tell it what it owns. Routing hands the work to that