@sjawhar/opencode-legion-envoy 8.22.0 → 8.23.1

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
@@ -13948,9 +13948,10 @@ function documentOwnerValidation(requireArtifact, alwaysRequireArtifact = false)
13948
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."
13949
13949
  };
13950
13950
  }
13951
+ var documentEditOwner = documentOwnerValidation(true);
13951
13952
  var documentEditValidation = {
13952
13953
  check: (value) => {
13953
- if (!documentOwnerValidation(true, true).check(value))
13954
+ if (!documentEditOwner.check(value))
13954
13955
  return false;
13955
13956
  const input = value;
13956
13957
  if (input.precondition === undefined)
@@ -13960,7 +13961,7 @@ var documentEditValidation = {
13960
13961
  const precondition = input.precondition;
13961
13962
  return typeof precondition.document === "string" !== Array.isArray(precondition.blocks);
13962
13963
  },
13963
- 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.`
13964
13965
  };
13965
13966
  var commentOwner = documentOwnerValidation(true);
13966
13967
  var commentValidation = {
@@ -14099,7 +14100,7 @@ var dispatchToolSpecs = [
14099
14100
  {
14100
14101
  name: "dispatch_claim",
14101
14102
  example: { issue: "DSP-1" },
14102
- 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,
14103
14104
  arguments: (z2) => ({
14104
14105
  issue: z2.string().describe(ISSUE_REFERENCE),
14105
14106
  release: z2.boolean().describe("Give up your claim instead of taking it; the issue's status does not change.").optional()
@@ -14276,7 +14277,7 @@ var dispatchToolSpecs = [
14276
14277
  arguments: (z2) => ({
14277
14278
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14278
14279
  project: z2.string().describe("Project key owning the document.").optional(),
14279
- 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(),
14280
14281
  ref: z2.string().describe("Optional dispatch:// issue or document reference.").optional(),
14281
14282
  ops: z2.array(z2.object({
14282
14283
  op: z2.enum(DOC_EDIT_OPS).describe("Edit operation."),
@@ -16263,7 +16264,7 @@ function renderAdvice(tool, key, advice, opts) {
16263
16264
  if (advice.decision_blocks === 0 && opts.isPrimarySpec === true && (tool === "dispatch_issue" || tool === "dispatch_artifact")) {
16264
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".');
16265
16266
  }
16266
- 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)) {
16267
16268
  triageAdviceShown.add(key);
16268
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".`);
16269
16270
  }
@@ -16420,6 +16421,7 @@ function componentsChange(input, after) {
16420
16421
  }
16421
16422
  }
