@sjawhar/opencode-legion-envoy 8.30.0 → 8.30.2

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 (34) hide show
  1. package/package.json +1 -1
  2. package/skills/envoy/SKILL.md +47 -286
  3. package/skills/envoy/references/check-settlement.md +62 -0
  4. package/skills/envoy/references/deliveries.md +62 -0
  5. package/skills/envoy/references/legion.md +12 -0
  6. package/skills/envoy/references/roles.md +10 -0
  7. package/skills/envoy/references/sources.md +41 -0
  8. package/skills/legion-architect/SKILL.md +41 -419
  9. package/skills/legion-architect/references/acceptance.md +153 -0
  10. package/skills/legion-architect/references/decomposition.md +77 -0
  11. package/skills/legion-architect/references/design-gate.md +148 -0
  12. package/skills/legion-architect/references/wake-routing.md +11 -6
  13. package/skills/legion-controller/SKILL.md +96 -469
  14. package/skills/legion-controller/references/admission.md +7 -6
  15. package/skills/legion-controller/references/backlog-stewardship.md +32 -27
  16. package/skills/legion-controller/references/daily-report.md +72 -0
  17. package/skills/legion-controller/references/escalation.md +37 -0
  18. package/skills/legion-controller/references/finding-asks.md +32 -14
  19. package/skills/legion-controller/references/session.md +98 -0
  20. package/skills/legion-controller/references/slack-mentions.md +10 -9
  21. package/skills/legion-controller/references/triage.md +81 -0
  22. package/skills/legion-controller/references/wakes.md +90 -0
  23. package/skills/legion-oracle/SKILL.md +26 -57
  24. package/skills/legion-retro/SKILL.md +116 -240
  25. package/skills/legion-retro/references/learnings.md +58 -0
  26. package/skills/legion-retro/references/pr-body-content.md +78 -0
  27. package/skills/legion-worker/SKILL.md +40 -462
  28. package/skills/legion-worker/references/conflicts-and-rewrites.md +2 -2
  29. package/skills/legion-worker/references/default-branch-check.md +16 -0
  30. package/skills/legion-worker/references/findings.md +2 -2
  31. package/skills/legion-worker/references/pr-body.md +1 -1
  32. package/skills/legion-worker/references/review-threads.md +1 -1
  33. package/skills/thermonuclear-code-quality/SKILL.md +34 -82
  34. package/skills/thermonuclear-deep-review/SKILL.md +34 -33
@@ -6,13 +6,14 @@ waits, or neither. Read its key against `admission` and its tree's root.
6
6
 
7
7
  | What `read_state` shows | What it means | What you do |
8
8
  |---|---|---|
