@sjawhar/pi-envoy 10.17.11 → 10.19.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.";
@@ -13947,9 +13948,10 @@ function documentOwnerValidation(requireArtifact, alwaysRequireArtifact = false)
13947
13948
  message: alwaysRequireArtifact ? "Exactly one of issue and project is required; artifact or ref must name the document." : "Exactly one of issue and project is required; with project, artifact names the document."
13948
13949
  };
13949
13950
  }
13951
+ var documentEditOwner = documentOwnerValidation(true);
13950
13952
  var documentEditValidation = {
13951
13953
  check: (value) => {
13952
- if (!documentOwnerValidation(true, true).check(value))
13954
+ if (!documentEditOwner.check(value))
13953
13955
  return false;
13954
13956
  const input = value;
13955
13957
  if (input.precondition === undefined)
@@ -13959,7 +13961,7 @@ var documentEditValidation = {
13959
13961
  const precondition = input.precondition;
13960
13962
  return typeof precondition.document === "string" !== Array.isArray(precondition.blocks);
13961
13963
  },
13962
- message: "Exactly one of issue and project is required; artifact or ref must name the document. A precondition selects exactly one of document or blocks."
13964
+ message: `${documentEditOwner.message} A precondition selects exactly one of document or blocks.`
13963
13965
  };
13964
13966
  var commentOwner = documentOwnerValidation(true);
13965
13967
  var commentValidation = {
@@ -13969,7 +13971,7 @@ var commentValidation = {
13969
13971
  },
13970
13972
  message: `${commentOwner.message} turn requires reply_to_ask.`
13971
13973
  };
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.";
13974
+ 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
13975
  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
13976
  var ISSUE_UPDATE_REASON_RULE = "reason goes only with status done";
13975
13977
  var ISSUE_UPDATE_RULE = `${ISSUE_UPDATE_FIELDS_RULE} ${ISSUE_UPDATE_DONE_RULE}; ${ISSUE_UPDATE_REASON_RULE}.`;
@@ -13983,7 +13985,7 @@ function issueUpdateProblem(value) {
13983
13985
  const status = typeof input.status === "string" ? `status ${input.status}` : "an update without status done";
13984
13986
  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
13987
  }
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";
13988
+ 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
13989
  return changesAField ? undefined : ISSUE_UPDATE_FIELDS_RULE;
13988
13990
  }