16422
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.";
16423
16425
  async function architectureGuidance(client, project, components) {
16424
16426
  if (components.mode === "none" || components.ids.length > 0)
16425
16427
  return;
@@ -16737,8 +16739,8 @@ function innerRefProblem(tool, ref, refText) {
16737
16739
  if (tool === "dispatch_read" || tool === "dispatch_ask")
16738
16740
  return;
16739
16741
  const owner = refOwnerName(ref);
16740
- const documentTool = tool === "dispatch_suggest" || tool === "dispatch_doc_edit";
16741
- 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}`;
16742
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}`;
16743
16745
  const item = ref.kind === "ask" ? "an ask" : ref.kind === "log" ? "the event log" : ref.kind === "children" ? "the children" : `a ${ref.kind}`;
16744
16746
  return `ref ${refText} names ${item} on ${owner}; dispatch ${commandName(tool)} would act on ${owner} and drop the ${ref.kind}: ${fix}`;
@@ -17924,14 +17926,17 @@ async function executeDispatchTool(input) {
17924
17926
  throw refusalWithCode(error48);
17925
17927
  }
17926
17928
  const held = after.claim;
17929
+ const legionManaged = after.legion_managed_status === true;
17927
17930
  let text;
17928
17931
  if (release) {
17929
- 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.`;
17930
17934
  } else {
17931
17935
  if (held === null) {
17932
17936
  throw new Error(`Dispatch claimed ${issueKey} but answered with no claim`);
17933
17937
  }
17934
- 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.`;
17935
17940
  }
17936
17941
  return {
17937
17942
  text: `${text}${await notSubscribed(issueTopic(issueKey), input.subscriptions)}`,
@@ -18289,7 +18294,7 @@ ${await followsAsk(askOwner, input.subscriptions)}`,
18289
18294
  };
18290
18295
  }
18291
18296
  case "dispatch_doc_edit": {
18292
- const resolved = await resolveDocument(documentOwner(), stringArg(args, "artifact"));
18297
+ const resolved = await resolveDocument(documentOwner(), optionalString(args, "artifact"));
18293
18298
  const ops = args.ops;
18294
18299
  const summary = optionalString(args, "summary");
18295
18300
  const { precondition: rawPrecondition } = args;
@@ -13805,9 +13805,10 @@ function documentOwnerValidation(requireArtifact, alwaysRequireArtifact = false)
13805
13805
  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."
13806
13806
  };
13807
13807
  }
13808
+ var documentEditOwner = documentOwnerValidation(true);
13808
13809
  var documentEditValidation = {
13809
13810
  check: (value) => {
13810
- if (!documentOwnerValidation(true, true).check(value))
13811
+ if (!documentEditOwner.check(value))
13811
13812
  return false;
13812
13813
  const input = value;
13813
13814
  if (input.precondition === undefined)
@@ -13817,7 +13818,7 @@ var documentEditValidation = {
13817
13818
  const precondition = input.precondition;
13818
13819
  return typeof precondition.document === "string" !== Array.isArray(precondition.blocks);
13819
13820
  },
13820
- 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."
13821
+ message: `${documentEditOwner.message} A precondition selects exactly one of document or blocks.`
13821
13822
  };
13822
13823
  var commentOwner = documentOwnerValidation(true);
13823
13824
  var commentValidation = {
@@ -13956,7 +13957,7 @@ var dispatchToolSpecs = [
13956
13957
  {
13957
13958
  name: "dispatch_claim",
13958
13959
  example: { issue: "DSP-1" },
13959
- 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,
13960
+ 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,
13960
13961
  arguments: (z2) => ({
13961
13962
  issue: z2.string().describe(ISSUE_REFERENCE),
13962
13963
  release: z2.boolean().describe("Give up your claim instead of taking it; the issue's status does not change.").optional()
@@ -14133,7 +14134,7 @@ var dispatchToolSpecs = [
14133
14134
  arguments: (z2) => ({
14134
14135
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14135
14136
  project: z2.string().describe("Project key owning the document.").optional(),
14136
- artifact: z2.string().describe("Project document artifact id, slug, or filename; optional when ref names it.").optional(),
14137
+ artifact: z2.string().describe("Optional project document artifact id, slug, or filename; primary document by default for an issue.").optional(),
14137
14138
  ref: z2.string().describe("Optional dispatch:// issue or document reference.").optional(),
14138
14139
  ops: z2.array(z2.object({
14139
14140
  op: z2.enum(DOC_EDIT_OPS).describe("Edit operation."),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/opencode-legion-envoy",
3
- "version": "8.22.0",
3
+ "version": "8.23.1",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "dist/src/server.js",
@@ -27,12 +27,12 @@ dispatch claim --issue LEGION-234 --release # I have stopped; it is free
27
27
 
28
28
  ## Issue status is yours to move
29
29
 
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.
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.
31
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.
32
32
  - Clears differ: `--clear-priority`, `--parent ''` / `--route ''` / `--repo ''`, `--clear-external-links` / `--clear-blocked-by`. See `dispatch issue-update --help`.
33
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.
34
34
  - Link the delivering PR with `--external-link` when you open it.
35
- - `… 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.
36
36
 
37
37
  ## Priority is yours to set
38
38
 
@@ -250,11 +250,12 @@ stays out of the workflow.
250
250
  ## 3. Children complete
251
251
 
252
252
  No notice marks the last child's close as the end-game: each closure arrives as its own
253
- `child-closed`, and none is a reason to close the parent. Today's daemon does not order the root's
253
+ `child-closed`. Today's daemon does not order the root's
254
254
  own phases after its children: when the design gate opens it starts every admitted issue of the
255
- tree, the root included, so the root's tester and reviewer can run, and the root reach `accepting`,
256
- before any child merges; your acceptance is where the root waits for its children (section 7). To
257
- get parent integration evidence against the repository's default branch your addressing line names
255
+ tree, the root included, so the root's tester and reviewer run and the root reaches `accepting`
256
+ before any child merges; the daemon holds it there — it refuses a `sign_off` that runs ahead of
257
+ the issues beneath it (section 6), and a merged root's close waits for the tree (section 7).
258
+ To get parent integration evidence against the repository's default branch your addressing line names
258
259
  after the last child merges, file the parent's integration check as a final child whose acceptance
259
260
  is every parent criterion proven on that default branch, release it with `release_children` only
260
261
  once every other child has closed, and accept the root (section 6) only after it closes. This holds
@@ -262,23 +263,21 @@ until the redesign's
262
263
  integrating phase ships (dispatch://LEGION-223). When that check's tester fails, the daemon sends
263
264
  the child back to its implementer; a failure whose fix belongs in other work becomes a new
264
265
  corrective child wave you create and release, and the tree returns to children-in-flight. Do not
265
- downgrade the parent criterion or silently carry the failure forward. If the root stays `done`
266
- through its linger with a child still in the workflow, the daemon hands that child to Legion as a
267
- root of its own (`todo` with the `legion` label). It carries that child's phase, gate, phase records
268
- and pull request into a new generation and starts the role of that phase; that root's architect
269
- gets a catch-up that says `handed_over` and `carried` and resumes from the phase, never
270
- registering its spec or asking for approval again. A child whose old root had no gate carries
271
- none, and its catch-up says `handed_over` without `carried`. Its workers already run their phases,
272
- so its architect decomposes nothing. It reads the root's phase from the catch-up (for a root
273
- `held`, the phase its `held` notice names). Before `awaiting_merge` it registers its spec, the gate
274
- its READY waits for, and starts nothing on that gate's `design-approved`; a child carried in
275
- `accepting` is yours to accept (section 6). In `awaiting_merge` the READY is already through, so it
276
- registers no spec and asks no human to approve one: the merge closes the issue, and the production
277
- check after it is the implementer's. `awaiting_merge` can move back: a red or
278
- conflicting head returns the root to `implementing` and its next READY reads the gate again, so if
279
- a later `ready-refused` arrives because the head was sent back, it registers then, as that row
280
- says. The child stays its own tree after hand-over; reopening the old root later does not take it
281
- back. Park a child first when its work should stop with the root.
266
+ downgrade the parent criterion or silently carry the failure forward. A root that stays `done`
267
+ through its linger with a child still in the workflow — a person's close now (section 7) — has
268
+ that child handed to Legion as a root of its own (`todo` with
269
+ the `legion` label), its phase, gate, phase records and pull request carried into a new generation
270
+ and that phase's role started. Its architect's catch-up says `handed_over` and `carried`: it
271
+ resumes from the phase and never registers a spec or asks for approval again. Without a gate to
272
+ carry the catch-up says `handed_over` alone; its workers already run their phases, so it
273
+ decomposes nothing and reads the root's phase from the catch-up (for a root `held`, the phase its
274
+ `held` notice names). Before `awaiting_merge` it registers its spec, the gate its READY waits for,
275
+ and starts nothing on that gate's `design-approved`; a child carried in `accepting` is yours to
276
+ accept (section 6). In `awaiting_merge` the READY is through, so it registers none. `awaiting_merge` can
277
+ move back — a red or conflicting head returns the root to `implementing` and its next READY reads
278
+ the gate again — so a later `ready-refused` is when it registers, as that row says. The child
279
+ stays its own tree; reopening the old root does not take it back. Park a child first when its
280
+ work should stop with the root.
282
281
 
283
282
  ## 4. Integration verification
284
283
 
@@ -314,12 +313,15 @@ The reviewer's approval of the head moves the issue to `accepting` (its Dispatch
314
313
  notice with an `APPROVED` review is the cue. Read the evidence against every acceptance line of the
315
314
  spec and the plan: the tester's handoff (`.legion/<issue>/test.json` at the approved head), the PR
316
315
  body's `E2E (implementer)` and `E2E (tester)` lines, the reviewer's approval of that head by SHA
317
- (`gh api repos/<o>/<r>/pulls/<n>/reviews`), and, for a parent, that every child is `done`
318
- (section 3). Accept only when scope is fully met, integration evidence is current, corrective work
319
- is complete, review is clean, and no necessary work was silently deferred. Post the acceptance as a
316
+ (`gh api repos/<o>/<r>/pulls/<n>/reviews`). Accept only when scope is fully met, integration
317
+ evidence is current, corrective work is complete, review is clean, and no necessary work was
318
+ silently deferred. Post the acceptance as a
320
319
  `dispatch comment` explicit about that evidence, then accept with `sign_off`, which runs only in
321
320
  `accepting` (refused `SIGNOFF_OUTSIDE_ACCEPTING` elsewhere) and moves the issue to `retro` with the
322
- status `retro`:
321
+ status `retro`. The daemon keeps that order: `sign_off` is refused 409
322
+ `SIGNOFF_CHILD_IN_WORKFLOW` while any issue beneath this one is still in the workflow, naming each
323
+ of them; accept once the last of them is done, or take one out with `park_child`. Only what is
324
+ *beneath* counts: a child with nothing beneath it is accepted while its own root runs.
323
325
 
324
326
  ```text
325
327
  dispatch comment --issue LEGION-40 --body-file - <<'EOF'
@@ -334,8 +336,7 @@ fails; the daemon starts that phase's role with the reason in its task and count
334
336
  as a reviewer's request for changes does. A `handoff_complete` from you in `accepting` is refused
335
337
  (`ACCEPTANCE_IS_SIGN_OFF`). CI red at the head in `accepting` returns the issue to `implementing`
336
338
  by the daemon's table, as in review. No `Production:` line is asked here: the production check is
337
- the implementer's after the merge closes the issue (section 7), and nothing after the merge is
338
- yours.
339
+ the implementer's, after the close (section 7).
339
340
 
340
341
  The daemon keeps this order from its fixed table; you start none of its steps:
341
342
 
@@ -368,9 +369,8 @@ The daemon keeps this order from its fixed table; you start none of its steps:
368
369
  back to `reviewing`, the move's reason naming the code and what it found, so the reviewer runs a
369
370
  new round. A READY refused because the head still carries `.legion/<issue>/` goes back to
370
371
  `retro`, so the implementer's retro removes it.
371
- 5. the merge lands under the repository's rules and closes the issue (section 7). The implementer's
372
- production check follows the close, in its own session and on the pull request; you are asked
373
- nothing for it, and a defect it finds is a `dispatch ask`, never an issue.
372
+ 5. the merge lands under the repository's rules and closes the issue — a root's close only once
373
+ the rest of its tree has left the workflow (section 7).
374
374
 
375
375
  What returns the tree to review: a changed diff — a commit above the approved head that
376
376
  touches anything outside `docs/solutions/` and the issue's `.legion/<issue>/`, or a
@@ -409,52 +409,28 @@ role topic, or its review body); the reviewer resolves none.
409
409
 
410
410
  The merge closes the issue: when GitHub reports the pull request merged while the issue is in
411
411
  `awaiting_merge` (a merge before that closes it the moment it gets there), the daemon writes
412
- `done`. A child's close reaches you as `child-closed`; the daemon retires five of its six claims,
413
- suspends its implementer with its session kept, and sets its Sandbox `Suspended` with its volume,
414
- because the implementer still owes the production check: handed to it in its live session at the
415
- merge, it is work after the close that holds no pod while it waits for the deploy
416
- (`skill://legion-worker/references/merge-gate.md`, *The production check after the close*). A
417
- root's close writes `done`, frees the tree's admission slot, and lingers the tree for
418
- `linger_hours` with every member suspended; a child still in the workflow at the end of the linger
419
- is handed over (section 3), and the tree's close waits for a member's open check.
420
-
421
- Nothing of the check is yours: you post no sign-off after the merge and wait for no record. Its
422
- record lives on the pull request alone, since Dispatch refuses a write on a closed issue; a defect
423
- it finds is a `dispatch ask`, never an issue; and
412
+ `done`. A root is the exception: a merge landing while another issue of its tree is still in the
413
+ workflow is recorded and the root stays in `awaiting_merge` — nothing suspended, no linger, slot
414
+ and workers kept — until the last of them is done or parked, when it closes. One `pr-merged`
415
+ notice names what it waits for and none follows as they depart, so read the pull request on
416
+ GitHub. A child's close reaches you as `child-closed`: the daemon retires five of its six claims
417
+ and suspends its implementer, session kept and Sandbox `Suspended` with its volume, since it still
418
+ owes the production check handed to it at the merge — work after the close that holds no pod while
419
+ it waits for the deploy (`skill://legion-worker/references/merge-gate.md`, *The production check
420
+ after the close*). A root's close writes `done`, frees the tree's admission slot and lingers the
421
+ tree for `linger_hours` with every member suspended; a child still in the workflow at the linger's
422
+ end is handed over (section 3), and the tree's close waits for a member's open check.
423
+
424
+ Nothing of the check is yours. Its record lives on the pull request alone, since Dispatch refuses
425
+ a write on a closed issue; a defect it finds is a `dispatch ask`, never an issue; and
424
426
  `read_record` shows a closed issue's open check (`check`: the pull request, the merge commit, the
425
- looks, what woke the last one and when the next is due). A parent waits in `accepting` until
426
- every child is `done` (section 3): accepted earlier it would merge before its children, the state
427
- dispatch://LEGION-703 has the daemon rule out.
427
+ looks, what woke the last one and when the next is due).
428
428
 
