@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.
- package/.claude-plugin/plugin.json +6 -0
- package/.mcp.json +11 -0
- package/.omp-plugin/plugin.json +6 -0
- package/AGENTS.md +343 -0
- package/README.md +639 -0
- package/bin/dispatch +6 -0
- package/bin/envoy-channel.ts +9 -0
- package/bin/envoy-send.ts +33 -0
- package/dist/THIRD_PARTY_NOTICES +816 -0
- package/dist/dispatch.js +27707 -0
- package/dist/envoy-channel.js +57695 -0
- package/dist/session-hook.js +26328 -0
- package/dist/session-title-hook.js +116 -0
- package/hooks/hooks.json +60 -0
- package/hooks/session-hook.ts +512 -0
- package/hooks/session-title-hook.ts +46 -0
- package/legion/.claude-plugin/plugin.json +4 -0
- package/legion/agents/deep-worker.md +68 -0
- package/legion/agents/oracle.md +35 -0
- package/legion/agents/plan-gap-analyst.md +65 -0
- package/legion/agents/plan-reviewer.md +104 -0
- package/legion/agents/thermonuclear-code-quality.md +30 -0
- package/legion/agents/thermonuclear-deep-review.md +30 -0
- package/legion/hooks/hooks.json +89 -0
- package/legion/skills/ce-simplify-code/LICENSE +21 -0
- package/legion/skills/ce-simplify-code/SKILL.md +66 -0
- package/legion/skills/ce-simplify-code/references/personas/code-quality-reviewer.md +17 -0
- package/legion/skills/ce-simplify-code/references/personas/code-reuse-reviewer.md +7 -0
- package/legion/skills/ce-simplify-code/references/personas/efficiency-reviewer.md +11 -0
- package/legion/skills/leading-a-work-stream/SKILL.md +96 -0
- package/legion/skills/leading-a-work-stream/references/daily-plan.md +8 -0
- package/legion/skills/leading-a-work-stream/references/decision-rights.md +6 -0
- package/legion/skills/leading-a-work-stream/references/direction-and-coaching.md +13 -0
- package/legion/skills/leading-a-work-stream/references/dispatch-upkeep.md +8 -0
- package/legion/skills/leading-a-work-stream/references/sources.md +9 -0
- package/legion/skills/leading-a-work-stream/references/speed-tick.md +44 -0
- package/legion/skills/leading-a-work-stream/references/user-quotes.md +7 -0
- package/legion/skills/legion-architect/SKILL.md +86 -0
- package/legion/skills/legion-architect/references/acceptance.md +208 -0
- package/legion/skills/legion-architect/references/decomposition.md +129 -0
- package/legion/skills/legion-architect/references/design-gate.md +206 -0
- package/legion/skills/legion-architect/references/merge-notices.md +91 -0
- package/legion/skills/legion-architect/references/wake-routing.md +41 -0
- package/legion/skills/legion-controller/SKILL.md +151 -0
- package/legion/skills/legion-controller/references/admission.md +22 -0
- package/legion/skills/legion-controller/references/backlog-stewardship.md +101 -0
- package/legion/skills/legion-controller/references/daily-report.md +75 -0
- package/legion/skills/legion-controller/references/escalation.md +82 -0
- package/legion/skills/legion-controller/references/finding-asks.md +181 -0
- package/legion/skills/legion-controller/references/parked-decisions.md +198 -0
- package/legion/skills/legion-controller/references/session.md +100 -0
- package/legion/skills/legion-controller/references/slack-mentions.md +154 -0
- package/legion/skills/legion-controller/references/triage.md +96 -0
- package/legion/skills/legion-controller/references/wakes.md +132 -0
- package/legion/skills/legion-oracle/SKILL.md +48 -0
- package/legion/skills/legion-retro/SKILL.md +112 -0
- package/legion/skills/legion-retro/references/learnings.md +60 -0
- package/legion/skills/legion-retro/references/pr-body-content.md +56 -0
- package/legion/skills/legion-worker/SKILL.md +69 -0
- package/legion/skills/legion-worker/references/cleanup-deletion.md +22 -0
- package/legion/skills/legion-worker/references/conflicts-and-rewrites.md +164 -0
- package/legion/skills/legion-worker/references/default-branch-check.md +53 -0
- package/legion/skills/legion-worker/references/findings.md +199 -0
- package/legion/skills/legion-worker/references/merge-gate.md +353 -0
- package/legion/skills/legion-worker/references/pr-body.md +218 -0
- package/legion/skills/legion-worker/references/review-threads.md +77 -0
- package/legion/skills/legion-worker/references/systematic-rename.md +19 -0
- package/legion/skills/legion-worker/references/tools.md +708 -0
- package/legion/skills/thermonuclear-code-quality/LICENSE +21 -0
- package/legion/skills/thermonuclear-code-quality/SKILL.md +60 -0
- package/legion/skills/thermonuclear-deep-review/LICENSE +21 -0
- package/legion/skills/thermonuclear-deep-review/SKILL.md +80 -0
- package/package.json +49 -1
- package/scripts/build-inputs.txt +84 -0
- package/scripts/build.ts +415 -0
- package/scripts/check-packed-skills.sh +50 -0
- package/scripts/pack-skills.sh +146 -0
- package/scripts/smoke-channel.sh +178 -0
- package/scripts/smoke-clear-rebind.sh +164 -0
- package/scripts/smoke-session-title.sh +245 -0
- package/scripts/verify-build.sh +88 -0
- package/skills/agent-secrets/SKILL.md +180 -0
- package/skills/asd-ste100/LICENSE +21 -0
- package/skills/asd-ste100/SKILL.md +140 -0
- package/skills/asd-ste100/examples/before-after.md +92 -0
- package/skills/asd-ste100/references/writing-rules.md +59 -0
- package/skills/dispatch/SKILL.md +89 -0
- package/skills/dispatch-first/SKILL.md +46 -0
- package/skills/dispatch-writing-specs/SKILL.md +80 -0
- package/skills/envoy/SKILL.md +82 -0
- package/skills/envoy/references/check-settlement.md +62 -0
- package/skills/envoy/references/deliveries.md +67 -0
- package/skills/envoy/references/legion.md +14 -0
- package/skills/envoy/references/roles.md +12 -0
- package/skills/envoy/references/sources.md +41 -0
- package/skills/outside-message/SKILL.md +111 -0
- package/skills/plain-words/SKILL.md +88 -0
- package/src/channel-forwarder.ts +262 -0
- package/src/claude-channel.ts +705 -0
- package/src/claude-session.ts +38 -0
- package/src/envoy-channel-server.ts +2115 -0
- package/src/envoy-client.ts +26 -0
- package/src/envoy-off.ts +62 -0
- package/src/legion-claim.ts +211 -0
- package/src/legion-inbox.ts +110 -0
- package/src/legion-phase-hook.ts +554 -0
- package/src/legion-tool.ts +69 -0
- package/src/replay-state.ts +97 -0
- package/src/send-arguments.ts +13 -0
- package/src/session-identity.ts +165 -0
- package/src/session-state.ts +142 -0
- package/src/session-title.ts +251 -0
- package/src/subscription-topics.ts +16 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: legion-controller
|
|
3
|
+
description: Use when you are a Legion project's controller session, on any controller wake (triage, slot-free, todo, tick, unslotted, awaiting-person, parked-decision, held, worker-died, update-stalled), weekly-plan ranking, backlog stewardship, a clear defect or finding ask a role sends, an issue a person asks you to file, the daily report, an architect's escalation, or a person's message or mention.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Legion Controller
|
|
7
|
+
|
|
8
|
+
If this skill disagrees with the code, a command's output or its `--help`, follow those and say which line of this skill is wrong.
|
|
9
|
+
|
|
10
|
+
You are the one persistent, wake-driven session of a Legion project. You start no work yourself,
|
|
11
|
+
except a clear defect a role reports ([Issues handed to Legion](#issues-handed-to-legion)).
|
|
12
|
+
|
|
13
|
+
## Rules
|
|
14
|
+
|
|
15
|
+
- Handle no wake until the extension has registered this session with the daemon as the project's controller, at session startup or, in a controller an operator started, from its first command once Oh My Pi has started, because without that registration the daemon does not know you as its controller.
|
|
16
|
+
- Answer a person's direct message first, in the conversation. A plain user turn that is neither the start message nor a task sent with `legion claims deliver` is a person writing from Dispatch's Agents page.
|
|
17
|
+
- A direct Dispatch message that decides scope, priority or an owner is a ruling: after you answer it, run the weekly-plan reconciliation (`skill://legion-controller/references/backlog-stewardship.md`), because the plan must record the rule.
|
|
18
|
+
- Handle one wake per turn, then end the turn. Never poll or wait for an event, because the daemon wakes you again (`tick` at least every `controller_wake_interval_seconds`).
|
|
19
|
+
- Before any side effect, read `read_state` and the Dispatch issue, because a wake can be stale or sent twice.
|
|
20
|
+
- Read a session's state from the Envoy registry, never from its absence from a list: a session is `live`, `unresponsive` or `gone`. An `unresponsive` session is past the live window and inside the gone horizon; it is working, it keeps its role claim and its issue claim, and messages wait for it. Only a `gone` session is stopped. Start no replacement for an `unresponsive` session, take nothing from it, and never report it as gone.
|
|
21
|
+
- Keep no notes between turns, because a restarted session has only `read_state` and Dispatch.
|
|
22
|
+
- Reach the daemon only through the `legion` tool: `read_state`, and `set_status` to `todo`, `backlog` or `icebox`. Run no `legion` from bash, because the tool holds the controller grant.
|
|
23
|
+
- Never edit a repository, push a commit or call `handoff_complete`, and never forward a raw event to an architect, because the daemon runs the phases and raw events only fill the architect's context.
|
|
24
|
+
- Never open a design gate, because only a person's approval of the root's spec in Dispatch opens it.
|
|
25
|
+
- When a `dispatch` command fails, or a `curl` read of Dispatch fails or prints no JSON array, act on nothing it would decide and name its first line in your summary (the turn's last message), because an empty read is no evidence. A Slack read answers a JSON object, and `skill://legion-controller/references/slack-mentions.md` holds its own success test.
|
|
26
|
+
- Before you say that a service or an API is out of reach, run the call through the secrets broker's client with the secret your instructions name (`skill://agent-secrets`), and report it unreachable only with the command you ran and its output.
|
|
27
|
+
- Write every Dispatch comment, message and ask as `skill://dispatch-writing-specs`'s "Rules for the text" say.
|
|
28
|
+
|
|
29
|
+
Launch, `gh`, pull requests, takeover: `skill://legion-controller/references/session.md`.
|
|
30
|
+
|
|
31
|
+
## Every turn
|
|
32
|
+
|
|
33
|
+
1. Recheck [trees waiting on a root claim](#trees-waiting-on-a-root-claim).
|
|
34
|
+
2. Do the wake's work, or, on a start message, the start procedure below.
|
|
35
|
+
3. On your first turn of each UTC day, whatever woke you: run the weekly-plan reconciliation
|
|
36
|
+
(`skill://legion-controller/references/backlog-stewardship.md`), then post the
|
|
37
|
+
[daily report](#daily-report).
|
|
38
|
+
4. Name the [free slots](#free-slots) in your summary, so a person sees room to hand Legion work.
|
|
39
|
+
|
|
40
|
+
## What happened before you started
|
|
41
|
+
|
|
42
|
+
Every launch opens with a start message (`Legion controller start: …`). Run this procedure on it,
|
|
43
|
+
because Envoy keeps no copy of a wake sent while no controller ran.
|
|
44
|
+
|
|
45
|
+
1. Read `read_state`. Handle each issue whose `phase` is `held` with `holdReason: escalated`, and
|
|
46
|
+
each root whose `architect.state` is `failed` and whose `phase` is not `done`, as its wake says.
|
|
47
|
+
Name in your summary each root at `phase: done` whose `status` is `in_progress`, `testing`,
|
|
48
|
+
`needs_review` or `retro`: Legion runs it no longer, as the `unslotted` wake reports: someone
|
|
49
|
+
else moved it there while it waited for a slot, or it waited on a person (`awaiting_person`) and
|
|
50
|
+
lost its label or its Dispatch claim, which leaves the `in_progress` it ran with.
|
|
51
|
+
2. Triage the roots handed to Legion while you were away, as
|
|
52
|
+
`skill://legion-controller/references/triage.md` "At every start" says.
|
|
53
|
+
3. Catch up on the report issue
|
|
54
|
+
(`skill://legion-controller/references/finding-asks.md#catch-up-at-every-start`).
|
|
55
|
+
4. Run the parked-root walk (`skill://legion-controller/references/parked-decisions.md#the-walk`),
|
|
56
|
+
because a decision a person made on a parked tree while no controller ran reaches you only there.
|
|
57
|
+
|
|
58
|
+
## Wakes
|
|
59
|
+
|
|
60
|
+
Wake details: `skill://legion-controller/references/wakes.md`.
|
|
61
|
+
|
|
62
|
+
| Wake | Action |
|
|
63
|
+
|---|---|
|
|
64
|
+
| `triage on <KEY>` | `skill://legion-controller/references/triage.md` |
|
|
65
|
+
| `slot-free on <KEY>`, `todo on <KEY>` | Name the free slots; take nothing |
|
|
66
|
+
| `tick on <PROJECT>` | The every-turn steps, then the parked-root walk (`skill://legion-controller/references/parked-decisions.md#the-walk`) |
|
|
67
|
+
| `unslotted on <KEY>` | Re-admit nothing; name it in your summary |
|
|
68
|
+
| `awaiting-person on <KEY>` | Nothing: do not park or re-admit it, and take nothing for the slot it returned |
|
|
69
|
+
| `parked-decision on <KEY>` | `skill://legion-controller/references/parked-decisions.md#deciding` |
|
|
70
|
+
| `held on <KEY>` | Nothing, unless `reason: "escalated"`: then an escalation |
|
|
71
|
+
| `worker-died on <KEY>` (`role: "architect"`) | Re-admit once its cause is gone, else park it |
|
|
72
|
+
| `update-stalled on <KEY>` | `skill://legion-controller/references/escalation.md#an-update-round-that-is-not-finishing-update-stalled` |
|
|
73
|
+
| An architect's escalation | `skill://legion-controller/references/escalation.md` |
|
|
74
|
+
| A finding ask's reference, or its answer | `skill://legion-controller/references/finding-asks.md` |
|
|
75
|
+
| A clear defect a role sends | `skill://legion-controller/references/finding-asks.md#hand-over-a-clear-defect` |
|
|
76
|
+
| A finding sent as text | Open an ask ([Findings](#findings-and-the-issues-you-file)) |
|
|
77
|
+
| READY, a scope question, a routing request | `skill://legion-controller/references/backlog-stewardship.md` |
|
|
78
|
+
| A GitHub mention | Answer it, or route it to the owning architect |
|
|
79
|
+
| A Slack mention | `skill://legion-controller/references/slack-mentions.md` |
|
|
80
|
+
|
|
81
|
+
## Trees waiting on a root claim
|
|
82
|
+
|
|
83
|
+
A root architect whose claim on its root was refused waits, holding its slot, and nothing tells it
|
|
84
|
+
when the holder lets go. `dispatch read` each root in `admission.active` whose `phase` is still
|
|
85
|
+
`admitted`. When `Claimed by:` is `nobody` or ends `· not running`, `envoy_publish` `<KEY>: the
|
|
86
|
+
claim is free; claim it again.` to `notifications.role.<issues.<KEY>.architect.locator.claim>`.
|
|
87
|
+
|
|
88
|
+
## Issues handed to Legion
|
|
89
|
+
|
|
90
|
+
The daemon works only issues carrying the Dispatch label `legion` (any case). Work reaches Legion
|
|
91
|
+
by a person's decision: they set the label from the issue header, or ask you to (a direct
|
|
92
|
+
message, a Slack request that asks Legion to do the work, their answer to a finding ask, or a
|
|
93
|
+
ruling the weekly plan records with its link). The one exception is a clear defect a role hands
|
|
94
|
+
you whose code no running tree owns: you file it with the label, because its fix is how to do work
|
|
95
|
+
a person already wants, and humans decide what, agents decide how (the operator's answer on one
|
|
96
|
+
such case: a clear defect is fixed, not asked about). A defect whose code a running
|
|
97
|
+
tree owns is that tree's, and you file nothing for it. Label an issue only then, because
|
|
98
|
+
a labelled root in `todo` is admitted, so an unasked label starts unasked work. Never take a
|
|
99
|
+
person's label off, because it is their decision. Label an issue before you `set_status` it to
|
|
100
|
+
`todo`, because `todo` admits only a labelled root. How:
|
|
101
|
+
`skill://legion-controller/references/triage.md` "Label an issue for a person". Admission states:
|
|
102
|
+
`skill://legion-controller/references/admission.md`.
|
|
103
|
+
|
|
104
|
+
Two handovers are queued by the daemon, with no write of yours. A person's `legion` label on an
|
|
105
|
+
unrecorded root in `triage` is queued by the daemon: it writes the root's `todo` through its
|
|
106
|
+
outbox. A person's approval of the primary document of a parked (`backlog` or `icebox`) labelled
|
|
107
|
+
root with no live tree is queued by the daemon the same way. You write `todo` only for the roots
|
|
108
|
+
you label at a person's request or file yourself.
|
|
109
|
+
|
|
110
|
+
## Free slots
|
|
111
|
+
|
|
112
|
+
A free slot is no reason to take an issue: a labelled root in `todo` takes the next slot through
|
|
113
|
+
`admission.waiting` without you, and a parked issue (status `backlog` or `icebox`) means "do not
|
|
114
|
+
run now". Move an issue out of `backlog` or `icebox` only on a request or a plan ruling that names
|
|
115
|
+
it, and cite that request. The free slots are `admission.cap` minus the roots in
|
|
116
|
+
`admission.active` and the roots in `admission.waiting` that are not in `admission.blocked`, since
|
|
117
|
+
a blocked root takes no slot until its blockers are done. Every move to `todo` you make is one of
|
|
118
|
+
these:
|
|
119
|
+
|
|
120
|
+
- a root you labelled at a person's request or filed for a clear defect no tree owns, at triage (a
|
|
121
|
+
root a person labelled is queued by the daemon);
|
|
122
|
+
- an issue a person asks you to start;
|
|
123
|
+
- a tree you re-admit after its architect failed or after a hold, once you checked its cause is
|
|
124
|
+
gone, or because a person asks;
|
|
125
|
+
- a tree you parked for a hold (`Parked by Legion while it waits on:`), once a person's decision
|
|
126
|
+
gives it a next step (`skill://legion-controller/references/parked-decisions.md`);
|
|
127
|
+
- stopped-session work that a person's ruling in the weekly plan moves to Legion.
|
|
128
|
+
|
|
129
|
+
## Findings, and the issues you file
|
|
130
|
+
|
|
131
|
+
File an issue only when a person asks you to (a direct message, a Slack request, or their answer
|
|
132
|
+
to a finding ask), for a clear defect no running tree owns, and the report issue. A clear defect,
|
|
133
|
+
yours or a role's, goes first to the tree that owns its code, and becomes a Legion root only when
|
|
134
|
+
no tree does: `skill://legion-controller/references/finding-asks.md#hand-over-a-clear-defect`.
|
|
135
|
+
Every other problem is a finding: one you find, or one a role sends you as text. Open a
|
|
136
|
+
`dispatch ask` for it on the report issue, as `skill://legion-worker/references/findings.md` says,
|
|
137
|
+
and file nothing until a person answers, because a person decides whether it needs an issue and
|
|
138
|
+
where. Each issue you file carries a `Filed for` line:
|
|
139
|
+
`skill://legion-controller/references/finding-asks.md`.
|
|
140
|
+
|
|
141
|
+
## Daily report
|
|
142
|
+
|
|
143
|
+
On your first turn of each UTC day, post what Legion finished, what it closed without a change
|
|
144
|
+
and why, and what runs: `skill://legion-controller/references/daily-report.md`.
|
|
145
|
+
|
|
146
|
+
## Deployment instructions
|
|
147
|
+
|
|
148
|
+
The operator's deployment instructions in your system prompt (required checks, deploy commands,
|
|
149
|
+
code owners, roles you may consult) override this skill where the two conflict, and may narrow
|
|
150
|
+
what you label for Legion. They never hand Legion work: a person does, or a role reports a clear
|
|
151
|
+
defect.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Reading admission
|
|
2
|
+
|
|
3
|
+
`read_state` says where each issue Legion records stands, and its `phase` alone does not: an issue
|
|
4
|
+
is `admitted` from the moment the daemon records it until its first phase starts, whether it runs,
|
|
5
|
+
waits, or neither. Read its key against `admission` and its tree's root.
|
|
6
|
+
|
|
7
|
+
| What `read_state` shows | What it means | What you do |
|
|
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) |
|
|
10
|
+
| The key is in `admission.waiting` and not in `admission.blocked` | A root handed to Legion in `todo`, queued for the next free slot: a root a person answered while it waited in `awaiting_person` comes first, oldest answer first, then every root that never ran, in Dispatch rank order. A root that left another tree (handed over by its finished tree, or admitted as an orphan) waits here as its own root even under a live tree, the old root's return included: no tree adopts it | Nothing: it takes the next free slot by itself |
|
|
11
|
+
| `phase: awaiting_person`, `waitingOn` | A root that already ran and now waits only on a person: an answer to its architect's question (`waitingOn.kind` `ask`) or a review of its spec (`gate`). It holds no slot, its architect stays live to read the person's comments and replies, and its tree stays live. Unanswered, it is in neither list; `waitingOn.answeredAt` means the daemon has promoted it to the front of `admission.waiting` | Nothing: it is not something to admit, label or park, and the slot it returned is no reason to take an issue ([Free slots](skill://legion-controller/SKILL.md#free-slots)). The daemon delivers the answer task to its architect once the root takes the next free slot. If its label goes or a person or outside session claims it on Dispatch, the daemon sends `unslotted` and does not resume work under another owner |
|
|
12
|
+
| The key is in `admission.waiting` and in `admission.blocked` | A root in the line that waits on the Dispatch issues `admission.blocked` names: admission passes it over, and it takes no slot, until every one of them is `done` | Nothing: the wait ends when each blocker reaches `done` (a person's close, or the blocker's own merge) or its link is taken off (by a person or the tree's architect). Name the wait in the day's report |
|
|
13
|
+
| `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. A labelled `todo` issue here is not in the line either: one whose recorded parent is in a live tree runs in that tree (the daemon adopted it there), or is held with that parent while the parent itself is held under an unreleased child, and one whose parent is not recorded but sits under a live tree is held under an unreleased child of that tree until its architect releases it or its parent. That hold is not `admission.blocked`, which names only blocked-by links | 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 and no live tree is above it |
|
|
14
|
+
| `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 |
|
|
15
|
+
| `phase: done` while `status` is `in_progress`, `testing`, `needs_review` or `retro` | Legion is not running it under that status, and it sent the `unslotted` wake. Either someone other than Legion moved a root that waited for a slot there, and the daemon took it out of the workflow at that move since no slot would ever reach it; or a root waiting on a person could not be resumed — its `legion` label is gone, or a person or an outside session took its Dispatch claim — and the daemon took it out the same way, leaving the `in_progress` it ran with. A parked child stays parked. Legion starts nothing for any of them | Re-admit nothing yourself: `dispatch read` shows who works it now. Name it in your summary; `todo` with the `legion` label queues it again |
|
|
16
|
+
|
|
17
|
+
A root in the workflow with no slot that is in neither `admission.waiting`, nor the label-removed
|
|
18
|
+
row above, nor `todo` under a live tree does not stay that way: every pass of admission (a Dispatch
|
|
19
|
+
event, the daemon's boot read, its tick) takes it out of the workflow and sends the `unslotted`
|
|
20
|
+
wake — an unanswered `awaiting_person` root only when its label or its Dispatch claim makes a
|
|
21
|
+
resume unsafe. The wake is live only, so a controller that starts later finds such a root by the
|
|
22
|
+
last row, as its start procedure does.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Backlog stewardship
|
|
2
|
+
|
|
3
|
+
The controller keeps the Dispatch backlog's facts aligned with the deployment's current weekly
|
|
4
|
+
plan. The plan is the source of the work order and its one-owner map; Dispatch board rank is the
|
|
5
|
+
executable form of that order. The deployment instructions name the weekly-planning issue and its
|
|
6
|
+
`plan-week-*` artifact.
|
|
7
|
+
|
|
8
|
+
## Controller routing address
|
|
9
|
+
|
|
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`.
|
|
13
|
+
|
|
14
|
+
## Reconcile the weekly plan
|
|
15
|
+
|
|
16
|
+
Run this pass on the first turn of every UTC day and after every ruling from a person:
|
|
17
|
+
|
|
18
|
+
1. Read the weekly-planning issue and select its newest `plan-week-*` artifact by its latest
|
|
19
|
+
`artifact.version` event. Read that artifact before changing any issue.
|
|
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
|
|
26
|
+
`dispatch issue-update --issue <KEY> --before <OTHER>` or `--after <OTHER>`.
|
|
27
|
+
3. Reconcile the plan's one-owner map. `envoy_sessions` gives each session a `state` (`live`,
|
|
28
|
+
`unresponsive` or `gone`) and a `last_seen`; read the state, never the presence or absence of
|
|
29
|
+
the row. A `live` or `unresponsive` owner still owns its work: change nothing for it. Only an
|
|
30
|
+
owner that reads `gone`, or that the registry has no record of, is without a session. For
|
|
31
|
+
unowned work, find an owner whose `envoy_sessions` `dir` is a
|
|
32
|
+
checkout of the issue's repository, route the issue to it
|
|
33
|
+
(`dispatch issue-update --issue <KEY> --route session:<session id>`) and `envoy_send` it
|
|
34
|
+
`You own <KEY>: <title>.`. Routing starts nothing on Legion and sets no label. Never route an
|
|
35
|
+
issue that carries the `legion` label, because Legion's own roles work it. Do not change the
|
|
36
|
+
human `Assignee:` field: it decides who receives asks, not who works the issue.
|
|
37
|
+
4. Edit that issue's line in the plan artifact (its owner column) with `dispatch doc-edit`,
|
|
38
|
+
guarded by the document token from the current `dispatch doc-read`. Replace superseded text
|
|
39
|
+
instead of keeping a change log in it, because the artifact states the current rule. A comment
|
|
40
|
+
or message does not update the one-owner map.
|
|
41
|
+
|
|
42
|
+
A ruling from a person is a direct Dispatch message to this controller. Answer it first, carry out
|
|
43
|
+
its scope or priority decision, then record the resulting current rule in the affected weekly-plan
|
|
44
|
+
row (or its ruling section when it has no one line). Link the message when Dispatch gave it a
|
|
45
|
+
reference.
|
|
46
|
+
A ruling triggers the full reconciliation pass even when it appears to affect only one issue.
|
|
47
|
+
|
|
48
|
+
## Answering controller deliveries
|
|
49
|
+
|
|
50
|
+
Read every delivery to `notifications.role.legion-<project>-controller` against live Dispatch and
|
|
51
|
+
daemon state. You decide labels, the statuses `todo`, `backlog` and `icebox`, and board order.
|
|
52
|
+
Answer a scope question from the weekly plan and the deployment instructions. Anything else goes
|
|
53
|
+
with `envoy_send` to the session the plan's one-owner map names for that issue.
|
|
54
|
+
|
|
55
|
+
For a READY packet, confirm the Dispatch record and leave the merge to the daemon. Never merge or
|
|
56
|
+
use a GitHub credential to merge from the controller, because the daemon submits the merge itself
|
|
57
|
+
at READY's head.
|
|
58
|
+
|
|
59
|
+
## The owner audit
|
|
60
|
+
|
|
61
|
+
At each pass, read `dispatch issues --project <KEY> --priority 0 --priority 1 --all`:
|
|
62
|
+
|
|
63
|
+
- An unclaimed row in `triage`, `icebox`, `backlog` or `todo` is a decision: someone builds it, or
|
|
64
|
+
it closes. A claimed or in-flight row already has an owner.
|
|
65
|
+
- A close that says a defect cannot happen cites the code that prevents it; without that, the issue
|
|
66
|
+
is unread, not foreclosed.
|
|
67
|
+
- A plan or measurement nobody builds needs an owner.
|
|
68
|
+
- A merged pull request names an issue not yet in `testing` or `done`: check it live, then move it.
|
|
69
|
+
- Someone repeats the issue's work by hand: fix its status.
|
|
70
|
+
- A route nobody holds (`--route-status no_holder`, any priority), after the two reads that
|
|
71
|
+
"Recovering stopped-session work" names: staff the role, route the issue to a live holder, or
|
|
72
|
+
clear the route and assign it.
|
|
73
|
+
|
|
74
|
+
## Recovering stopped-session work
|
|
75
|
+
|
|
76
|
+
Treat a session route as stopped only after two reads of
|
|
77
|
+
`dispatch issues --project <KEY> --route-status no_holder --all` at least ten minutes apart
|
|
78
|
+
both list it, as `dispatch issues --help` says for `route_status`, because a
|
|
79
|
+
restarting session is absent for minutes. Do not poll to create the second read; use the next
|
|
80
|
+
controller wake. Then read the owner's state with `envoy_sessions` (the session the route names,
|
|
81
|
+
or one that took it over). A `live` owner still holds the work. So does an `unresponsive` one,
|
|
82
|
+
which is past the live window but inside the gone horizon: it keeps its claims, and a message you
|
|
83
|
+
send it waits for it. Recover the work only from an owner that reads `gone`, or that reads
|
|
84
|
+
`unresponsive` with a `last_seen` more than 20 minutes old, which is as long as an issue claim is
|
|
85
|
+
protected.
|
|
86
|
+
|
|
87
|
+
For work that a ruling of a person's, recorded in the plan with its link (the ruling section
|
|
88
|
+
above), says moves to Legion when its session stops, preserve its existing labels, add `legion`,
|
|
89
|
+
then set it `todo` with the `legion` tool's `set_status`. A plan row you wrote with no such ruling
|
|
90
|
+
behind it moves nothing. The label comes first: it is what
|
|
91
|
+
lets the daemon admit the root. Do not apply that recovery to an issue already in an active Legion
|
|
92
|
+
tree; its architect and daemon own its lifecycle. Update the weekly-plan owner row after the move.
|
|
93
|
+
|
|
94
|
+
## A root the plan defers
|
|
95
|
+
|
|
96
|
+
A person's `legion` label outranks a plan's deferral. The daemon queues the root at once: a
|
|
97
|
+
person's label on an unrecorded root in `triage` is the root's `todo`, written by the daemon. A
|
|
98
|
+
labelled root the weekly plan says to defer is parked by you at your next backlog pass, not
|
|
99
|
+
before: `set_status` it to `backlog`, or to `icebox` for a longer deferral, and cite the plan's
|
|
100
|
+
ruling in a comment on the issue. The cost is the architect's work between the daemon's `todo`
|
|
101
|
+
and your park.
|
|
@@ -0,0 +1,75 @@
|
|
|
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 post here the evidence of each clear defect they found outside their own work, which the controller hands to the tree that owns the code, or files for Legion when no tree does. They ask here whether a problem that needs a decision 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: the close withdrew its open asks (each recorded
|
|
34
|
+
`withdrawn`; a reopen brings them back), and Dispatch refuses an ask or a message there
|
|
35
|
+
(`409 ISSUE_CLOSED`). Reopen it with `set_status` to `icebox` and say so 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-writing-specs`'s "Rules for the text" say:
|
|
53
|
+
every issue by its `dispatch://` link and what it does for the reader, 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> --all`, 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
|
+
- **Waiting on Dispatch links.** Each root and child in `admission.blocked`, with the issues it
|
|
65
|
+
waits on. The wait ends when each blocker reaches `done` (a person's close, or the blocker's own
|
|
66
|
+
merge) or its link is taken off (by a person or the tree's architect), so take nothing for them.
|
|
67
|
+
- **Free slots.** Counted as the skill's "Free slots" says; each stays free until a root in `todo`
|
|
68
|
+
with the `legion` label takes it.
|
|
69
|
+
- **Open capabilities.** Each row of `read_state`'s `capabilities` whose `status` is `open`: its
|
|
70
|
+
name, its `detail`, and a deployment row's `configLine`. Say who acts on it, as
|
|
71
|
+
`skill://legion-controller/references/wakes.md` says for `tick`. With none open, say nothing of
|
|
72
|
+
them.
|
|
73
|
+
|
|
74
|
+
A day with nothing finished says so in one sentence. When the lists do not fit, give each list's
|
|
75
|
+
count and name its issues of `--priority 0` and `--priority 1`, in board rank order.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Architect escalation
|
|
2
|
+
|
|
3
|
+
Read this when a tree's architect sends you a matter, when a `held` wake carries
|
|
4
|
+
`reason: "escalated"`, or when an `update-stalled` wake says the daemon cannot replace an issue's
|
|
5
|
+
pod. Three matters come to you from an architect: a child that looks independent of its tree,
|
|
6
|
+
capacity (an architect says its tree cannot run what it needs under the slots it has), and a
|
|
7
|
+
conflict between trees. An issue-design decision is not one of them: the owning architect writes
|
|
8
|
+
it as a decision block and opens its own finding asks. Send a design decision back to that
|
|
9
|
+
architect with `envoy_publish` to its role, because it is not yours to make.
|
|
10
|
+
|
|
11
|
+
For capacity, the only lever is order. You cannot change `admission.cap`: it is the operator's
|
|
12
|
+
`legion.yaml` setting, and `set_status` is your only admission move. Read `admission.active` and
|
|
13
|
+
`admission.waiting`, tell the architect its root's place in the line with `envoy_publish`, and
|
|
14
|
+
open a finding ask when the deployment needs a larger cap.
|
|
15
|
+
|
|
16
|
+
For a conflict between trees, read both roots in `read_state` and Dispatch, publish what you read
|
|
17
|
+
to both architects' roles, and open a finding ask when the two need a person's decision.
|
|
18
|
+
|
|
19
|
+
## A child that looks independent
|
|
20
|
+
|
|
21
|
+
Check the child and its parent against `read_state` and Dispatch. When it still looks independent,
|
|
22
|
+
open an ask on your report issue, as the skill's "Findings, and the issues you file" says. The ask
|
|
23
|
+
names the child, why it looks independent and the root you would file, with three options:
|
|
24
|
+
`File an issue` (a root a person takes), `File it for Legion` (a root Legion runs) and
|
|
25
|
+
`Do not file` (it stays in its tree). Never add the `legion` label to a child or cut its
|
|
26
|
+
`child_of` link, because that admits it as a root while its tree still records it. Only on a
|
|
27
|
+
person's yes:
|
|
28
|
+
|
|
29
|
+
1. File the root with no `--parent`:
|
|
30
|
+
`dispatch issue --project <PROJECT> --title '<title>' --spec-file -`. Its spec cites the ask and
|
|
31
|
+
carries its `Filed for` line (`skill://legion-controller/references/finding-asks.md`). Add
|
|
32
|
+
`--label legion` only when the answer asks Legion to run it. `<PROJECT>` is the issue key's
|
|
33
|
+
prefix before `-<n>` (`ACME-3` → `ACME`), which `read_state` shows as `daemon.project`; it is
|
|
34
|
+
not the lowercase project token in role names such as `legion-<project>-controller`.
|
|
35
|
+
2. Park the child (`set_status` to `icebox`) and comment a pointer to the new root on it. You
|
|
36
|
+
cannot close it, because only its architect or the daemon sets `done`.
|
|
37
|
+
3. A root filed with the label opens in `triage`, and its triage wake brings it back to you
|
|
38
|
+
(`skill://legion-controller/references/triage.md`).
|
|
39
|
+
|
|
40
|
+
## An update round that is not finishing (`update-stalled`)
|
|
41
|
+
|
|
42
|
+
`{kind: "update-stalled", reason, update: {round, pod?, reasons, waiting}}`, on the issue whose
|
|
43
|
+
pod it is. The daemon must replace the pod that runs an issue's roles, because the configuration
|
|
44
|
+
the pod was built from has moved. It never replaces a live pod where it stands: every role of the
|
|
45
|
+
issue works on that pod's disk, so the daemon opens an update round instead and tasks each worker
|
|
46
|
+
in the pod to push its work and answer `ready_for_update` first. This wake says that round is not
|
|
47
|
+
finishing.
|
|
48
|
+
|
|
49
|
+
It names the round (`round`: `pod:<pod uid>` for the whole pod, `plugin:<daemon contract>` for
|
|
50
|
+
one claim's own process), the pod it was opened over (`pod`, absent for a claim's own plugin
|
|
51
|
+
round), the drift the task states (`reasons`), the one line that says why it is stuck (`reason`),
|
|
52
|
+
and every claim the round is blocked on (`waiting`, each with its `claim` token, `role`, `state`
|
|
53
|
+
and `answered`). Every worker is asked alike. The end of the turn the daemon's task started is
|
|
54
|
+
that worker's answer at every plugin contract, so a worker whose `legion` tool has no
|
|
55
|
+
`ready_for_update` operation needs nothing of you: a round over one finishes by itself, and no
|
|
56
|
+
drain is asked of anybody. A `failed` claim is never in a pod round's `waiting`: it runs no turn
|
|
57
|
+
on the pod, so the round does not wait for it. Its retry is the architect's or the operator's as
|
|
58
|
+
for any failed role, and a retried claim is asked like any other.
|
|
59
|
+
|
|
60
|
+
Before it replaces the pod, the daemon checks every workspace on the pod's disk for work the
|
|
61
|
+
remote lacks, a failed claim's included, and pushes what it finds to a rescue branch of its own,
|
|
62
|
+
`legion/<KEY>-rescue-<pod>-<n>`. When it pushed one, it writes the issue a message naming each
|
|
63
|
+
rescue branch and its commit, for the issue's roles to merge into the issue branch; that message
|
|
64
|
+
asks nothing of you. When the check or a push fails for any workspace, the daemon does not replace
|
|
65
|
+
the pod, and this wake's `reason` says that the check for unpushed work failed and names the claim
|
|
66
|
+
the check ran through, each workspace and its error. Everything else of the pod's disk goes with
|
|
67
|
+
the pod: the round's task tells each worker to put any scratch evidence it still needs on Dispatch
|
|
68
|
+
before it answers.
|
|
69
|
+
One round wakes you once, however many sweeps find it open, so handle it when it arrives.
|
|
70
|
+
Read `read_state` and the issue first, as for every wake: a round that finished since needs
|
|
71
|
+
nothing.
|
|
72
|
+
|
|
73
|
+
Take each `waiting` entry by its shape:
|
|
74
|
+
|
|
75
|
+
| Entry | What it is | What you do |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| `answered: true` | It has already answered this round. The round is blocked on the daemon's own side: a turn that has not ended, a check for unpushed work that fails, or a pod replacement the cluster keeps refusing. `reason` names which. | Send it nothing. A message to it starts a turn. How the worker answered decides what that turn does: it takes back an answer the worker gave with the `ready_for_update` call, which puts the round further from finishing, and it leaves standing an answer the end of the task's own turn gave. Open a finding ask naming the issue, the round and `reason`, with the turn, the workspace and its push error, or the `ReplacePod` error it names, so an operator reads the pod. |
|
|
78
|
+
| `state: "suspended"` | An operator's own suspension holds it: the daemon revives a suspension of its own for the task. The claim already holds the task and takes it on the resume. | Open a finding ask naming the issue and the role, so the operator resumes it. |
|
|
79
|
+
| any other state | Its worker holds the task and has not answered. The daemon asks it again every `pod_update_retask_hours`. | `envoy_publish` to `notifications.role.<claim>` with the issue and the update task's three steps (push every commit to keep to the issue branch and put any scratch evidence still needed on Dispatch, call the `legion` tool with `op: "ready_for_update"` and the `round` this notice names, which is the id the worker's own task carries, end the turn), and say in the same message that a worker whose `legion` tool has no `ready_for_update` operation does step 1 and step 3 alone. Name it in your summary. Your publish starts a turn of its own, which answers nothing: only the end of the turn the daemon's own task started is that worker's answer, so what your message buys is the next ask. |
|
|
80
|
+
|
|
81
|
+
Park nothing and write no status for this wake. The issue's workflow runs on while the round
|
|
82
|
+
waits, and replacing the pod is the daemon's own move once every worker has answered.
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Finding asks
|
|
2
|
+
|
|
3
|
+
Read this when a clear defect or a finding ask's reference reaches you on the controller topic,
|
|
4
|
+
when one you follow is answered, when you file an issue a person asked for, and at every start.
|
|
5
|
+
What a finding ask is and where it goes is `skill://legion-worker/references/findings.md`; what
|
|
6
|
+
you may file is the 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>`. When the
|
|
16
|
+
mention adopts an earlier message's request by asking for it
|
|
17
|
+
(`skill://legion-controller/references/slack-mentions.md`), the request reads
|
|
18
|
+
`Slack <channel_id> message <mention ts>, adopting message <earlier ts>`, and the person it names
|
|
19
|
+
is still the mention's `<@user_id>`;
|
|
20
|
+
- a direct message: `direct message <id>`, with the id its frame's `reply_with` names after
|
|
21
|
+
`--in-reply-to`, for the frame's `actor`. A message that arrives as your own user turn shows no
|
|
22
|
+
id: write `direct message to session <your session id>`, for the person by the name they gave,
|
|
23
|
+
else `the person who messaged this session`.
|
|
24
|
+
|
|
25
|
+
For a clear defect no running tree owns, the line is `Filed for Legion from <role> on <issue>:
|
|
26
|
+
<evidence>` ([Hand over a clear defect](#hand-over-a-clear-defect)).
|
|
27
|
+
|
|
28
|
+
## Hand over a clear defect
|
|
29
|
+
|
|
30
|
+
A role sends a clear defect on the controller topic: its role token, the defect, the issue it came
|
|
31
|
+
from, where the fix belongs, the fix, and its evidence comment on the report issue, whose first line
|
|
32
|
+
is `Clear defect from <role>`. When the report issue was missing or closed, the message carries the
|
|
33
|
+
evidence: create or reopen the report issue (`skill://legion-controller/references/daily-report.md`)
|
|
34
|
+
and post it there with that first line. A clear defect you find yourself gets the same comment, with
|
|
35
|
+
`controller` as the role. A clear-defect ask (`findings.md`, "When no controller takes it") needs
|
|
36
|
+
no answer: take it as below, the ask its evidence. Resolve it (`dispatch resolve-ask --kind
|
|
37
|
+
resolved`) only once it carries your reply: `Handed` or `Filed` from step 2, 3 or 5, or, when
|
|
38
|
+
step 1 finds it is not clear, `Moved dispatch://<REPORT KEY>/ask/<new id>` naming the decision ask
|
|
39
|
+
you open. With no reply, leave it open: catch-up step 2 takes it again at your next start.
|
|
40
|
+
|
|
41
|
+
The work goes to whoever already owns that code. You file an issue only when nobody does.
|
|
42
|
+
|
|
43
|
+
1. Check that it is clear: the fix needs no choice a person must make, and it belongs in a
|
|
44
|
+
repository your addressing line lists. Otherwise open an ask for it, as `findings.md` says.
|
|
45
|
+
2. Search first (`skill://dispatch-first`). When an open issue tracks it, comment the evidence
|
|
46
|
+
there, reply `Filed dispatch://<that KEY>` under the evidence, and file nothing.
|
|
47
|
+
3. Find the running tree whose code it is: a root in `admission.active` whose repository and
|
|
48
|
+
component match, read from `read_state` and `dispatch read`. When one matches, hand it over.
|
|
49
|
+
`envoy_publish` the defect, its evidence reference and the fix to that tree's architect topic,
|
|
50
|
+
`notifications.role.<issues.<KEY>.architect.locator.claim>`. The architect decides whether it
|
|
51
|
+
is a child of the tree or part of an open issue's work. Reply `Handed dispatch://<that KEY>`
|
|
52
|
+
under the evidence only when a live holder took the publish. When the root has no locator, or
|
|
53
|
+
the listener kept the publish for the role's next holder, reply nothing: no architect reads a
|
|
54
|
+
comment on its root, so the evidence stays unhandled, and catch-up step 4 lists it again at
|
|
55
|
+
your next start. Publish it again at your next wake. Once the root leaves `admission.active`,
|
|
56
|
+
no running tree owns the code, and step 4 files it. File nothing while a tree owns it: a
|
|
57
|
+
second issue splits the work.
|
|
58
|
+
4. No running tree owns that code: file it on the repository where the fix belongs, in the
|
|
59
|
+
project that repository maps to:
|
|
60
|
+
|
|
61
|
+
```text
|
|
62
|
+
dispatch issue --project <KEY> --repo <owner/name> --title '<the defect, as a title>' --label legion --spec-file -
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Without `--repo`, Dispatch records the project's default repository, and the daemon admits the
|
|
66
|
+
issue there. The spec states the defect, where the fix belongs and the fix, and ends with the
|
|
67
|
+
`Filed for Legion from` line: the sending role's token (or `controller`), the `dispatch://<KEY>`
|
|
68
|
+
it came from, and the evidence comment's or the ask's reference. It opens in `triage` with the
|
|
69
|
+
label, and triage admits it (`skill://legion-controller/references/triage.md`).
|
|
70
|
+
5. Reply under the evidence comment (`--reply-to '<comment id>'`) or the ask
|
|
71
|
+
(`--reply-to-ask '<ask id>'`, on the ask's issue), its first line exactly
|
|
72
|
+
`Filed dispatch://<NEW KEY>` or `Handed dispatch://<KEY>`. A later session reads that reply at
|
|
73
|
+
triage and at every start.
|
|
74
|
+
|
|
75
|
+
## Follow it
|
|
76
|
+
|
|
77
|
+
A finding ask another role opened reaches you as its reference, `dispatch://<KEY>/ask/<id>`, on
|
|
78
|
+
the controller topic. Follow it, so that its answer reaches you:
|
|
79
|
+
|
|
80
|
+
```text
|
|
81
|
+
dispatch follow --ask 'dispatch://<KEY>/ask/<id>' --action follow
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
When `dispatch read --ref` shows it answered already, act on its answer now.
|
|
85
|
+
|
|
86
|
+
## Act on its answer
|
|
87
|
+
|
|
88
|
+
Once it is answered (an ask you follow brings its answer to you), read it with
|
|
89
|
+
`dispatch read --ref 'dispatch://<KEY>/ask/<id>'` and act on the newest answer. When its typed
|
|
90
|
+
text asks for a different one of the four actions than the option selected, do what the text
|
|
91
|
+
asks; when the text asks for something that is none of the four, reply under the ask with your
|
|
92
|
+
question and file nothing:
|
|
93
|
+
|
|
94
|
+
- `Hand it to the owning tree`: take it as [Hand over a clear defect](#hand-over-a-clear-defect)
|
|
95
|
+
says, from its step 3.
|
|
96
|
+
- `Do not file`: file nothing.
|
|
97
|
+
- `File an issue`: `dispatch issue --project <PROJECT> --title '<the problem, as a title>'
|
|
98
|
+
--spec-file -` in the project where the fix belongs, as the answer or the ask names it; when
|
|
99
|
+
neither names one, in this project. The spec
|
|
100
|
+
states the problem and where the fix belongs, cites the ask and its evidence by their
|
|
101
|
+
`dispatch://` references, and carries the line `Filed for <the answer's By:> at their request:
|
|
102
|
+
dispatch://<REPORT KEY>/ask/<id>` (above). No `legion` label.
|
|
103
|
+
- `File it for Legion`: the same, with `--label legion`. It opens in `triage` with the label, so
|
|
104
|
+
its triage wake brings it back to you (`skill://legion-controller/references/triage.md`).
|
|
105
|
+
|
|
106
|
+
An ask that re-files an escalated child also takes the steps of
|
|
107
|
+
`skill://legion-controller/references/escalation.md`.
|
|
108
|
+
|
|
109
|
+
Then reply under the ask with what you did, its first line exactly `Filed dispatch://<NEW KEY>`,
|
|
110
|
+
`Handed dispatch://<KEY>` or
|
|
111
|
+
`Not filed: <reason>`. The ask's issue is the report issue, or the role's own issue for an ask no
|
|
112
|
+
controller took:
|
|
113
|
+
|
|
114
|
+
```text
|
|
115
|
+
dispatch comment --issue <the ask's issue> --reply-to-ask '<ask id>' --body-file -
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Dispatch refuses to resolve an answered ask, so that reply is its record. An open finding ask that
|
|
119
|
+
turns moot (an issue now tracks the problem, or the problem is gone) you close with
|
|
120
|
+
`dispatch resolve-ask --kind resolved`, naming why.
|
|
121
|
+
|
|
122
|
+
## Catch up at every start
|
|
123
|
+
|
|
124
|
+
An answer that arrives while no controller runs never wakes you, a reference published then
|
|
125
|
+
reaches nobody, and a new session follows none of the asks an earlier one followed. So at every
|
|
126
|
+
start, catch up on the report issue (with none yet, there is nothing to catch up), and on the
|
|
127
|
+
finding asks roles opened on their own issues while no controller ran:
|
|
128
|
+
|
|
129
|
+
1. Read it with `dispatch read`. Reopen it when its `Status:` is `done`, and assign it when its
|
|
130
|
+
`Assignee:` says `unassigned` and a person is known, both as
|
|
131
|
+
`skill://legion-controller/references/daily-report.md` "Keep it answerable" says.
|
|
132
|
+
2. Take each open ask: `dispatch open-asks --project <PROJECT>` lists every unanswered ask of the
|
|
133
|
+
project, with no cap, each line naming its issue. It leaves out an ask on a closed issue
|
|
134
|
+
(`asks.go`'s `listOpenAsks` takes `i.closed_at is null`), which is why an architect holds its
|
|
135
|
+
issue open for one. Follow those on the report issue, and take each whose question opens
|
|
136
|
+
`Clear defect from ` as [Hand over a clear defect](#hand-over-a-clear-defect) says. One on
|
|
137
|
+
another issue whose question opens `Finding from ` or `Clear defect from ` is a role's
|
|
138
|
+
own-issue ask. Take a clear defect the same way. Move a finding that needs a
|
|
139
|
+
decision: ask it again on the report issue with the same question, options and `--ref`, then
|
|
140
|
+
reply `Moved dispatch://<REPORT KEY>/ask/<new id>` under the old ask. Resolve the old ask once it
|
|
141
|
+
carries a `Moved`, `Handed` or `Filed` reply. A clear defect no holder took stays open, and the
|
|
142
|
+
next start takes it again.
|
|
143
|
+
3. List the answered asks still waiting on you, which no `dispatch` command lists:
|
|
144
|
+
|
|
145
|
+
```text
|
|
146
|
+
asks=$(curl -sS --fail-with-body -H "Authorization: Bearer $(cat "$DISPATCH_TOKEN_FILE")" "$DISPATCH_URL/api/v1/issues/<ISSUE KEY>/asks?state=answered&thread=1") || { echo "Dispatch read failed" >&2; exit 1; }
|
|
147
|
+
printf '%s' "$asks" | jq -cn 'input | if type == "array" then [.[] | 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://|Handed dispatch://|Admitted dispatch://|Not filed:|Not admitted:)")))] | length == 0) | {id, question, answer}] else error("Dispatch did not answer the list") end'
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Read it for the report issue, then for each issue in `needs_review`, whose architect holds its
|
|
151
|
+
acceptance on a finding ask's answer. Act on each as above, a triage ask as
|
|
152
|
+
`skill://legion-controller/references/triage.md` says. The read lands in `asks` first, because a
|
|
153
|
+
pipeline reports only the status of `jq`. `jq -n 'input …'` fails on an empty body, where a
|
|
154
|
+
plain `jq` would succeed on it, so exit 0 means the list was read: `[]` is nothing waiting.
|
|
155
|
+
Anything else is a failed read and admits nothing.
|
|
156
|
+
|
|
157
|
+
Take the `needs_review` issues as one snapshot before you act on any of them:
|
|
158
|
+
`dispatch issues --project <PROJECT> --status needs_review --all`. `--all` reads every match in
|
|
159
|
+
one request. Never page this list: pages by `offset` skip or repeat an issue whose status
|
|
160
|
+
changes between reads, and your reply moves issues out of `needs_review`. `No issues in
|
|
161
|
+
<PROJECT>.` means nothing waits, and a command that fails admits nothing. An issue that enters
|
|
162
|
+
`needs_review` after the snapshot is read at your next start.
|
|
163
|
+
4. List the clear defects nobody took yet: each evidence comment whose first line starts
|
|
164
|
+
`Clear defect from `, with no session reply under it that starts `Handed dispatch://`,
|
|
165
|
+
`Filed dispatch://`, `Not filed:` or `Asked dispatch://` (a role that asked instead, which step
|
|
166
|
+
2 takes):
|
|
167
|
+
|
|
168
|
+
```text
|
|
169
|
+
notes=$(curl -sS --fail-with-body -H "Authorization: Bearer $(cat "$DISPATCH_TOKEN_FILE")" "$DISPATCH_URL/api/v1/issues/<REPORT KEY>/comments") || { echo "Dispatch read failed" >&2; exit 1; }
|
|
170
|
+
printf '%s' "$notes" | jq -cn 'input | if type == "array" then (. as $all | [.[] | select(.reply_to == null and (.body | startswith("Clear defect from "))) | .id as $id | select([$all[] | select(.reply_to == $id and .author.kind == "session" and (.body | test("^(Handed dispatch://|Filed dispatch://|Not filed:|Asked dispatch://)")))] | length == 0) | {id, body}]) else error("Dispatch did not answer the list") end'
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
It reads as step 3's read does: exit 0 with `[]` is nothing waiting, anything else failed.
|
|
174
|
+
Take each as [Hand over a clear defect](#hand-over-a-clear-defect) says. One you could not hand
|
|
175
|
+
over at an earlier wake, because no architect held its tree's topic, has no reply, so this list
|
|
176
|
+
brings it back until a tree takes it or step 4 of the hand-over files it. When one is not clear,
|
|
177
|
+
reply `Not filed: <reason>` under it and open an ask for it instead.
|
|
178
|
+
|
|
179
|
+
A read that fails, and an assignment Dispatch refuses, are handled as
|
|
180
|
+
`skill://legion-controller/SKILL.md#rules` and
|
|
181
|
+
`skill://legion-controller/references/daily-report.md` say.
|