13989
13991
  var messageValidation = {
@@ -14068,10 +14070,10 @@ var dispatchToolSpecs = [
14068
14070
  name: "dispatch_issue_update",
14069
14071
  example: {
14070
14072
  issue: "DSP-1",
14071
- status: "done",
14072
- reason: "Shipped in owner/repo#7; verified on the production dashboard."
14073
+ priority: 1,
14074
+ before: "DSP-2"
14073
14075
  },
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}`,
14076
+ 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
14077
  arguments: (z2) => ({
14076
14078
  issue: z2.string().describe(ISSUE_REFERENCE),
14077
14079
  status: z2.enum(ISSUE_STATUSES).describe("New lifecycle status.").optional(),
@@ -14084,7 +14086,9 @@ var dispatchToolSpecs = [
14084
14086
  parent: z2.string().describe("Parent issue key in the same project; an empty string clears the parent.").optional(),
14085
14087
  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
14088
  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()
14089
+ 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(),
14090
+ 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(),
14091
+ 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
14092
  }),
14089
14093
  validation: {
14090
14094
  check: (value) => issueUpdateProblem(value) === undefined,
@@ -14096,7 +14100,7 @@ var dispatchToolSpecs = [
14096
14100
  {
14097
14101
  name: "dispatch_claim",
14098
14102
  example: { issue: "DSP-1" },
14099
- description: "Claim a Dispatch issue before you start implementing it, so no other session takes the same work, " + "and release it when you stop. Pass the issue alone to claim it, or --release to give it up. " + "Your claim records your own session and shows on every read of the issue: the dashboard header, the " + "issue list and board, dispatch read, and dispatch issues. Claiming is refused with 409 ISSUE_CLAIMED " + "when another session holds the issue and is still running; the refusal names that session, so talk to " + "it instead of working the same issue in parallel. When a human holds the claim the refusal names the " + "person, not a session: there is nothing running to message, so ask them on the issue rather than " + "taking it. 409 CLAIM_CONTENDED means the issue changed " + "hands twice while your call ran, so nothing was applied and nobody's liveness was checked: read " + "the issue and decide again. A claim whose session is no longer running may be " + "taken: the takeover is recorded on the issue and the session that lost it is told. A claim is not the " + "issue's status \u2014 claiming moves nothing, so also move the issue to in_progress with " + "dispatch issue-update when you start. A claim is released by its holder or any human, and by " + "any agent once the holder's session is no longer running. " + ISSUE_REFERENCE,
14103
+ description: "Claim a Dispatch issue before you start implementing it, so no other session takes the same work, " + "and release it when you stop. Pass the issue alone to claim it, or --release to give it up. " + "Your claim records your own session and shows on every read of the issue: the dashboard header, the " + "issue list and board, dispatch read, and dispatch issues. Claiming is refused with 409 ISSUE_CLAIMED " + "when another session holds the issue and is still running; the refusal names that session, so talk to " + "it instead of working the same issue in parallel. When a human holds the claim the refusal names the " + "person, not a session: there is nothing running to message, so ask them on the issue rather than " + "taking it. 409 CLAIM_CONTENDED means the issue changed " + "hands twice while your call ran, so nothing was applied and nobody's liveness was checked: read " + "the issue and decide again. A claim whose session is no longer running may be " + "taken: the takeover is recorded on the issue and the session that lost it is told. A claim is not the " + "issue's status \u2014 claiming moves nothing, and the result names who moves the status: you, with " + "dispatch issue-update, or Legion's daemon on an issue it runs, where a status written by anyone " + "else is undone. A claim is released by its holder or any human, and by " + "any agent once the holder's session is no longer running. " + ISSUE_REFERENCE,
14100
14104
  arguments: (z2) => ({
14101
14105
  issue: z2.string().describe(ISSUE_REFERENCE),
14102
14106
  release: z2.boolean().describe("Give up your claim instead of taking it; the issue's status does not change.").optional()
@@ -14273,7 +14277,7 @@ var dispatchToolSpecs = [
14273
14277
  arguments: (z2) => ({
14274
14278
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14275
14279
  project: z2.string().describe("Project key owning the document.").optional(),
14276
- artifact: z2.string().describe("Project document artifact id, slug, or filename; optional when ref names it.").optional(),
14280
+ artifact: z2.string().describe("Optional project document artifact id, slug, or filename; primary document by default for an issue.").optional(),
14277
14281
  ref: z2.string().describe("Optional dispatch:// issue or document reference.").optional(),
14278
14282
  ops: z2.array(z2.object({
14279
14283
  op: z2.enum(DOC_EDIT_OPS).describe("Edit operation."),
@@ -16260,7 +16264,7 @@ function renderAdvice(tool, key, advice, opts) {
16260
16264
  if (advice.decision_blocks === 0 && opts.isPrimarySpec === true && (tool === "dispatch_issue" || tool === "dispatch_artifact")) {
16261
16265
  lines.push('This spec holds no ask blocks, so nothing here reaches a human\'s inbox. Want human feedback? See the `dispatch` skill, "Decision blocks".');
16262
16266
  }
16263
- if (advice.issue_status === "triage" && tool !== "dispatch_issue" && !(tool === "dispatch_issue_update" && opts.setsStatus === true) && !triageAdviceShown.has(key)) {
16267
+ if (advice.issue_status === "triage" && advice.legion_managed_status !== true && tool !== "dispatch_issue" && !(tool === "dispatch_issue_update" && opts.setsStatus === true) && !triageAdviceShown.has(key)) {
16264
16268
  triageAdviceShown.add(key);
16265
16269
  lines.push(`${key} is still in triage \u2014 nobody can see its development status. See the \`dispatch\` skill, "Issue status is yours to move".`);
16266
16270
  }
@@ -16417,6 +16421,7 @@ function componentsChange(input, after) {
16417
16421
  }
16418
16422
  }
16419
16423
  var architectureComponentsAction = 'Review the `dispatch` skill, "Architecture components", to attach it to the parts it changes or mark it as non-architectural with a reason.';
