@sjawhar/claude-envoy 0.0.0 → 1.0.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.
Files changed (113) hide show
  1. package/.claude-plugin/plugin.json +6 -0
  2. package/.mcp.json +11 -0
  3. package/.omp-plugin/plugin.json +6 -0
  4. package/AGENTS.md +343 -0
  5. package/README.md +639 -0
  6. package/bin/dispatch +6 -0
  7. package/bin/envoy-channel.ts +9 -0
  8. package/bin/envoy-send.ts +33 -0
  9. package/dist/THIRD_PARTY_NOTICES +816 -0
  10. package/dist/dispatch.js +27707 -0
  11. package/dist/envoy-channel.js +57695 -0
  12. package/dist/session-hook.js +26328 -0
  13. package/dist/session-title-hook.js +116 -0
  14. package/hooks/hooks.json +60 -0
  15. package/hooks/session-hook.ts +512 -0
  16. package/hooks/session-title-hook.ts +46 -0
  17. package/legion/.claude-plugin/plugin.json +4 -0
  18. package/legion/agents/deep-worker.md +68 -0
  19. package/legion/agents/oracle.md +35 -0
  20. package/legion/agents/plan-gap-analyst.md +65 -0
  21. package/legion/agents/plan-reviewer.md +104 -0
  22. package/legion/agents/thermonuclear-code-quality.md +30 -0
  23. package/legion/agents/thermonuclear-deep-review.md +30 -0
  24. package/legion/hooks/hooks.json +89 -0
  25. package/legion/skills/ce-simplify-code/LICENSE +21 -0
  26. package/legion/skills/ce-simplify-code/SKILL.md +66 -0
  27. package/legion/skills/ce-simplify-code/references/personas/code-quality-reviewer.md +17 -0
  28. package/legion/skills/ce-simplify-code/references/personas/code-reuse-reviewer.md +7 -0
  29. package/legion/skills/ce-simplify-code/references/personas/efficiency-reviewer.md +11 -0
  30. package/legion/skills/leading-a-work-stream/SKILL.md +96 -0
  31. package/legion/skills/leading-a-work-stream/references/daily-plan.md +8 -0
  32. package/legion/skills/leading-a-work-stream/references/decision-rights.md +6 -0
  33. package/legion/skills/leading-a-work-stream/references/direction-and-coaching.md +13 -0
  34. package/legion/skills/leading-a-work-stream/references/dispatch-upkeep.md +8 -0
  35. package/legion/skills/leading-a-work-stream/references/sources.md +9 -0
  36. package/legion/skills/leading-a-work-stream/references/speed-tick.md +44 -0
  37. package/legion/skills/leading-a-work-stream/references/user-quotes.md +7 -0
  38. package/legion/skills/legion-architect/SKILL.md +86 -0
  39. package/legion/skills/legion-architect/references/acceptance.md +208 -0
  40. package/legion/skills/legion-architect/references/decomposition.md +129 -0
  41. package/legion/skills/legion-architect/references/design-gate.md +206 -0
  42. package/legion/skills/legion-architect/references/merge-notices.md +91 -0
  43. package/legion/skills/legion-architect/references/wake-routing.md +41 -0
  44. package/legion/skills/legion-controller/SKILL.md +151 -0
  45. package/legion/skills/legion-controller/references/admission.md +22 -0
  46. package/legion/skills/legion-controller/references/backlog-stewardship.md +101 -0
  47. package/legion/skills/legion-controller/references/daily-report.md +75 -0
  48. package/legion/skills/legion-controller/references/escalation.md +82 -0
  49. package/legion/skills/legion-controller/references/finding-asks.md +181 -0
  50. package/legion/skills/legion-controller/references/parked-decisions.md +198 -0
  51. package/legion/skills/legion-controller/references/session.md +100 -0
  52. package/legion/skills/legion-controller/references/slack-mentions.md +154 -0
  53. package/legion/skills/legion-controller/references/triage.md +96 -0
  54. package/legion/skills/legion-controller/references/wakes.md +132 -0
  55. package/legion/skills/legion-oracle/SKILL.md +48 -0
  56. package/legion/skills/legion-retro/SKILL.md +112 -0
  57. package/legion/skills/legion-retro/references/learnings.md +60 -0
  58. package/legion/skills/legion-retro/references/pr-body-content.md +56 -0
  59. package/legion/skills/legion-worker/SKILL.md +69 -0
  60. package/legion/skills/legion-worker/references/cleanup-deletion.md +22 -0
  61. package/legion/skills/legion-worker/references/conflicts-and-rewrites.md +164 -0
  62. package/legion/skills/legion-worker/references/default-branch-check.md +53 -0
  63. package/legion/skills/legion-worker/references/findings.md +199 -0
  64. package/legion/skills/legion-worker/references/merge-gate.md +353 -0
  65. package/legion/skills/legion-worker/references/pr-body.md +218 -0
  66. package/legion/skills/legion-worker/references/review-threads.md +77 -0
  67. package/legion/skills/legion-worker/references/systematic-rename.md +19 -0
  68. package/legion/skills/legion-worker/references/tools.md +708 -0
  69. package/legion/skills/thermonuclear-code-quality/LICENSE +21 -0
  70. package/legion/skills/thermonuclear-code-quality/SKILL.md +60 -0
  71. package/legion/skills/thermonuclear-deep-review/LICENSE +21 -0
  72. package/legion/skills/thermonuclear-deep-review/SKILL.md +80 -0
  73. package/package.json +49 -1
  74. package/scripts/build-inputs.txt +84 -0
  75. package/scripts/build.ts +415 -0
  76. package/scripts/check-packed-skills.sh +50 -0
  77. package/scripts/pack-skills.sh +146 -0
  78. package/scripts/smoke-channel.sh +178 -0
  79. package/scripts/smoke-clear-rebind.sh +164 -0
  80. package/scripts/smoke-session-title.sh +245 -0
  81. package/scripts/verify-build.sh +88 -0
  82. package/skills/agent-secrets/SKILL.md +180 -0
  83. package/skills/asd-ste100/LICENSE +21 -0
  84. package/skills/asd-ste100/SKILL.md +140 -0
  85. package/skills/asd-ste100/examples/before-after.md +92 -0
  86. package/skills/asd-ste100/references/writing-rules.md +59 -0
  87. package/skills/dispatch/SKILL.md +89 -0
  88. package/skills/dispatch-first/SKILL.md +46 -0
  89. package/skills/dispatch-writing-specs/SKILL.md +80 -0
  90. package/skills/envoy/SKILL.md +82 -0
  91. package/skills/envoy/references/check-settlement.md +62 -0
  92. package/skills/envoy/references/deliveries.md +67 -0
  93. package/skills/envoy/references/legion.md +14 -0
  94. package/skills/envoy/references/roles.md +12 -0
  95. package/skills/envoy/references/sources.md +41 -0
  96. package/skills/outside-message/SKILL.md +111 -0
  97. package/skills/plain-words/SKILL.md +88 -0
  98. package/src/channel-forwarder.ts +262 -0
  99. package/src/claude-channel.ts +705 -0
  100. package/src/claude-session.ts +38 -0
  101. package/src/envoy-channel-server.ts +2115 -0
  102. package/src/envoy-client.ts +26 -0
  103. package/src/envoy-off.ts +62 -0
  104. package/src/legion-claim.ts +211 -0
  105. package/src/legion-inbox.ts +110 -0
  106. package/src/legion-phase-hook.ts +554 -0
  107. package/src/legion-tool.ts +69 -0
  108. package/src/replay-state.ts +97 -0
  109. package/src/send-arguments.ts +13 -0
  110. package/src/session-identity.ts +165 -0
  111. package/src/session-state.ts +142 -0
  112. package/src/session-title.ts +251 -0
  113. package/src/subscription-topics.ts +16 -0