429
429
  ## Wake routing
430
430
 
431
- Handle one delivered wake by verifying the relevant live artifact and then performing the
432
- corresponding lifecycle procedure. Architect-addressed wakes always reach the architect that owns
433
- the payload issue: a claimed child sub-architect, otherwise the nearest claimed ancestor, then the
434
- root. A wake about an architect role's own launch reaches the architect above it. `phase-finished`,
435
- worker lifecycle, child lifecycle, and design-gate wakes are architect-only and never go to an
436
- active phase worker.
437
-
438
- | Wake | Procedure |
439
- | --- | --- |
440
- | `carried-gate-opened` | Only a root whose catch-up said `carried` gets it. Its `version` and `reason`: a human approved the old root's spec document at `version`, which opened the gate this root carried; the daemon has released any READY it refused and started any child waiting on it. It is not `design-approved`: do not proceed to section 2 or release a wave because of it, and start nothing. |
441
- | `child-status` | A child of your tree left the workflow or re-entered it; the notice's reason names the child and its new status. `todo` (a human's move, your `release_children`, or your `rerun_child`) means the child runs again under your tree from planning, and the daemon starts it; you start nothing. `backlog`, `icebox` or `triage` (a human's move, or your `park_child`) means the daemon has suspended the child's workers, and it advances no further until it is set back to `todo`. What the rest of the tree does is your decision. |
442
- | `child-closed` | Read the child completion and remaining open children. Re-scope open work, park obsolete work with `park_child`, and release an appropriate next wave with `release_children`. When a worker waits on this child for a missing surface (see `phase-finished`), tell it to continue with `envoy_publish` to its role topic. The last child's close is not the end-game (section 3). |
443
- | `child-release-stalled` | Its `reason` names a child your `release_children` released while Dispatch already showed it `todo`, and that child's parent, whose release has not reached the daemon after three tries. Read the parent's record with the `legion` tool's `read_record` (`issue` the parent); run `release_children` for the child again once the parent is recorded in your tree, and tell the controller if it never is. |
444
- | `child-not-recorded` | Its `reason` names a child your tree did not record and why: its repository resolves to none (neither it nor an ancestor names one, and the project has no default), or to one not mapped to the project, or to one the daemon leaves out for a missing GitHub App. The daemon said why on the child too. Set the child's repository to one of the project's (`dispatch issue-update --repo owner/name`), then release it again with `release_children`; tell the controller when the repository it needs is not served. |
445
- | `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. |
446
- | `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. |
447
- | `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 issue is in `accepting` and the acceptance is yours (section 6): read the evidence, then `sign_off` or send it back with `request_backward_move`. 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. |
448
- | `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`, `accepting`, `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 `accepting`, where no worker holds a phase, the move is yours: `request_backward_move` with `issue`, naming the failing checks. 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; your own send-back from `accepting` by the reason you gave. 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 rounds the cap is for. Each `review_round_cap` notice is a decision you make in the turn it wakes, never one left for the next round to make: read what every counted round found, choose one of the three below, act on it in the same turn, and post it as a `dispatch comment` on the issue naming the rounds you read and the choice. **Narrow**: the change itself is sound but each round finds more around it; tell the implementer and the reviewer (`envoy_publish` to each one's role topic) which acceptance lines the next round answers; a correctness finding is still fixed in this pull request, and only naming, duplication or wording cleanup moves to the `Fast-follow:` line. **Split**: part of the work cannot settle in this pull request. On a child, file that part as a child of this issue's parent, never of this one (a parent waits in `accepting` for every child, and the split part usually builds on this pull request's unmerged code): `dispatch issue --project <project> --parent <parent> --title '<outcome>' --blocked-by <this issue> --spec-file -`, released with `release_children` once this issue closes. On the root, narrow it to what settles here and open a `dispatch ask` for the rest (`skill://legion-worker/references/findings.md`): a person decides whether that becomes an issue. Tell the implementer and the reviewer that this pull request no longer carries it. **Accept the rounds**: each round found less than the one before it, and the last found no defect in the change itself (a stale line, a comment that states an old default); let the loop run and say why in the comment. A later notice is a new decision: accept again only when its round found less than the round you last accepted, and otherwise narrow or split. A finding that changes what the spec accepts is a decision block in the root spec (section 1), and `dispatch ask` is only for a decision that needs a human's authority; neither replaces the choice. 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. |
449
- | `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 merge closes the issue the moment it reaches `awaiting_merge` (section 7), and you start nothing; the production check after the close is the implementer's, on the pull request, and asks nothing of you. Until then the phases run as they would, your acceptance included. |
450
- | `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. |
451
- | `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 closes the issue. Never bypass a rule. |
452
- | `merge-stalled` | Its `reason` names the pull request, the state and head GitHub shows (closed, or another head), since when GitHub has shown it, and READY's head, which the record still awaits. GitHub's last change to the pull request is more than 10 minutes old and the record never heard of it, so its webhook never reached the daemon: the daemon has stopped submitting that merge, and the issue stays in `awaiting_merge`. Read the pull request on GitHub. Open at READY's head, GitHub has caught up: a person merges it under the repository's rules, and the merge closes the issue. Closed, or at another head the daemon never recorded: ask a person with `dispatch ask`, naming the pull request, what GitHub shows, and READY's head. Never bypass a rule. |
453
- | `merge-removed` | Its `reason` names who took the pull request out of the merge queue or turned its auto-merge off at READY's head after the daemon submitted it, or says GitHub turned it off on its own (over a repository setting such as a merge method the repository blocks) and names the App that submitted the merge, which GitHub names as the actor of its own disables; then what was removed, GitHub's reason (`manual`, `merge_conflict`, `manually_disabled`, `merge_commit_blocked`, …), when, and READY's head. It was not a failed merge, which comes as a `checks-red` instead. The daemon submits that merge no further, so the removal stands, and the issue stays in `awaiting_merge`. Read the pull request on GitHub. A `merge_conflict` conflicts with the base or with a pull request queued ahead of it: once the conflict is on the base, the daemon's own conflict check sends the issue back to `implementing` by itself. Otherwise ask a person with `dispatch ask` (the one who removed it, when the reason names a person rather than GitHub on its own), naming the pull request at READY's head and the reason; that person merges or queues it again under the repository's rules, and the merge closes the issue. Never bypass a rule. |
454
- | A reply on an ask you follow | Interpret the reply in the issue's design context. Answer in its thread (`dispatch comment --reply-to <comment id>`; under an open ask whose next move is yours, such as the approval request you must revise or hand back, `--reply-to-ask <ask id> --turn agent`, since a default-turn reply hands that request back to the human and a corrected `--summary` is then refused), then adjust the plan or relay it via `envoy_publish` to the responsible worker's role token; scope and product decisions remain with you. |
455
- | `worker-died` | Its `role` and `phase`. That role's claim failed: its launches or prompts ran out. For a phase worker the daemon holds the issue (phase `held`) and starts nothing more on it. Reassess the work, then decide with `retry_or_escalate`: `retry` when the failure looks agent-specific or transient, `escalate` to hand the held issue to the controller when it looks environmental. |
456
- | `held` | Its `role` and `phase` (the phase the issue left), or `phase` with `reason: "escalated"`. Without `reason`, that phase's worker ran out of launches or prompts: the issue is held (phase `held`), the `worker-died` that comes with it is yours to answer with `retry_or_escalate` as that row says, and the controller hears of the hold too. With `reason: "escalated"`, it records your own `escalate`: the controller has the issue now, and you start nothing for it. |
457
- | `ready-refused` | Its `version` and `reason`. The merger's READY was refused, and its packet is kept with the completion so no second READY is ever needed. With `version` greater than 0, `reason` reads `READY refused: approve design version <N> before requesting READY.`: the root's spec is already registered at that version, so get it approved (section 1), and the daemon advances the merge and posts the kept packet the moment it is. With `version` 0, `reason` reads `READY refused: no design version is approved; register and approve the tree's spec before requesting READY.`: the root has no spec registered yet, so register it and get it approved (section 1) exactly as above; the daemon releases the kept packet the same way — at registration itself when that opens the gate (`gates.design: off`, or Dispatch already shows the version approved), otherwise the moment a human approves the version you registered. Either way, nothing else is needed once the gate opens. On a root whose catch-up said `carried`, the gate is a copy on the old root's spec document: run nothing of section 1; tell the controller the READY waits on a human's approval of that document, and `carried-gate-opened` follows the approval. A `reason` starting `READY_PACKET_MISSING` names a READY refused before the daemon kept packets: that READY is void and the issue stays in merging, so start it over (`park_child` then `rerun_child`, for a child) or end it. |
431
+ Handle every delivered wake from the table in
432
+ `skill://legion-architect/references/wake-routing.md`: read it when a wake arrives, and follow
433
+ the row for that wake.
458
434
 
459
435
  ## Escalation judgment
460
436
 
@@ -0,0 +1,29 @@
1
+ ## Wake routing
2
+
3
+ Handle one delivered wake by verifying the relevant live artifact and then performing the
4
+ corresponding lifecycle procedure. Architect-addressed wakes always reach the architect that owns
5
+ the payload issue: a claimed child sub-architect, otherwise the nearest claimed ancestor, then the
6
+ root. A wake about an architect role's own launch reaches the architect above it. `phase-finished`,
7
+ worker lifecycle, child lifecycle, and design-gate wakes are architect-only and never go to an
8
+ active phase worker.
9
+
10
+ | Wake | Procedure |
11
+ | --- | --- |
12
+ | `carried-gate-opened` | Only a root whose catch-up said `carried` gets it. Its `version` and `reason`: a human approved the old root's spec document at `version`, which opened the gate this root carried; the daemon has released any READY it refused and started any child waiting on it. It is not `design-approved`: do not proceed to section 2 or release a wave because of it, and start nothing. |
13
+ | `child-status` | A child of your tree left the workflow or re-entered it; the notice's reason names the child and its new status. `todo` (a human's move, your `release_children`, or your `rerun_child`) means the child runs again under your tree from planning, and the daemon starts it; you start nothing. `backlog`, `icebox` or `triage` (a human's move, or your `park_child`) means the daemon has suspended the child's workers, and it advances no further until it is set back to `todo`. What the rest of the tree does is your decision. |
14
+ | `child-closed` | Read the child completion and remaining open children. Re-scope open work, park obsolete work with `park_child`, and release an appropriate next wave with `release_children`. When a worker waits on this child for a missing surface (see `phase-finished`), tell it to continue with `envoy_publish` to its role topic. The last child's close is not the end-game (section 3). |
15
+ | `child-release-stalled` | Its `reason` names a child your `release_children` released while Dispatch already showed it `todo`, and that child's parent, whose release has not reached the daemon after three tries. Read the parent's record with the `legion` tool's `read_record` (`issue` the parent); run `release_children` for the child again once the parent is recorded in your tree, and tell the controller if it never is. |
16
+ | `child-not-recorded` | Its `reason` names a child your tree did not record and why: its repository resolves to none (neither it nor an ancestor names one, and the project has no default), or to one not mapped to the project, or to one the daemon leaves out for a missing GitHub App. The daemon said why on the child too. Set the child's repository to one of the project's (`dispatch issue-update --repo owner/name`), then release it again with `release_children`; tell the controller when the repository it needs is not served. |
17
+ | `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. |
18
+ | `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. |
19
+ | `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 issue is in `accepting` and the acceptance is yours (section 6): read the evidence, then `sign_off` or send it back with `request_backward_move`. 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. |
20
+ | `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`, `accepting`, `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 `accepting`, where no worker holds a phase, the move is yours: `request_backward_move` with `issue`, naming the failing checks. 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; your own send-back from `accepting` by the reason you gave. 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 rounds the cap is for. Each `review_round_cap` notice is a decision you make in the turn it wakes, never one left for the next round to make: read what every counted round found, choose one of the three below, act on it in the same turn, and post it as a `dispatch comment` on the issue naming the rounds you read and the choice. **Narrow**: the change itself is sound but each round finds more around it; tell the implementer and the reviewer (`envoy_publish` to each one's role topic) which acceptance lines the next round answers; a correctness finding is still fixed in this pull request, and only naming, duplication or wording cleanup moves to the `Fast-follow:` line. **Split**: part of the work cannot settle in this pull request. On a child, file that part as a child of this issue's parent, never of this one (a parent waits in `accepting` for every child, and the split part usually builds on this pull request's unmerged code): `dispatch issue --project <project> --parent <parent> --title '<outcome>' --blocked-by <this issue> --spec-file -`, released with `release_children` once this issue closes. On the root, narrow it to what settles here and open a `dispatch ask` for the rest (`skill://legion-worker/references/findings.md`): a person decides whether that becomes an issue. Tell the implementer and the reviewer that this pull request no longer carries it. **Accept the rounds**: each round found less than the one before it, and the last found no defect in the change itself (a stale line, a comment that states an old default); let the loop run and say why in the comment. A later notice is a new decision: accept again only when its round found less than the round you last accepted, and otherwise narrow or split. A finding that changes what the spec accepts is a decision block in the root spec (section 1), and `dispatch ask` is only for a decision that needs a human's authority; neither replaces the choice. 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. |
21
+ | `pr-merged` | The pull request merged and the issue did not close; its `reason` says why. Merged before the issue reached `awaiting_merge`: the workflow runs on, and the merge closes the issue the moment it gets there, your acceptance and retro first. Merged while the rest of the tree still runs: the reason names the pull request and every issue the close waits for, and the root waits in `awaiting_merge`, slot and workers kept, until the last of them is done or parked (section 7). Either way you start nothing and no further notice follows. |
22
+ | `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. |
23
+ | `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 closes the issue. Never bypass a rule. |
24
+ | `merge-stalled` | Its `reason` names the pull request, the state and head GitHub shows (closed, or another head), since when GitHub has shown it, and READY's head, which the record still awaits. GitHub's last change to the pull request is more than 10 minutes old and the record never heard of it, so its webhook never reached the daemon: the daemon has stopped submitting that merge, and the issue stays in `awaiting_merge`. Read the pull request on GitHub. Open at READY's head, GitHub has caught up: a person merges it under the repository's rules, and the merge closes the issue. Closed, or at another head the daemon never recorded: ask a person with `dispatch ask`, naming the pull request, what GitHub shows, and READY's head. Never bypass a rule. |
25
+ | `merge-removed` | Its `reason` names who took the pull request out of the merge queue or turned its auto-merge off at READY's head after the daemon submitted it, or says GitHub turned it off on its own (over a repository setting such as a merge method the repository blocks) and names the App that submitted the merge, which GitHub names as the actor of its own disables; then what was removed, GitHub's reason (`manual`, `merge_conflict`, `manually_disabled`, `merge_commit_blocked`, …), when, and READY's head. It was not a failed merge, which comes as a `checks-red` instead. The daemon submits that merge no further, so the removal stands, and the issue stays in `awaiting_merge`. Read the pull request on GitHub. A `merge_conflict` conflicts with the base or with a pull request queued ahead of it: once the conflict is on the base, the daemon's own conflict check sends the issue back to `implementing` by itself. Otherwise ask a person with `dispatch ask` (the one who removed it, when the reason names a person rather than GitHub on its own), naming the pull request at READY's head and the reason; that person merges or queues it again under the repository's rules, and the merge closes the issue. Never bypass a rule. |
26
+ | A reply on an ask you follow | Interpret the reply in the issue's design context. Answer in its thread (`dispatch comment --reply-to <comment id>`; under an open ask whose next move is yours, such as the approval request you must revise or hand back, `--reply-to-ask <ask id> --turn agent`, since a default-turn reply hands that request back to the human and a corrected `--summary` is then refused), then adjust the plan or relay it via `envoy_publish` to the responsible worker's role token; scope and product decisions remain with you. |
27
+ | `worker-died` | Its `role` and `phase`. That role's claim failed: its launches or prompts ran out. For a phase worker the daemon holds the issue (phase `held`) and starts nothing more on it. Reassess the work, then decide with `retry_or_escalate`: `retry` when the failure looks agent-specific or transient, `escalate` to hand the held issue to the controller when it looks environmental. |
28
+ | `held` | Its `role` and `phase` (the phase the issue left), or `phase` with `reason: "escalated"`. Without `reason`, that phase's worker ran out of launches or prompts: the issue is held (phase `held`), the `worker-died` that comes with it is yours to answer with `retry_or_escalate` as that row says, and the controller hears of the hold too. With `reason: "escalated"`, it records your own `escalate`: the controller has the issue now, and you start nothing for it. |
29
+ | `ready-refused` | Its `version` and `reason`. The merger's READY was refused, and its packet is kept with the completion so no second READY is ever needed. With `version` greater than 0, `reason` reads `READY refused: approve design version <N> before requesting READY.`: the root's spec is already registered at that version, so get it approved (section 1), and the daemon advances the merge and posts the kept packet the moment it is. With `version` 0, `reason` reads `READY refused: no design version is approved; register and approve the tree's spec before requesting READY.`: the root has no spec registered yet, so register it and get it approved (section 1) exactly as above; the daemon releases the kept packet the same way — at registration itself when that opens the gate (`gates.design: off`, or Dispatch already shows the version approved), otherwise the moment a human approves the version you registered. Either way, nothing else is needed once the gate opens. On a root whose catch-up said `carried`, the gate is a copy on the old root's spec document: run nothing of section 1; tell the controller the READY waits on a human's approval of that document, and `carried-gate-opened` follows the approval. A `reason` starting `READY_PACKET_MISSING` names a READY refused before the daemon kept packets: that READY is void and the issue stays in merging, so start it over (`park_child` then `rerun_child`, for a child) or end it. |
@@ -218,9 +218,12 @@ reviewing until you finish.
218
218
 
219
219
  - **The merge closes the issue; the production check is the implementer's work after the close.**
220
220
  When the pull request merges, the daemon writes `done` and, in the implementer's session, still
221
- live, hands it the check task. Its first line is `Production check of <KEY>: <title>.`, and it
222
- names the repository, the pull request, the merge commit, the look's number, what woke the look
223
- (`the merge`, for the first) and whether it is the last. A resumed session tells the check task
221
+ live, hands it the check task. On a root whose tree still runs the close, and so the check task,
222
+ comes later: the merge is recorded, the root waits in `awaiting_merge` until the last other
223
+ issue of its tree is done or parked, and the check opens with that close. Its first line is
224
+ `Production check of <KEY>: <title>.`, and it names the repository, the pull request, the merge
225
+ commit, the look's number, what woke the look (`the merge`, for the first) and whether it is the
226
+ last. A resumed session tells the check task
224
227
  from its last phase task by that first line. The check carries no phase and no handoff: a look
225
228
  ends with `handoff_complete` carrying a `verdict` and a `summary`, no commit, and the daemon
226
229
  refuses a report on an issue with no open check (`CHECK_NOT_OPEN`), on a look already ended