@sjawhar/claude-envoy 1.11.2 → 1.12.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 +1 -1
- package/.omp-plugin/plugin.json +1 -1
- package/dist/dispatch.js +1 -0
- package/dist/envoy-channel.js +16 -4
- package/dist/session-hook.js +1 -0
- package/legion/skills/legion-architect/SKILL.md +3 -3
- package/legion/skills/legion-architect/references/design-gate.md +82 -35
- package/legion/skills/legion-architect/references/wake-routing.md +4 -4
- package/legion/skills/legion-controller/SKILL.md +1 -1
- package/legion/skills/legion-controller/references/parked-decisions.md +7 -1
- package/legion/skills/legion-controller/references/triage.md +3 -3
- package/legion/skills/legion-controller/references/wakes.md +6 -4
- package/legion/skills/legion-worker/references/merge-gate.md +7 -3
- package/legion/skills/legion-worker/references/tools.md +49 -171
- package/legion/skills/legion-worker/references/withheld-capability.md +158 -0
- package/package.json +2 -2
- package/skills/outside-message/SKILL.md +4 -2
package/.omp-plugin/plugin.json
CHANGED
package/dist/dispatch.js
CHANGED
|
@@ -16849,6 +16849,7 @@ var LEGION_ROLES = [
|
|
|
16849
16849
|
"reviewer",
|
|
16850
16850
|
"merger"
|
|
16851
16851
|
];
|
|
16852
|
+
var GATED_STEPS = ["decisions", "criteria", "spec"];
|
|
16852
16853
|
var PROJECT_TOKEN = /^[a-z0-9]+$/;
|
|
16853
16854
|
var DISPATCH_KEY_PATTERN = /^([A-Z][A-Z0-9]*)-([0-9]+)$/;
|
|
16854
16855
|
function roleForPhase(phase) {
|
package/dist/envoy-channel.js
CHANGED
|
@@ -38498,6 +38498,7 @@ var LEGION_ROLES = [
|
|
|
38498
38498
|
"reviewer",
|
|
38499
38499
|
"merger"
|
|
38500
38500
|
];
|
|
38501
|
+
var GATED_STEPS = ["decisions", "criteria", "spec"];
|
|
38501
38502
|
var PROJECT_TOKEN = /^[a-z0-9]+$/;
|
|
38502
38503
|
var DISPATCH_KEY_PATTERN = /^([A-Z][A-Z0-9]*)-([0-9]+)$/;
|
|
38503
38504
|
function roleForPhase(phase) {
|
|
@@ -52032,6 +52033,15 @@ var legionGateView = object({
|
|
|
52032
52033
|
currentVersion: number2().int().positive(),
|
|
52033
52034
|
approvedVersion: number2().int().positive().nullable()
|
|
52034
52035
|
});
|
|
52036
|
+
var legionDesignStep = _enum2(GATED_STEPS);
|
|
52037
|
+
var legionGatedStepView = object({
|
|
52038
|
+
step: legionDesignStep,
|
|
52039
|
+
artifactId: nonEmptyString.optional(),
|
|
52040
|
+
currentVersion: number2().int().positive().optional(),
|
|
52041
|
+
approvedVersion: number2().int().positive().nullable(),
|
|
52042
|
+
status: _enum2(["unregistered", "waiting", "approved", "changes_requested"]),
|
|
52043
|
+
changesReason: nonEmptyString.optional()
|
|
52044
|
+
});
|
|
52035
52045
|
var legionSlotView = object({
|
|
52036
52046
|
index: number2().int().nonnegative(),
|
|
52037
52047
|
admittedAt: timestamp,
|
|
@@ -52119,6 +52129,7 @@ var legionIssue = object({
|
|
|
52119
52129
|
workers: partialRecord(string2(), legionPhaseView),
|
|
52120
52130
|
pullRequest: legionPullRequestView.optional(),
|
|
52121
52131
|
designGate: legionGateView.optional(),
|
|
52132
|
+
gatedSteps: array(legionGatedStepView).optional(),
|
|
52122
52133
|
slot: legionSlotView.optional(),
|
|
52123
52134
|
waitingOn: legionWaitingOn.optional(),
|
|
52124
52135
|
check: legionCheckView.optional(),
|
|
@@ -52301,7 +52312,7 @@ var LegionControllerRegisterResponse = object({
|
|
|
52301
52312
|
});
|
|
52302
52313
|
var LegionControllerSecretResponse = object({
|
|
52303
52314
|
secret: nonEmptyString,
|
|
52304
|
-
|
|
52315
|
+
designGateSteps: array(legionDesignStep)
|
|
52305
52316
|
});
|
|
52306
52317
|
var LegionControllerCredentialResponse = object({
|
|
52307
52318
|
owners: record(nonEmptyString, object({
|
|
@@ -52405,7 +52416,8 @@ var LegionGateRegisterRequest = strictObject({
|
|
|
52405
52416
|
grantId: nonEmptyString,
|
|
52406
52417
|
issue: nonEmptyString,
|
|
52407
52418
|
artifactId: uuid2(),
|
|
52408
|
-
version: number2().int().positive()
|
|
52419
|
+
version: number2().int().positive(),
|
|
52420
|
+
step: legionDesignStep.optional()
|
|
52409
52421
|
});
|
|
52410
52422
|
var LegionWaveReleaseRequest = strictObject({
|
|
52411
52423
|
grantId: nonEmptyString,
|
|
@@ -55133,9 +55145,9 @@ var import_nats2 = __toESM(require_mod4(), 1);
|
|
|
55133
55145
|
|
|
55134
55146
|
// package.json
|
|
55135
55147
|
var name = "@sjawhar/claude-envoy";
|
|
55136
|
-
var version2 = "1.
|
|
55148
|
+
var version2 = "1.12.0";
|
|
55137
55149
|
var legion = {
|
|
55138
|
-
daemonApiVersion:
|
|
55150
|
+
daemonApiVersion: 55
|
|
55139
55151
|
};
|
|
55140
55152
|
|
|
55141
55153
|
// src/channel-forwarder.ts
|
package/dist/session-hook.js
CHANGED
|
@@ -15848,6 +15848,7 @@ var LEGION_ROLES = [
|
|
|
15848
15848
|
"reviewer",
|
|
15849
15849
|
"merger"
|
|
15850
15850
|
];
|
|
15851
|
+
var GATED_STEPS = ["decisions", "criteria", "spec"];
|
|
15851
15852
|
var PROJECT_TOKEN = /^[a-z0-9]+$/;
|
|
15852
15853
|
var DISPATCH_KEY_PATTERN = /^([A-Z][A-Z0-9]*)-([0-9]+)$/;
|
|
15853
15854
|
function roleForPhase(phase) {
|
|
@@ -29,9 +29,9 @@ You own one issue tree from its first decision to the merge that closes it. The
|
|
|
29
29
|
- A child issue is a separately deliverable change: its own pull request in its own repository, or its own deploy. Never use a child to track a to-do, a fix found in review, a proof of the root's acceptance, or a part of your own work: do it in this tree. Each child costs a workspace, an admission wait and a review loop. Before any `dispatch issue` that creates a child, and before `release_children`, run the red-team check in `skill://legion-architect/references/decomposition.md`.
|
|
30
30
|
- Every child names the running system its criteria are proven on (the command, service or page the tester drives) and the repository skill that drives it. If the repository cannot exercise a criterion end to end, building that path is work of this tree.
|
|
31
31
|
- The issue's primary document is the root spec: extend it in place, because a second "spec" artifact replaces the human's document. Mark each requirement of it as a `requirement` block, and write the issue's Verification document (`verification.md`) beside it: one `check` block per thing that must be proved, each with `serves` naming the requirements it serves, its `surface` (`browser`, `cli` or `api`), its `run`, its `when` (`before_merge`, `after_merge` or `default_branch`) and its `negative_control`. The checks are the gate every later role reads: the implementer and the tester record a result against each, and the reviewer approves only once every `before_merge` check of the issue has a passing result current at the head. The waves and the plan's tasks go in the children and the planner's `plan` handoff, not the root spec.
|
|
32
|
-
- Read `skill://legion-architect/references/design-gate.md` before you write the root spec and before any edit to it after approval, because any new version closes the gate. It also
|
|
33
|
-
- File every child of your decomposition before `dispatch request-approval`, and before `register_gate`
|
|
34
|
-
- A root architect then ends its turn and releases no wave until a delivered wake shows `design-approved
|
|
32
|
+
- Read `skill://legion-architect/references/design-gate.md` before you write the root spec and before any edit to it after approval, because any new version closes the gate. It also names the design gate steps your `Legion addressing` line declares, the document each step records, and says that a sub-architect runs none.
|
|
33
|
+
- File every child of your decomposition before `dispatch request-approval`, and before `register_gate` where your line gates no step: the gate's opening starts your root's own first phase unless Dispatch shows an open child under it, so a child filed after it opens leaves the root working beside its children.
|
|
34
|
+
- A root architect then ends its turn and releases no wave until every gated step is open: a delivered wake shows `design-approved` on the root, and under more than one step `gatedSteps` in your record (the `legion` tool's `read_record`) shows each step `approved`. The daemon starts no phase in the tree before that. A root that waited on a person's answer gets no `design-approved` notice: a resume task saying "this resume is your design-approved wake" is that wake. A sub-architect waits for none and releases its children with `release_children`.
|
|
35
35
|
|
|
36
36
|
## 2. Children in flight
|
|
37
37
|
|
|
@@ -30,21 +30,35 @@ answer, Slack message or direct message of a `Filed for <person> at their reques
|
|
|
30
30
|
evidence of a controller's `Filed for Legion from <role> on <issue>:` line
|
|
31
31
|
([The `Filed for` line](skill://legion-controller/references/finding-asks.md#the-filed-for-line)).
|
|
32
32
|
|
|
33
|
-
## Which
|
|
33
|
+
## Which steps are gated
|
|
34
34
|
|
|
35
|
-
The
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
The `Design gate steps` sentence of your `Legion addressing` line names the steps this deployment
|
|
36
|
+
gates, in the order it gates them, each in backticks: ``Design gate steps: `spec`.``,
|
|
37
|
+
``Design gate steps: `decisions`, `criteria`.``, or `Design gate steps: none.` A sub-architect on
|
|
38
|
+
a child issue has no such sentence and never runs the gate.
|
|
39
|
+
|
|
40
|
+
Each step is one document of the root issue, and the daemon keeps one gate record for each step:
|
|
41
|
+
|
|
42
|
+
| Step | The document it records |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| `decisions` | the issue's decisions document (`decisions.md`) |
|
|
45
|
+
| `criteria` | its Verification document (`verification.md`) |
|
|
46
|
+
| `spec` | its primary document |
|
|
47
|
+
|
|
48
|
+
The tree's work starts when every step the sentence names is open, so run the armed sequence below
|
|
49
|
+
once for each of them, each on that step's own document. Under `Design gate steps: none.` write
|
|
50
|
+
the spec and register it with `register_gate` at its current version, with no `step` and no
|
|
51
|
+
approval step: do not request approval and do not wait for `design-approved`; the gate opens at
|
|
52
|
+
registration. File every child of your decomposition before that registration, since the gate's
|
|
53
|
+
opening starts the root's own first phase unless Dispatch shows an open child under it.
|
|
42
54
|
|
|
43
55
|
## The armed gate
|
|
44
56
|
|
|
45
|
-
|
|
46
|
-
your decomposition filed before step 4, since the gate's opening starts the root's own first
|
|
47
|
-
unless Dispatch shows an open child under it
|
|
57
|
+
Run this exact sequence for each gated step, before the tree's work starts, with every child of
|
|
58
|
+
your decomposition filed before step 4, since the gate's opening starts the root's own first
|
|
59
|
+
phase unless Dispatch shows an open child under it. One approval request covers the `criteria`
|
|
60
|
+
and the `spec` step together (step 4), and each of them is registered on its own document
|
|
61
|
+
(step 5); the `decisions` step is a request of its own.
|
|
48
62
|
|
|
49
63
|
1. Extend the primary document in place:
|
|
50
64
|
|
|
@@ -74,22 +88,31 @@ unless Dispatch shows an open child under it:
|
|
|
74
88
|
```
|
|
75
89
|
|
|
76
90
|
`--include-verification` asks for the spec and the Verification document in one request, each
|
|
77
|
-
at its own current settled version, and the human's answer records a review on each
|
|
78
|
-
|
|
79
|
-
document has no settled
|
|
91
|
+
at its own current settled version, and the human's answer records a review on each: that one
|
|
92
|
+
request covers the `criteria` and the `spec` step together. An issue with no `verification.md`
|
|
93
|
+
is refused `NO_VERIFICATION_DOCUMENT`, and one whose Verification document has no settled
|
|
94
|
+
version `NO_VERSION`.
|
|
95
|
+
|
|
96
|
+
The `decisions` step is a request of its own, since `--include-verification` is for the issue's
|
|
97
|
+
spec alone and any other document is refused `NOT_SPEC_DOCUMENT`:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
dispatch request-approval --issue <root issue> --artifact decisions.md --summary '<what the human is approving> Reviews: <gap result>, <claim result>; <what they do not show>.'
|
|
101
|
+
```
|
|
80
102
|
|
|
81
103
|
`--summary` says in one to three sentences what the human is approving and nothing else (no
|
|
82
104
|
commentary and no open question), then one sentence with both reviews' results and what they do
|
|
83
105
|
not show, for example `Reviews: no open gap, 14 of 14 claims verified; neither shows that the
|
|
84
106
|
design works once built.` The primary document is the default; the
|
|
85
107
|
command prints `Approval requested for spec.md (document id <UUID>) at version <N> (ask <id>)`.
|
|
86
|
-
5. Register
|
|
108
|
+
5. Register each gated step's document and version with the daemon, one call for each step:
|
|
87
109
|
|
|
88
110
|
```ts
|
|
89
111
|
legion({
|
|
90
112
|
op: "register_gate",
|
|
91
113
|
issue: "<root issue>",
|
|
92
|
-
|
|
114
|
+
step: "<decisions|criteria|spec>", // the step this document is for
|
|
115
|
+
artifactId: "<UUID>", // that step's document id, which the request printed
|
|
93
116
|
version: <N>, // the version the human is asked to approve
|
|
94
117
|
})
|
|
95
118
|
```
|
|
@@ -99,10 +122,12 @@ unless Dispatch shows an open child under it:
|
|
|
99
122
|
document's approval events by that id alone; the `legion` tool looks a slug or file name up in
|
|
100
123
|
Dispatch and hands the daemon the id.
|
|
101
124
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
125
|
+
Each step records its own document, so register the spec's id and version under `step: "spec"`
|
|
126
|
+
and the Verification document's under `step: "criteria"`: the request that named both sent an
|
|
127
|
+
`artifact.approved` event for each, and each step opens on its own document's alone. Leave
|
|
128
|
+
`step` out only where your line names one step, which is the whole list a one-step deployment
|
|
129
|
+
gates. Register no step your line does not name: a step the deployment does not gate is
|
|
130
|
+
refused, and a record of it would hold nothing open.
|
|
106
131
|
|
|
107
132
|
Facts about the request:
|
|
108
133
|
|
|
@@ -124,18 +149,38 @@ Facts about the request:
|
|
|
124
149
|
latest version Dispatch holds for that document (the message names both), and a version Dispatch
|
|
125
150
|
never wrote is no registration: the gate is as it was. Read the document's latest version, check
|
|
126
151
|
it is the spec the human is asked to approve, and `register_gate` that version.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
152
|
+
- `register_gate` refused 400 `GATE_STEP_REQUIRED` means this deployment gates more than one step
|
|
153
|
+
and your call named none; the message names the steps it gates. Call again with `step` naming
|
|
154
|
+
the one that document is for.
|
|
155
|
+
- `register_gate` refused 409 `GATE_STEP_NOT_CONFIGURED` means the `step` you named is not one
|
|
156
|
+
this deployment gates; the message names the steps it does gate. Register those and no other.
|
|
157
|
+
- `register_gate` refused 409 `GATE_STEP_DOCUMENT_MISMATCH` means the document you named is not
|
|
158
|
+
that step's document of that issue; the message names the step and the document it records.
|
|
159
|
+
Register that step's own document, as the table in "Which steps are gated" says.
|
|
160
|
+
|
|
161
|
+
Each of those four refusals is given before anything is recorded, so the gate is as it was.
|
|
162
|
+
|
|
163
|
+
A `design-approved` or `design-changes-requested` notice carries a version and a reason and names
|
|
164
|
+
no step. Read your own issue record with the `legion` tool's `read_record` to see which step the
|
|
165
|
+
review was of: `gatedSteps` carries one entry for each gated step, with its `artifactId`, its
|
|
166
|
+
`currentVersion`, its `approvedVersion`, its `status` (`unregistered`, `waiting`, `approved` or
|
|
167
|
+
`changes_requested`) and the `changesReason` of a step whose reviewer asked for changes. That
|
|
168
|
+
list, and not the notice, says which steps are open and which still hold the tree's work.
|
|
169
|
+
|
|
170
|
+
Then end your turn. The registrations hand those documents to the human: when nothing else of the
|
|
171
|
+
tree is at work, the root returns its admission slot and waits on its design gate
|
|
172
|
+
(`awaiting_person`), as a root question handed to the human does ("Every turn" in
|
|
173
|
+
`skill://legion-architect/SKILL.md`). Stay live to read the human's comments and replies. Their
|
|
174
|
+
approval or changes request delivers your next task, and so does a newer spec version once
|
|
175
|
+
Dispatch shows the request back on your turn. While the root waits on a question instead, the
|
|
176
|
+
human's approval or changes request of the spec answers that wait too, and the task says how the
|
|
177
|
+
spec stands. Release no wave until every gated step is open: a delivered wake shows
|
|
178
|
+
`design-approved` on the root, or a task says its design gate is open, and under more than one
|
|
179
|
+
step `gatedSteps` in your record shows each one `approved`. The daemon sends a root waiting on a
|
|
180
|
+
person no `design-approved` notice, and "this resume is your design-approved wake" is that wake.
|
|
181
|
+
On `design-changes-requested`: revise the document that step records as the human's reason asks,
|
|
182
|
+
run step 3's two reviews on the new version, resolve every finding, request approval again and
|
|
183
|
+
end your turn. A
|
|
139
184
|
`design-changes-requested` may name a version below the latest, or arrive after another for the
|
|
140
185
|
same version: it is a request the human made that reached the daemon late, and no approval came
|
|
141
186
|
after it. Each request arrives once. The gate is as the newest review left it; still read the
|
|
@@ -143,9 +188,11 @@ reason, and answer it in the spec if the latest version does not already.
|
|
|
143
188
|
|
|
144
189
|
## After approval
|
|
145
190
|
|
|
146
|
-
|
|
147
|
-
`design-approved`-type wake says so; you know from
|
|
148
|
-
|
|
191
|
+
Each step's approval is pinned to the version of the document that step records: a new version of
|
|
192
|
+
that document closes that step again, and no `design-approved`-type wake says so; you know from
|
|
193
|
+
your own edit, or from the `artifact.version` event on your issue. One step closing holds the
|
|
194
|
+
tree's work whatever the other steps stand at. The exceptions are the version a human's own
|
|
195
|
+
answer to a decision block
|
|
149
196
|
writes when that answer is the whole change since the version they approved, and a version whose
|
|
150
197
|
text is exactly the text of the version they approved, with no decision block in it decided
|
|
151
198
|
since, such as an accidental edit undone exactly: for either, Dispatch records an `approved`
|
|
@@ -18,14 +18,14 @@ and 7 (a conflict, the hand-over, the close) is in
|
|
|
18
18
|
|
|
19
19
|
| Wake | Procedure |
|
|
20
20
|
| --- | --- |
|
|
21
|
-
| `carried-gate-opened` | Only a root whose catch-up said `carried` gets it. Its `version` and `reason`: a human approved the old root's spec document at `version`, which opened the gate this root carried; the daemon has released any READY it refused and started any child waiting on it. It is not `design-approved`: do not proceed to section 2 or release a wave because of it, and start nothing. |
|
|
21
|
+
| `carried-gate-opened` | Only a root whose catch-up said `carried` gets it. Its `version` and `reason`: a human approved the old root's spec document at `version`, which opened the gate this root carried; the daemon has released any READY it refused and started any child waiting on it. It names no step: the carry brings the old root's gate records, and the catch-up reports every gated step the carry did not bring as unregistered, so register each of those on this issue's own document for that step (`skill://legion-architect/references/design-gate.md`). It is not `design-approved`: do not proceed to section 2 or release a wave because of it, and start nothing. |
|
|
22
22
|
| `child-status` | A child of your tree left the workflow or re-entered it, or joined your tree from the admission queue; the reason names the child and its new status, or reads `<KEY> joined the tree of <ROOT> from the admission queue`. `todo` (a human's move, your `release_children`, or your `rerun_child`) means it runs again under your tree from planning, and the daemon starts it; you start nothing. A child held for a blocker starts the same way once nothing holds it, and the reason, naming it and its root, says what cleared the hold: its last blocker's close, naming the issues it waited on, now done, or a person or you taking its blocked-by links off; you start nothing for it either. `backlog`, `icebox` or `triage` (a human's move, or your `park_child`) means the daemon has suspended its workers, and it advances no further until it is set back to `todo`. Joined from the admission queue means a root that waited for a slot and never ran, which a person made a child of your tree or your `release_children` adopted, now runs in your tree with no architect of its own and takes an admission slot of its own when its work starts, from planning when it is `todo` and parked otherwise; one that waits on an unfinished blocked-by issue is held for that blocker like any other child, and starts when nothing holds it; you start nothing. A reason saying an issue is beneath an ancestor whose reviewed head the daemon has already accepted is a person's `todo` the daemon recorded and holds at `admitted`: nothing starts it until that ancestor closes, and a `request_backward_move` of the ancestor is what runs it under the ancestor now. What the rest of the tree does is your decision. When a park leaves no child that an issue in `accepting` waits on, run that issue's last round (`skill://legion-architect/references/acceptance.md`). |
|
|
23
23
|
| `child-closed` | Read the child completion and remaining open children. Re-scope open work, park obsolete work for good with `park_child` (first take its link off each sibling that waits on it, or park that sibling too, as section 2 says), and file any new child the result calls for, after the red-team check in `skill://legion-architect/references/decomposition.md`, and release it at once with `release_children`, blocked by what it builds on (section 2). When a worker waits on this child for a missing surface (see `phase-finished`), tell it to continue with `envoy_publish` to its role topic. The last child's close is not the end-game (section 3). When it was the last child an issue in `accepting` waits on, run that issue's last round (`skill://legion-architect/references/acceptance.md`). |
|
|
24
24
|
| `child-release-stalled` | Its `reason` names a child your `release_children` released that your tree has not recorded, its parent, and why, one of three: the parent's release has not reached the daemon after three tries; or GitHub did not answer the App installation read of a repository on the child's parent chain, and the wave left the child unrecorded because a parent on that chain is not `todo`, or did not write the child `todo` because its parent is not recorded; or the daemon's recovery of such a repository, once GitHub answered, still left children unrecorded (one notice per tree lists them all, each with its parent). Read the parent's record with the `legion` tool's `read_record` (`issue` the parent); release the parent first when the `reason` says to, then run `release_children` for the child again once the parent is recorded in your tree, and tell the controller if it never is. |
|
|
25
25
|
| `child-not-recorded` | Its `reason` names a child your tree did not record and why: its repository resolves to none (neither it nor an ancestor names one, and the project has no default), or to one not mapped to the project, or to one the daemon leaves out for a missing GitHub App. The daemon said why on the child too. Set the child's repository to one of the project's (`dispatch issue-update --repo owner/name`), then release it again with `release_children`; tell the controller when the repository it needs is not served. |
|
|
26
26
|
| `children-first` | An issue of your tree starts no phase of its own while Dispatch shows an open child under it, a child you filed and have not released included. The reason names the parent and each open child with the Dispatch status that holds it, and each cycle it found (a child `blocked_by` an issue above it on its own parent chain, which only a person taking that link off ends: open a `dispatch ask`). It comes once per issue and run of its tree. Release every child that should run, and close, `park_child` or re-parent the rest; the daemon starts the parent's first phase at its next admission slot once the last open child has left the workflow. You start nothing. |
|
|
27
|
-
| `design-approved` | Its `version` is the approved one. A human approved the
|
|
28
|
-
| `design-changes-requested` | Its `version` and `reason`. A human asked for changes to
|
|
27
|
+
| `design-approved` | Its `version` is the approved one, and it names no step: under more than one gated step, read `gatedSteps` in your own record (the `legion` tool's `read_record`) to see which step that version opened and whether another still holds the tree. A human approved the document of one gated step at its current version; that step is open. Proceed to section 2 once every gated step is `approved` — except on a root whose catch-up said `handed_over`: its workers already run their phases, so start nothing; the approval releases any READY the gate refused. One whose `version` is the version a human's own answer to a decision block wrote, after an earlier approval (Dispatch carried that approval to it, "After approval" in `skill://legion-architect/references/design-gate.md`), opens nothing new: carry on, and release no wave because of it. A root that was waiting on a question in `awaiting_person` gets no such notice: the task that resumes it says "Your spec is approved at version N and the design gate is open: this resume is your design-approved wake", and that resume is this wake — proceed to section 2 on it. |
|
|
28
|
+
| `design-changes-requested` | Its `version` and `reason`, and no step: under more than one gated step, read `gatedSteps` in your own record (the `legion` tool's `read_record`) and take the step whose `status` is `changes_requested`. A human asked for changes to that step's document at `version`, for `reason`. Revise that document as the reason asks and request approval again as "The armed gate" in `skill://legion-architect/references/design-gate.md` says; end your turn; that step is closed. On a root that was waiting on a question in `awaiting_person`, a review the human gave after the wait began comes in the resume task, not in this notice, whether it answered the wait or came after something else did: the task names the review with its version and reason, unless a later approval superseded it. A `version` below the latest, or a second request for one version, is a request that reached the daemon late: read it as the same section's last paragraph says. Each request arrives once. |
|
|
29
29
|
| `phase-finished` | The daemon has already moved the issue to its next phase by its fixed table and started that phase's role; you start nothing. Read the finishing phase's handoff on the Dispatch issue (`dispatch handoff-read --issue <issue> --phase <word>`); if it shows unresolved gaps, tell the role now working the issue (`envoy_publish` to its role topic). A `planner` notice that names a departure from the spec's design defers to the full condition in "After approval" of `skill://legion-architect/references/design-gate.md`: when the approved Summary, the requirements, the checks, the scope and the settled decisions still hold, the plan is the record; otherwise change the root spec and request approval again as that section says. A `reviewer` notice whose GitHub review is `CHANGES_REQUESTED` needs nothing from you: the daemon has returned the issue to `implementing` (Dispatch `in_progress`) and started the **implementer**, whose correction goes through the tester and the reviewer again, never straight to retro. A `reviewer` notice with an `APPROVED` review means the issue is in `accepting` and the acceptance is yours (section 6): read the evidence, then `sign_off` (a parent only after its last round, "The last round" in `skill://legion-architect/references/acceptance.md`) or send it back with `request_backward_move`. A `tester` notice with `verdict: "fail"` — its handoff carries `implementerProof.verdict: "rejected"`, or a failure naming a check with no current passing result — has gone back to the **implementer** by the daemon's table; never record that check's result from another role. A worker that reports no surface reaches the changed path sends that report instead of completing its phase, so the daemon starts nothing more on that issue and the worker stays idle in its session, not suspended: build the surface in this tree: a child issue (infrastructure, tooling, or a skill) after the red-team check in `skill://legion-architect/references/decomposition.md`, or, when the red team folds it, tell the waiting worker to move the issue back to `implementing` with `request_backward_move`, naming the surface. When it is a child, tell the waiting worker to continue with `envoy_publish` to its role topic once that child's `child-closed` arrives. That report is never a reason to advance the phase. |
|
|
30
30
|
| `pr-blocked` | Its `reason` names the pull request and the `max_fix_attempts` it reached, or reads `Issue reached review_round_cap=<N>.` (the end of this row). The count is of heads pushed onto a red verdict: no handoff is a commit, so every such push counts except a push by the review App (a planner's, tester's, reviewer's or architect's), and the head pushed onto a red the tester's red tests earned (a planned red) does not count either. Published once per exhausted count, not on every later red verdict for that count. Read the failed CI evidence and recovery attempts. The notice moves nothing, and no later CI red within the attempt that ran the count out (the counted head, and every head pushed on top of it until a push lands on a head CI judged green or on a settled head whose base branch requires no check and no workflow, the tester's red tests onto the held red included) sends the work back from `testing`, `reviewing`, `accepting`, `retro` or `merging` either: the issue stays in its phase until you move it, or a worker's own decision does (a tester's `fail`, a reviewer's request for changes). A head pushed onto green CI, or onto a settled head whose base branch requires no check and no workflow, leaves that attempt, so its red still comes as a `checks-red`. In `implementing`, give the implementer the failing checks (`envoy_publish` to its role topic). In any later phase a worker holds, tell that worker (`envoy_publish` to its role topic) to move the issue back to `implementing` with `request_backward_move`, naming the failing checks, and the daemon starts the implementer. In `accepting`, where no worker holds a phase, the move is yours: `request_backward_move` with `issue`, naming the failing checks. In `awaiting_merge`, where no worker holds a phase and a backward move is refused, open a `dispatch ask` naming the failing checks for the human who merges. Do not treat the blocked PR as final. A `review_round_cap` reason is the implementer's round count instead: every move back to an earlier phase counts one, and the notice comes again with each round past the cap. It moves nothing either, since the round that reached the cap has already started its worker. Tell the rounds apart by what announced each: a tester's fail by its `phase-finished` with `verdict: "fail"`; a request for changes by the reviewer's `phase-finished` over a `CHANGES_REQUESTED` review; the daemon's own move by a `checks-red` notice, whose reason names a conflict (`the head conflicts with <base>`), the required checks that failed (`CI is red at <head>`), or the merge queue's failed merge and its failing run (`the merge queue took pull request #<n> out at READY's head …`); a worker's `request_backward_move` by a `phase-finished` from the role that asked, whose reason reaches only the task of the worker the move started, so ask that worker; your own send-back from `accepting` by the reason you gave. Judge the cap by the rounds that are not conflicts. Conflict returns need nothing from you beyond the next round. Tester fails, requests for changes and CI-red returns (a `checks-red` notice is raised for CI only when a required check fails on code the branch pushed, or when the merge queue fails the merge) are the rounds the cap is for. Each `review_round_cap` notice is a decision you make in the turn it wakes, never one left for the next round to make: read what every counted round found, choose one of the three below, act on it in the same turn, and post it as a `dispatch comment` on the issue naming the rounds you read and the choice. **Narrow**: the change itself is sound but each round finds more around it; tell the implementer and the reviewer (`envoy_publish` to each one's role topic) which checks of the issue the next round answers; a correctness finding is still fixed in this pull request, and so is a wording nit or any clear fix in a file it touches, so the `Fast-follow:` line carries only cleanup in files this pull request does not touch. **Split**: part of the work cannot settle in this pull request and is a separately deliverable change (`skill://legion-architect`, section 1). On a child, run the red-team check in `skill://legion-architect/references/decomposition.md`, then file that part as a child of this issue's parent, never of this one (this issue closes at its own merge, and the split part usually builds on this pull request's unmerged code): `dispatch issue --project <project> --parent <parent> --title '<outcome>' --blocked-by <this issue> --spec-file -`. Release it now with `release_children`: Legion holds it on that `--blocked-by` link until this issue closes, and starts it then. On the root, narrow it to what settles here and open a `dispatch ask` for the rest (`skill://legion-worker/references/findings.md`): a person decides whether that becomes an issue. Tell the implementer and the reviewer that this pull request no longer carries it. **Accept the rounds**: each round found less than the one before it, and the last found no defect in the change itself (a stale line, a comment that states an old default); let the loop run and say why in the comment. A later notice is a new decision: accept again only when its round found less than the round you last accepted, and otherwise narrow or split. A finding that changes what the spec requires or what a check proves is a decision block in the root spec ("After approval" in `skill://legion-architect/references/design-gate.md`), and `dispatch ask` is only for a decision that needs a human's authority; neither replaces the choice. Judge a worker's backward move by the reason that worker gives. A move you ordered for a conflict found before `awaiting_merge` ("A conflicting head" in `skill://legion-architect/references/acceptance.md`) is a conflict return. The merger's move back to `reviewing` for a READY refusal (`READY_UNREVIEWED_CHANGE`, or `READY_NOT_APPROVED` when its task names no approved head), and the reviewer's no-fix request for changes that follows it so the tester runs the head, are not the work going in circles either. A move that sends back code found wrong counts with the tester fails. |
|
|
31
31
|
| `pr-merged` | The pull request merged and the issue did not close; its `reason` says why. Merged before the issue reached `awaiting_merge`: the workflow runs on, and the merge closes the issue the moment it gets there, your acceptance and retro first. Merged while the rest of the tree still runs: the reason names the pull request and every issue the close waits for, and the root waits in `awaiting_merge`, its workers kept and live and no admission slot held, until the last of them is done or parked ("The close" in `skill://legion-architect/references/acceptance.md`). Either way you start nothing and no further notice follows. |
|
|
@@ -39,7 +39,7 @@ and 7 (a conflict, the hand-over, the close) is in
|
|
|
39
39
|
| A reply on an ask you follow | Interpret the reply in the issue's design context. Answer in its thread (`dispatch comment --reply-to <comment id>`; under an open ask whose next move is yours, such as the approval request you must revise or hand back, `--reply-to-ask <ask id> --turn agent`, since a default-turn reply hands that request back to the human and a corrected `--summary` is then refused), then adjust the plan or relay it via `envoy_publish` to the responsible worker's role token; scope and product decisions remain with you. |
|
|
40
40
|
| `worker-died` | Its `role` and `phase`. That role's claim failed: its launches or prompts ran out. For a phase worker the daemon holds the issue (phase `held`) and starts nothing more on it. Reassess the work, then decide with `retry_or_escalate`: `retry` when the failure looks agent-specific or transient, `escalate` to hand the held issue to the controller when it looks environmental. |
|
|
41
41
|
| `held` | Its `role` and `phase` (the phase the issue left), or `phase` with `reason: "escalated"`. Without `reason`, that phase's worker ran out of launches or prompts: the issue is held (phase `held`), the `worker-died` that comes with it is yours to answer with `retry_or_escalate` as that row says, and the controller hears of the hold too. With `reason: "escalated"`, it records your own `escalate`: the controller has the issue now, and you start nothing for it. |
|
|
42
|
-
| `ready-refused` | Its `version` and `reason
|
|
42
|
+
| `ready-refused` | Its `version` and `reason`, and the `reason` names the gated step the READY waits on. The merger's READY was refused, and its packet is kept with the completion so no second READY is ever needed. With `version` greater than 0, `reason` reads `READY refused: approve design version <N> of the tree's <step> document before requesting READY.`: that step's document is already registered at that version, so get it approved (`skill://legion-architect/references/design-gate.md`), and the daemon advances the merge and posts the kept packet the moment every gated step is open. With `version` 0, `reason` reads `READY refused: no design version of the tree's <step> document is approved; register and approve it before requesting READY.`: that step is not registered yet, so register it and get it approved (`skill://legion-architect/references/design-gate.md`) exactly as above; the daemon releases the kept packet the same way — at registration itself when that opens the step (a deployment that gates no step, or Dispatch already shows the version approved), otherwise the moment a human approves the version you registered. The refusal names the first closed step, in the order the deployment gates them, and no second READY is asked for however many steps were closed: read `gatedSteps` in your own record (the `legion` tool's `read_record`) for every step still closed, and open each one. Nothing else is needed once every gated step is open. On a root whose catch-up said `carried`, the gate is a copy on the old root's spec document: run nothing of `skill://legion-architect/references/design-gate.md` for that step; tell the controller the READY waits on a human's approval of that document, and `carried-gate-opened` follows the approval. A `reason` starting `READY_PACKET_MISSING` names a READY refused before the daemon kept packets: that READY is void and the issue stays in merging, so start it over (`park_child` then `rerun_child`, for a child) or end it. |
|
|
43
43
|
|
|
44
44
|
## A root that joins your tree, and a refused release
|
|
45
45
|
|
|
@@ -21,7 +21,7 @@ except a clear defect a role reports ([Issues handed to Legion](#issues-handed-t
|
|
|
21
21
|
- Keep no notes between turns, because a restarted session has only `read_state` and Dispatch.
|
|
22
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
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
|
|
24
|
+
- Never open a design gate, because only a person's approval in Dispatch opens one: the deployment gates the steps your `Design gate steps:` sentence names, each step records one document of the root issue (`decisions.md`, `verification.md` or the primary document), and the tree's work starts when a person has approved every one of them.
|
|
25
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
26
|
- Before you say that a service or an API is out of reach, run the call through the secrets broker's client (`skill://agent-secrets`), and report it unreachable only with the command you ran and its output.
|
|
27
27
|
- Write every Dispatch comment, message and ask as `skill://dispatch-writing-specs`'s "Rules for the text" say.
|
|
@@ -97,7 +97,10 @@ an approval of a document that is not the root's primary one.
|
|
|
97
97
|
on the issue where the decision was made, has judged it: do nothing.
|
|
98
98
|
4. **Judge the words, not the kind.** Read the decision (`dispatch read --ref <ref>` for an ask or
|
|
99
99
|
a reply; for a request for changes, the notice's `reason` or the issue's `Spec approval:` line;
|
|
100
|
-
for an approval, `issues.<root>.designGate`
|
|
100
|
+
for an approval, `issues.<root>.designGate` and `issues.<root>.gatedSteps` beside it, which
|
|
101
|
+
carries one entry for each design gate step the deployment gates, with that step's
|
|
102
|
+
`artifactId`, `currentVersion`, `approvedVersion` and `status`). An approval or a request for
|
|
103
|
+
changes is two
|
|
101
104
|
events at one time, the approval request's `ask.answered` and `artifact.approved` or
|
|
102
105
|
`artifact.changes_requested`: judge them as one decision, whose `ref` is `dispatch://<KEY>/spec`.
|
|
103
106
|
- It gives the work a next step: an approval of the spec, a request for changes that says what
|
|
@@ -105,6 +108,9 @@ an approval of a document that is not the root's primary one.
|
|
|
105
108
|
the agent to go on. An approval counts when `issues.<root>.designGate.approvedVersion` equals
|
|
106
109
|
`issues.<root>.designGate.currentVersion`, and also when it is lower: then the architect's
|
|
107
110
|
start task names both versions, and the architect requests approval of the current one.
|
|
111
|
+
Where `gatedSteps` carries more than one entry, read each one the same way: the tree runs
|
|
112
|
+
again only once every entry is `approved`, and an entry still `waiting`, `unregistered` or
|
|
113
|
+
`changes_requested` is work the architect holds.
|
|
108
114
|
- It is your own hold (step 2): set the root to `todo` (`set_status`); admission queues it and
|
|
109
115
|
its architect resumes. Post one comment on the issue where the decision was made
|
|
110
116
|
(`dispatch comment --issue <KEY> --body-file -`): `Parked decision read: <ref> by <login>
|
|
@@ -68,9 +68,9 @@ Read this on a `triage` wake, at every start, and when a person hands you an iss
|
|
|
68
68
|
who will get this tree's questions and its design approval`, or, when `Assignee:` says
|
|
69
69
|
`unassigned`, `Unassigned — nobody's Inbox shows this tree's questions or its design approval
|
|
70
70
|
until someone takes it from the issue header (Assignee, beside Priority)`. Promise the design
|
|
71
|
-
approval only when your system prompt's `Design gate
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
approval only when your system prompt's `Design gate steps:` sentence names at least one step;
|
|
72
|
+
when it reads `Design gate steps: none.` nobody approves a design, so drop "and its design
|
|
73
|
+
approval" (and "or its design approval"). Take the assignee from the read, never
|
|
74
74
|
from how the issue was created, because an issue filed with no assignee still lands on the
|
|
75
75
|
project's default assignee where a person set one. An unassigned root still runs, but its
|
|
76
76
|
architect's asks reach only the Inbox's `Unassigned` filter.
|
|
@@ -132,7 +132,9 @@ Send any other to the owning architect: find its role in `read_state` and `envoy
|
|
|
132
132
|
mention's URL, its text and the issue key you checked. Never build a role token from a partial
|
|
133
133
|
issue reference, because a wrong token reaches nobody.
|
|
134
134
|
|
|
135
|
-
A person who asks how to let a root past its design gate approves
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
135
|
+
A person who asks how to let a root past its design gate approves, in Dispatch, the document of
|
|
136
|
+
each step the deployment gates, which your system prompt's `Design gate steps:` sentence names:
|
|
137
|
+
the `Approve` control in that document's header, or the approval question the architect's request
|
|
138
|
+
opened in the Inbox. The root's work starts when every one of those steps is approved. No
|
|
139
|
+
operator command opens a gate; a project that wants no gate sets `gates.design` to the empty list
|
|
140
|
+
(`design: []`) in `legion.yaml`.
|
|
@@ -145,7 +145,9 @@ reviewing until you finish.
|
|
|
145
145
|
the architect as a gap the tester and the reviewer named and neither refused, so the
|
|
146
146
|
architect's acceptance is the only gate on it: the acceptance comment names that gap and
|
|
147
147
|
decides there, accepting the issue or sending it back with the checks the architect writes,
|
|
148
|
-
which closes
|
|
148
|
+
which closes the root's `criteria` step where the deployment gates that step, since that step
|
|
149
|
+
records the root's Verification document and each new version of a step's document closes it;
|
|
150
|
+
a child's Verification document is gated by nothing. The architect also
|
|
149
151
|
reads the reviewer's own prose commits here, with the rest of the evidence, and that read is no
|
|
150
152
|
new round. It does not
|
|
151
153
|
accept while the issue holds a finding ask no controller has taken (`findings.md`, "When no
|
|
@@ -259,8 +261,10 @@ reviewing until you finish.
|
|
|
259
261
|
ordinary arm cannot merge the person-only head. Ordinary READY does not wait for the disarm. The
|
|
260
262
|
daemon tells that READY with the note at its head and the packet's `Merge:` line replaced, so the
|
|
261
263
|
issue and the merge queue role are never asked to arm it. A READY the design gate refused is
|
|
262
|
-
submitted by the daemon
|
|
263
|
-
|
|
264
|
+
submitted by the daemon once every gated step of the root's design gate is open, when the
|
|
265
|
+
approval that opens the last closed one moves the issue to `awaiting_merge`; the refusal named
|
|
266
|
+
one step and the version to approve, and no second READY is asked for however many steps held
|
|
267
|
+
it. A push that moves the head past a READY the gate holds voids it before then:
|
|
264
268
|
the architect is told `READY_HEAD_MOVED` and has the merger build a new READY at the new head.
|
|
265
269
|
The packet's second line records the step,
|
|
266
270
|
`Merge: the daemon submits pull request #<n> at <head> once READY is accepted`.
|
|
@@ -14,7 +14,10 @@ limit, and one command that fills it ends every role's turn, not only yours.
|
|
|
14
14
|
In the pod you compile and test the packages your change touches, build the program a check of
|
|
15
15
|
your issue drives (`go build ./cmd/legion`), vet the module you changed (`go vet -tags e2e ./...`,
|
|
16
16
|
which compiles and runs no test), and, in a package whose tests need Postgres, run only the tests
|
|
17
|
-
your change touches by name (`go test -run`).
|
|
17
|
+
your change touches by name (`go test -run`). To run Go's race detector, prefix the command with
|
|
18
|
+
`CGO_ENABLED=1`: `CGO_ENABLED=1 go test -race -count=1 -run '<TestName>' ./internal/<package>/`.
|
|
19
|
+
The worker image sets `CGO_ENABLED=0` and carries gcc (both in
|
|
20
|
+
`packages/daemon/docker/worker.Dockerfile`), and Go refuses `-race` without cgo.
|
|
18
21
|
|
|
19
22
|
A whole module's or whole package's test run goes to CI on the pull request, where a job of that
|
|
20
23
|
pull request runs it: `go test ./...`, a whole Postgres-backed package such as
|
|
@@ -28,14 +31,31 @@ heavy run goes where your deployment's instructions say.
|
|
|
28
31
|
file `OMP_SESSION_SQL_DSN_FILE`: that is the live sessions database every pod shares, and it
|
|
29
32
|
holds every role's transcript.
|
|
30
33
|
- Give every Postgres variable a test reads `$LEGION_TEST_PG_DSN`, your pod's own server:
|
|
31
|
-
`LEGION_TEST_PG_DSN`, `DISPATCH_TEST_DATABASE_URL` and `BROKER_TEST_DATABASE_URL`.
|
|
32
|
-
and
|
|
34
|
+
`LEGION_TEST_PG_DSN`, `DISPATCH_TEST_DATABASE_URL` and `BROKER_TEST_DATABASE_URL`. Most suites
|
|
35
|
+
make and drop one of their own per test on that server — a database through `internal/testpg`,
|
|
36
|
+
a schema through `internal/broker/store/storetest`'s `URL` — so your pod's roles can share the
|
|
37
|
+
server. Four write the database the URL itself names instead, and take a database of their own
|
|
38
|
+
from the next bullet's `createdb` line: `packages/envoy/internal/broker/store`, whose
|
|
39
|
+
`store_test.go` helpers call `Open(ctx, testDatabaseURL(t))` and then `Migrate` on the database
|
|
40
|
+
`BROKER_TEST_DATABASE_URL` names; and three that open the database `DISPATCH_TEST_DATABASE_URL`
|
|
41
|
+
names and migrate it, `packages/envoy/internal/dispatch/store` (`openTestStore`, in its
|
|
42
|
+
`store_test.go`), `packages/envoy/internal/dispatch/delivery` (`deliveryTestPool`, which also
|
|
43
|
+
deletes every delivery table's rows) and `packages/envoy/internal/dispatch/redeliver`
|
|
44
|
+
(`redeliverTestLeases`). In an empty database of its own, one run of the broker's left 24 tables
|
|
45
|
+
on 2026-11-06, and one whole run of the Dispatch store's left 104 on 2026-11-07, a count that
|
|
46
|
+
grows with Dispatch's migrations.
|
|
33
47
|
- Anything that migrates into the database its DSN names, rather than making one per test, needs
|
|
34
48
|
a database of your own, because a sibling role writing the same one collides with you: the
|
|
35
49
|
Dispatch browser suite's `DATABASE_URL`, which resets what it is given, and the stage scripts'
|
|
36
50
|
`LEGION_E2E_PG_DSN`, which a daemon's `postgres_dsn` becomes and migrates Legion's schema into.
|
|
37
51
|
Make one and point the variable at it:
|
|
38
52
|
`createdb -h 127.0.0.1 -p 5433 -U legion_test "e2e_$(date +%s)"`.
|
|
53
|
+
The DSN's own database is owned by your login and writable, so what keeps two roles out of it is
|
|
54
|
+
this rule rather than the server. `packages/daemon/internal/daemon` is the third case, whose
|
|
55
|
+
`TestMain` takes one database for the package's run through `internal/testpg/testdb`.
|
|
56
|
+
Empty the served database by dropping your own suite's schemas and your own objects in `public`,
|
|
57
|
+
never `public` itself: `vector` goes with it, your login may not create it again, and
|
|
58
|
+
`ensure` only ever installs it at a database's creation.
|
|
39
59
|
- The server is empty in every new pod and dies with it. Every database made from `template1`
|
|
40
60
|
has pgvector.
|
|
41
61
|
- `LEGION_TEST_PG_DSN` set and the connection refused: that is not yours to repair. Starting the
|
|
@@ -48,12 +68,14 @@ heavy run goes where your deployment's instructions say.
|
|
|
48
68
|
is starting one and nothing will appear while you wait. With `legion-testpg` on `PATH`, start
|
|
49
69
|
one yourself on the issue's volume: the directory it defaults to is a 1 MiB filesystem your
|
|
50
70
|
role's own mounts cover, so `export LEGION_TESTPG_DIR=$XDG_DATA_HOME/testpg` first, then
|
|
51
|
-
`export LEGION_TEST_PG_DSN=postgres://legion_test@127.0.0.1:5433/
|
|
52
|
-
and `legion-testpg ensure`, which makes the cluster, the role
|
|
71
|
+
`export LEGION_TEST_PG_DSN=postgres://legion_test@127.0.0.1:5433/legion_test?sslmode=disable`
|
|
72
|
+
and `legion-testpg ensure`, which makes the cluster, the role, `vector` in `template1` and the
|
|
73
|
+
database the DSN names, owned by that role. That
|
|
53
74
|
server lives in your generation: it dies when your generation ends, which takes a sibling
|
|
54
75
|
role's connections and databases with it mid-test, and the next `ensure` of either role finds
|
|
55
|
-
the same cluster on disk and serves it again. With no
|
|
56
|
-
`legion-
|
|
76
|
+
the same cluster on disk and serves it again. With no `legion-testpg` either, stand up one with
|
|
77
|
+
the harness of `skill://legion-worker/references/withheld-capability.md` (**The worked case**)
|
|
78
|
+
and set the DSN.
|
|
57
79
|
|
|
58
80
|
## Reading a shared database
|
|
59
81
|
|
|
@@ -160,169 +182,23 @@ So that writable `$HOME`, and the issue's disk, are where an install for yoursel
|
|
|
160
182
|
the mechanics the answer is not a flat no: `redis-cli`, `sqlite3` and `dig` did run from an
|
|
161
183
|
extraction prefix under `$HOME` with `LD_LIBRARY_PATH` set, and `psql` did not — Debian's
|
|
162
184
|
`/usr/bin/psql` is a Perl wrapper that loads `PgCommon.pm` through Perl's own search path, which no
|
|
163
|
-
extraction prefix is on. What rules such an attempt out is not the unpacking: it is the
|
|
164
|
-
|
|
165
|
-
`$HOME` fails that test's first condition whatever you scope it to,
|
|
166
|
-
pod's disk rather than the issue's volume — the placement
|
|
167
|
-
(`packages/daemon/internal/runtime/sandbox/manifest.go`) exists to move writes
|
|
168
|
-
extraction is the harness's: onto the issue's volume, and exported around its
|
|
169
|
-
further (the Dispatch e2e stack
|
|
170
|
-
carry, the one documented step is a pinned run through one of the
|
|
171
|
-
— plus `uv run --with` when what you need is a library rather than
|
|
172
|
-
no runner reaches is an image change ("When no runner reaches it", below).
|
|
185
|
+
extraction prefix is on. What rules such an attempt out is not the unpacking: it is the test of
|
|
186
|
+
`skill://legion-worker/references/withheld-capability.md#the-test-where-it-lands-and-what-it-leaves-resolving`,
|
|
187
|
+
and an extraction under `$HOME` fails that test's first condition whatever you scope it to,
|
|
188
|
+
because `/home/legion` is the pod's disk rather than the issue's volume — the placement
|
|
189
|
+
`roleEnvironment` (`packages/daemon/internal/runtime/sandbox/manifest.go`) exists to move writes
|
|
190
|
+
off. The sanctioned extraction is the harness's: onto the issue's volume, and exported around its
|
|
191
|
+
own commands and no further (the Dispatch e2e stack of that reference is the worked case). For
|
|
192
|
+
everything the image does not carry, the one documented step is a pinned run through one of the
|
|
193
|
+
three package runners it does — plus `uv run --with` when what you need is a library rather than
|
|
194
|
+
a command — and a tool no runner reaches is an image change ("When no runner reaches it", below).
|
|
173
195
|
|
|
174
196
|
## What the withheld capability is, and what it is not
|
|
175
197
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
three artefacts in this repository are what draw the line — not a claim about what the ruling meant:
|
|
181
|
-
|
|
182
|
-
- **What the image ships for a role to use.** `packages/daemon/docker/worker.Dockerfile` installs
|
|
183
|
-
`uv` and `uvx`, `bun` with its `bunx` link, and the Go toolchain, and the `toolchain` capability
|
|
184
|
-
row (`packages/daemon/internal/capabilities/capabilities.go`) grants them by name. A runner put
|
|
185
|
-
in the image deliberately and granted by a capability row is not a route around a withheld one.
|
|
186
|
-
- **What the pod configures for those runners to write.** A role's environment relocates
|
|
187
|
-
`UV_CACHE_DIR` and `UV_PYTHON_INSTALL_DIR` (`podkind.go`) and `BUN_INSTALL_CACHE_DIR`, `GOCACHE`
|
|
188
|
-
and `GOMODCACHE` (`roleEnvironment`, `packages/daemon/internal/runtime/sandbox/manifest.go`)
|
|
189
|
-
onto the issue's volume, shared by the issue's roles. Legion provisions the caches a pinned run
|
|
190
|
-
fills; filling them is what they are for.
|
|
191
|
-
- **What Legion itself does at launch.** `installBlock`
|
|
192
|
-
(`packages/daemon/internal/runtime/sandbox/plugins.go`) renders a `bun add` under
|
|
193
|
-
`/usr/bin/flock` into that shared bun install cache on every installing launch. Fetching a pinned
|
|
194
|
-
package at run time is something Legion does, not something it withholds.
|
|
195
|
-
|
|
196
|
-
So the withheld row is about a dependency **the pod carries**: something that outlives the command
|
|
197
|
-
that created it, and that a later launch or another role's process then resolves *without asking
|
|
198
|
-
for it*.
|
|
199
|
-
|
|
200
|
-
### The test: where it lands, and what it leaves resolving
|
|
201
|
-
|
|
202
|
-
An install of your own — an interpreter, a browser, a second toolchain, an extraction prefix — is
|
|
203
|
-
on the sanctioned side when both of these hold: what it writes lands on the issue's own volume,
|
|
204
|
-
under `/legion`, which outlives a role's relaunch in its pod and is shared by the issue's roles —
|
|
205
|
-
whether through a variable the runtime sets for you there or under a path on that volume your
|
|
206
|
-
harness names itself — but outside the issue's workspace (`$LEGION_WORKSPACE`, under
|
|
207
|
-
`/legion/workspaces`) and the runtime's own directories (`/legion/repos`, `/legion/feed`); and
|
|
208
|
-
after the command exits nothing else resolves differently. The workspace is excluded because its jj
|
|
209
|
-
working copy tracks every new file under jj's `snapshot.max-new-file-size` (1 MiB for a role), so
|
|
210
|
-
a root written there is added to the change every role shares, and a larger new
|
|
211
|
-
file is left untracked, every jj command in the workspace then warning `Refused to snapshot some
|
|
212
|
-
files`. The one write into it that is sanctioned is a repository's own dependency install into
|
|
213
|
-
its ignored directories, below. The variables the runtime sets on the volume, where a tool's own
|
|
214
|
-
cache already goes, are
|
|
215
|
-
`XDG_CACHE_HOME` (`/legion/scratch/cache`), `XDG_DATA_HOME`, `XDG_STATE_HOME`, `TMPDIR`, `GOCACHE`,
|
|
216
|
-
`GOMODCACHE`, `BUN_INSTALL_CACHE_DIR`, `UV_CACHE_DIR` (`/legion/uv/cache`) and
|
|
217
|
-
`UV_PYTHON_INSTALL_DIR` (`/legion/uv/python/<issue>`). (`XDG_CONFIG_HOME` is set too and is not
|
|
218
|
-
one of them: it is your role's own config home, a copy of the pod's made for your role on the
|
|
219
|
-
pod's disk, which no other role reads.) `$HOME` and `/tmp` are not on the volume:
|
|
220
|
-
both are your role's own, on the pod's disk.
|
|
221
|
-
|
|
222
|
-
It is the withheld capability when either condition fails — anything that writes into a
|
|
223
|
-
system prefix, and anything that leaves a toolchain, shim or binary which a later launch or
|
|
224
|
-
another role's process will resolve without asking for it. Between two installs that both land in
|
|
225
|
-
the right place, the test is resolution, not disk, and it reads as scope: a second toolchain
|
|
226
|
-
exported for the commands of your own run — by absolute path, or on a `PATH` your harness's own
|
|
227
|
-
script sets around its own invocations — is sanctioned; the same toolchain left where a later
|
|
228
|
-
launch, another role or a subagent picks it up without naming it is operator setup, whoever typed
|
|
229
|
-
the command.
|
|
230
|
-
|
|
231
|
-
What each shape leaves resolving:
|
|
232
|
-
|
|
233
|
-
- A pinned runner run: nothing. `uvx --from httpie@3.2.4 http --version` leaves a cache entry
|
|
234
|
-
addressed by that coordinate and that version, which the next command reaches only by writing the
|
|
235
|
-
same pin; no `PATH`, no interpreter and no library search of any other process changes. That is
|
|
236
|
-
the sanctioned side, and it is the side the forms below are on.
|
|
237
|
-
- A repository's own dependency set: nothing either, which is why the caches are provisioned at
|
|
238
|
-
all. `uv sync` or `bun install` in your workspace materialises what that project already
|
|
239
|
-
declares, into that project's own directory, which a process reaches by asking for that project
|
|
240
|
-
and no other way — and the runtime puts `UV_PYTHON_INSTALL_DIR` and the workspace itself on the
|
|
241
|
-
issue's volume exactly so every role of the issue's pod reuses it.
|
|
242
|
-
- An extraction prefix: whatever you leave resolving through it, which is yours to decide. Unpack
|
|
243
|
-
Debian packages under `$HOME`, export `LD_LIBRARY_PATH` in the shell you go on working in, and
|
|
244
|
-
you change what every later command in that shell — and every subagent that inherits the
|
|
245
|
-
environment — resolves `psql` or its libraries to, asked for or not, on a prefix nothing records,
|
|
246
|
-
pins or rebuilds: that fails both conditions, and it is the withheld capability. The harness
|
|
247
|
-
below is the shape that passes. The opposite shape is in the tree too, and superseded: telling
|
|
248
|
-
every subagent to prepend a directory to `PATH` because a subagent's shell starts without it
|
|
249
|
-
hands over an environment to processes that never named it. That is the difference between
|
|
250
|
-
standing a harness up and getting yourself a CLI.
|
|
251
|
-
|
|
252
|
-
The test is about what a command leaves for other processes, never about getting the same bytes
|
|
253
|
-
twice — because a pinned line does not promise the same bytes. What a pin binds is the named
|
|
254
|
-
distribution at that version; its dependency closure is resolved when the command runs. With
|
|
255
|
-
`UV_CACHE_DIR` emptied, `uvx -v --from httpie@3.2.4 http --version` logs `Selecting:
|
|
256
|
-
requests==2.34.2 [compatible]` and resolves `urllib3` and `certifi` from ranges, where the warm run
|
|
257
|
-
prints `3.2.4` at once off the cache that first run filled. When your work needs the closure pinned
|
|
258
|
-
too — a comparison that has to hold across days or across pods — that is a project's own lockfile
|
|
259
|
-
with `uv run` or `bun install` in a workspace, not a one-off runner line.
|
|
260
|
-
|
|
261
|
-
A pinned run whose tool then installs something of its own — an interpreter, a browser, a second
|
|
262
|
-
toolchain — is where the first condition does the work. `uv run --with pyyaml==6.0.2 --no-project`
|
|
263
|
-
passes both: the wheel lands in `UV_CACHE_DIR`, under `wheels-v6/pypi/pyyaml/6.0.2-…`, and
|
|
264
|
-
`UV_PYTHON_INSTALL_DIR` is never created, because the image's own `python3` served the run. The
|
|
265
|
-
interpreter is the other run: a project whose `.python-version` names an interpreter uv must fetch
|
|
266
|
-
puts about 70 MB under `UV_PYTHON_INSTALL_DIR`, where every role of the issue's pod finds it, while
|
|
267
|
-
the cache holds no wheel. A browser download passes too, and for Playwright — the browser driver this
|
|
268
|
-
repository's own Dispatch suite installs (`playwright install`, `packages/dispatch`'s
|
|
269
|
-
`e2e:install`) — it passes as it stands, with nothing pointed anywhere: playwright-core resolves
|
|
270
|
-
its browser registry under `XDG_CACHE_HOME`, which the runtime has already put on the issue's
|
|
271
|
-
volume, so the issue's roles share one registry under the exclusive install lock that
|
|
272
|
-
`docs/kubernetes.md` writes down in "Anatomy of a Sandbox pod", the paragraph on sharing the cache
|
|
273
|
-
home — worth reading before an install, for the two things that follow from sharing. Pointing
|
|
274
|
-
`PLAYWRIGHT_BROWSERS_PATH` elsewhere is what costs you: that shared registry, and the lock that
|
|
275
|
-
makes a second role installing at the same time wait rather than race. The condition is for a tool
|
|
276
|
-
whose cache the runtime does not already place — Puppeteer is one, resolving its browser cache to
|
|
277
|
-
`join(homedir(), '.cache', 'puppeteer')` and so to your role's own home on the pod's disk whatever
|
|
278
|
-
`XDG_CACHE_HOME` says. For that one, point the tool's own cache variable (`PUPPETEER_CACHE_DIR`
|
|
279
|
-
here) at a directory under one of the paths above yourself, since an unset one leaves the download
|
|
280
|
-
wherever the tool defaults, which is nowhere this runtime chose.
|
|
281
|
-
|
|
282
|
-
Where it lands is what you pay. Under one of those variables it is on the issue's volume: it
|
|
283
|
-
outlives a role's relaunch in its pod, and every role of the issue is told the same path — under
|
|
284
|
-
every one of those variables but
|
|
285
|
-
`XDG_STATE_HOME`, which is `/legion/scratch/state/<role>`, a directory named for your role
|
|
286
|
-
(`roleEnvironment`) because Oh My Pi names its browser broker's lock from the state root and an
|
|
287
|
-
abstract socket name is one name across every role of the pod, which share its network. It is not
|
|
288
|
-
private: every role of the issue can write it, as it can every path above — though it does not
|
|
289
|
-
outlive the pod: the issue's disk is the pod's own and is deleted with it, so a later pod of the
|
|
290
|
-
issue starts without it. Under
|
|
291
|
-
`$HOME` — `/home/legion`, which no volume is mounted on — it is your role's own home, an overlay
|
|
292
|
-
over the image's whose writes land on the pod's disk (an `emptyDir` every role's home and `/tmp`
|
|
293
|
-
share): one copy per role, per pod, kept across the role's relaunches in that pod and gone when
|
|
294
|
-
the pod is replaced, and charged to the pod's ephemeral-storage limit, which every role of the pod
|
|
295
|
-
shares and which is smaller than the volume. Moving exactly these writes off that
|
|
296
|
-
filesystem is what `roleEnvironment` (`packages/daemon/internal/runtime/sandbox/manifest.go`)
|
|
297
|
-
exists for, after GKE Autopilot evicted issue pods that charged their caches to it — so an install
|
|
298
|
-
under `$HOME` fails the first condition and is outside the sanctioned form, however little else it
|
|
299
|
-
changes. The three artefacts above are the whole of that line's ground. Disk is not: every cache is
|
|
300
|
-
already relocated onto the issue's volume, which has the room for them (`docs/kubernetes.md`, "The
|
|
301
|
-
tools a role's own work needs", carries the figure), and a sanctioned install is paid once per
|
|
302
|
-
issue pod, on the issue's own disk, by the issue's roles together.
|
|
303
|
-
|
|
304
|
-
**The worked case, and the one a role actually meets: the Dispatch e2e stack.** A change to
|
|
305
|
-
`packages/dispatch` is proved in the pod by driving the specs it touches, one spec a command
|
|
306
|
-
(`bun run e2e -- e2e/<one spec file>`); the whole browser suite runs in CI's `dispatch` job on the
|
|
307
|
-
pull request. One spec needs the same stack as all of them: a Postgres with pgvector. On a digest
|
|
308
|
-
carrying `legion-testpg`, use the pod's own server ("The database your tests use", above) and
|
|
309
|
-
unpack nothing. On a digest without it, the way
|
|
310
|
-
that stack is stood up here is Debian packages unpacked with `dpkg-deb -x` into one root on the
|
|
311
|
-
issue's volume — the five packages and the start sequence are in
|
|
312
|
-
`docs/solutions/testing/the-real-dispatch-stack-runs-inside-a-legion-pod-with-no-postgres-no-docker-and-no-root-INFRA-42.md`,
|
|
313
|
-
and
|
|
314
|
-
`docs/solutions/testing/a-browser-harness-in-an-issue-pod-lives-on-the-issue-volume-INFRA-43.md`
|
|
315
|
-
puts it on the volume, moves the caches with it, and writes it as one idempotent script the next
|
|
316
|
-
role re-runs. That passes both conditions, and the second is the one to get right: export the
|
|
317
|
-
root's `PATH` and `LD_LIBRARY_PATH` around the harness's own commands — `initdb`, `pg_ctl`, the
|
|
318
|
-
server, the Playwright run — rather than into the shell you go on working in. Handing the harness
|
|
319
|
-
to the next role is the same test passed again, and
|
|
320
|
-
`docs/solutions/legion/a-port-held-in-an-issue-pod-is-a-sibling-roles-harness-move-your-run-with-the-five-port-variables-and-your-own-database-and-never-stop-the-holder-LEGION-695.md`
|
|
321
|
-
is the illustration: it leaves the server running and records the `LD_LIBRARY_PATH`, `PATH` and
|
|
322
|
-
`DATABASE_URL` in its handoff's `trickyParts`, so what crosses to the next role is a note that
|
|
323
|
-
role sets deliberately rather than an environment it inherits. The suite shells out to `psql`,
|
|
324
|
-
and `psql` being in the image is what makes that harness work now without your extracting a
|
|
325
|
-
client for yourself at all.
|
|
198
|
+
What the withheld row says, the three artefacts that draw the line between it and a pinned runner
|
|
199
|
+
form, and the worked harness case are in
|
|
200
|
+
`skill://legion-worker/references/withheld-capability.md`: read it before you unpack, extract or
|
|
201
|
+
install anything the image does not carry.
|
|
326
202
|
|
|
327
203
|
## The three pinned runner forms
|
|
328
204
|
|
|
@@ -528,8 +404,9 @@ to its role topic), with the tool, what your phase needs it for, and the runner
|
|
|
528
404
|
ruled out. What is not the step is improvising a CLI for your own convenience — a vendored binary,
|
|
529
405
|
a hand-rolled substitute, or an extraction prefix dropped on your own `PATH`: the architect decides
|
|
530
406
|
whether the image change is work to schedule. An extraction a harness builds for its own runs is
|
|
531
|
-
not improvising and is not what this rules out (the Dispatch e2e stack
|
|
532
|
-
|
|
407
|
+
not improvising and is not what this rules out (the Dispatch e2e stack of
|
|
408
|
+
`skill://legion-worker/references/withheld-capability.md`): the two cases part on its test,
|
|
409
|
+
"The test: where it lands, and what it leaves resolving", which the convenience case fails.
|
|
533
410
|
|
|
534
411
|
The operator's side of this — why the image carries those five clients rather than anything else,
|
|
535
412
|
the five priced options with their measurements, and what the clients cost the image — is in
|
|
@@ -716,8 +593,9 @@ something to count.
|
|
|
716
593
|
`go test -count=1 -timeout 40m ./internal/workflow/` runs whole in the pod, with no docker
|
|
717
594
|
panic.
|
|
718
595
|
- **The rest, by package**, for a named run the pod does make. A Postgres unpacked from Debian
|
|
719
|
-
packages (the harness under **The worked case**) has only the
|
|
720
|
-
locales, so its default collation is byte order, and
|
|
596
|
+
packages (the harness under **The worked case** of `withheld-capability.md`) has only the
|
|
597
|
+
`C`, `C.utf8` and `POSIX` locales, so its default collation is byte order, and
|
|
598
|
+
`internal/dispatch/api`'s
|
|
721
599
|
`TestIssueRanksStayOrderedPastLowercaseDigitsAndProductionShapedMoves` refuses such a database
|
|
722
600
|
by its own message. The pod's own `legion-testpg` server is made with ICU `en-US`, so that
|
|
723
601
|
test runs there, as it does in CI's `envoy-go` job, whose Postgres has `en_US.utf8`.
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# What the withheld capability is, and what it is not
|
|
2
|
+
|
|
3
|
+
Part of `skill://legion-worker`, read from `skill://legion-worker/references/tools.md` before you
|
|
4
|
+
unpack, extract or install anything the worker image does not carry. Every path it cites is in
|
|
5
|
+
cybermonklabs/legion.
|
|
6
|
+
|
|
7
|
+
The withheld row reads `installs or configures its own dependencies in the pod: Legion owns its
|
|
8
|
+
dependencies` (`packages/daemon/internal/capabilities/capabilities.go`, ruling
|
|
9
|
+
dispatch://LEGION-200). Read as a sentence on its own, that wording would also forbid `uvx`,
|
|
10
|
+
`bunx --package` and `go run`, each of which fetches a package and writes a cache. It does not, and
|
|
11
|
+
three artefacts in this repository are what draw the line — not a claim about what the ruling meant:
|
|
12
|
+
|
|
13
|
+
- **What the image ships for a role to use.** `packages/daemon/docker/worker.Dockerfile` installs
|
|
14
|
+
`uv` and `uvx`, `bun` with its `bunx` link, and the Go toolchain, and the `toolchain` capability
|
|
15
|
+
row (`packages/daemon/internal/capabilities/capabilities.go`) grants them by name. A runner put
|
|
16
|
+
in the image deliberately and granted by a capability row is not a route around a withheld one.
|
|
17
|
+
- **What the pod configures for those runners to write.** A role's environment relocates
|
|
18
|
+
`UV_CACHE_DIR` and `UV_PYTHON_INSTALL_DIR` (`podkind.go`) and `BUN_INSTALL_CACHE_DIR`, `GOCACHE`
|
|
19
|
+
and `GOMODCACHE` (`roleEnvironment`, `packages/daemon/internal/runtime/sandbox/manifest.go`)
|
|
20
|
+
onto the issue's volume, shared by the issue's roles. Legion provisions the caches a pinned run
|
|
21
|
+
fills; filling them is what they are for.
|
|
22
|
+
- **What Legion itself does at launch.** `installBlock`
|
|
23
|
+
(`packages/daemon/internal/runtime/sandbox/plugins.go`) renders a `bun add` under
|
|
24
|
+
`/usr/bin/flock` into that shared bun install cache on every installing launch. Fetching a pinned
|
|
25
|
+
package at run time is something Legion does, not something it withholds.
|
|
26
|
+
|
|
27
|
+
So the withheld row is about a dependency **the pod carries**: something that outlives the command
|
|
28
|
+
that created it, and that a later launch or another role's process then resolves *without asking
|
|
29
|
+
for it*.
|
|
30
|
+
|
|
31
|
+
### The test: where it lands, and what it leaves resolving
|
|
32
|
+
|
|
33
|
+
An install of your own — an interpreter, a browser, a second toolchain, an extraction prefix — is
|
|
34
|
+
on the sanctioned side when both of these hold: what it writes lands on the issue's own volume,
|
|
35
|
+
under `/legion`, which outlives a role's relaunch in its pod and is shared by the issue's roles —
|
|
36
|
+
whether through a variable the runtime sets for you there or under a path on that volume your
|
|
37
|
+
harness names itself — but outside the issue's workspace (`$LEGION_WORKSPACE`, under
|
|
38
|
+
`/legion/workspaces`) and the runtime's own directories (`/legion/repos`, `/legion/feed`); and
|
|
39
|
+
after the command exits nothing else resolves differently. The workspace is excluded because its jj
|
|
40
|
+
working copy tracks every new file under jj's `snapshot.max-new-file-size` (1 MiB for a role), so
|
|
41
|
+
a root written there is added to the change every role shares, and a larger new
|
|
42
|
+
file is left untracked, every jj command in the workspace then warning `Refused to snapshot some
|
|
43
|
+
files`. The one write into it that is sanctioned is a repository's own dependency install into
|
|
44
|
+
its ignored directories, below. The variables the runtime sets on the volume, where a tool's own
|
|
45
|
+
cache already goes, are
|
|
46
|
+
`XDG_CACHE_HOME` (`/legion/scratch/cache`), `XDG_DATA_HOME`, `XDG_STATE_HOME`, `TMPDIR`, `GOCACHE`,
|
|
47
|
+
`GOMODCACHE`, `BUN_INSTALL_CACHE_DIR`, `UV_CACHE_DIR` (`/legion/uv/cache`) and
|
|
48
|
+
`UV_PYTHON_INSTALL_DIR` (`/legion/uv/python/<issue>`). (`XDG_CONFIG_HOME` is set too and is not
|
|
49
|
+
one of them: it is your role's own config home, a copy of the pod's made for your role on the
|
|
50
|
+
pod's disk, which no other role reads.) `$HOME` and `/tmp` are not on the volume:
|
|
51
|
+
both are your role's own, on the pod's disk.
|
|
52
|
+
|
|
53
|
+
It is the withheld capability when either condition fails — anything that writes into a
|
|
54
|
+
system prefix, and anything that leaves a toolchain, shim or binary which a later launch or
|
|
55
|
+
another role's process will resolve without asking for it. Between two installs that both land in
|
|
56
|
+
the right place, the test is resolution, not disk, and it reads as scope: a second toolchain
|
|
57
|
+
exported for the commands of your own run — by absolute path, or on a `PATH` your harness's own
|
|
58
|
+
script sets around its own invocations — is sanctioned; the same toolchain left where a later
|
|
59
|
+
launch, another role or a subagent picks it up without naming it is operator setup, whoever typed
|
|
60
|
+
the command.
|
|
61
|
+
|
|
62
|
+
What each shape leaves resolving:
|
|
63
|
+
|
|
64
|
+
- A pinned runner run: nothing. `uvx --from httpie@3.2.4 http --version` leaves a cache entry
|
|
65
|
+
addressed by that coordinate and that version, which the next command reaches only by writing the
|
|
66
|
+
same pin; no `PATH`, no interpreter and no library search of any other process changes. That is
|
|
67
|
+
the sanctioned side, and it is the side the runner forms of
|
|
68
|
+
`skill://legion-worker/references/tools.md` are on.
|
|
69
|
+
- A repository's own dependency set: nothing either, which is why the caches are provisioned at
|
|
70
|
+
all. `uv sync` or `bun install` in your workspace materialises what that project already
|
|
71
|
+
declares, into that project's own directory, which a process reaches by asking for that project
|
|
72
|
+
and no other way — and the runtime puts `UV_PYTHON_INSTALL_DIR` and the workspace itself on the
|
|
73
|
+
issue's volume exactly so every role of the issue's pod reuses it.
|
|
74
|
+
- An extraction prefix: whatever you leave resolving through it, which is yours to decide. Unpack
|
|
75
|
+
Debian packages under `$HOME`, export `LD_LIBRARY_PATH` in the shell you go on working in, and
|
|
76
|
+
you change what every later command in that shell — and every subagent that inherits the
|
|
77
|
+
environment — resolves `psql` or its libraries to, asked for or not, on a prefix nothing records,
|
|
78
|
+
pins or rebuilds: that fails both conditions, and it is the withheld capability. The harness
|
|
79
|
+
below is the shape that passes. The opposite shape is in the tree too, and superseded: telling
|
|
80
|
+
every subagent to prepend a directory to `PATH` because a subagent's shell starts without it
|
|
81
|
+
hands over an environment to processes that never named it. That is the difference between
|
|
82
|
+
standing a harness up and getting yourself a CLI.
|
|
83
|
+
|
|
84
|
+
The test is about what a command leaves for other processes, never about getting the same bytes
|
|
85
|
+
twice — because a pinned line does not promise the same bytes. What a pin binds is the named
|
|
86
|
+
distribution at that version; its dependency closure is resolved when the command runs. With
|
|
87
|
+
`UV_CACHE_DIR` emptied, `uvx -v --from httpie@3.2.4 http --version` logs `Selecting:
|
|
88
|
+
requests==2.34.2 [compatible]` and resolves `urllib3` and `certifi` from ranges, where the warm run
|
|
89
|
+
prints `3.2.4` at once off the cache that first run filled. When your work needs the closure pinned
|
|
90
|
+
too — a comparison that has to hold across days or across pods — that is a project's own lockfile
|
|
91
|
+
with `uv run` or `bun install` in a workspace, not a one-off runner line.
|
|
92
|
+
|
|
93
|
+
A pinned run whose tool then installs something of its own — an interpreter, a browser, a second
|
|
94
|
+
toolchain — is where the first condition does the work. `uv run --with pyyaml==6.0.2 --no-project`
|
|
95
|
+
passes both: the wheel lands in `UV_CACHE_DIR`, under `wheels-v6/pypi/pyyaml/6.0.2-…`, and
|
|
96
|
+
`UV_PYTHON_INSTALL_DIR` is never created, because the image's own `python3` served the run. The
|
|
97
|
+
interpreter is the other run: a project whose `.python-version` names an interpreter uv must fetch
|
|
98
|
+
puts about 70 MB under `UV_PYTHON_INSTALL_DIR`, where every role of the issue's pod finds it, while
|
|
99
|
+
the cache holds no wheel. A browser download passes too, and for Playwright — the browser driver this
|
|
100
|
+
repository's own Dispatch suite installs (`playwright install`, `packages/dispatch`'s
|
|
101
|
+
`e2e:install`) — it passes as it stands, with nothing pointed anywhere: playwright-core resolves
|
|
102
|
+
its browser registry under `XDG_CACHE_HOME`, which the runtime has already put on the issue's
|
|
103
|
+
volume, so the issue's roles share one registry under the exclusive install lock that
|
|
104
|
+
`docs/kubernetes.md` writes down in "Anatomy of a Sandbox pod", the paragraph on sharing the cache
|
|
105
|
+
home — worth reading before an install, for the two things that follow from sharing. Pointing
|
|
106
|
+
`PLAYWRIGHT_BROWSERS_PATH` elsewhere is what costs you: that shared registry, and the lock that
|
|
107
|
+
makes a second role installing at the same time wait rather than race. The condition is for a tool
|
|
108
|
+
whose cache the runtime does not already place — Puppeteer is one, resolving its browser cache to
|
|
109
|
+
`join(homedir(), '.cache', 'puppeteer')` and so to your role's own home on the pod's disk whatever
|
|
110
|
+
`XDG_CACHE_HOME` says. For that one, point the tool's own cache variable (`PUPPETEER_CACHE_DIR`
|
|
111
|
+
here) at a directory under one of the paths above yourself, since an unset one leaves the download
|
|
112
|
+
wherever the tool defaults, which is nowhere this runtime chose.
|
|
113
|
+
|
|
114
|
+
Where it lands is what you pay. Under one of those variables it is on the issue's volume: it
|
|
115
|
+
outlives a role's relaunch in its pod, and every role of the issue is told the same path — under
|
|
116
|
+
every one of those variables but
|
|
117
|
+
`XDG_STATE_HOME`, which is `/legion/scratch/state/<role>`, a directory named for your role
|
|
118
|
+
(`roleEnvironment`) because Oh My Pi names its browser broker's lock from the state root and an
|
|
119
|
+
abstract socket name is one name across every role of the pod, which share its network. It is not
|
|
120
|
+
private: every role of the issue can write it, as it can every path above — though it does not
|
|
121
|
+
outlive the pod: the issue's disk is the pod's own and is deleted with it, so a later pod of the
|
|
122
|
+
issue starts without it. Under
|
|
123
|
+
`$HOME` — `/home/legion`, which no volume is mounted on — it is your role's own home, an overlay
|
|
124
|
+
over the image's whose writes land on the pod's disk (an `emptyDir` every role's home and `/tmp`
|
|
125
|
+
share): one copy per role, per pod, kept across the role's relaunches in that pod and gone when
|
|
126
|
+
the pod is replaced, and charged to the pod's ephemeral-storage limit, which every role of the pod
|
|
127
|
+
shares and which is smaller than the volume. Moving exactly these writes off that
|
|
128
|
+
filesystem is what `roleEnvironment` (`packages/daemon/internal/runtime/sandbox/manifest.go`)
|
|
129
|
+
exists for, after GKE Autopilot evicted issue pods that charged their caches to it — so an install
|
|
130
|
+
under `$HOME` fails the first condition and is outside the sanctioned form, however little else it
|
|
131
|
+
changes. The three artefacts above are the whole of that line's ground. Disk is not: every cache is
|
|
132
|
+
already relocated onto the issue's volume, which has the room for them (`docs/kubernetes.md`, "The
|
|
133
|
+
tools a role's own work needs", carries the figure), and a sanctioned install is paid once per
|
|
134
|
+
issue pod, on the issue's own disk, by the issue's roles together.
|
|
135
|
+
|
|
136
|
+
**The worked case, and the one a role actually meets: the Dispatch e2e stack.** A change to
|
|
137
|
+
`packages/dispatch` is proved in the pod by driving the specs it touches, one spec a command
|
|
138
|
+
(`bun run e2e -- e2e/<one spec file>`); the whole browser suite runs in CI's `dispatch` job on the
|
|
139
|
+
pull request. One spec needs the same stack as all of them: a Postgres with pgvector. On a digest
|
|
140
|
+
carrying `legion-testpg`, use the pod's own server
|
|
141
|
+
(`skill://legion-worker/references/tools.md#the-database-your-tests-use`) and
|
|
142
|
+
unpack nothing. On a digest without it, the way
|
|
143
|
+
that stack is stood up here is Debian packages unpacked with `dpkg-deb -x` into one root on the
|
|
144
|
+
issue's volume — the five packages and the start sequence are in
|
|
145
|
+
`docs/solutions/testing/the-real-dispatch-stack-runs-inside-a-legion-pod-with-no-postgres-no-docker-and-no-root-INFRA-42.md`,
|
|
146
|
+
and
|
|
147
|
+
`docs/solutions/testing/a-browser-harness-in-an-issue-pod-lives-on-the-issue-volume-INFRA-43.md`
|
|
148
|
+
puts it on the volume, moves the caches with it, and writes it as one idempotent script the next
|
|
149
|
+
role re-runs. That passes both conditions, and the second is the one to get right: export the
|
|
150
|
+
root's `PATH` and `LD_LIBRARY_PATH` around the harness's own commands — `initdb`, `pg_ctl`, the
|
|
151
|
+
server, the Playwright run — rather than into the shell you go on working in. Handing the harness
|
|
152
|
+
to the next role is the same test passed again, and
|
|
153
|
+
`docs/solutions/legion/a-port-held-in-an-issue-pod-is-a-sibling-roles-harness-move-your-run-with-the-five-port-variables-and-your-own-database-and-never-stop-the-holder-LEGION-695.md`
|
|
154
|
+
is the illustration: it leaves the server running and records the `LD_LIBRARY_PATH`, `PATH` and
|
|
155
|
+
`DATABASE_URL` in its handoff's `trickyParts`, so what crosses to the next role is a note that
|
|
156
|
+
role sets deliberately rather than an environment it inherits. The suite shells out to `psql`,
|
|
157
|
+
and `psql` being in the image is what makes that harness work now without your extracting a
|
|
158
|
+
client for yourself at all.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sjawhar/claude-envoy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Claude Code channel plugin for Legion's Envoy bus",
|
|
6
6
|
"type": "module",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"AGENTS.md"
|
|
20
20
|
],
|
|
21
21
|
"legion": {
|
|
22
|
-
"daemonApiVersion":
|
|
22
|
+
"daemonApiVersion": 55
|
|
23
23
|
},
|
|
24
24
|
"repository": {
|
|
25
25
|
"type": "git",
|
|
@@ -33,7 +33,7 @@ report, a disclosure, and a reply to a maintainer who wrote first.
|
|
|
33
33
|
|
|
34
34
|
Hold an extremely high bar. Optimise every upstream report, patch or public post for a human
|
|
35
35
|
reader and for being received well, so each one builds the company's reputation rather than a
|
|
36
|
-
|
|
36
|
+
reputation for low-quality or careless submissions.
|
|
37
37
|
|
|
38
38
|
## The four steps
|
|
39
39
|
|
|
@@ -95,7 +95,9 @@ review batch, never a per-message "can I send this" ask. Write its entry — wha
|
|
|
95
95
|
serious; who receives it and what they do; the exact text or patch; its evidence; a non-author
|
|
96
96
|
reviewer's APPROVE under your stream's reviewer-model rule; what the operator decides; its owner
|
|
97
97
|
— into the batch, add it under the batch's Waiting list, and tell the batch owner. Your stream's
|
|
98
|
-
brief names the batch and its owner.
|
|
98
|
+
brief names the batch and its owner. Where your stream's brief names no batch, ask the operator
|
|
99
|
+
for one, or hold the entry until there is a batch; a missing batch is not a reason to send a
|
|
100
|
+
per-message ask.
|
|
99
101
|
|
|
100
102
|
## What this skill does not cover
|
|
101
103
|
|