@@ -0,0 +1,164 @@
1
+ # Conflicts, retargets, fingerprints, and rewriting pushed commits
2
+
3
+ Part of `skill://legion-worker`. Read it when GitHub reports the pull request `CONFLICTING`, the
4
+ controller asks you to resolve a conflict, the change needs something only the base has, the pull
5
+ request is retargeted to a new base, you compare two heads after such a merge, a push is refused
6
+ because the handoff import moved the branch, or you are about to rewrite a commit you already
7
+ pushed.
8
+ Every path it cites is in cybermonklabs/legion.
9
+
10
+ ## Reintegrating the base
11
+
12
+ - **Merge the base forward only on a real conflict, a need only the base meets, a retarget, or a
13
+ parent's last round, and never with `jj rebase`.**
14
+ The implementer merges the base into the issue branch only when GitHub reports it `CONFLICTING`, the controller asks
15
+ because of a conflict, the architect's backward move names a parent's last round,
16
+ the change needs something that exists only on the base (a fix for its
17
+ only red check, or a sentence it must change), or after the pull request is retargeted to a new
18
+ base. Otherwise, never reintegrate the base to pick up the default branch or refresh CI. Each
19
+ merge is a new head, reruns CI and can void a review. A single failed CI job is re-run on its
20
+ own (*Never waiting on CI* in `skill://legion-worker/references/pr-body.md`). After any forward merge, build the branch and
21
+ record the issue's checks again at the merge head, even with no text conflict: a clean merge once changed a
22
+ function's arity under the branch's own callers. Before you race another tree's pull request on
23
+ shared files, `envoy_publish` to its implementer's role topic, ask when its next push lands, and
24
+ merge once, after it. A forward merge that leaves the branch's diff unchanged is a
25
+ confirmation, not a new round (see *The unchanged-diff check* below). The check compares the
26
+ pull request's own lines file by file: a changed file sends the head through the tester again,
27
+ and your comment names it. Before merging, record
28
+ the fingerprint at the current tip; after pushing the merged branch, record it at the new
29
+ tip; post one PR comment (Legion footer):
30
+ `forward merge <old-tip-sha> → <new-tip-sha>; fingerprint <before> → <after>; unchanged|changed`.
31
+ The fingerprint answers one question only: whether the branch's own diff changed. It cannot
32
+ show an instance the merge brought in from the base, so before you push the merge search its
33
+ head for the class of every finding of every earlier review round and record each search
34
+ beside the fingerprints.
35
+ In a parent's last round, end that comment with `parent's last round: full test round`, the line
36
+ the tester reads for a full round (`skill://legion-architect/references/acceptance.md`,
37
+ "The last round").
38
+ On the tmux runtime, where issue workspaces are still jj workspaces of one shared clone,
39
+ `jj undo`, `jj abandon` and `jj op restore` rewrite the shared operation log, so a mistake there
40
+ is recovered forward (`jj -R "$LEGION_WORKSPACE" new`, or `jj -R "$LEGION_WORKSPACE" restore
41
+ <paths>` of files). In that shared repository jj always rebases every descendant of any commit it
42
+ rewrites — a revset naming the root of your own chain and rewriting it in place also rewrites
43
+ whatever another tree has stacked on that root, whichever selector chose it (`-s`, `-b`, and
44
+ `-r` all rewrite descendants; `-r` only re-parents them to fill the hole, which is worse).
45
+ Resolve the conflict with a forward merge instead of a rewrite — merge the branch's own
46
+ bookmark with the destination in one new commit, so nothing existing is rewritten and nothing
47
+ built on your prior commits, in this tree or another, ever moves. `base` is the pull request's
48
+ base branch (the repository's default branch unless the pull request is stacked), read as a jj
49
+ string literal: jj parses a bare bookmark name as an expression, and a name pasted between
50
+ quotes can close them itself (`release"@origin|"stable` would select `release` and `stable`), so
51
+ the revset is built from the serialized name alone:
52
+
53
+ ```bash
54
+ base=$(gh pr view <n> -R <owner>/<repo> --json baseRefName -q '.baseRefName | @json') && \
55
+ jj -R "$LEGION_WORKSPACE" new legion/<KEY> "${base}@origin" -m "merge: resolve conflict against ${base}@origin"
56
+ ```
57
+
58
+ Merge from the bookmark, never from `@`: a `jj split` leaves `@` an empty, undescribed
59
+ commit above the described one the bookmark already names, and `jj git push` refuses to push
60
+ any commit without a description — merging from `@` drags that undescribed commit into the
61
+ ancestry and the push fails (`Won't push commit … since it has no description`); the bookmark
62
+ is always on a described, already-pushed commit. If the merge conflicts, resolve it in that
63
+ one commit — edit the markers directly; there is nothing to squash, since the merge is the
64
+ only new commit. Then `jj -R "$LEGION_WORKSPACE" new` to move off it, and push it (*Every role
65
+ pushes its own commits* in `skill://legion-worker`; a merge is a code push like any other): the
66
+ merge descends from both the bookmark's old position and the destination, so it is a genuine
67
+ fast-forward and *Rewriting pushed commits* never applies.
68
+ - **After the handoff import moved the branch.** `legion handoffs import` moves `legion/<KEY>`
69
+ once, with a commit whose message is `chore(legion): <KEY>'s handoffs are on its Dispatch issue
70
+ now`, which removes the `.legion/` directory where handoffs lived before they moved to Dispatch.
71
+ A push it refuses as a moved branch is cured forward: `jj -R "$LEGION_WORKSPACE" git fetch`,
72
+ then `jj -R "$LEGION_WORKSPACE" new @- legion/<KEY>@origin -m "merge: legion/<KEY>@origin after
73
+ the handoff import"` (from `@-`, your own described tip, since your local bookmark may now
74
+ disagree with the remote one), `jj -R "$LEGION_WORKSPACE" new`, and the push — never a rewrite.
75
+ - **After a retarget.** Retargeting a pull request to a new base does not re-run Tests. Read
76
+ `base` again and merge the bookmark onto it in one command
77
+ (`base=$(gh pr view <n> -R <owner>/<repo> --json baseRefName -q '.baseRefName | @json') && jj -R "$LEGION_WORKSPACE" new legion/<KEY> "${base}@origin" -m "<message>"`) and push it the
78
+ ordinary way — a genuine fast-forward, never `--allow-backwards` — so the new head runs Tests
79
+ against the new merge result, and cite that run in the PR body.
80
+
81
+ ## The unchanged-diff check
82
+
83
+ The fingerprint every role compares after a forward merge (every flag and the fileset
84
+ verified on jj 0.45.1):
85
+
86
+ ```bash
87
+ base=$(gh pr view <n> -R <owner>/<repo> --json baseRefName -q '.baseRefName | @json') && \
88
+ cd -- "$LEGION_WORKSPACE" && jj -R "$LEGION_WORKSPACE" git fetch && \
89
+ fingerprint=$(set -o pipefail; jj -R "$LEGION_WORKSPACE" diff --from "fork_point(${base}@origin | <head-sha>)" --to <head-sha> \
90
+ --git --context 0 '~(.legion | docs/solutions)' \
91
+ | sed -e '/^@@/d' -e '/^index /d' | sha256sum) && echo "$fingerprint"
92
+ ```
93
+
94
+ - `<head-sha>` is a full commit SHA; a jj commit id is the git SHA GitHub shows.
95
+ - `fork_point(${base}@origin | <head-sha>)` is the base the branch was cut from *at that head*:
96
+ the old base for the head before the merge, the new base for the merge head, so one command serves
97
+ both sides. `base` is the pull request's base branch as a jj string literal, as in the merge
98
+ above.
99
+ - A head the merge replaced is still addressable by its SHA in the shared workspace. The check
100
+ prints a fingerprint only when every step of it succeeded: the base read, the fetch and the
101
+ diff (`pipefail` keeps the diff's failure through `sed` and `sha256sum`, which would otherwise
102
+ hash empty input to `e3b0c442…`). A nonzero exit or no printed fingerprint (a failed read of the
103
+ base, or a SHA the workspace cannot resolve) counts as a changed diff — never as unchanged.
104
+ - `--context 0` drops context lines; the `sed` drops `@@` hunk headers (line positions move
105
+ on a merge) and `index` lines (blob ids move when the base's copy of a file changed). What
106
+ is left is exactly the added and removed lines per file.
107
+ - The single fileset `'~(.legion | docs/solutions)'` leaves out `.legion/`, where handoffs lived
108
+ before they moved to Dispatch (the handoff import's commit removes it), and retro's learnings:
109
+ paths the merge gate lets through above an approval
110
+ (`skill://legion-worker/references/merge-gate.md`), which change
111
+ between one role's verified head and the next without changing the product. This is what lets
112
+ each role compare against *its own* last verified head instead of trusting another role's
113
+ numbers. It must be one expression: jj unions positional filesets, so two separate
114
+ `'~.legion' '~docs/solutions'` arguments select every file and exclude nothing. Where
115
+ `.legion/` is gone, jj warns `No matching entries for paths: .legion` on stderr; the hash is
116
+ unaffected.
117
+
118
+ Where each role gets its two heads: the implementer — the tip before and after its own forward merge;
119
+ the tester — the commit its latest results name and the new head; the reviewer — the `commit_id` of
120
+ its last submitted review (`gh api --paginate repos/{owner}/{repo}/pulls/{n}/reviews --jq '.[] | {commit_id, state, user: .user.login, body}'`, whose pages concatenate in order, so the last line is the last submitted review, and whose body tells a no-fix request that answered a READY refusal from any other)
121
+ and the new head; the merger never computes a fingerprint — it quotes the `--summary` lines
122
+ in READY, and the daemon holds the head to retro's commits.
123
+
124
+ The fingerprint is not the whole check: a file the merge adds only moves the hash, exactly as an ordinary round of work does.
125
+ So beside it, compare the *set* of files the pull request changes against its base, before and after the merge (`gh pr diff <n> --name-only`, or `jj diff --stat` at each of the two heads), and explain every path that appears or disappears.
126
+ Remove a path that belongs to an already-merged pull request.
127
+ The case that named this rule: #2588's forward merge, made after #2446 was squashed into main, re-added changelog fragments under `packages/envoy/.changes/unreleased/` whose entries main's fold had already written into `packages/envoy/CHANGELOG.md` 27.1.0, so the next fold would have written every one of them a second time.
128
+
129
+ ## Rewriting pushed commits
130
+
131
+ **Rewriting pushed commits** — a `jj squash --into` a commit already on GitHub, or any other
132
+ rewrite of a commit you already pushed — is the hazard *Reintegrating the base* describes, in a
133
+ second shape: jj rebases every descendant of any commit it rewrites, and in the one shared
134
+ repository a descendant can be another tree's branch stacked on your pushed commit, which then
135
+ moves, with its bookmark, onto a rewritten copy. So look for a descendant outside your own chain
136
+ first, after a fetch and while your chain still descends from the pushed tip:
137
+
138
+ ```bash
139
+ cd -- "$LEGION_WORKSPACE" && \
140
+ jj -R "$LEGION_WORKSPACE" git fetch && \
141
+ foreign=$(jj -R "$LEGION_WORKSPACE" log --no-graph -T 'commit_id.short() ++ "\n"' \
142
+ -r 'descendants(<the commit you are about to rewrite>) ~ ::@') && \
143
+ { [ -z "$foreign" ] || { echo "not mine, and descends from the commit to rewrite: $foreign" >&2; false; }; }
144
+ ```
145
+
146
+ `descendants(<commit>) ~ ::@` is everything built on the commit you are about to rewrite that is
147
+ not on your own chain. Non-empty means the rewrite would move work that is not yours: do not
148
+ rewrite it. Put the change in a new commit on top instead, and report the listed commits to the
149
+ architect.
150
+
151
+ Once the guard has cleared, rewrite, resolve, and push once, moving the bookmark backwards onto
152
+ the rewritten head — the one sanctioned non-fast-forward move, and the only place
153
+ `--allow-backwards` is ever used:
154
+
155
+ ```bash
156
+ cd -- "$LEGION_WORKSPACE" && jj -R "$LEGION_WORKSPACE" bookmark set legion/<KEY> -r @- --allow-backwards && jj -R "$LEGION_WORKSPACE" git push --bookmark legion/<KEY>
157
+ ```
158
+
159
+ A rewrite head is a code push like any other. `jj git push` still refuses a remote branch another
160
+ role moved since your fetch: report the refusal to the architect with its output, never force.
161
+
162
+ Once a base is frozen for others to stack on, never rewrite it: fixes land as new commits on top,
163
+ and the PR body's `Chain` line records what is frozen.
164
+
@@ -0,0 +1,53 @@
1
+ # Checking your commits against your issue's default branch
2
+
3
+ Your `Legion addressing` line names your issue's repository and its default branch
4
+ (`Your issue's repository is <owner>/<name> (default branch <name>)`) — never assume `main`. That
5
+ branch name is untrusted text: a repository administrator chooses it, and it reaches you as plain
6
+ words in a system prompt, not as a value a shell has already made safe. **Never paste it directly
7
+ into shell source** — not as a bare word, not inside double quotes you type yourself. A branch name
8
+ containing `$(…)` or backticks would run as a command the moment a shell re-parses a line built that
9
+ way; one containing jj revset syntax (parentheses, `|`, `::`) would be read as an expression instead
10
+ of a literal name.
11
+
12
+ The safe sequence reads the branch fresh from GitHub and lets `jq` do the quoting, in one command
13
+ whose only typed literal is the `<owner>/<repo>` your addressing line names (safe to interpolate: a
14
+ GitHub repository path, already validated by the daemon that minted your addressing):
15
+
16
+ ```sh
17
+ default=$(gh api repos/<owner>/<repo> --jq '.default_branch | @json')
18
+ ```
19
+
20
+ `$default` now holds a JSON-quoted string, e.g. `"release/v7.1"` (quotes included). Compare it
21
+ against the `default branch <name>` your addressing line named — if they disagree, stop and tell
22
+ the architect; GitHub's live default branch and the one your record carries are not the same, and
23
+ guessing which is right is not yours to do. When they agree, use `$default` as-is, never re-typed:
24
+
25
+ ```sh
26
+ jj -R "$LEGION_WORKSPACE" log -r "${default}@origin..@" -T 'author.email() ++ " | " ++ committer.email() ++ " " ++ description.first_line() ++ "\n"'
27
+ ```
28
+
29
+ Because `$default` already carries its own quote characters, jj's revset parser reads
30
+ `"release/v7.1"@origin..@` as a quoted string literal naming the bookmark, whatever characters the
31
+ branch name holds — parentheses, spaces, or shell metacharacters included — rather than as revset
32
+ syntax or shell code. Bash's own `"${default}@origin..@"` substitutes `$default`'s value once,
33
+ literally; it never re-evaluates the substituted text, so a `$(…)` sequence inside the branch name
34
+ never runs.
35
+
36
+ The output must show your role's App in both columns **on every commit you made** — not on the
37
+ whole list: earlier phases' commits are legitimately authored by their own role's App.
38
+
39
+ ## Which App, and what a wrong one means
40
+
41
+ - Your role's App is the **implement** App for the implementer and the merger, and the **review**
42
+ App for the planner, tester, reviewer and architect; your pane's `JJ_USER` and `JJ_EMAIL` name
43
+ its bot account. A planner's commits authored by the review App are right.
44
+ - Never set `user.name` or `user.email` in any jj or Git scope (`jj config set`, `--config`,
45
+ `git config`): `--config` outranks the pane's identity and puts the wrong App on your commits,
46
+ and the repository-scoped jj config is one file shared by every issue workspace of the clone.
47
+ - A wrong identity on your own commit (the other App, or none) is a pane-environment problem:
48
+ report it to the architect, never pin it (cybermonklabs/legion
49
+ `docs/solutions/legion/shared-main-repo-hazards-for-concurrent-issue-workspaces.md`, Hazard 1).
50
+ - Another role's commit with you as committer, which you did not rewrite under the guard in
51
+ *Rewriting pushed commits* (`skill://legion-worker/references/conflicts-and-rewrites.md`), means
52
+ something rewrote commits it should not have: stop and send the architect this log. A forward
53
+ merge rewrites nothing, so it changes no committer.
@@ -0,0 +1,199 @@
1
+ # A finding outside your issue
2
+
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
5
+ every Legion role (each phase worker, each architect, the controller) and for each of their
6
+ subagents, and it replaces the filing step of `skill://dispatch-first` and `skill://dispatch`. The
7
+ controller fixes nothing itself: it hands a finding to the tree that owns the code, as
8
+ `skill://legion-controller`, "Findings, and the issues you file", says.
9
+
10
+ ## Fix it where you are, else hand it over, and ask only for a decision
11
+
12
+ A finding outside your issue never becomes a `dispatch issue` you file. Take the first of these
13
+ three steps that fits:
14
+
15
+ 1. **Fix it here.** A defect with a clear fix in code your tree touches or depends on goes into
16
+ your tree's own pull request, while you are in that code. The fix needs no choice a person must
17
+ make, and the evidence shows the defect. Say in your handoff and in the pull request body what
18
+ you fixed beside the issue's own work. A fix deferred to another issue is a fix nobody makes.
19
+ 2. **Hand it over.** Any other clear defect goes to the controller with its evidence, as [Send a
20
+ finding to the controller](#send-a-finding-to-the-controller) says. The controller gives it to
21
+ the running tree whose code it is. Only when no tree owns that code does it file a Legion root.
22
+ 3. **Ask.** A finding that needs a person's authority, taste or risk appetite is a `dispatch ask`
23
+ with one recommendation, as [Where it goes](#where-it-goes) says: whether the work is wanted at
24
+ all, which of several designs, who owns it, or a change of scope, cost or risk.
25
+
26
+ A fix you are not sure of is not step 1: hand it over, or ask when a person must decide.
27
+
28
+ - A problem inside your issue is not such a finding: fix it in your change, since nothing needed
29
+ for correctness is deferred.
30
+ - A problem inside your tree is work its root spec's acceptance needs. A phase worker's such
31
+ problem outside its own issue goes to the tree's architect (`skill://legion-worker`, *Stop
32
+ rules*), which adds it to an open pull request of the tree, or files a child when it is a
33
+ separately deliverable change. Anything else, in the same
34
+ repository or not, is a finding.
35
+ - Search first (`skill://dispatch-first`): when an open issue already tracks the problem, comment
36
+ the evidence on that issue and send or ask nothing. When an open ask on the report issue already
37
+ asks about it (`dispatch open-asks --project <PROJECT>` lists them), add your evidence under that
38
+ ask (`dispatch comment --issue <REPORT KEY> --reply-to-ask '<ask id>' --body-file -`, the
39
+ evidence on stdin) and open no second one.
40
+
41
+ These issues are filed:
42
+
43
+ - an architect's children of its own tree (`dispatch issue --parent`, `skill://legion-architect`)
44
+ - a Legion root the controller files for a clear defect no running tree owns
45
+ - an issue a person asked the controller to file, in a direct message, in a Slack request, or in
46
+ their answer to an ask
47
+ - the report issue itself, which the controller creates once when the project has none
48
+
49
+ `skill://legion-controller`, "Findings, and the issues you file", and
50
+ `skill://legion-controller/references/daily-report.md` say how.
51
+
52
+ ## Send a finding to the controller
53
+
54
+ Post the evidence first as a `dispatch comment` on the report issue ([Where it goes](#where-it-goes)
55
+ says which), its first line exactly `Clear defect from <your role token>`. It prints
56
+ `Posted comment <id>`. Then send the controller one message with
57
+ `envoy_publish` to the controller topic your `Legion addressing` line names, with
58
+ `expects_reply: none`. It holds:
59
+
60
+ - your role token, from your `Legion addressing` line
61
+ - the defect: what went wrong, where and to whom, with the numbers, and the issue it came from
62
+ (`dispatch://<KEY>`)
63
+ - where the fix belongs: the repository and the component
64
+ - the fix
65
+ - the evidence comment, `dispatch://<REPORT KEY>/comment/<id>`
66
+
67
+ The controller hands it to the running tree whose code it is, and files a Legion root only when no
68
+ tree owns that code. Either way it replies under your evidence comment with where the work went.
69
+ You file nothing, and you wait for nothing. When the report issue is missing or closed, put the
70
+ evidence in the message itself. A production check's recorded result then names the defect as
71
+ sent to the controller.
72
+
73
+ ### When no controller takes it
74
+
75
+ The controller topic is a live wake: a message published while no controller runs reaches nobody
76
+ at the time. `envoy_publish`'s own answer tells you whether one took it. A publish to a role
77
+ answers with its live `holder`. With no claim on `legion-<project>-controller` at all, the answer
78
+ says Envoy kept the message instead (`packages/envoy/cmd/listener`, the test "unheld role is kept
79
+ and answered 202 without publishing on the role topic"): the next session that claims the role is
80
+ handed each kept message once, within one hour of the keep. A keep is not a delivery: a
81
+ controller can stay away for the whole hour, and a message kept longer than an hour is dropped
82
+ unread, so a kept finding can reach nobody. It fails with `role legion-<project>-controller
83
+ holder <session> lapsed; claim retained`, naming the holder, its `holder_state` and its
84
+ `last_seen`, when a controller holds the role and its session has not registered within the live
85
+ window: an `unresponsive` controller is not a stopped one, so release nothing, take nothing from
86
+ it, and publish no second copy.
87
+ Treat a kept answer as a controller not taking it. Either way the finding has not been taken yet,
88
+ so make it an ask, as
89
+ [What the ask says](#what-the-ask-says) spells it, on the first
90
+ of these that holds, so a person sees it and the controller finds it when it returns:
91
+
92
+ 1. **The report issue is open:** ask there, the question opening `Clear defect from <your role
93
+ token>` and `--ref` naming your evidence comment. Then reply `Asked dispatch://<REPORT
94
+ KEY>/ask/<id>` under that comment, so the controller takes the ask and not the comment. The
95
+ report issue's assignee sees the ask, and the controller's catch-up takes it.
96
+ 2. **The report issue is missing or closed:** ask on your own issue, the evidence posted there as
97
+ a comment first. Then publish the ask's reference to your tree's architect (`skill://legion-worker`,
98
+ *Stop rules*). An architect holds its own. The architect holds your issue's
99
+ acceptance until a controller has taken the ask (`skill://legion-architect`, "Acceptance and the
100
+ merge"). That hold is what keeps the ask readable: an open ask on a closed issue drops out of
101
+ the controller's own listing (`asks.go`'s `listOpenAsks` takes `i.closed_at is null`), so an
102
+ issue that closed over one hides it for good.
103
+ 3. **Your own issue is closed too** (Dispatch refuses the ask `409 ISSUE_CLOSED`, as on a
104
+ production check): put the report issue back in reach yourself, with the `dispatch` commands
105
+ below, and ask there. Never `legion({op: "set_status"})`, which the controller's report steps
106
+ (`skill://legion-controller/references/daily-report.md`) use: that op is the controller's
107
+ alone, and your session is refused `set_status is not
108
+ available to a phase-worker session` (`packages/pi-legion/src/tools.ts`'s `OPERATIONS`). No
109
+ other issue can hold an answerable ask, so this is the one case a role writes the report issue.
110
+ - It exists and is closed: `dispatch issue-update --issue <REPORT KEY> --status icebox`. Send
111
+ the status by itself. A closed issue takes a reopening status alone, and refuses it beside
112
+ an assignee or any other field (`api/issue_patch.go`, the `before.ClosedAt` branch). This is
113
+ the one lifecycle status your own prompt lets you write. The report issue is in no tree, so
114
+ the daemon re-asserts nothing on it: `workflow`'s `sessionStatusWrite` acts only for a
115
+ session that claims the issue's tree, or for an admitted root.
116
+ - A search finds none: create it as "Where it goes" names it, `dispatch issue --project
117
+ <PROJECT> --title 'Legion daily report' --spec-file -`, with the Summary
118
+ `skill://legion-controller/references/daily-report.md` gives, then park it with
119
+ `dispatch issue-update --issue <NEW KEY> --status icebox`. It carries no `legion` label.
120
+ - Then post the evidence there and ask there, as [What the ask says](#what-the-ask-says)
121
+ spells it. Say in your summary that you reopened or created it, so the controller knows.
122
+
123
+ Name the ask in your summary, and in the result a production check records.
124
+
125
+ ## Where it goes
126
+
127
+ An ask goes on the controller's report issue whenever it is open: the issue the deployment
128
+ instructions name for Legion's reports, else the project's `Legion daily report`, which
129
+ `dispatch search --query '"Legion daily report"' --project <PROJECT>` finds. It stays open in
130
+ `icebox`, so the ask stays answerable whatever happens to the issue it came from; once an issue
131
+ closes, its close withdraws its open asks (a reopen brings them back), and Dispatch refuses their
132
+ answers (`409 ISSUE_CLOSED`). The question names the issue the finding came from by its
133
+ `dispatch://` reference.
134
+
135
+ When the project has no report issue yet, or a person closed it (`dispatch read` shows `Status:
136
+ done`, and Dispatch refuses the ask with `409 ISSUE_CLOSED`), do not create or reopen one yourself
137
+ (the controller does): send the finding to the controller as text, with `envoy_publish` to the
138
+ controller topic your `Legion addressing` line names (`expects_reply: none`), holding the problem,
139
+ the issue it came from, the evidence and the decision it needs. The controller creates or reopens
140
+ the report issue and opens the ask. When that publish fails because no controller holds the role,
141
+ take step 2 or 3 of [When no controller takes it](#when-no-controller-takes-it). A production
142
+ check's recorded result then names the finding as sent to the controller, or names the ask.
143
+
144
+ ## What the ask says
145
+
146
+ The question stands alone, since the ask view shows no comment. It holds at most 2,000 characters,
147
+ the `Ref: <reference>` line `--ref` appends included (about 75 characters for a comment's
148
+ reference). Its first line names who asks: `Clear defect from <your role token>` for a clear
149
+ defect no controller took, else `Finding from <your role token>`. The architect's acceptance and the
150
+ controller's catch-up find a finding ask by that line. Then it names the concrete case before any
151
+ general statement of it:
152
+
153
+ - what went wrong, where and to whom: the issue it came from (`dispatch://<KEY>`), the machine,
154
+ file, command or session, and the numbers;
155
+ - where the fix would belong: the repository and component, and a plain statement that it is not
156
+ Legion's when it is not. A problem is not Legion's because you work in Legion: name the
157
+ component that misbehaved;
158
+ - the decision a person must make, what happens to whom under each option, and your
159
+ recommendation and its reason. A clear defect no controller took carries the fix instead, and its
160
+ options ask where the work goes, with `Hand it to the owning tree` first and recommended.
161
+
162
+ Evidence the decision does not need (full command output, logs, every file and line, the pull
163
+ request) goes in a comment. Post it first as a `dispatch comment` on the issue the ask goes on,
164
+ which prints `Posted comment <id>`, and cite that comment with `--ref`, the one reference an ask
165
+ takes. That issue,
166
+ `<ASK KEY>` below, is the report issue, except as [When no controller takes
167
+ it](#when-no-controller-takes-it) says:
168
+
169
+ ```bash
170
+ dispatch comment --issue '<ASK KEY>' --body-file - <<'EOF'
171
+ <the evidence: commands, output with its time, file:line, links>
172
+ EOF
173
+ # Keep `File it for Legion` only when your Legion addressing line lists the repository where the
174
+ # fix belongs. For a clear defect no controller took, replace the `File an issue` option below with
175
+ # 'Hand it to the owning tree: <the tree, when you know it>', the first option and recommended.
176
+ dispatch ask --issue '<ASK KEY>' --ref 'dispatch://<ASK KEY>/comment/<id>' --question-file - \
177
+ --option 'File an issue: <what it would track, and who would own it>' \
178
+ --option 'File it for Legion: Legion works it once it is filed' \
179
+ --option 'Do not file: <what stays as it is>' <<'EOF'
180
+ Finding from <your role token>
181
+ <what went wrong, where and to whom, with dispatch://<KEY> and the numbers; where the fix belongs, and that it is not Legion's when it is not; what each option does to whom; your recommendation and why>
182
+ EOF
183
+ ```
184
+
185
+ Legion refuses to record an issue on a repository the project does not map, hence that condition.
186
+
187
+ ## Tell the controller about the ask
188
+
189
+ The person answers in Dispatch, and the answer reaches only the ask's followers. So once the ask is
190
+ open, `envoy_publish` its reference, `dispatch://<REPORT KEY>/ask/<id>` (the id from the
191
+ `Asked <id> on …` line), to the controller topic your `Legion addressing` line names, with
192
+ `expects_reply: none`. The controller follows the ask and files what the person's answer asks for;
193
+ you file nothing, whatever the answer says. The controller itself skips this step, since it follows
194
+ each ask it opens. When the listener kept that publish rather than a holder taking it, do nothing
195
+ more for an ask on the report issue: the controller's catch-up follows it at its next start.
196
+
197
+ An ask on the report issue blocks nothing of your own issue: its work goes on. One on your own
198
+ issue holds its acceptance, as step 2 of [When no controller takes
199
+ it](#when-no-controller-takes-it) says.