@sjawhar/pi-legion 10.30.0 → 11.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/dist/legion.js +10 -3
- package/dist/skills/legion-architect/SKILL.md +2 -1
- package/dist/skills/legion-architect/references/acceptance.md +5 -3
- package/dist/skills/legion-architect/references/decomposition.md +7 -3
- package/dist/skills/legion-architect/references/wake-routing.md +3 -3
- package/dist/skills/legion-controller/SKILL.md +3 -1
- package/dist/skills/legion-controller/references/admission.md +2 -1
- package/dist/skills/legion-controller/references/daily-report.md +3 -0
- package/dist/skills/legion-controller/references/wakes.md +8 -1
- package/dist/skills/legion-worker/references/tools.md +22 -18
- package/package.json +2 -2
package/dist/legion.js
CHANGED
|
@@ -15900,10 +15900,15 @@ var daemonInfo = exports_external.strictObject({
|
|
|
15900
15900
|
firstBootAt: timestamp,
|
|
15901
15901
|
startedAt: timestamp
|
|
15902
15902
|
});
|
|
15903
|
+
var legionBlockedIssue = exports_external.strictObject({
|
|
15904
|
+
issue: issueKey,
|
|
15905
|
+
blockedBy: exports_external.array(issueKey)
|
|
15906
|
+
});
|
|
15903
15907
|
var legionAdmission = exports_external.strictObject({
|
|
15904
15908
|
cap: exports_external.number().int().nonnegative(),
|
|
15905
15909
|
active: exports_external.array(nonEmptyString),
|
|
15906
15910
|
waiting: exports_external.array(nonEmptyString),
|
|
15911
|
+
blocked: exports_external.array(legionBlockedIssue),
|
|
15907
15912
|
free: exports_external.number().int().nonnegative().optional()
|
|
15908
15913
|
});
|
|
15909
15914
|
var legionControllerLocator = exports_external.strictObject({
|
|
@@ -16051,7 +16056,9 @@ var LegionHandoffCompleteResponse = exports_external.strictObject({
|
|
|
16051
16056
|
note: nonEmptyString.optional()
|
|
16052
16057
|
});
|
|
16053
16058
|
var LegionWaveReleaseResponse = exports_external.strictObject({
|
|
16054
|
-
released: exports_external.array(nonEmptyString)
|
|
16059
|
+
released: exports_external.array(nonEmptyString),
|
|
16060
|
+
started: exports_external.array(issueKey),
|
|
16061
|
+
held: exports_external.array(legionBlockedIssue)
|
|
16055
16062
|
});
|
|
16056
16063
|
var LegionGrantRequest = exports_external.strictObject({
|
|
16057
16064
|
sessionId: nonEmptyString,
|
|
@@ -17039,7 +17046,7 @@ async function bootLegionClaim(host2) {
|
|
|
17039
17046
|
// package.json
|
|
17040
17047
|
var package_default = {
|
|
17041
17048
|
name: "@sjawhar/pi-legion",
|
|
17042
|
-
version: "
|
|
17049
|
+
version: "11.0.0",
|
|
17043
17050
|
license: "Apache-2.0",
|
|
17044
17051
|
type: "module",
|
|
17045
17052
|
omp: {
|
|
@@ -17051,7 +17058,7 @@ var package_default = {
|
|
|
17051
17058
|
]
|
|
17052
17059
|
},
|
|
17053
17060
|
legion: {
|
|
17054
|
-
daemonApiVersion:
|
|
17061
|
+
daemonApiVersion: 39
|
|
17055
17062
|
},
|
|
17056
17063
|
repository: {
|
|
17057
17064
|
type: "git",
|
|
@@ -32,8 +32,9 @@ You own one issue tree from its first decision to the merge that closes it. The
|
|
|
32
32
|
|
|
33
33
|
## 2. Children in flight
|
|
34
34
|
|
|
35
|
-
-
|
|
35
|
+
- Set a child's dependencies in one call naming every issue it waits on: `dispatch issue-update --issue <child> --blocked-by <a> --blocked-by <b>`, or `--blocked-by` on `dispatch issue` at creation. The call replaces the child's whole list, so read its existing links first (`dispatch read`). Release it with the rest: `legion({ op: "release_children", issues: ["LEGION-41", "LEGION-42"] })`. Legion holds a released child that waits on an issue not `done` and starts it itself at its last blocker's close. A child that builds on another repository's merged change is not held this way: release it as `skill://legion-architect/references/decomposition.md` "Across repositories" says.
|
|
36
36
|
- On each `child-closed`, read the child's handoff. Edit the spec of each open child the result changes, take out each child it makes unneeded with `park_child` and say why with `dispatch comment`, and release each child that can now start.
|
|
37
|
+
- Before you park a child for good (one made unneeded, or parked so its tree can finish), set the `--blocked-by` list of each sibling that waits on it again without it (one call with the rest of its list, or `--clear-blocked-by` when it was the only one), or park that sibling too: a parked child is stopped, not done, and its dependents stay held with no notice. A park followed by `rerun_child` keeps its dependents' links: they wait through the rerun and start when the rerun's merge closes the child.
|
|
37
38
|
- A released child runs in your tree's admission slot, never its own, and never becomes a root while your tree is live.
|
|
38
39
|
|
|
39
40
|
## 3. Children complete
|
|
@@ -136,9 +136,11 @@ the looks, what woke the last one and when the next is due).
|
|
|
136
136
|
|
|
137
137
|
When a person closes a root and it stays `done` through its linger, each child still in the
|
|
138
138
|
workflow is handed to Legion as a root of its own: `todo` with the `legion` label, its phase, gate,
|
|
139
|
-
phase records and pull request carried into a new generation, and that phase's role started.
|
|
140
|
-
child
|
|
141
|
-
|
|
139
|
+
phase records and pull request carried into a new generation, and that phase's role started. A
|
|
140
|
+
child that never started but still waits on an unfinished issue is handed over too, carrying no
|
|
141
|
+
run: it waits in the line until its blockers are done, and its architect starts as a fresh root
|
|
142
|
+
and registers its own spec. The child stays its own tree; reopening the old root does not take it
|
|
143
|
+
back. Park a child with `park_child` before then when its work should stop with the root.
|
|
142
144
|
|
|
143
145
|
If you are that child's architect:
|
|
144
146
|
|
|
@@ -19,7 +19,9 @@ Put every adopted child into the first wave and release it with
|
|
|
19
19
|
`legion({ op: "release_children", issues: ["LEGION-41", "LEGION-42"] })`. The daemon runs nothing on
|
|
20
20
|
a child until it is released. Once it is released, the daemon moves it to `todo` and, while the
|
|
21
21
|
root's design gate is open, starts its phases itself. It starts no sub-architect, and you start no
|
|
22
|
-
worker.
|
|
22
|
+
worker. A release is accepted whole whatever the `--blocked-by` links say: its answer names
|
|
23
|
+
`started` and `held` (each held child with the issues it waits on) beside `released`, and a held
|
|
24
|
+
child waits in your tree until Legion starts it.
|
|
23
25
|
|
|
24
26
|
## Creating children
|
|
25
27
|
|
|
@@ -56,8 +58,10 @@ creation, because the daemon fixes it when it records the child.
|
|
|
56
58
|
Release a child that depends on another repository's change only after that change's pull request
|
|
57
59
|
merges, because the child's work must build on the merged change. Name the merged commit in its
|
|
58
60
|
task (`gh api repos/<owner>/<repo>/pulls/<n> --jq .merge_commit_sha`); for a repository that pins the
|
|
59
|
-
other one by commit (a lock file or dependency pin), the pin is that commit.
|
|
60
|
-
|
|
61
|
+
other one by commit (a lock file or dependency pin), the pin is that commit. Do not hold such a
|
|
62
|
+
child with `--blocked-by` instead, because Legion would start it at the blocker's close, before you
|
|
63
|
+
name the merged commit. Read the other repositories with your gh's read-only access
|
|
64
|
+
(`-R owner/repo`).
|
|
61
65
|
|
|
62
66
|
## Architecture components
|
|
63
67
|
|
|
@@ -19,14 +19,14 @@ and 7 (a conflict, the hand-over, the close) is in
|
|
|
19
19
|
| Wake | Procedure |
|
|
20
20
|
| --- | --- |
|
|
21
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. |
|
|
22
|
-
| `child-status` | A child of your tree left the workflow or re-entered it; the
|
|
23
|
-
| `child-closed` | Read the child completion and remaining open children. Re-scope open work, park obsolete work with `park_child
|
|
22
|
+
| `child-status` | A child of your tree left the workflow or re-entered it; the reason names the child and its new status. `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`. What the rest of the tree does is your decision. |
|
|
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 release an appropriate next wave with `release_children`. 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). |
|
|
24
24
|
| `child-release-stalled` | Its `reason` names a child your `release_children` released while Dispatch already showed it `todo`, and that child's parent, whose release has not reached the daemon after three tries. Read the parent's record with the `legion` tool's `read_record` (`issue` the parent); 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
|
| `design-approved` | Its `version` is the approved one. A human approved the root spec document at its current version; the gate is open. Proceed to section 2 — 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. |
|
|
27
27
|
| `design-changes-requested` | Its `version` and `reason`. A human asked for changes to the root spec at `version`, for `reason`. Revise the spec as the reason asks and request approval again as "The armed gate" in `skill://legion-architect/references/design-gate.md` says; end your turn; the gate is closed. |
|
|
28
28
|
| `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 committed handoff for the finishing phase; 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, Acceptance, scope and 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` or send it back with `request_backward_move`. A `tester` notice with `verdict: "fail"` — its handoff carries `implementerProof.verdict: "rejected"`, or a failure naming the production-like proof — has gone back to the **implementer** by the daemon's table; never supply the proof 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: file a child issue in this tree to build the surface (infrastructure, tooling, or a skill), and when that child's `child-closed` arrives, tell the waiting worker to continue with `envoy_publish` to its role topic. That report is never a reason to advance the phase. |
|
|
29
|
-
| `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 that changed something outside `.legion/` — handoff-only pushes (`.legion/` paths only) never count, a push by the review App (a planner's, tester's, reviewer's or architect's) never counts, and the head after a red the tester's red tests earned (a review-App push that changed a path outside `.legion/`, however many handoff-only pushes follow it) does not count either — so after the tester's handoff-only push onto the implementer's red, the implementer's next push does count; a push the daemon cannot classify (a listener without `changed_paths`, a list the listener stopped at 100 paths or 32,768 runes of text, a push listing no commits) does. 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 and handoff 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; or file a corrective child. 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 acceptance lines the next round answers; a correctness finding is still fixed in this pull request, and only naming, duplication or wording cleanup moves to the `Fast-follow:` line. **Split**: part of the work cannot settle in this pull request. On a child, file that part as a child of this issue's parent, never of this one (a parent waits in `accepting` for every child, 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
|
|
29
|
+
| `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 that changed something outside `.legion/` — handoff-only pushes (`.legion/` paths only) never count, a push by the review App (a planner's, tester's, reviewer's or architect's) never counts, and the head after a red the tester's red tests earned (a review-App push that changed a path outside `.legion/`, however many handoff-only pushes follow it) does not count either — so after the tester's handoff-only push onto the implementer's red, the implementer's next push does count; a push the daemon cannot classify (a listener without `changed_paths`, a list the listener stopped at 100 paths or 32,768 runes of text, a push listing no commits) does. 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 and handoff 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; or file a corrective child. 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 acceptance lines the next round answers; a correctness finding is still fixed in this pull request, and only naming, duplication or wording cleanup moves to the `Fast-follow:` line. **Split**: part of the work cannot settle in this pull request. On a child, file that part as a child of this issue's parent, never of this one (a parent waits in `accepting` for every child, 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 accepts 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. |
|
|
30
30
|
| `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`, slot and workers kept, 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. |
|
|
31
31
|
| `pr-closed-unmerged` | Decide from current scope whether the work is reopened, started over (`park_child` then `rerun_child`, for a child), or ended with a reason. Delegate the repository action to the responsible phase worker and keep ownership. |
|
|
32
32
|
| `merge-refused` | GitHub refused the daemon's submission of READY's merge in a way a retry would meet again, so it submits that merge no further: ask a person with `dispatch ask` to fix the repository setting or App permission and merge at READY's head (`skill://legion-architect/references/merge-notices.md#merge-refused`). |
|
|
@@ -91,7 +91,9 @@ A free slot is no reason to take an issue: a labelled root in `todo` takes the n
|
|
|
91
91
|
`admission.waiting` without you, and a parked issue (status `backlog` or `icebox`) means "do not
|
|
92
92
|
run now". Move an issue out of `backlog` or `icebox` only on a request or a plan ruling that names
|
|
93
93
|
it, and cite that request. The free slots are `admission.cap` minus the roots in
|
|
94
|
-
`admission.active` and `admission.waiting
|
|
94
|
+
`admission.active` and the roots in `admission.waiting` that are not in `admission.blocked`, since
|
|
95
|
+
a blocked root takes no slot until its blockers are done. Every move to `todo` you make is one of
|
|
96
|
+
these:
|
|
95
97
|
|
|
96
98
|
- a root a person handed over, at triage;
|
|
97
99
|
- an issue a person asks you to start;
|
|
@@ -7,7 +7,8 @@ waits, or neither. Read its key against `admission` and its tree's root.
|
|
|
7
7
|
| What `read_state` shows | What it means | What you do |
|
|
8
8
|
|---|---|---|
|
|
9
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` | A root handed to Legion in `todo`, queued in Dispatch rank order for the next free slot | Nothing: it takes the next free slot by itself |
|
|
10
|
+
| The key is in `admission.waiting` and not in `admission.blocked` | A root handed to Legion in `todo`, queued in Dispatch rank order for the next free slot | Nothing: it takes the next free slot by itself |
|
|
11
|
+
| 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 |
|
|
11
12
|
| `phase: admitted`, in neither list, and Dispatch shows a `child_of` parent | A child: it runs or waits with its tree, the root its `child_of` chain reaches. Under a root whose `phase` is `done` it waits with it | Leave it to its tree. It runs again when its root does, or as a root of its own once it is `todo` with the `legion` label |
|
|
12
13
|
| `phase: admitted`, `status: todo`, in neither list, no parent | A root a person took out of the line by taking its label off | Nothing: the label puts it back in the line, and a person's label is their decision |
|
|
13
14
|
| `phase: done` while `status` is `in_progress`, `testing`, `needs_review` or `retro` | Someone other than Legion moved it to a running tree's status while Legion was not running it. A root that waited for a slot is taken out of the workflow at that move, since no slot would ever reach it, and the daemon sends the `unslotted` wake; a parked child stays parked. Legion starts nothing for either | Re-admit nothing yourself: `dispatch read` shows who works it now. Name it in your summary; `todo` with the `legion` label queues it again |
|
|
@@ -61,6 +61,9 @@ every issue by its key and title, every pull request by its URL.
|
|
|
61
61
|
- **Running.** Each root in `admission.active` with its `issues.<KEY>.phase`, and the roots in
|
|
62
62
|
`admission.waiting`. Name as waiting on a claim each root that this turn's "Trees waiting on a
|
|
63
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.
|
|
64
67
|
- **Free slots.** Counted as the skill's "Free slots" says; each stays free until a root in `todo`
|
|
65
68
|
with the `legion` label takes it.
|
|
66
69
|
- **Open capabilities.** Each row of `read_state`'s `capabilities` whose `status` is `open`: its
|
|
@@ -25,7 +25,7 @@ root took it. Name the free slots; take nothing.
|
|
|
25
25
|
daemon waits half a minute and folds that window's events into one wake. Take nothing: it is not
|
|
26
26
|
Legion's.
|
|
27
27
|
|
|
28
|
-
## `tick on <PROJECT>` (`{kind: "tick", openCapabilities?}`)
|
|
28
|
+
## `tick on <PROJECT>` (`{kind: "tick", openCapabilities?, blocked?}`)
|
|
29
29
|
|
|
30
30
|
The daemon's periodic wake, a minute after it starts and then every
|
|
31
31
|
`controller_wake_interval_seconds` (an hour by default), whatever the slots. Run the skill's every-turn
|
|
@@ -41,6 +41,13 @@ has no `configLine`. The next daily report names each row still open; take no ot
|
|
|
41
41
|
because the operator closes or decides a deployment row, and the operator or the issue's architect
|
|
42
42
|
fixes a live row's session.
|
|
43
43
|
|
|
44
|
+
`blocked`, when present, is `[{issue, blockedBy}, …]`: every root and child Legion is waiting to
|
|
45
|
+
start behind an unfinished Dispatch issue, each with the issues it waits on. `read_state` shows the
|
|
46
|
+
same list under `admission.blocked`, and `legion state` prints it as `blocked on Dispatch links:`.
|
|
47
|
+
The wait ends when each blocker reaches `done` (a person's close, or the blocker's own merge) or its
|
|
48
|
+
link is taken off (by a person or the tree's architect): name the waits in the day's report and
|
|
49
|
+
take nothing for them.
|
|
50
|
+
|
|
44
51
|
## `unslotted on <KEY>` (`{kind: "unslotted", reason}`)
|
|
45
52
|
|
|
46
53
|
A root that waited for a slot was moved from `todo` to the status `reason` names, by a person or by
|
|
@@ -31,7 +31,7 @@ which you have; those need no runner.
|
|
|
31
31
|
|
|
32
32
|
## Why a sixth is an image change, not an install
|
|
33
33
|
|
|
34
|
-
Your
|
|
34
|
+
Your role runs as an unprivileged user with no writable system prefix: `/usr/local/bin` and `/opt`
|
|
35
35
|
are read-only, `/home` is read-only too, and `apt-get` cannot take dpkg's lock even to download.
|
|
36
36
|
`$HOME` — `/home/legion`, the one writable directory under that read-only `/home` — is yours to
|
|
37
37
|
write.
|
|
@@ -48,7 +48,7 @@ extraction prefix under `$HOME` with `LD_LIBRARY_PATH` set, and `psql` did not
|
|
|
48
48
|
extraction prefix is on. What rules such an attempt out is not the unpacking: it is the next
|
|
49
49
|
section's test ("The test: where it lands, and what it leaves resolving"), and an extraction under
|
|
50
50
|
`$HOME` fails that test's first condition whatever you scope it to, because `/home/legion` is the
|
|
51
|
-
|
|
51
|
+
pod's disk rather than the issue's volume — the placement `roleEnvironment`
|
|
52
52
|
(`packages/daemon/internal/runtime/sandbox/manifest.go`) exists to move writes off. The sanctioned
|
|
53
53
|
extraction is the harness's: onto the issue's volume, and exported around its own commands and no
|
|
54
54
|
further (the Dispatch e2e stack, below, is the worked case). For everything the five do not cover,
|
|
@@ -68,7 +68,7 @@ three artefacts in this repository are what draw the line — not a claim about
|
|
|
68
68
|
`uv` and `uvx`, `bun` with its `bunx` link, and the Go toolchain, and the `toolchain` capability
|
|
69
69
|
row (`packages/daemon/internal/capabilities/capabilities.go`) grants them by name. A runner put
|
|
70
70
|
in the image deliberately and granted by a capability row is not a route around a withheld one.
|
|
71
|
-
- **What the pod configures for those runners to write.** A role
|
|
71
|
+
- **What the pod configures for those runners to write.** A role's environment relocates
|
|
72
72
|
`UV_CACHE_DIR` and `UV_PYTHON_INSTALL_DIR` (`podkind.go`) and `BUN_INSTALL_CACHE_DIR`, `GOCACHE`
|
|
73
73
|
and `GOMODCACHE` (`roleEnvironment`, `packages/daemon/internal/runtime/sandbox/manifest.go`)
|
|
74
74
|
onto the issue's volume, shared by the issue's roles. Legion provisions the caches a pinned run
|
|
@@ -91,8 +91,8 @@ whether through a variable the runtime sets for you there or under a path on tha
|
|
|
91
91
|
harness names itself — but outside the issue's workspace (`$LEGION_WORKSPACE`, under
|
|
92
92
|
`/legion/workspaces`) and the runtime's own directories (`/legion/repos`, `/legion/feed`); and
|
|
93
93
|
after the command exits nothing else resolves differently. The workspace is excluded because its jj
|
|
94
|
-
working copy tracks every new file under jj's `snapshot.max-new-file-size` (1 MiB
|
|
95
|
-
|
|
94
|
+
working copy tracks every new file under jj's `snapshot.max-new-file-size` (1 MiB for a role), so
|
|
95
|
+
a root written there is added to the change every role shares, and a larger new
|
|
96
96
|
file is left untracked, every jj command in the workspace then warning `Refused to snapshot some
|
|
97
97
|
files`. The one write into it that is sanctioned is a repository's own dependency install into
|
|
98
98
|
its ignored directories, below. The variables the runtime sets on the volume, where a tool's own
|
|
@@ -100,8 +100,9 @@ cache already goes, are
|
|
|
100
100
|
`XDG_CACHE_HOME` (`/legion/scratch/cache`), `XDG_DATA_HOME`, `XDG_STATE_HOME`, `TMPDIR`, `GOCACHE`,
|
|
101
101
|
`GOMODCACHE`, `BUN_INSTALL_CACHE_DIR`, `UV_CACHE_DIR` (`/legion/uv/cache`) and
|
|
102
102
|
`UV_PYTHON_INSTALL_DIR` (`/legion/uv/python/<issue>`). (`XDG_CONFIG_HOME` is set too and is not
|
|
103
|
-
one of them: it is
|
|
104
|
-
|
|
103
|
+
one of them: it is your role's own config home, a copy of the pod's made for your role on the
|
|
104
|
+
pod's disk, which no other role reads.) `$HOME` and `/tmp` are not on the volume:
|
|
105
|
+
both are your role's own, on the pod's disk.
|
|
105
106
|
|
|
106
107
|
It is the withheld capability when either condition fails — anything that writes into a
|
|
107
108
|
system prefix, and anything that leaves a toolchain, shim or binary which a later launch or
|
|
@@ -158,22 +159,25 @@ home — worth reading before an install, for the two things that follow from sh
|
|
|
158
159
|
`PLAYWRIGHT_BROWSERS_PATH` elsewhere is what costs you: that shared registry, and the lock that
|
|
159
160
|
makes a second role installing at the same time wait rather than race. The condition is for a tool
|
|
160
161
|
whose cache the runtime does not already place — Puppeteer is one, resolving its browser cache to
|
|
161
|
-
`join(homedir(), '.cache', 'puppeteer')` and so to
|
|
162
|
+
`join(homedir(), '.cache', 'puppeteer')` and so to your role's own home on the pod's disk whatever
|
|
162
163
|
`XDG_CACHE_HOME` says. For that one, point the tool's own cache variable (`PUPPETEER_CACHE_DIR`
|
|
163
164
|
here) at a directory under one of the paths above yourself, since an unset one leaves the download
|
|
164
165
|
wherever the tool defaults, which is nowhere this runtime chose.
|
|
165
166
|
|
|
166
167
|
Where it lands is what you pay. Under one of those variables it is on the issue's volume: it
|
|
167
|
-
outlives a role's relaunch in its pod, and every role of the issue
|
|
168
|
-
those variables but
|
|
169
|
-
|
|
170
|
-
because Oh My Pi names its browser broker's lock from the state root and an
|
|
171
|
-
is one name across every
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
168
|
+
outlives a role's relaunch in its pod, and every role of the issue is told the same path — under
|
|
169
|
+
every one of those variables but
|
|
170
|
+
`XDG_STATE_HOME`, which is `/legion/scratch/state/<role>`, a directory named for your role
|
|
171
|
+
(`roleEnvironment`) because Oh My Pi names its browser broker's lock from the state root and an
|
|
172
|
+
abstract socket name is one name across every role of the pod, which share its network. It is not
|
|
173
|
+
private: every role of the issue can write it, as it can every path above — though it does not
|
|
174
|
+
outlive the pod: the issue's disk is the pod's own and is deleted with it, so a later pod of the
|
|
175
|
+
issue starts without it. Under
|
|
176
|
+
`$HOME` — `/home/legion`, which no volume is mounted on — it is your role's own home, an overlay
|
|
177
|
+
over the image's whose writes land on the pod's disk (an `emptyDir` every role's home and `/tmp`
|
|
178
|
+
share): one copy per role, per pod, kept across the role's relaunches in that pod and gone when
|
|
179
|
+
the pod is replaced, and charged to the pod's ephemeral-storage limit, which every role of the pod
|
|
180
|
+
shares and which is smaller than the volume. Moving exactly these writes off that
|
|
177
181
|
filesystem is what `roleEnvironment` (`packages/daemon/internal/runtime/sandbox/manifest.go`)
|
|
178
182
|
exists for, after GKE Autopilot evicted issue pods that charged their caches to it — so an install
|
|
179
183
|
under `$HOME` fails the first condition and is outside the sanctioned form, however little else it
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sjawhar/pi-legion",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "11.0.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"omp": {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
]
|
|
13
13
|
},
|
|
14
14
|
"legion": {
|
|
15
|
-
"daemonApiVersion":
|
|
15
|
+
"daemonApiVersion": 39
|
|
16
16
|
},
|
|
17
17
|
"repository": {
|
|
18
18
|
"type": "git",
|