16424
+ var LEGION_OWNS_STATUS = "Legion's daemon moves this issue's status on an issue it runs, so leave it alone \u2014 a status written here is undone.";
16420
16425
  async function architectureGuidance(client, project, components) {
16421
16426
  if (components.mode === "none" || components.ids.length > 0)
16422
16427
  return;
@@ -16734,8 +16739,8 @@ function innerRefProblem(tool, ref, refText) {
16734
16739
  if (tool === "dispatch_read" || tool === "dispatch_ask")
16735
16740
  return;
16736
16741
  const owner = refOwnerName(ref);
16737
- const documentTool = tool === "dispatch_suggest" || tool === "dispatch_doc_edit";
16738
- const ownerArguments = ref.owner.kind === "issue" ? `issue ${ref.owner.issue}${documentTool ? " and artifact <the document's slug>" : ""}` : `project ${ref.owner.project} and artifact ${ref.artifact}`;
16742
+ const suggestTool = tool === "dispatch_suggest";
16743
+ const ownerArguments = ref.owner.kind === "issue" ? `issue ${ref.owner.issue}${suggestTool ? " and artifact <the document's slug>" : ""}` : `project ${ref.owner.project} and artifact ${ref.artifact}`;
16739
16744
  const fix = tool === "dispatch_comment" && ref.kind === "ask" ? `reply under it with reply_to_ask ${ref.id} and ${ownerArguments}` : tool === "dispatch_comment" && ref.kind === "comment" ? `reply in its thread with reply_to ${ref.id} and ${ownerArguments}` : `read it with dispatch read, or name ${owner} with ${ownerArguments}`;
16740
16745
  const item = ref.kind === "ask" ? "an ask" : ref.kind === "log" ? "the event log" : ref.kind === "children" ? "the children" : `a ${ref.kind}`;
16741
16746
  return `ref ${refText} names ${item} on ${owner}; dispatch ${commandName(tool)} would act on ${owner} and drop the ${ref.kind}: ${fix}`;
@@ -17805,6 +17810,8 @@ async function executeDispatchTool(input) {
17805
17810
  const parent = optionalString(args, "parent");
17806
17811
  const components = optionalComponents(args, "components");
17807
17812
  const repo2 = optionalString(args, "repo");
17813
+ const rankBefore = optionalString(args, "before");
17814
+ const rankAfter = optionalString(args, "after");
17808
17815
  const priority = optionalPriority(args, "priority");
17809
17816
  const labels = Array.isArray(args.labels) ? args.labels : undefined;
17810
17817
  const blockedBy = Array.isArray(args.blocked_by) ? args.blocked_by : undefined;
@@ -17833,6 +17840,10 @@ async function executeDispatchTool(input) {
17833
17840
  const linked = before.external_links.map((link) => link.url);
17834
17841
  const newLinks = requestedLinks?.filter((url2) => !linked.includes(url2)) ?? [];
17835
17842
  const sentLinks = requestedLinks === undefined ? undefined : requestedLinks.length === 0 ? [] : [...before.external_links, ...newLinks.map((url2) => ({ url: url2 }))];
17843
+ const rank = rankBefore === undefined && rankAfter === undefined ? undefined : {
17844
+ ...rankBefore === undefined ? {} : { before: rankBefore },
17845
+ ...rankAfter === undefined ? {} : { after: rankAfter }
17846
+ };
17836
17847
  let after;
17837
17848
  try {
17838
17849
  after = await client.updateIssue(issueKey, {
@@ -17846,6 +17857,7 @@ async function executeDispatchTool(input) {
17846
17857
  ...components === undefined ? {} : { components },
17847
17858
  ...repo2 === undefined ? {} : { repo: repo2 === "" ? null : repo2 },
17848
17859
  ...sentLinks === undefined ? {} : { external_links: sentLinks },
17860
+ ...rank === undefined ? {} : { rank },
17849
17861
  actor
17850
17862
  });
17851
17863
  } catch (error48) {
@@ -17867,6 +17879,12 @@ async function executeDispatchTool(input) {
17867
17879
  ...title === undefined ? [] : [`title "${after.title}"`],
17868
17880
  ...labels === undefined ? [] : [after.labels.length === 0 ? "labels cleared" : `labels ${after.labels.join(", ")}`],
17869
17881
  ...priority === undefined ? [] : [after.priority === null ? "priority cleared" : `priority -> P${after.priority}`],
17882
+ ...rank === undefined ? [] : [
17883
+ `rank -> ${[
17884
+ ...rankAfter === undefined ? [] : [`after ${rankAfter}`],
17885
+ ...rankBefore === undefined ? [] : [`before ${rankBefore}`]
17886
+ ].join(", ")}`
17887
+ ],
17870
17888
  ...requestedLinks === undefined ? [] : [
17871
17889
  requestedLinks.length === 0 ? "external links cleared" : newLinks.length === 0 ? `already linked ${requestedLinks.join(", ")} ${linkCount}` : `linked ${newLinks.join(", ")} ${linkCount}`
17872
17890
  ],
@@ -17908,14 +17926,17 @@ async function executeDispatchTool(input) {
17908
17926
  throw refusalWithCode(error48);
17909
17927
  }
17910
17928
  const held = after.claim;
17929
+ const legionManaged = after.legion_managed_status === true;
17911
17930
  let text;
17912
17931
  if (release) {
17913
- text = `${issueKey}: claim released; nobody is working it now. Its status is still ${after.status} \u2014 move it yourself if that is no longer where the work is.`;
17932
+ const released = `${issueKey}: claim released; nobody is working it now. Its status is still ${after.status}`;
17933
+ text = legionManaged ? `${released}; ${LEGION_OWNS_STATUS}` : `${released} \u2014 move it yourself if that is no longer where the work is.`;
17914
17934
  } else {
17915
17935
  if (held === null) {
17916
17936
  throw new Error(`Dispatch claimed ${issueKey} but answered with no claim`);
17917
17937
  }
17918
- text = `${issueKey}: claimed by you since ${held.at}. Its status is ${after.status}; a claim moves nothing, so move it to in_progress with dispatch issue-update when you start, and release the claim when you stop.`;
17938
+ const claimed = `${issueKey}: claimed by you since ${held.at}. Its status is ${after.status}`;
17939
+ text = legionManaged ? `${claimed}; ${LEGION_OWNS_STATUS} Release the claim when you stop.` : `${claimed}; a claim moves nothing, so move it to in_progress with dispatch issue-update when you start, and release the claim when you stop.`;
17919
17940
  }
17920
17941
  return {
17921
17942
  text: `${text}${await notSubscribed(issueTopic(issueKey), input.subscriptions)}`,
@@ -18273,7 +18294,7 @@ ${await followsAsk(askOwner, input.subscriptions)}`,
18273
18294
  };
18274
18295
  }
18275
18296
  case "dispatch_doc_edit": {
18276
- const resolved = await resolveDocument(documentOwner(), stringArg(args, "artifact"));
18297
+ const resolved = await resolveDocument(documentOwner(), optionalString(args, "artifact"));
18277
18298
  const ops = args.ops;
18278
18299
  const summary = optionalString(args, "summary");
18279
18300
  const { precondition: rawPrecondition } = args;
package/dist/envoy.js CHANGED
@@ -29917,6 +29917,7 @@ function dispatchToolSchema(spec, z2, opts) {
29917
29917
  return spec.validation === undefined ? z2.object(shape, schemaOptions) : z2.refineObject(shape, spec.validation.check, spec.validation.refusal ?? spec.validation.message, schemaOptions);
29918
29918
  }
29919
29919
  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.";
29920
+ 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.";
29920
29921
  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.";
29921
29922
  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.";
29922
29923
  var ASK_OPTIONS_CONTRACT = "Options carry the genuinely different approaches. Each option has a label, and its description " + "says what that approach costs.";
@@ -29934,9 +29935,10 @@ function documentOwnerValidation(requireArtifact, alwaysRequireArtifact = false)
29934
29935
  message: alwaysRequireArtifact ? "Exactly one of issue and project is required; artifact or ref must name the document." : "Exactly one of issue and project is required; with project, artifact names the document."
29935
29936
  };
29936
29937
  }
29938
+ var documentEditOwner = documentOwnerValidation(true);
29937
29939
  var documentEditValidation = {
29938
29940
  check: (value) => {
29939
- if (!documentOwnerValidation(true, true).check(value))
29941
+ if (!documentEditOwner.check(value))
29940
29942
  return false;
29941
29943
  const input = value;
29942
29944
  if (input.precondition === undefined)
@@ -29946,7 +29948,7 @@ var documentEditValidation = {
29946
29948
  const precondition = input.precondition;
29947
29949
  return typeof precondition.document === "string" !== Array.isArray(precondition.blocks);
29948
29950
  },
29949
- message: "Exactly one of issue and project is required; artifact or ref must name the document. A precondition selects exactly one of document or blocks."
29951
+ message: `${documentEditOwner.message} A precondition selects exactly one of document or blocks.`
29950
29952
  };
29951
29953
  var commentOwner = documentOwnerValidation(true);
29952
29954
  var commentValidation = {
@@ -29956,7 +29958,7 @@ var commentValidation = {
29956
29958
  },
29957
29959
  message: `${commentOwner.message} turn requires reply_to_ask.`
29958
29960
  };
29959
- 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.";
29961
+ 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.";
29960
29962
  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";
29961
29963
  var ISSUE_UPDATE_REASON_RULE = "reason goes only with status done";
29962
29964
  var ISSUE_UPDATE_RULE = `${ISSUE_UPDATE_FIELDS_RULE} ${ISSUE_UPDATE_DONE_RULE}; ${ISSUE_UPDATE_REASON_RULE}.`;
@@ -29970,7 +29972,7 @@ function issueUpdateProblem(value) {
29970
29972
  const status = typeof input.status === "string" ? `status ${input.status}` : "an update without status done";
29971
29973
  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.`;
29972
29974
  }
29973
- 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";
29975
+ 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";
29974
29976
  return changesAField ? undefined : ISSUE_UPDATE_FIELDS_RULE;
29975
29977
  }
29976
29978
  var messageValidation = {
@@ -30055,10 +30057,10 @@ var dispatchToolSpecs = [
30055
30057
  name: "dispatch_issue_update",
30056
30058
  example: {
30057
30059
  issue: "DSP-1",
30058
- status: "done",
30059
- reason: "Shipped in owner/repo#7; verified on the production dashboard."
30060
+ priority: 1,
30061
+ before: "DSP-2"
30060
30062
  },
30061
- 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}`,
30063
+ 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}`,
30062
30064
  arguments: (z2) => ({
30063
30065
  issue: z2.string().describe(ISSUE_REFERENCE),
30064
30066
  status: z2.enum(ISSUE_STATUSES).describe("New lifecycle status.").optional(),
@@ -30071,7 +30073,9 @@ var dispatchToolSpecs = [
30071
30073
  parent: z2.string().describe("Parent issue key in the same project; an empty string clears the parent.").optional(),
30072
30074
  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(),
30073
30075
  components: componentsArgument(z2).optional(),
30074
- 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()
30076
+ 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(),
30077
+ 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(),
30078
+ 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()
30075
30079
  }),
30076
30080
  validation: {
30077
30081
  check: (value) => issueUpdateProblem(value) === undefined,
@@ -30083,7 +30087,7 @@ var dispatchToolSpecs = [
30083
30087
  {
30084
30088
  name: "dispatch_claim",
30085
30089
  example: { issue: "DSP-1" },
30086
- description: "Claim a Dispatch issue before you start implementing it, so no other session takes the same work, " + "and release it when you stop. Pass the issue alone to claim it, or --release to give it up. " + "Your claim records your own session and shows on every read of the issue: the dashboard header, the " + "issue list and board, dispatch read, and dispatch issues. Claiming is refused with 409 ISSUE_CLAIMED " + "when another session holds the issue and is still running; the refusal names that session, so talk to " + "it instead of working the same issue in parallel. When a human holds the claim the refusal names the " + "person, not a session: there is nothing running to message, so ask them on the issue rather than " + "taking it. 409 CLAIM_CONTENDED means the issue changed " + "hands twice while your call ran, so nothing was applied and nobody's liveness was checked: read " + "the issue and decide again. A claim whose session is no longer running may be " + "taken: the takeover is recorded on the issue and the session that lost it is told. A claim is not the " + "issue's status \u2014 claiming moves nothing, so also move the issue to in_progress with " + "dispatch issue-update when you start. A claim is released by its holder or any human, and by " + "any agent once the holder's session is no longer running. " + ISSUE_REFERENCE,
30090
+ description: "Claim a Dispatch issue before you start implementing it, so no other session takes the same work, " + "and release it when you stop. Pass the issue alone to claim it, or --release to give it up. " + "Your claim records your own session and shows on every read of the issue: the dashboard header, the " + "issue list and board, dispatch read, and dispatch issues. Claiming is refused with 409 ISSUE_CLAIMED " + "when another session holds the issue and is still running; the refusal names that session, so talk to " + "it instead of working the same issue in parallel. When a human holds the claim the refusal names the " + "person, not a session: there is nothing running to message, so ask them on the issue rather than " + "taking it. 409 CLAIM_CONTENDED means the issue changed " + "hands twice while your call ran, so nothing was applied and nobody's liveness was checked: read " + "the issue and decide again. A claim whose session is no longer running may be " + "taken: the takeover is recorded on the issue and the session that lost it is told. A claim is not the " + "issue's status \u2014 claiming moves nothing, and the result names who moves the status: you, with " + "dispatch issue-update, or Legion's daemon on an issue it runs, where a status written by anyone " + "else is undone. A claim is released by its holder or any human, and by " + "any agent once the holder's session is no longer running. " + ISSUE_REFERENCE,
30087
30091
  arguments: (z2) => ({
30088
30092
  issue: z2.string().describe(ISSUE_REFERENCE),
30089
30093
  release: z2.boolean().describe("Give up your claim instead of taking it; the issue's status does not change.").optional()
@@ -30260,7 +30264,7 @@ var dispatchToolSpecs = [
30260
30264
  arguments: (z2) => ({
30261
30265
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
30262
30266
  project: z2.string().describe("Project key owning the document.").optional(),
30263
- artifact: z2.string().describe("Project document artifact id, slug, or filename; optional when ref names it.").optional(),
30267
+ artifact: z2.string().describe("Optional project document artifact id, slug, or filename; primary document by default for an issue.").optional(),
30264
30268
  ref: z2.string().describe("Optional dispatch:// issue or document reference.").optional(),
30265
30269
  ops: z2.array(z2.object({
30266
30270
  op: z2.enum(DOC_EDIT_OPS).describe("Edit operation."),
@@ -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
 
@@ -26,23 +27,23 @@ dispatch claim --issue LEGION-234 --release # I have stopped; it is free
26
27
 
27
28
  ## Issue status is yours to move
28
29
 
29
- - Move it as a person moves a card, root and owned children: `in_progress`, `testing`, `needs_review`, then `done` once driven in production. A merge is not `done`. An issue left in `triage` while work runs is a defect. Waiting for the deploy lane is not a status and is never announced.
30
+ - Move it as a person moves a card, root and owned children: `in_progress`, `testing`, `needs_review`, then `done` once driven in production. A merge is not `done`. An issue left in `triage` while work runs is a defect. Waiting for the deploy lane is not a status and is never announced. Never the status of an issue with the `legion` label or under one: the daemon writes those, and a status you write there is undone.
30
31
  - `--status done` needs `--reason`. Dispatch posts it as a message, then closes. If the close fails after the post, read the status first. A retry points its reason at the posted message.
31
32
  - Clears differ: `--clear-priority`, `--parent ''` / `--route ''` / `--repo ''`, `--clear-external-links` / `--clear-blocked-by`. See `dispatch issue-update --help`.
32
33
  - Repository: own `--repo`, else nearest ancestor's, else the project default, else none. Unmapped is `REPO_NOT_MAPPED`. `dispatch read` shows `Repository:`. `Repository: unknown (…)` is an older server: a `--repo` write there fails.
33
34
  - Link the delivering PR with `--external-link` when you open it.
34
- - `… is still in triage …` on a write: move the status once work starts.
35
+ - `… is still in triage …` on a write: move the status once work starts. It never appears on an issue with the `legion` label or under one, since the daemon moves those out of triage itself.
35
36
 
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-envoy",
3
- "version": "10.17.11",
3
+ "version": "10.19.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "omp": {