9
- | The key is in `admission.active` | A root holding a slot: its tree runs, `admitted` until its architect starts the first phase | Nothing, beyond [trees waiting on a root claim](skill://legion-controller/SKILL.md#trees-waiting-on-a-root-claim-go-daemon) |
9
+ | The key is in `admission.active` | A root holding a slot: its tree runs, `admitted` until its architect starts the first phase | Nothing, beyond [trees waiting on a root claim](skill://legion-controller/SKILL.md#trees-waiting-on-a-root-claim) |
10
10
  | The key is in `admission.waiting` | A root handed to Legion in `todo`, queued in Dispatch rank order for the next free slot | Nothing: it takes the next free slot by itself |
11
- | `phase: admitted`, in neither list, and Dispatch shows a `child_of` parent | A child: it runs or waits with its tree, the root its `child_of` chain reaches. Under a parked root (its `phase` is `done`) it is parked with it | Leave it to its tree. It runs again when its root does, or as a root of its own once it is `todo` with the `legion` label |
11
+ | `phase: admitted`, in neither list, and Dispatch shows a `child_of` parent | A child: it runs or waits with its tree, the root its `child_of` chain reaches. Under a root whose `phase` is `done` it waits with it | Leave it to its tree. It runs again when its root does, or as a root of its own once it is `todo` with the `legion` label |
12
12
  | `phase: admitted`, `status: todo`, in neither list, no parent | A root a person took out of the line by taking its label off | Nothing: the label puts it back in the line, and a person's label is their decision |
13
13
  | `phase: done` while `status` is `in_progress`, `testing`, `needs_review` or `retro` | Someone other than Legion moved it to a running tree's status while Legion was not running it. A root that waited for a slot is taken out of the workflow at that move, since no slot would ever reach it, and the daemon sends the `unslotted` wake; a parked child stays parked. Legion starts nothing for either | Re-admit nothing yourself: `dispatch read` shows who works it now. Name it in your summary; `todo` with the `legion` label queues it again |
14
14
 
15
- A root in the workflow with no slot that is neither waiting nor paused by its label does not stay
16
- that way: every pass of admission (a Dispatch event, the daemon's boot read, its tick) takes it out
17
- of the workflow and sends the `unslotted` wake. The wake is live only, so a controller that starts
18
- later finds such a root by the row above, as its start procedure does.
15
+ A root in the workflow with no slot that is in neither `admission.waiting` nor the label-removed
16
+ row above does not stay that way: every pass of admission (a Dispatch event, the daemon's boot
17
+ read, its tick) takes it out of the workflow and sends the `unslotted` wake. The wake is live
18
+ only, so a controller that starts later finds such a root by the last row, as its start procedure
19
+ does.
@@ -7,10 +7,9 @@ executable form of that order. The deployment instructions name the weekly-plann
7
7
 
8
8
  ## Controller routing address
9
9
 
10
- The controller keeps its startup role, `legion-<project>-controller`; it never claims
11
- `backlog-order`, which is retired. Send READY packets, scope questions, and routing requests to
12
- `notifications.role.legion-<project>-controller`. Do not add another role claim or manual
13
- subscription: changing roles ends the controller's wake delivery.
10
+ Claim no Envoy role other than `legion-<project>-controller`, your startup role, because changing
11
+ roles ends the controller's wake delivery. READY packets, scope questions and routing requests
12
+ reach you on `notifications.role.legion-<project>-controller`.
14
13
 
15
14
  ## Reconcile the weekly plan
16
15
 
@@ -18,21 +17,24 @@ Run this pass on the first turn of every UTC day and after every ruling from a p
18
17
 
19
18
  1. Read the weekly-planning issue and select its newest `plan-week-*` artifact by its latest
20
19
  `artifact.version` event. Read that artifact before changing any issue.
21
- 2. For every current plan line in this deployment's project, make the project's board order match
22
- the line order exactly: Legion admits waiting roots by board rank alone — unlike the ready order
23
- an agent takes its own next issue by, which is priority, then board rank — so board rank is what
24
- carries the plan's order. Set the coarse priority too, as the signal people and filters read; it
25
- changes no order. Keep each line's own stated sequence. Do not invent a second order from a
26
- title, a stale route, or an instruction. Reorder with
20
+ 2. For every plan line whose issue key carries this project's prefix (`daemon.project` in
21
+ `read_state`), make the project's board order match the line order exactly, because Legion
22
+ admits waiting roots by board rank alone. Set the priority the plan line states, and leave the
23
+ priority where it states none; priority changes no admission order. When one plan line lists
24
+ several issues, keep them in that order. Take the order from the plan alone, never from a
25
+ title or a route. Reorder with
27
26
  `dispatch issue-update --issue <KEY> --before <OTHER>` or `--after <OTHER>`.
28
- 3. Reconcile the plan's one-owner map. For each listed owner, verify the named session is live.
29
- For unowned work, find a suitable live owner with `envoy_sessions` filtered by `title` or `dir`,
30
- route the issue to that exact session, and tell it what it owns. Routing hands the work to that
31
- session; it starts nothing on Legion and sets no label. Do not change the human `Assignee:`
32
- field: it controls who receives asks, not the working session.
33
- 4. Rewrite the plan row with `dispatch doc-edit` guarded by the document token from the current
34
- `dispatch doc-read`. The artifact states the current rule; replace superseded text instead of
35
- keeping a change log in it. A comment or message does not update the one-owner map.
27
+ 3. Reconcile the plan's one-owner map. A session is live when `envoy_sessions` lists it; check
28
+ each listed owner that way. For unowned work, find an owner whose `envoy_sessions` `dir` is a
29
+ checkout of the issue's repository, route the issue to it
30
+ (`dispatch issue-update --issue <KEY> --route session:<session id>`) and `envoy_send` it
31
+ `You own <KEY>: <title>.`. Routing starts nothing on Legion and sets no label. Never route an
32
+ issue that carries the `legion` label, because Legion's own roles work it. Do not change the
33
+ human `Assignee:` field: it decides who receives asks, not who works the issue.
34
+ 4. Edit that issue's line in the plan artifact (its owner column) with `dispatch doc-edit`,
35
+ guarded by the document token from the current `dispatch doc-read`. Replace superseded text
36
+ instead of keeping a change log in it, because the artifact states the current rule. A comment
37
+ or message does not update the one-owner map.
36
38
 
37
39
  A ruling from a person is a direct Dispatch message to this controller. Answer it first, carry out
38
40
  its scope or priority decision, then record the resulting current rule in the affected weekly-plan
@@ -43,19 +45,22 @@ A ruling triggers the full reconciliation pass even when it appears to affect on
43
45
  ## Answering controller deliveries
44
46
 
45
47
  Read every delivery to `notifications.role.legion-<project>-controller` against live Dispatch and
46
- daemon state. Answer scope questions from the weekly plan and deployment instructions. Route a
47
- request to the verified owner when it is not the controller's decision.
48
+ daemon state. You decide labels, the statuses `todo`, `backlog` and `icebox`, and board order.
49
+ Answer a scope question from the weekly plan and the deployment instructions. Anything else goes
50
+ with `envoy_send` to the session the plan's one-owner map names for that issue.
48
51
 
49
- Until the deployment's daemon arms READY pull requests, answer each READY packet with its current
50
- queue or hold outcome and route it to the live owner named by the deployment's rules. Never merge
51
- or use a GitHub credential from the controller. Once the daemon arms READY pull requests, the
52
- controller confirms the Dispatch record and leaves the arm to the daemon.
52
+ For a READY packet, confirm the Dispatch record and leave the merge to the daemon. Never merge or
53
+ use a GitHub credential to merge from the controller, because the daemon submits the merge itself
54
+ at READY's head.
53
55
 
54
56
  ## Recovering stopped-session work
55
57
 
56
- Treat a session route as stopped only after the owner audit's two `no_holder` reads at least ten
57
- minutes apart. Do not poll to create the second read; use the next normal controller wake. Confirm
58
- that no live owner is available with `envoy_sessions` before changing the issue.
58
+ Treat a session route as stopped only after two reads of
59
+ `dispatch issues --project <KEY> --route-status no_holder --limit 250` at least ten minutes apart
60
+ both list it, as "The owner audit" in `skill://dispatch/references/issues.md` says, because a
61
+ restarting session is absent for minutes. Do not poll to create the second read; use the next
62
+ controller wake. Then confirm with `envoy_sessions` that no live session owns the work (the
63
+ session the route names, or one that took it over), because a live owner still holds it.
59
64
 
60
65
  For work that a ruling of a person's, recorded in the plan with its link (the ruling section
61
66
  above), says moves to Legion when its session stops, preserve its existing labels, add `legion`,
@@ -0,0 +1,72 @@
1
+ # Daily report
2
+
3
+ Read this on your first turn of each UTC day, whatever woke you, and when you open, follow or
4
+ answer a finding ask: the report issue holds them all.
5
+
6
+ ## Find the report issue
7
+
8
+ Use the issue the deployment instructions name for Legion's reports. With none named, use the
9
+ project's `Legion daily report` issue:
10
+ `dispatch search --query '"Legion daily report"' --project <PROJECT>`. When it does not exist,
11
+ create it once (the one issue you file unasked) and park it in `icebox`, so nobody takes it as
12
+ work. It never carries the `legion` label. Assign it to the person who answers for the project:
13
+ the one the deployment instructions name, else the owner `dispatch whoami` names, because its asks
14
+ reach that person's Inbox.
15
+
16
+ ```text
17
+ dispatch issue --project <PROJECT> --title 'Legion daily report' --assignee '<email>' --spec-file - <<'EOF'
18
+ ## Summary
19
+
20
+ Legion's controller posts one message here each day: what Legion finished, what it closed without a change and why, and what is running. Legion's agents also ask here whether a problem they found outside their own work needs an issue. This issue is not work, so it carries no `legion` label and stays in icebox.
21
+ EOF
22
+ legion({ op: "set_status", issue: "<report KEY>", status: "icebox" })
23
+ ```
24
+
25
+ With no person known, leave out `--assignee`: the issue then goes to your token's owner, or, under
26
+ the shared token, to the project's default assignee when a person set one, and is otherwise
27
+ unassigned.
28
+
29
+ ## Keep it answerable
30
+
31
+ Each time you read it:
32
+
33
+ - `Status: done` means someone closed it, and Dispatch then refuses an ask or a message there
34
+ (`409 ISSUE_CLOSED`) and hides its open asks. Reopen it with `set_status` to `icebox` and say so
35
+ in the day's report.
36
+ - `Assignee: unassigned` with a person known: assign it, naming your session (`dispatch whoami`):
37
+ `curl -sS -X PATCH -H "Authorization: Bearer $(cat "$DISPATCH_TOKEN_FILE")" -H 'Content-Type: application/json' -d '{"assignee": "<email>", "actor": {"kind": "session", "id": "<session id>"}}' "$DISPATCH_URL/api/v1/issues/<REPORT KEY>"`.
38
+ An answer that does not print the issue with its new `assignee` left the assignee as it was
39
+ (`400 ASSIGNEE_NOT_ALLOWED` names an email nobody has signed in to Dispatch with): name it, with
40
+ its first line, in your summary.
41
+ - While `Assignee:` says `unassigned`, open each report with one sentence asking a person to take
42
+ the issue from its header, because its asks reach only the Inbox's `Unassigned` filter.
43
+
44
+ ## When
45
+
46
+ After the turn's own work, read the report issue with `dispatch read`, keep it answerable, and
47
+ post when its `Events:` show no `message.created` from today.
48
+
49
+ ## What
50
+
51
+ One `dispatch message --issue <report KEY> --body-file -` of at most 2,000 characters, because a
52
+ person reads every report in their Inbox. Write it as `skill://dispatch`'s "Writing for the human" says:
53
+ every issue by its key and title, every pull request by its URL.
54
+
55
+ - **Finished.** `dispatch issues --project <PROJECT> --status done --updated-since <the previous
56
+ report's time, or 24 hours ago> --limit 250`, every page, keeping the issues `read_state` records
57
+ under `issues` whose `issue.closed` event is after the previous report. `dispatch read` each: the
58
+ pull request under `External links:` is the one that merged.
59
+ - **Closed without a change.** Those with no pull request, each with the reason its closing
60
+ message gave (the `message.created` just before `issue.closed` among `Events:`).
61
+ - **Running.** Each root in `admission.active` with its `issues.<KEY>.phase`, and the roots in
62
+ `admission.waiting`. Name as waiting on a claim each root that this turn's "Trees waiting on a
63
+ root claim" check found, with the holder its `Claimed by:` line names.
64
+ - **Free slots.** Counted as the skill's "Free slots" says; each stays free until a root in `todo`
65
+ with the `legion` label takes it.
66
+ - **Open capabilities.** Each row of `read_state`'s `capabilities` whose `status` is `open`: its
67
+ name, its `detail`, and a deployment row's `configLine`. Say who acts on it, as
68
+ `skill://legion-controller/references/wakes.md` says for `tick`. With none open, say nothing of
69
+ them.
70
+
71
+ A day with nothing finished says so in one sentence. When the lists do not fit, give each list's
72
+ count and name its issues of `--priority 0` and `--priority 1`, in board rank order.
@@ -0,0 +1,37 @@
1
+ # Architect escalation
2
+
3
+ Read this when a tree's architect sends you a matter, or a `held` wake carries
4
+ `reason: "escalated"`. Three matters come to you: a child that looks independent of its tree,
5
+ capacity (an architect says its tree cannot run what it needs under the slots it has), and a
6
+ conflict between trees. An issue-design decision is not one of them: the owning architect writes
7
+ it as a decision block and opens its own finding asks. Send a design decision back to that
8
+ architect with `envoy_publish` to its role, because it is not yours to make.
9
+
10
+ For capacity, the only lever is order. You cannot change `admission.cap`: it is the operator's
11
+ `legion.yaml` setting, and `set_status` is your only admission move. Read `admission.active` and
12
+ `admission.waiting`, tell the architect its root's place in the line with `envoy_publish`, and
13
+ open a finding ask when the deployment needs a larger cap.
14
+
15
+ For a conflict between trees, read both roots in `read_state` and Dispatch, publish what you read
16
+ to both architects' roles, and open a finding ask when the two need a person's decision.
17
+
18
+ ## A child that looks independent
19
+
20
+ Check the child and its parent against `read_state` and Dispatch. When it still looks independent,
21
+ open an ask on your report issue, as the skill's "Findings, and the issues you file" says. The ask
22
+ names the child, why it looks independent and the root you would file, with three options:
23
+ `File an issue` (a root a person takes), `File it for Legion` (a root Legion runs) and
24
+ `Do not file` (it stays in its tree). Never add the `legion` label to a child or cut its
25
+ `child_of` link, because that admits it as a root while its tree still records it. Only on a
26
+ person's yes:
27
+
28
+ 1. File the root with no `--parent`:
29
+ `dispatch issue --project <PROJECT> --title '<title>' --spec-file -`. Its spec cites the ask and
30
+ carries its `Filed for` line (`skill://legion-controller/references/finding-asks.md`). Add
31
+ `--label legion` only when the answer asks Legion to run it. `<PROJECT>` is the issue key's
32
+ prefix before `-<n>` (`ACME-3` → `ACME`), which `read_state` shows as `daemon.project`; it is
33
+ not the lowercase project token in role names such as `legion-<project>-controller`.
34
+ 2. Park the child (`set_status` to `icebox`) and comment a pointer to the new root on it. You
35
+ cannot close it, because only its architect or the daemon sets `done`.
36
+ 3. A root filed with the label opens in `triage`, and its triage wake brings it back to you
37
+ (`skill://legion-controller/references/triage.md`).
@@ -1,9 +1,22 @@
1
1
  # Finding asks
2
2
 
3
3
  Read this when a finding ask's reference reaches you on the controller topic, when one you follow
4
- is answered, and at every start. What a finding ask is and where it goes is
5
- `skill://legion-worker/references/findings.md`; what you may file is the skill's "Findings, and
6
- the issues you file" section.
4
+ is answered, when you file an issue a person asked for, and at every start. What a finding ask is
5
+ and where it goes is `skill://legion-worker/references/findings.md`; what you may file is the
6
+ skill's "Findings, and the issues you file" section.
7
+
8
+ ## The `Filed for` line
9
+
10
+ Every issue you file for a person carries one line in its spec, `Filed for <person> at their
11
+ request: <request>`, because triage reads it to tell your filing from anyone else's label
12
+ (`skill://legion-controller/references/triage.md`). The request, and the person it names, are:
13
+
14
+ - an answer to an ask: `dispatch://<REPORT KEY>/ask/<id>`, for the person who answered it;
15
+ - a Slack request: `Slack <channel_id> message <ts>`, for the mention's `<@user_id>`;
16
+ - a direct message: `direct message <id>`, with the id its frame's `reply_with` names after
17
+ `--in-reply-to`, for the frame's `actor`. A message that arrives as your own user turn shows no
18
+ id: write `direct message to session <your session id>`, for the person by the name they gave,
19
+ else `the person who messaged this session`.
7
20
 
8
21
  ## Follow it
9
22
 
@@ -17,20 +30,23 @@ dispatch follow --ask 'dispatch://<KEY>/ask/<id>' --action follow
17
30
  ## Act on its answer
18
31
 
19
32
  Once it is answered (an ask you follow brings its answer to you), read it with
20
- `dispatch read --ref 'dispatch://<KEY>/ask/<id>'` and act on the newest answer, its typed text over
21
- the selected option when the two disagree:
33
+ `dispatch read --ref 'dispatch://<KEY>/ask/<id>'` and act on the newest answer. When its typed
34
+ text asks for a different one of the three actions than the option selected, do what the text
35
+ asks; when the text asks for something that is none of the three, reply under the ask with your
36
+ question and file nothing:
22
37
 
23
38
  - `Do not file`: file nothing.
24
39
  - `File an issue`: `dispatch issue --project <PROJECT> --title '<the problem, as a title>'
25
- --spec-file -` in the project where the fix belongs, as the answer or the ask names it. The spec
40
+ --spec-file -` in the project where the fix belongs, as the answer or the ask names it; when
41
+ neither names one, in this project. The spec
26
42
  states the problem and where the fix belongs, cites the ask and its evidence by their
27
43
  `dispatch://` references, and carries the line `Filed for <the answer's By:> at their request:
28
- dispatch://<REPORT KEY>/ask/<id>` (the skill's "Findings, and the issues you file"). No `legion`
29
- label.
44
+ dispatch://<REPORT KEY>/ask/<id>` (above). No `legion` label.
30
45
  - `File it for Legion`: the same, with `--label legion`. It opens in `triage` with the label, so
31
- its triage wake brings it back to you (the skill's "New issue triage").
46
+ its triage wake brings it back to you (`skill://legion-controller/references/triage.md`).
32
47
 
33
- An ask that re-files an escalated child also takes the steps of the skill's "Architect escalation".
48
+ An ask that re-files an escalated child also takes the steps of
49
+ `skill://legion-controller/references/escalation.md`.
34
50
 
35
51
  Then reply under the ask with what you did, its first line exactly `Filed dispatch://<NEW KEY>` or
36
52
  `Not filed: <reason>`:
@@ -50,12 +66,14 @@ reaches nobody, and a new session follows none of the asks an earlier one follow
50
66
  start, catch up on the report issue (with none yet, there is nothing to catch up):
51
67
 
52
68
  1. Read it with `dispatch read`. Reopen it when its `Status:` is `done`, and assign it when its
53
- `Assignee:` says `unassigned` and a person is known, both as the skill's "Daily report" says.
69
+ `Assignee:` says `unassigned` and a person is known, both as
70
+ `skill://legion-controller/references/daily-report.md` "Keep it answerable" says.
54
71
  2. Follow each of its open asks: `dispatch open-asks --project <PROJECT>` lists every unanswered
55
72
  ask of the project, with no cap, each line naming its issue; follow those on the report issue.
56
73
  3. List the answered asks still waiting on you, which no `dispatch` command lists:
57
74
  `curl -sS -H "Authorization: Bearer $(cat "$DISPATCH_TOKEN_FILE")" "$DISPATCH_URL/api/v1/issues/<REPORT KEY>/asks?state=answered&thread=1" | jq -c '[.[] | select(any(.options[]; .label == "Do not file" or .label == "Admit it")) | .answer.at as $t | select([.thread.replies[]? | select(.author.kind == "session" and .created_at > $t and (.body | test("^(Filed dispatch://|Admitted dispatch://|Not filed:|Not admitted:)")))] | length == 0) | {id, question, answer}]'`.
58
- Act on each as above, a triage ask as the skill's "New issue triage" says.
75
+ Act on each as above, a triage ask as `skill://legion-controller/references/triage.md` says.
59
76
 
60
- A read that fails, and an assignment Dispatch refuses, are handled as the skill's "Findings, and
61
- the issues you file" says.
77
+ A read that fails, and an assignment Dispatch refuses, are handled as
78
+ `skill://legion-controller/SKILL.md#rules` and
79
+ `skill://legion-controller/references/daily-report.md` say.
@@ -0,0 +1,98 @@
1
+ # How the controller session runs
2
+
3
+ Read this when you need to know who launched you, what `read_state` returns, how to read a pull
4
+ request, when `gh` or `curl` fails, when `set_status` answers 403, or when a person takes the
5
+ controller over by hand. The `How this controller runs` part of your system prompt says who
6
+ launched you.
7
+
8
+ ## Launched by the operator (`controller: operator`, the default)
9
+
10
+ The operator ran `legion controller start --config controller.yaml [--daemon-url <url>]` on their
11
+ own machine, and you are that foreground session. The daemon reads your liveness from the Envoy
12
+ role registry (the holder of `legion-<project>-controller` and its `last_seen`) and launches no
13
+ controller itself, so do not exit the session: while none runs, the project has no controller. A
14
+ second `legion controller start` replaces you: it mints a new secret, your grants stop working
15
+ (every `set_status` answers 403), and the role moves to the new session.
16
+
17
+ ## Launched by the daemon (`controller: daemon`)
18
+
19
+ The daemon runs you in a pod and supervises you like a root architect. When the pod dies, the
20
+ daemon relaunches it and resumes this session with a new start message, so run the start procedure
21
+ each time one arrives. A person reaches you through Dispatch (a message on the Agents page, an
22
+ answer to your ask, a mention), Envoy or Slack. No `legion controller start` can replace you.
23
+
24
+ ## The `legion` tool
25
+
26
+ - `op: "read_state"` returns the daemon's whole state: `issues.<KEY>.phase`,
27
+ `issues.<KEY>.holdReason`, `issues.<KEY>.architect`, `admission.cap`, `admission.active`,
28
+ `admission.waiting`, `capabilities[]`, `daemon.project`, and `settings` (the daemon's resolved
29
+ `legion.yaml`, with no credential in it).
30
+ - `op: "set_status"` with `issue` and `status` (`todo`, `backlog` or `icebox`) moves an issue. Each
31
+ call mints its grant in-process from the secret the session registered with, so a stale secret
32
+ fails it with a 403. You cannot set `done`: only an issue's architect or the daemon closes an
33
+ issue.
34
+
35
+ ## `gh`
36
+
37
+ `gh` is Legion's wrapper and acts as the review App's bot account
38
+ (`gh api graphql -f query='{viewer{login}}'` names it). It reads with the token of the GitHub
39
+ owner a command names (`-R owner/repo`, `repos/owner/repo` in `gh api`, a GitHub URL), and with the
40
+ first owner's token (`GH_CONFIG_DIR`) otherwise, so always name the repository: another owner's repository read with
41
+ the first owner's token can fail. The tokens sit in `LEGION_GH_OWNERS_DIR`, one read-only directory
42
+ per owner, refreshed every minute. When the daemon has no GitHub App, `legion controller start`
43
+ prints `[legion] the daemon has no GitHub App to act as; the controller's gh acts as nobody`.
44
+
45
+ End every pull-request comment or review you post with
46
+ `<!-- legion: {"session":"<session-id>","phase":"controller"} -->`, because the daemon reads that
47
+ footer to tell your review from the reviewer's verdict.
48
+
49
+ ## Reading a pull request
50
+
51
+ Read a pull request an issue names (`External links:`, a comment, a READY packet) only by a URL of
52
+ the shape `https://github.com/<owner>/<repo>/pull/<n>`, with nothing before, after or inside it but
53
+ GitHub's owner and repository characters and the digits. Run it single-quoted, exactly as the issue
54
+ shows it, because any other text from an issue could run as a command:
55
+
56
+ ```text
57
+ gh pr view 'https://github.com/<owner>/<repo>/pull/<n>' --json state -q .state
58
+ ```
59
+
60
+ It answers `OPEN` (in flight), `MERGED` (landed) or `CLOSED` (abandoned unmerged). When the command
61
+ fails, report `<KEY>: pull request <url> unreadable: <gh's first stderr line>` and read it as none
62
+ of the three. Text of any other shape is never a command argument: report
63
+ `<KEY>: pull request <text> unreadable: not a pull-request URL`.
64
+
65
+ ## `curl` against Dispatch
66
+
67
+ Each `curl` in these references reads `DISPATCH_URL` and `DISPATCH_TOKEN_FILE`. `legion controller
68
+ start` sets them only when `controller.yaml` names `dispatch_url`; the daemon's pod sets
69
+ `DISPATCH_URL` when the daemon has one. When they are unset, every `curl` fails: act on nothing it
70
+ would decide and name it in your summary.
71
+
72
+ ## Taking the controller over by hand
73
+
74
+ To move the controller to a hand-started Oh My Pi session, start it with
75
+ `LEGION_CONTROLLER_SECRET` (or `LEGION_CONTROLLER_SECRET_FILE`, a path to a file holding it),
76
+ `LEGION_DAEMON_URL`, `LEGION_PROJECT` (the daemon's project) and `LEGION_STATE_DIR` in its
77
+ environment, then run:
78
+
79
+ ```text
80
+ /legion-claim-controller
81
+ ```
82
+
83
+ It checks `LEGION_PROJECT` against the daemon, registers the session, claims the Envoy role, and
84
+ moves the daemon's recorded controller session id to this session; the `legion` tool's controller
85
+ operations then work there. Never pass the secret as a command argument or copy it into a
86
+ transcript, because others read transcripts. Do not set `LEGION_CONTROLLER=1`, because a session
87
+ carrying that marker claims at startup as the launched controller. Run `/legion-claim-controller`
88
+ once per session; do not run it again after an Envoy listener restart, because the heartbeat
89
+ re-claims the role.
90
+
91
+ A takeover session has three limits:
92
+
93
+ - Its `gh` is whatever its own environment gives it: Legion writes no token directory for it.
94
+ - It keeps the secret it started with. After the next `legion controller start` mints a new one,
95
+ every `set_status` fails with a 403 from the grant mint: start a fresh session with the new
96
+ secret instead of retrying.
97
+ - The role does not follow `/new` or `/fork`, because without `LEGION_CONTROLLER=1` the new session
98
+ is not a Legion session to the extension: run `/legion-claim-controller` again after either.
@@ -12,7 +12,8 @@ nothing here applies.
12
12
  Your session hears each listed channel's mentions by itself, on
13
13
  `notifications.slack.*.<channel>.mention`, for as long as it holds the controller role: a resumed or
14
14
  relaunched session hears the channels its launch names and no others, and drops every Slack topic an
15
- earlier session subscribed to by hand. Never `envoy_subscribe` a Slack topic yourself.
15
+ earlier session subscribed to by hand. Never `envoy_subscribe` a Slack topic yourself, because a
16
+ hand subscription is dropped on the next relaunch and hears channels the launch does not name.
16
17
 
17
18
  A delivery from Slack whose `channel_id` the line does not name is not yours: do nothing with it.
18
19
 
@@ -53,10 +54,10 @@ Every answer is one `slack_post` call:
53
54
  `slack_post` is sent once and never retried. When it answers that Slack did not answer, the reply
54
55
  may already be in the thread: do not post it again. When it answers with Slack's refusal, the
55
56
  reply is not posted: `not_in_channel` means the app has not been added to that channel, which only
56
- a person in the workspace can fix, so say so in your next report rather than retrying. When it
57
+ a person in the workspace can fix, so say so in the next daily report rather than retrying. When it
57
58
  answers that the secrets broker has not granted `LEGION_SLACK_BOT_TOKEN` (its request waits for
58
59
  approval, or was denied), nothing was posted and no reply can be until the operator stores the
59
- token with owner `legion-daemon` and tier `agent`: name the error in your next report.
60
+ token with owner `legion-daemon` and tier `agent`: name the error in the next daily report.
60
61
 
61
62
  ## Decide what the person wants
62
63
 
@@ -68,7 +69,7 @@ For a person's mention, take the first of these that fits.
68
69
  - **A question about state.** "What is Legion working on?", "Where is ACME-12?", "Why is it held?"
69
70
  Answer from `read_state` and Dispatch as they stand now, never from memory: each issue's
70
71
  `issues.<KEY>.phase`, its pull request, `admission.active` and `admission.waiting`. Say what you
71
- read, not what you infer.
72
+ read, citing for each fact the `read_state` field or the `dispatch read` line it came from.
72
73
  - **A request for work.** Someone wants a change made, a bug fixed, something investigated. Search
73
74
  first (`skill://dispatch-first`): when an open issue already tracks it, reply with that issue and
74
75
  file nothing. Otherwise file one issue in the project:
@@ -83,15 +84,15 @@ For a person's mention, take the first of these that fits.
83
84
  EOF
84
85
  ```
85
86
 
86
- The last line is the skill's `Filed for` line ("Findings, and the issues you file"), which triage
87
+ The last line is the `Filed for` line (`skill://legion-controller/references/finding-asks.md`), which triage
87
88
  reads. Add `--label legion` only when the person asks Legion to do the work ("Legion, fix…", "can
88
89
  Legion take…"); a request for someone to look at something, or one that does not say who, gets
89
90
  no label and waits for a person to hand it over. Never label a request outside the scope the
90
91
  deployment instructions state. Reply with the new issue's key, its title, the link and assignee
91
92
  the `Created` line names (naming nobody, Dispatch assigns the issue to your token's owner, or,
92
93
  under the shared token, to the project's default assignee when a person set one),
93
- and whether Legion will take it: with the label, triage admits it in its next turn unless it
94
- should wait.
94
+ and whether Legion will take it: with the label, triage admits it in its next turn unless the
95
+ request, the weekly plan or the deployment instructions say to wait.
95
96
  - **Something an issue's architect owns.** A question about a tree's design, a change of direction,
96
97
  a decision only the tree's architect can make about an issue Legion runs. Find the tree: follow
97
98
  `Links:` up through each `child_of` parent with `dispatch read` until you reach the issue
@@ -102,5 +103,5 @@ For a person's mention, take the first of these that fits.
102
103
  that in the thread instead and what `read_state` shows for the tree. Never invent a role token.
103
104
  - **Anything else** (a greeting, a question about Legion itself): answer it briefly in the thread.
104
105
 
105
- A request to approve a design is answered as the skill's "Mentions" section says: a person
106
- approves the spec in Dispatch, and you never open a gate.
106
+ A request to approve a design is answered as `skill://legion-controller/references/wakes.md`
107
+ "GitHub mentions" says: a person approves the spec in Dispatch, and you never open a gate.
@@ -0,0 +1,81 @@
1
+ # Triage and handover
2
+
3
+ Read this on a `triage` wake, at every start, and when a person hands you an issue. Dispatch and
4
+ `read_state`, not the wake text, decide triage.
5
+
6
+ ## At every start
7
+
8
+ 1. List the project's triage issues handed to Legion:
9
+ `dispatch issues --project <PROJECT> --status triage --label legion --limit 250 --offset 0`.
10
+ When the first line ends `(showing 1-250 of N)`, read the next page with `--offset 250`, and so
11
+ on until you have all N rows.
12
+ 2. The rows show no parent, so `dispatch read` each one and follow `Links:` up through each
13
+ `child_of` parent. A `child_of` under `Referenced by:` is a child of this issue, not its parent.
14
+ 3. Leave a child that has an ancestor in `admission.active` or `admission.waiting`, because that
15
+ tree's architect owns it.
16
+ 4. Triage, as a root, every other row with no `issues.<KEY>` entry in `read_state`. This includes
17
+ a child whose tree is in neither `admission.active` nor `admission.waiting`.
18
+ 5. A root `read_state` records and that is now in `triage` is one a person pulled back. Never
19
+ re-admit it yourself; ask the person in your summary: "<KEY> was pulled back to triage; what do
20
+ you want?".
21
+
22
+ ## Triage a root
23
+
24
+ 1. Read `read_state`, then the issue with `dispatch read`. Check that it is in this project, that
25
+ it is a root, and whether it already has children. A child the daemon carried to a root of its
26
+ own when its old root closed (`skill://legion-controller/references/wakes.md`) is not new work:
27
+ leave it. Note its `Assignee:`: that person answers the tree's asks, because their Inbox opens on the
28
+ issues they hold. Never reassign it, because who holds an issue is the people's decision, made
29
+ from the issue header.
30
+ 2. Find who handed it over. `dispatch read` does not show who set a label, so list its events:
31
+ `curl -sS -H "Authorization: Bearer $(cat "$DISPATCH_TOKEN_FILE")" "$DISPATCH_URL/api/v1/issues/<issue>/events?limit=200"`.
32
+ The first `issue.created` or `issue.updated` event whose `payload.labels` holds `legion` (any
33
+ case) set it, and its `actor` is who.
34
+ - `actor.kind` is `user`: a person handed it over.
35
+ - Your own filing for a person hands it over too: its spec as filed (`dispatch doc-read --issue
36
+ <issue> --version 1`) holds a `Filed for <person> at their request: <request>` line, and its
37
+ `actor` is this session (`dispatch whoami` names it) or an earlier one of yours. For a request
38
+ that is an ask, an earlier session is yours when its reply under that ask (`dispatch read
39
+ --ref`) starts `Filed dispatch://<this KEY>` after a newest answer of `File it for Legion`.
40
+ - Any other actor hands over nothing. Leave the issue in `triage` and ask the person on your
41
+ report issue whether Legion should take it, naming the issue and who labelled it, with the
42
+ options `Admit it` and `Leave it`. On `Admit it`, go on to step 3. Either way, reply under the
43
+ ask `Admitted dispatch://<KEY>` or `Not admitted: <reason>`, because the start procedure's
44
+ catch-up reads that reply as done.
45
+ 3. Admit it (`set_status` to `todo`) when a person handed it over, unless the request, the weekly
46
+ plan or the deployment instructions say to wait or put it out of scope:
47
+ `legion({ op: "set_status", issue: "<issue>", status: "todo" })`.
48
+ 4. When one of those says to wait, park it with `set_status` to `backlog`, or `icebox` when the
49
+ request asks for a longer deferral, instead of leaving it in `triage`; the label stays. Never
50
+ park a root for lack of a free slot, because in `todo` the daemon's waiting line already holds
51
+ it until a slot frees.
52
+ 5. Post a triage note (`dispatch comment` on the issue: what you decided and why) that names who
53
+ will be asked: `Assigned to <login>, who will get this tree's questions and its design
54
+ approval`, or, when `Assignee:` says `unassigned`, `Unassigned — nobody's Inbox shows this
55
+ tree's questions or its design approval until someone takes it from the issue header (Assignee,
56
+ beside Priority)`. Promise the design approval only when your system prompt's `Design gate
57
+ policy:` line says `gates.design: root-issues`; under `gates.design: off` nobody approves a
58
+ design, so drop "and its design approval" (and "or its design approval"). Take the assignee
59
+ from the read, never from how the issue was created, because an issue filed with no assignee
60
+ still lands on the project's default assignee where a person set one. An unassigned root still
61
+ runs, but its architect's asks reach only the Inbox's `Unassigned` filter.
62
+
63
+ ## Label an issue for a person
64
+
65
+ Keep the labels the issue has, which `Labels:` lists (`none` is no labels), because the `--label`
66
+ flags replace the whole set. Pass each label single-quoted, exactly as `Labels:` shows it:
67
+
68
+ ```text
69
+ dispatch issue-update --issue <KEY> --label '<each current label>' --label legion
70
+ ```
71
+
72
+ A label holding a `'` or a line break cannot be quoted and never goes in a command: tell the person
73
+ (`<KEY>: label <label> cannot be quoted`), so they hand it over from the issue header.
74
+
75
+ Then: in `todo` the label admits it. In `triage`, triage it in the same turn from step 3 above,
76
+ because the person's request is its handover. In any other status, `set_status` it to `todo` only
77
+ when the person asked Legion to start it now.
78
+
79
+ Taking the label off a waiting root drops it from the waiting line; taking it off an admitted tree
80
+ does not stop it. A child needs no label: it runs under its tree's architect once its root is
81
+ admitted. A child outside a live tree is admitted as a root of its own and needs the label.