@sjawhar/claude-envoy 1.8.2 → 1.9.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.
@@ -2,5 +2,5 @@
2
2
  "name": "claude-envoy",
3
3
  "description": "Deliver Envoy events as Claude Code channel notifications and expose Envoy MCP tools.",
4
4
  "mcpServers": "./.mcp.json",
5
- "version": "1.8.2"
5
+ "version": "1.9.0"
6
6
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-envoy",
3
- "version": "1.8.2",
3
+ "version": "1.9.0",
4
4
  "description": "Deliver Envoy events as Claude Code channel notifications and expose Envoy MCP tools.",
5
5
  "mcpServers": {}
6
6
  }
@@ -51890,6 +51890,7 @@ var envoyToolSchemas = new Map;
51890
51890
  var nonEmptyString = string2().min(1);
51891
51891
  var issueKey = string2().regex(DISPATCH_KEY_PATTERN);
51892
51892
  var timestamp = exports_iso.datetime({ offset: true });
51893
+ var LegionInstant = timestamp.regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/, { error: "an RFC 3339 time with seconds, as 2026-11-02T09:00:00Z is" });
51893
51894
  var LEGION_WORKFLOW_PHASES = [
51894
51895
  "admitted",
51895
51896
  "awaiting_person",
@@ -52006,6 +52007,7 @@ var legionCheckView = object({
52006
52007
  pullRequest: number2().int().positive(),
52007
52008
  mergeCommit: string2(),
52008
52009
  openedAt: timestamp,
52010
+ role: nonEmptyString,
52009
52011
  looks: number2().int().nonnegative(),
52010
52012
  lookOpen: boolean2(),
52011
52013
  lastWake: legionCheckWakeView.optional(),
@@ -52244,7 +52246,7 @@ var legionHoldRecord = object({
52244
52246
  var LegionReleaseRecordsRequest = strictObject({
52245
52247
  claimToken: nonEmptyString,
52246
52248
  secret: nonEmptyString,
52247
- since: timestamp
52249
+ since: LegionInstant
52248
52250
  });
52249
52251
  var LegionReleaseRecordsResponse = object({
52250
52252
  releases: array(legionReleaseRecord),
@@ -52351,7 +52353,8 @@ var LegionHandoffCompleteRequest = strictObject({
52351
52353
  ready: boolean2(),
52352
52354
  commit: nonEmptyString.optional(),
52353
52355
  handoffId: nonEmptyString.optional(),
52354
- handoffVersion: number2().int().positive().optional()
52356
+ handoffVersion: number2().int().positive().optional(),
52357
+ wakeAt: LegionInstant.optional()
52355
52358
  });
52356
52359
  var LegionIssueStatusRequest = strictObject({
52357
52360
  grantId: nonEmptyString,
@@ -55090,9 +55093,9 @@ var import_nats2 = __toESM(require_mod4(), 1);
55090
55093
 
55091
55094
  // package.json
55092
55095
  var name = "@sjawhar/claude-envoy";
55093
- var version2 = "1.8.2";
55096
+ var version2 = "1.9.0";
55094
55097
  var legion = {
55095
- daemonApiVersion: 53
55098
+ daemonApiVersion: 54
55096
55099
  };
55097
55100
 
55098
55101
  // src/channel-forwarder.ts
@@ -56835,7 +56838,9 @@ var requiredString = (op, field) => {
56835
56838
  const message = `${op} requires ${field}`;
56836
56839
  return string2({ error: message }).refine((value) => value.trim() !== "", { error: message });
56837
56840
  };
56838
- var windowStart = requiredString("read_release_records", "since").refine((value) => value.trim() === "" || exports_iso.datetime({ offset: true }).safeParse(value).success, { error: "read_release_records' since is an RFC 3339 time, as 2026-11-02T09:00:00Z is" });
56841
+ var rfc3339String = (op, field, message) => requiredString(op, field).refine((value) => value.trim() === "" || LegionInstant.safeParse(value).success, { error: message });
56842
+ var windowStart = rfc3339String("read_release_records", "since", "read_release_records' since is an RFC 3339 time, as 2026-11-02T09:00:00Z is");
56843
+ var nextLook = rfc3339String("handoff_complete", "wake_at", "handoff_complete's wake_at is an RFC 3339 time, as 2026-11-02T09:00:00Z is");
56839
56844
  var operation = (op, shape) => strictObject({ op: literal(op), ...shape }, {
56840
56845
  error: (issue) => issue.code === "unrecognized_keys" ? issue.keys.map((key) => `${op} does not accept field "${key}"`).join("; ") : undefined
56841
56846
  });
@@ -56849,7 +56854,8 @@ var PhaseOpArguments = discriminatedUnion("op", [
56849
56854
  verdict: _enum2(["pass", "pending", "fail"], {
56850
56855
  error: "handoff_complete's verdict is pass, pending or fail"
56851
56856
  }).optional(),
56852
- ready: boolean2({ error: "handoff_complete's ready is true or false" }).optional()
56857
+ ready: boolean2({ error: "handoff_complete's ready is true or false" }).optional(),
56858
+ wake_at: nextLook.optional()
56853
56859
  }),
56854
56860
  operation("request_backward_move", {
56855
56861
  issue: string2().optional(),
@@ -56892,7 +56898,7 @@ var PHASE_OP_SENTENCES = {
56892
56898
  requestBackwardMove: "request_backward_move returns an issue to an earlier phase (`to`, `reason`): a phase worker moves its own issue; " + "the tree's architect moves an issue in accepting, named by `issue`, back to implementing or testing, which counts a review round. "
56893
56899
  };
56894
56900
  function legionToolDescription(host) {
56895
- return "Perform the workflow operation the Legion daemon assigned this role. The daemon advances phases; this tool cannot spawn workers. " + (host.before ?? "") + "handoff_complete (phase workers and sub-architects; the daemon accepts a completion only from the role working the issue's current phase, " + "and the implementer's report on a look of its closed issue's production check) reports this phase complete: `summary` (two sentences for the " + "architect, or the merger's READY packet), `verdict` pass|fail from the tester alone on a phase completion (the daemon refuses a verdict from " + "any other role there) and pass|pending|fail from the implementer on a production check report (pending: production does not carry the merge " + "yet), `ready: true` for the merger's READY. A planning, implementing, testing or reviewing phase ends with a handoff record on the " + "Dispatch issue: write it with `dispatch handoff-write --issue <issue> --phase <plan|implement|test|review> --body-file <file>` before " + "calling. This call reads the phase's newest record from Dispatch and reports it; the implementer's round also reports the commit " + "`legion/<issue>@origin` names, so push the branch first; retro and the merger's READY report the commit the workspace stands on. It " + "refuses, posting nothing, a phase with no record, a Dispatch that does not answer, or an implementer's `@-` not yet on " + "`legion/<issue>@origin`, each naming the remedy. A production check report " + "(the task whose first line is `Production check of <KEY>`, handed to the implementer once the merge closed its issue) needs no handoff and no " + "commit: it carries its verdict and summary alone, and the daemon refuses it when no check is open (CHECK_NOT_OPEN). The daemon refuses " + "HANDOFF_SESSION_MISMATCH (another session wrote the record), HANDOFF_NOT_NEW (the record is the one you named for your previous phase: " + "write this phase's handoff), " + "HANDOFF_STALE_ASSIGNMENT (the issue left your phase and came back while your turn ran, so this completion is of the assignment before the one " + "the phase hands you now: end the turn without completing; the task of the assignment it hands you now is work of its own, so do that work and " + "complete it with a handoff of its own, never this record or verdict again), the implementer's completion of implementing with no open " + "pull request on `legion/<issue>` and none recorded " + "merged (HANDOFF_NO_PULL_REQUEST: open it; one naming a closed pull request is the architect's to decide) and, for READY, each " + "refusal the merger's role names; a refused completion changed nothing, so fix what it names and complete again. An accepted READY whose `note` " + "begins MANUAL_MERGE_REQUIRED: is one a person merges: arm nothing. " + "ready_for_update answers the daemon's update task on this issue's pod: `round` is the round id that task names, and the " + "daemon takes your answer for that round alone. Push every commit you want kept to the issue branch " + "first, because the pod and its disk are replaced after every worker of the pod has answered. The call ends no phase. The " + "daemon holds your next task until your process comes back on the new pod, in this same session. " + (host.after ?? "") + `What a later phase needs goes in your handoff; a question for another live role goes to its role topic with ${host.envoyPublish}.`;
56901
+ return "Perform the workflow operation the Legion daemon assigned this role. The daemon advances phases; this tool cannot spawn workers. " + (host.before ?? "") + "handoff_complete (phase workers and sub-architects; the daemon accepts a completion only from the role working the issue's current phase, " + "and the checking role's report on a look of its closed issue's production check) reports this phase complete: `summary` (two sentences for " + "the architect, or the merger's READY packet), `verdict` pass|fail from the tester alone on a phase completion (the daemon refuses a verdict " + "from any other role there) and pass|pending|fail on a production check report (pending: production does not carry the merge " + "yet), `ready: true` for the merger's READY. A planning, implementing, testing or reviewing phase ends with a handoff record on the " + "Dispatch issue: write it with `dispatch handoff-write --issue <issue> --phase <plan|implement|test|review> --body-file <file>` before " + "calling. This call reads the phase's newest record from Dispatch and reports it; the implementer's round also reports the commit " + "`legion/<issue>@origin` names, so push the branch first; retro and the merger's READY report the commit the workspace stands on. It " + "refuses, posting nothing, a phase with no record, a Dispatch that does not answer, or an implementer's `@-` not yet on " + "`legion/<issue>@origin`, each naming the remedy. A production check report " + "(the task whose first line is `Production check of <KEY>`, handed once the merge closed the issue to the role the check records) needs no " + "handoff and no commit: it carries its verdict and summary alone. A pending report also names `wake_at`, an RFC 3339 time: when you want the " + "next look. A pass, a fail, and a pending on the check's last look end the check, so none of them carries `wake_at` " + "(CHECK_WAKE_UNEXPECTED), and a pending on any other look is refused without one (CHECK_WAKE_REQUIRED). A report on an issue whose check is " + "closed or never opened is refused (CHECK_NOT_OPEN), and so is one from a role the check does not record (CHECK_ROLE_MISMATCH). " + "The daemon refuses " + "HANDOFF_SESSION_MISMATCH (another session wrote the record), HANDOFF_NOT_NEW (the record is the one you named for your previous phase: " + "write this phase's handoff), " + "HANDOFF_STALE_ASSIGNMENT (the issue left your phase and came back while your turn ran, so this completion is of the assignment before the one " + "the phase hands you now: end the turn without completing; the task of the assignment it hands you now is work of its own, so do that work and " + "complete it with a handoff of its own, never this record or verdict again), the implementer's completion of implementing with no open " + "pull request on `legion/<issue>` and none recorded " + "merged (HANDOFF_NO_PULL_REQUEST: open it; one naming a closed pull request is the architect's to decide) and, for READY, each " + "refusal the merger's role names; a refused completion changed nothing, so fix what it names and complete again. An accepted READY whose `note` " + "begins MANUAL_MERGE_REQUIRED: is one a person merges: arm nothing. " + "ready_for_update answers the daemon's update task on this issue's pod: `round` is the round id that task names, and the " + "daemon takes your answer for that round alone. Push every commit you want kept to the issue branch " + "first, because the pod and its disk are replaced after every worker of the pod has answered. The call ends no phase. The " + "daemon holds your next task until your process comes back on the new pod, in this same session. " + (host.after ?? "") + `What a later phase needs goes in your handoff; a question for another live role goes to its role topic with ${host.envoyPublish}.`;
56896
56902
  }
56897
56903
  async function mintClaimGrant(daemon, claim) {
56898
56904
  const { grantId } = await daemon.grant({
@@ -57037,25 +57043,40 @@ async function releaseRecords(runtime, since) {
57037
57043
  async function completeHandoff(runtime, args) {
57038
57044
  const { daemon, claim } = runtime;
57039
57045
  const record = recordFrom(await daemon.state(), claim.issue);
57040
- if (claim.role === "implementer" && record.phase === "done") {
57041
- if (record.check !== undefined) {
57046
+ if (record.phase === "done") {
57047
+ const check = record.check;
57048
+ if (check !== undefined) {
57042
57049
  if (args.verdict === undefined) {
57043
57050
  throw new Error("handoff_complete on a production check names a verdict of pass, pending or fail");
57044
57051
  }
57045
57052
  if (args.ready === true) {
57046
57053
  throw new Error("handoff_complete on a production check is no READY: leave ready out");
57047
57054
  }
57055
+ if (claim.role !== check.role) {
57056
+ throw new Error(`${claim.issue}'s production check is the ${check.role}'s, and this session is the ${claim.role}'s: only the role the check records reports on it`);
57057
+ }
57058
+ if (args.verdict === "pending" && args.wake_at === undefined && !check.last) {
57059
+ throw new Error(`handoff_complete's pending report names wake_at, when the next look of ${claim.issue}'s production check is due`);
57060
+ }
57061
+ if (args.wake_at !== undefined && (args.verdict !== "pending" || check.last)) {
57062
+ const why = args.verdict === "pending" ? `look ${check.looks} is the check's last, after which the check closes as not run` : `a ${args.verdict} report closes the check`;
57063
+ throw new Error(`handoff_complete names wake_at and no look follows it: ${why}; send it again without a time`);
57064
+ }
57048
57065
  }
57049
57066
  return daemon.handoffComplete({
57050
57067
  grantId: await mintClaimGrant(daemon, claim),
57051
57068
  summary: args.summary,
57052
57069
  verdict: args.verdict ?? "",
57053
- ready: false
57070
+ ready: false,
57071
+ ...args.wake_at === undefined ? {} : { wakeAt: args.wake_at }
57054
57072
  });
57055
57073
  }
57056
57074
  if (args.verdict === "pending") {
57057
57075
  throw new Error("handoff_complete's verdict is pass or fail on a phase completion; pending is a production check's");
57058
57076
  }
57077
+ if (args.wake_at !== undefined) {
57078
+ throw new Error("handoff_complete's wake_at is a production check's pending report alone; a phase completion carries none");
57079
+ }
57059
57080
  const report = await runtime.handoffReport(record.phase, claim.role);
57060
57081
  return daemon.handoffComplete({
57061
57082
  grantId: await mintClaimGrant(daemon, claim),
@@ -44,7 +44,8 @@ legion({ op: "sign_off", issue: "LEGION-40" })
44
44
  that phase's role with your reason in its task.
45
45
  - `handoff_complete` from you in `accepting` is refused `ACCEPTANCE_IS_SIGN_OFF`. CI red at the head
46
46
  in `accepting` returns the issue to `implementing` by the daemon's table.
47
- - No `after_merge` result is asked here: the production check records it after the close.
47
+ - No `after_merge` result is asked here: the production check records it after the close, where
48
+ the project's `production_check` opens one.
48
49
  - A finding ask on the issue holds the acceptance: one a worker sent you, or one `dispatch read`
49
50
  lists under `Open asks:` whose question opens `Finding from ` or `Clear defect from `
50
51
  (`skill://legion-worker/references/findings.md`, "When no controller takes it"). Follow each
@@ -175,8 +176,11 @@ The merge closes the issue: when GitHub reports the pull request merged while th
175
176
  is done or parked. One
176
177
  `pr-merged` notice names what it waits for and none follows as they depart, so read the pull
177
178
  request on GitHub.
178
- - A child's close reaches you as `child-closed`. The daemon retires five of its six claims and
179
- leaves the implementer running, because it still owes the production check. When the issue's
179
+ - A child's close reaches you as `child-closed`. Where the project's `production_check` names the
180
+ role that works the check — the tester, unless the setting is `off` — the daemon retires five
181
+ of its six claims and leaves that role running, because the issue still owes the production
182
+ check; where the setting is `off` no check opens, and the close retires all six and releases
183
+ the pod. When the issue's
180
184
  pod and Sandbox go is in `skill://legion-worker/references/merge-gate.md`, *The production check
181
185
  after the close*.
182
186
  - A root's close writes `done`, releases any admission slot the tree's issues still held, and lingers the tree for
@@ -184,10 +188,12 @@ The merge closes the issue: when GitHub reports the pull request merged while th
184
188
  every member's open production check has closed.
185
189
 
186
190
  Nothing of the production check is yours. Its record lives on the pull request alone, since
187
- Dispatch refuses a write on a closed issue. A defect it finds is never an issue the implementer
191
+ Dispatch refuses a write on a closed issue. A defect it finds is never an issue the checking role
188
192
  files: a clear defect goes to the controller, and any other is a `dispatch ask`. `read_record`
189
- shows a closed issue's open check (`check`: the pull request, the merge commit, the looks, what
190
- woke the last one and when the next is due).
193
+ shows a closed issue's open check (`check`: the pull request, the merge commit, the role the
194
+ check records as the one that works it, the looks, what woke the last one, and `nextWakeAt`, when
195
+ the next look is due — the time the checking agent named for itself on a `pending`, the retry of
196
+ a look nobody reported, or the check's own bound where neither stands).
191
197
 
192
198
  ## Hand-over at a root's linger
193
199
 
@@ -94,8 +94,10 @@ for a finished retro only.
94
94
  EOF
95
95
  ```
96
96
 
97
- The merge's close hands you the production check's first look in this session; the daemon
98
- wakes a later look at a deploy only after you report `pending`
97
+ The merge's close hands the production check's first look to the role the project's
98
+ `production_check` setting names — the tester, unless the setting is `off`, where the close
99
+ opens no check at all — so it reaches that role's session, not yours, and the look after it is
100
+ the one that look's own `wake_at` names
99
101
  (`skill://legion-worker/references/merge-gate.md#the-production-check-after-the-close`).
100
102
  6. **Complete** with the `legion` tool's `handoff_complete`, its summary two sentences for the
101
103
  architect. It reports the commit your workspace stands on, which is the head you found: retro
@@ -39,11 +39,11 @@ Read each by its `skill://` link: a read by path stops at 300 lines.
39
39
  | Before a push, to check your commits' App identity | `skill://legion-worker/references/default-branch-check.md` |
40
40
  | You reply on, list or resolve review threads, or judge a bot's finding | `skill://legion-worker/references/review-threads.md` |
41
41
  | A conflict, a need only the base meets, a retarget, a parent's last round, comparing heads after a forward merge, or rewriting a pushed commit | `skill://legion-worker/references/conflicts-and-rewrites.md` |
42
- | You review or approve, build READY, or run the production check | `skill://legion-worker/references/merge-gate.md` |
42
+ | You review or approve, build READY, or take a look of a closed issue's production check | `skill://legion-worker/references/merge-gate.md` |
43
43
  | The issue renames a repository, package, or URL across the codebase | `skill://legion-worker/references/systematic-rename.md` |
44
44
  | The issue deletes code, a command, or documentation | `skill://legion-worker/references/cleanup-deletion.md` |
45
45
  | Before you read a shared database, a session store or another role's transcript among them; before you run a build or a test suite in your pod; you need a tool the image may not carry, before you install anything; before you measure Oh My Pi's binary; or a test fails in your pod or needs a database | `skill://legion-worker/references/tools.md` |
46
- | A problem outside your issue, or a failed production check | `skill://legion-worker/references/findings.md` |
46
+ | A problem outside your issue, or a production check that failed | `skill://legion-worker/references/findings.md` |
47
47
  | Your text would reach a person outside the company: a comment, issue or pull request on a repository the company does not own, an email, a message to another project's maintainers | `skill://outside-message` |
48
48
 
49
49
  ## Every role pushes its own commits
@@ -58,11 +58,11 @@ Read each by its `skill://` link: a read by path stops at 300 lines.
58
58
  - The implementer opens the pull request with the line `Dispatch: <KEY>` in its body, the daemon's link to the issue when the branch name is ambiguous. Later phases push to that branch and edit that pull request, never a new one.
59
59
  - **No deferrals.** A finding that changes behaviour, hides an error, or breaks a gate is fixed in this pull request, and so is a wording nit or any clear fix in a file it touches: a later round costs a CI pass and a re-read of the whole change. The body's one `Fast-follow:` line reads "none", or names cleanup in files this pull request does not touch.
60
60
  - **The reviewer fixes a prose-only finding itself** (wording, a count, a tense, a citation) in its own commit, pushed before its review. The exception is the round after a READY refusal. Code or behaviour goes back (`skill://legion-worker/references/merge-gate.md`).
61
- - **The merge gate**, in order: the tester's evidence green → the reviewer's approval of the head → the architect's acceptance → retro → the merger's READY → the merge, which closes the issue → the implementer's production check, after the close. When the daemon accepts READY, it submits the merge of READY's head, unless the answer's note begins `MANUAL_MERGE_REQUIRED:`, where a person merges it; no role runs `gh pr merge`.
61
+ - **The merge gate**, in order: the tester's evidence green → the reviewer's approval of the head → the architect's acceptance → retro → the merger's READY → the merge, which closes the issue → the production check after the close, which the project's `production_check` setting gives to the tester unless it is `off`, and whose every look but the last names its own next look with `wake_at`. When the daemon accepts READY, it submits the merge of READY's head, unless the answer's note begins `MANUAL_MERGE_REQUIRED:`, where a person merges it; no role runs `gh pr merge`.
62
62
  - Append `<!-- legion: {"session":"<session-id>","phase":"<phase>"} -->` to every pull-request comment and review you post: the daemon finds the session behind a review by it.
63
63
  - Never write a GitHub issue, by `gh issue` or a non-GET `gh api` on `/issues` (post or edit a PR conversation comment with `gh pr comment`): Legion's issues live on Dispatch. Never run `gh config set` or `gh alias set`, and leave `GH_TOKEN`, `GITHUB_TOKEN` and `GH_HOST` empty: the daemon rewrites your gh directory in place, and a set variable outranks it.
64
64
  - Before you report a resource unreachable, run the call through the secrets broker's client (`skill://agent-secrets`, which holds the form, the exit statuses and where a name comes from), and check `AWS_CONFIG_FILE` (read its profiles): a deployment can give your pod either. Your `task` subagents and background jobs inherit both, and your `gh` and `git`. Request a secret only to use it for your issue's work.
65
65
 
66
66
  ## Deployment instructions
67
67
 
68
- Deployment instructions in your prompt override this skill where they conflict, except four rules: no deferrals; bringing the base into the branch only on a real conflict, a need only the base meets, a retarget, or a parent's last round (`skill://legion-worker/references/conflicts-and-rewrites.md#reintegrating-the-base`); the implementer's own result for every `before_merge` check, on a production-like surface at the head that merges, an applied simplify head included (`skill://legion-worker/references/pr-body.md#what-a-result-is`, `skill://legion-worker/references/pr-body.md#the-rules-every-phases-evidence-follows`); and the implementer's production check after the close (`skill://legion-worker/references/merge-gate.md#the-production-check-after-the-close`).
68
+ Deployment instructions in your prompt override this skill where they conflict, except four rules: no deferrals; bringing the base into the branch only on a real conflict, a need only the base meets, a retarget, or a parent's last round (`skill://legion-worker/references/conflicts-and-rewrites.md#reintegrating-the-base`); the implementer's own result for every `before_merge` check, on a production-like surface at the head that merges, an applied simplify head included (`skill://legion-worker/references/pr-body.md#what-a-result-is`, `skill://legion-worker/references/pr-body.md#the-rules-every-phases-evidence-follows`); and the production check after the close, the tester's wherever the project's `production_check` does not say `off` (`skill://legion-worker/references/merge-gate.md#the-production-check-after-the-close`).
@@ -1,7 +1,8 @@
1
1
  # A finding outside your issue
2
2
 
3
3
  Read this when you find a problem the issue you work does not cover: a defect in another
4
- component, repository or tool, a gap in Legion itself, or a production check's `fail`. It holds for
4
+ component, repository or tool, a gap in Legion itself, or a `fail` on a look of the production
5
+ check the project's `production_check` setting gives the tester. It holds for
5
6
  every Legion role (each phase worker, each architect, the controller) and for each of their
6
7
  subagents, and it replaces the filing step of `skill://dispatch-first` and `skill://dispatch`. The
7
8
  controller fixes nothing itself: it hands a finding to the tree that owns the code, as
@@ -68,7 +69,8 @@ The controller hands it to the running tree whose code it is, and files a Legion
68
69
  tree owns that code. Either way it replies under your evidence comment with where the work went.
69
70
  You file nothing, and you wait for nothing. When the report issue is missing or closed, put the
70
71
  evidence in the message itself. A production check's recorded result then names the defect as
71
- sent to the controller.
72
+ sent to the controller, and the `fail` that records it closes the check, so that look names no
73
+ next look of its own.
72
74
 
73
75
  ### When no controller takes it
74
76
 
@@ -120,7 +122,9 @@ of these that holds, so a person sees it and the controller finds it when it ret
120
122
  - Then post the evidence there and ask there, as [What the ask says](#what-the-ask-says)
121
123
  spells it. Say in your summary that you reopened or created it, so the controller knows.
122
124
 
123
- Name the ask in your summary, and in the result a production check records.
125
+ Name the ask in your summary, and in the result a production check records. A look that must wait
126
+ for the answer reports `pending` with a `wake_at` of its own, the time you want to look again:
127
+ nothing else starts the next look.
124
128
 
125
129
  ## Where it goes
126
130
 
@@ -1,12 +1,13 @@
1
1
  # The merge gate: review, acceptance, retro, READY, the merge, and the production check
2
2
 
3
3
  Part of `skill://legion-worker`. Read it when you are the reviewer submitting a review or an
4
- approval, the implementer on a look of its closed issue's production check, or the merger
4
+ approval, the tester on a look of its closed issue's production check, or the merger
5
5
  building READY. Every path it cites is in cybermonklabs/legion.
6
6
 
7
7
  The order, in full: the tester's evidence green → the reviewer's approval of the head → the
8
8
  architect's acceptance, in `accepting` → retro → the merger's READY → the merge, which closes the
9
- issue → the implementer's production check, after the close. Every handoff is a record on the
9
+ issue → the production check after the close, which the project's `production_check` setting
10
+ gives to a role or takes away. Every handoff is a record on the
10
11
  Dispatch issue, never a commit, so the head that merges carries none. Retro commits nothing
11
12
  (*Retro*, below), so the approved head is the head that merges. READY still lets a commit that
12
13
  changes only `docs/solutions/` stand, and a change whose every path is under `.legion/`, where
@@ -152,7 +153,8 @@ reviewing until you finish.
152
153
  Verification document at its own head, so a child's result against a root's check proves
153
154
  nothing for the root: the root's checks are proven at the root's own head, in its last round.
154
155
  The acceptance asks for no `after_merge` result: that one is recorded
155
- after the close (*The production check after the close*, below).
156
+ after the close, where the project opens a check (*The production check after the close*,
157
+ below).
156
158
  CI red at the head in `accepting` returns the issue to `implementing` as it does in review. A
157
159
  head the review App pushed is the exception: the daemon marks it a planned red and acts on it
158
160
  nowhere, and GitHub holds the merge under the base branch's own rules.
@@ -275,19 +277,32 @@ reviewing until you finish.
275
277
 
276
278
  ## The production check after the close
277
279
 
278
- - **The merge closes the issue; the production check is the implementer's work after the close.**
279
- When the pull request merges, the daemon writes `done` and, in the implementer's session, still
280
- live, hands it the check task. On a root whose tree still runs the close, and so the check task,
281
- comes later: the merge is recorded, the root waits in `awaiting_merge` until the last other
280
+ - **The merge closes the issue; the production check is the work of the role the project names,
281
+ after the close.** `projects.<KEY>.production_check` in the project's `legion.yaml` takes `off`
282
+ or `tester`, and a project that names no key gets `tester`, so by default the tester works the
283
+ check; the loader refuses every other value, naming the key and the value it read. Where the
284
+ setting names the tester, the merge makes the daemon write `done` and, in the tester's session,
285
+ still live, hand it the check task. Where it is `off` nothing is opened at all — no record, no
286
+ bound and no look — and the merged close is like any other done: every claim of the issue is
287
+ retired and its pod released with the disk it ran on. A merged root still lingers its tree with
288
+ every claim suspended, as before. On a root whose tree still runs the close, and so the check
289
+ task, comes later: the merge is recorded, the root waits in `awaiting_merge` until the last other
282
290
  issue of its tree is done or parked, and the check opens with that close. Its first line is
283
291
  `Production check of <KEY>: <title>.`, and it names the repository, the pull request, the merge
284
292
  commit, the look's number, what woke the look (`the merge`, for the first) and whether it is the
285
293
  last. A resumed session tells the check task
286
- from its last phase task by that first line. The check carries no phase and no handoff: a look
287
- ends with `handoff_complete` carrying a `verdict` and a `summary`, no commit, and the daemon
294
+ from its last phase task by that first line.
295
+ - **The check's record holds the role that works it, written when the check opened.** Changing the
296
+ setting moves no check already open: a check keeps the role it opened with, and a check open
297
+ before this release carries the implementer. Only that role may report on it, so a report from
298
+ any other role is refused (`CHECK_ROLE_MISMATCH`). The check carries no phase and no handoff: a
299
+ look ends with `handoff_complete` carrying a `verdict` and a `summary`, no commit, and the daemon
288
300
  refuses a report on an issue with no open check (`CHECK_NOT_OPEN`), on a look already ended
289
301
  (`CHECK_LOOK_NOT_OPEN`), or with no verdict of `pass`, `pending` or `fail`
290
302
  (`CHECK_VERDICT_REQUIRED`).
303
+ One refusal is about the turn rather than the report: a completion sent from a turn that is not
304
+ a look's own task is refused `CHECK_LOOK_NOT_SERVED` and records nothing, so report again in the
305
+ turn of the look's task.
291
306
  - **Each look does three things.** Do what the deployment instructions say to reach the deploy:
292
307
  when they name an agent or role for it, `envoy_publish` the merge commit to that role's topic
293
308
  with `expects_reply: none`, and await no reply. Read whether production carries the merge. Then
@@ -301,7 +316,8 @@ reviewing until you finish.
301
316
  staging gate does not run every resource production does.
302
317
  - `pending`, when production does not carry the merge yet (a shared profile still holding the
303
318
  previous release, a daemon still running the previous commit, a deploy nobody has run): the
304
- summary names what is missing, nothing is recorded, and the check's line in the body stays at
319
+ summary names what is missing and what you will look for next, `wake_at` names your next look
320
+ (below), nothing is recorded, and the check's line in the body stays at
305
321
  `- Check <block id> (after_merge): not yet run`.
306
322
  - `fail`, for a defect: record the failing result with `dispatch result --verdict fail`, file no
307
323
  issue, and take the path
@@ -316,32 +332,37 @@ reviewing until you finish.
316
332
  records of its releases, resumes and holds: its one argument `since` is the RFC 3339 instant
317
333
  the window starts at, and the tool's own description says what the answer carries. It reads
318
334
  them with this session's own claim, so there is no credential to fetch and none on disk.
319
- - **Nothing waits in the session for a deploy.** A `pending` ends the look. Where no wake is
320
- queued yet, the daemon suspends your claim with its session kept, and the issue's Sandbox goes
321
- with its pod and disk; where a wake arrived while the look was open, the next look starts at once
322
- on the same pod. Either way the daemon wakes the check (after a suspension a new pod comes back
323
- on a fresh disk, and your session resumes with a new check task) on Dispatch's
324
- `issue.deployed` event for the pull request; when the project's `legion.yaml` names the
325
- repository's deploy workflow (`projects.<KEY>.deploys`), at each completed run of that workflow
326
- on the default branch whose named step (such as `Apply`) succeeded after the merge, once per such
327
- job; or, for `cybermonklabs/legion`, on a Legion daemon's boot on a build containing the merge, on
328
- Dispatch's `server.started` announcing that every server process serves such a build, or on the
329
- build the daemon read from Dispatch at its boot. The woken look's task names the event
330
- (`Dispatch's issue.deployed: run <id> completed <t>`,
331
- `the deploy workflow's run: <repo> <workflow> run <id> attempt <n>, job <job name>: <step> succeeded at <t>`,
332
- `the Legion daemon's boot on build <sha>`, `Dispatch serving build <sha> on every server process`,
333
- `Dispatch's build <sha>`), and the result you record names it too, so the pull request carries
334
- the cause. Never poll for the deploy, never wait on a reply, and never ask anyone for approval
335
- to deploy. Open a `dispatch ask` to a person only when the instructions name nobody for that
336
- deploy; it starts with the production gap and why it matters, then names the required install or
337
- restart step, its risk, and outcome-named options, and the look still ends `pending`.
338
- - **The bounds.** A look is bounded at 2 hours: one that reports nothing ends as a `pending`
339
- would. The whole check is bounded by the project's `production_check_hours` (336, 14 days, by
340
- default) from the close, and the wake at that bound is the last look, told so in its task
335
+ - **You name your own next look, and nothing else wakes the check.** A `pending` on any look but
336
+ the check's last carries `wake_at`, an RFC 3339 time: when you want to look again. Name a time
337
+ you have a reason for — when the deploy you asked for should have run, when the next release
338
+ lands — and say in the summary what you are waiting for, since that summary is what the next
339
+ look's task tells you woke it. A `pending` with no `wake_at` on a look that is not the last is
340
+ refused (`CHECK_WAKE_REQUIRED`): nothing is recorded, the look stays open, and the same report
341
+ with a time is taken. A `wake_at` that no look follows is refused (`CHECK_WAKE_UNEXPECTED`): on
342
+ a `pass`, on a `fail`, and on a `pending` on the last look, each of which ends the check. A time
343
+ less than 60 seconds ahead becomes 60 seconds ahead, and a time at or after the check's own
344
+ bound starts no look of its own, since the look at that bound already stands. No deploy run, no
345
+ Dispatch `issue.deployed`, no Dispatch build and no daemon boot wakes a check: the daemon reads
346
+ no deployment's production workflows at all, and your `wake_at` is the only thing that starts
347
+ the look after this one.
348
+ - **Nothing waits in the session for a deploy.** A `pending` ends the look: the daemon stops the
349
+ checking role's process, keeps its session and releases the issue's pod, whose Sandbox goes with
350
+ its disk, and at the time you named a new pod comes back and your session resumes with the next
351
+ look's task. So end the turn with the report: never poll for the deploy, never wait on a reply,
352
+ and never ask anyone for approval to deploy. Open a `dispatch ask` to a person only when the
353
+ instructions name nobody for that deploy; it starts with the production gap and why it matters,
354
+ then names the required install or restart step, its risk, and outcome-named options, and the
355
+ look still ends `pending`.
356
+ - **The bounds.** A look is bounded at 2 hours, and a look nobody reported — its 2 hours, an
357
+ operator's hold, or a claim that failed to start — starts the next look two hours later; on the
358
+ last look it closes the check as not run. The whole check is bounded by the project's
359
+ `production_check_hours` (336, 14 days, by default) from the close, and the look that bound
360
+ starts is the last, told so in its task
341
361
  (`the bound: production did not carry the merge within <N days>`). A `pass` or `fail` there
342
- closes the check as any look's would; a `pending` closes it as not run, and before reporting it
343
- you write on the pull request, in the check's line and a comment, that the check did not
344
- run because production did not carry the merge within the bound.
362
+ closes the check as any look's would; a `pending` there carries no `wake_at` and closes the
363
+ check as not run, and before reporting it you write on the pull request, in the check's line and
364
+ a comment, that the check did not run because production did not carry the merge within the
365
+ bound.
345
366
  - **The record lives on the pull request and in the result.** Dispatch refuses a message, a
346
367
  comment or an ask on a closed issue (`ISSUE_CLOSED`) and takes only a result for an
347
368
  `after_merge` or `default_branch` check, so the deployment instructions' "on the pull request
@@ -166,7 +166,9 @@ gap and decides there, which is the only gate on such an issue.
166
166
  `failures`). Otherwise record your own result for each `before_merge` check before you complete,
167
167
  and put each printed line in the body. A code path whose first execution is after merge — a
168
168
  deploy workflow's inline step, a post-merge helper, a production-only resource — is a check
169
- marked `after_merge`, recorded after the merge, unless the implementer can execute it against a
169
+ marked `after_merge`, recorded after the merge on a look of the production check by the role
170
+ the project's `production_check` names — the tester, unless the setting is `off`, where the
171
+ close opens no check and nothing records it — unless the implementer can execute it against a
170
172
  devN stack before then; if no surface can reach it, the
171
173
  tester names that missing surface as the blocker instead of passing the phase. Environment or
172
174
  secret-scrub evidence (e.g. "`LEGION_*`/`DISPATCH_*`/`ENVOY_*` unset") is recorded once, in
@@ -219,6 +221,7 @@ the architect with `envoy_publish` to its role topic. The architect builds it in
219
221
  once it lands. A code path
220
222
  whose first execution would be after the merge — a deploy
221
223
  workflow's inline step, a post-merge helper, a production-only resource — is a check marked
222
- `after_merge`, recorded once the merge reaches production, and no reason to leave a
224
+ `after_merge`, recorded once the merge reaches production by the role the project's
225
+ `production_check` names, on the look its own `wake_at` brought back, and no reason to leave a
223
226
  `before_merge` check unrecorded; completing with a unit suite as the only evidence is the
224
227
  failure this rule exists to stop.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/claude-envoy",
3
- "version": "1.8.2",
3
+ "version": "1.9.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Claude Code channel plugin for Legion's Envoy bus",
6
6
  "type": "module",
@@ -19,7 +19,7 @@
19
19
  "AGENTS.md"
20
20
  ],
21
21
  "legion": {
22
- "daemonApiVersion": 53
22
+ "daemonApiVersion": 54
23
23
  },
24
24
  "repository": {
25
25
  "type": "git",