@sjawhar/pi-legion 10.20.0 → 10.22.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
CHANGED
|
@@ -14367,9 +14367,10 @@ function documentOwnerValidation(requireArtifact, alwaysRequireArtifact = false)
|
|
|
14367
14367
|
message: alwaysRequireArtifact ? "Exactly one of issue and project is required; artifact or ref must name the document." : "Exactly one of issue and project is required; with project, artifact names the document."
|
|
14368
14368
|
};
|
|
14369
14369
|
}
|
|
14370
|
+
var documentEditOwner = documentOwnerValidation(true);
|
|
14370
14371
|
var documentEditValidation = {
|
|
14371
14372
|
check: (value) => {
|
|
14372
|
-
if (!
|
|
14373
|
+
if (!documentEditOwner.check(value))
|
|
14373
14374
|
return false;
|
|
14374
14375
|
const input = value;
|
|
14375
14376
|
if (input.precondition === undefined)
|
|
@@ -14379,7 +14380,7 @@ var documentEditValidation = {
|
|
|
14379
14380
|
const precondition = input.precondition;
|
|
14380
14381
|
return typeof precondition.document === "string" !== Array.isArray(precondition.blocks);
|
|
14381
14382
|
},
|
|
14382
|
-
message:
|
|
14383
|
+
message: `${documentEditOwner.message} A precondition selects exactly one of document or blocks.`
|
|
14383
14384
|
};
|
|
14384
14385
|
var commentOwner = documentOwnerValidation(true);
|
|
14385
14386
|
var commentValidation = {
|
|
@@ -14518,7 +14519,7 @@ var dispatchToolSpecs = [
|
|
|
14518
14519
|
{
|
|
14519
14520
|
name: "dispatch_claim",
|
|
14520
14521
|
example: { issue: "DSP-1" },
|
|
14521
|
-
description: "Claim a Dispatch issue before you start implementing it, so no other session takes the same work, " + "and release it when you stop. Pass the issue alone to claim it, or --release to give it up. " + "Your claim records your own session and shows on every read of the issue: the dashboard header, the " + "issue list and board, dispatch read, and dispatch issues. Claiming is refused with 409 ISSUE_CLAIMED " + "when another session holds the issue and is still running; the refusal names that session, so talk to " + "it instead of working the same issue in parallel. When a human holds the claim the refusal names the " + "person, not a session: there is nothing running to message, so ask them on the issue rather than " + "taking it. 409 CLAIM_CONTENDED means the issue changed " + "hands twice while your call ran, so nothing was applied and nobody's liveness was checked: read " + "the issue and decide again. A claim whose session is no longer running may be " + "taken: the takeover is recorded on the issue and the session that lost it is told. A claim is not the " + "issue's status \u2014 claiming moves nothing,
|
|
14522
|
+
description: "Claim a Dispatch issue before you start implementing it, so no other session takes the same work, " + "and release it when you stop. Pass the issue alone to claim it, or --release to give it up. " + "Your claim records your own session and shows on every read of the issue: the dashboard header, the " + "issue list and board, dispatch read, and dispatch issues. Claiming is refused with 409 ISSUE_CLAIMED " + "when another session holds the issue and is still running; the refusal names that session, so talk to " + "it instead of working the same issue in parallel. When a human holds the claim the refusal names the " + "person, not a session: there is nothing running to message, so ask them on the issue rather than " + "taking it. 409 CLAIM_CONTENDED means the issue changed " + "hands twice while your call ran, so nothing was applied and nobody's liveness was checked: read " + "the issue and decide again. A claim whose session is no longer running may be " + "taken: the takeover is recorded on the issue and the session that lost it is told. A claim is not the " + "issue's status \u2014 claiming moves nothing, and the result names who moves the status: you, with " + "dispatch issue-update, or Legion's daemon on an issue it runs, where a status written by anyone " + "else is undone. A claim is released by its holder or any human, and by " + "any agent once the holder's session is no longer running. " + ISSUE_REFERENCE,
|
|
14522
14523
|
arguments: (z2) => ({
|
|
14523
14524
|
issue: z2.string().describe(ISSUE_REFERENCE),
|
|
14524
14525
|
release: z2.boolean().describe("Give up your claim instead of taking it; the issue's status does not change.").optional()
|
|
@@ -14695,7 +14696,7 @@ var dispatchToolSpecs = [
|
|
|
14695
14696
|
arguments: (z2) => ({
|
|
14696
14697
|
issue: z2.string().describe(ISSUE_REFERENCE).optional(),
|
|
14697
14698
|
project: z2.string().describe("Project key owning the document.").optional(),
|
|
14698
|
-
artifact: z2.string().describe("
|
|
14699
|
+
artifact: z2.string().describe("Optional project document artifact id, slug, or filename; primary document by default for an issue.").optional(),
|
|
14699
14700
|
ref: z2.string().describe("Optional dispatch:// issue or document reference.").optional(),
|
|
14700
14701
|
ops: z2.array(z2.object({
|
|
14701
14702
|
op: z2.enum(DOC_EDIT_OPS).describe("Edit operation."),
|
|
@@ -15725,6 +15726,7 @@ var legionPullRequestView = exports_external.strictObject({
|
|
|
15725
15726
|
repo: nonEmptyString,
|
|
15726
15727
|
number: exports_external.number().int().positive(),
|
|
15727
15728
|
head: nonEmptyString,
|
|
15729
|
+
state: exports_external.enum(["open", "merged", "closed"]),
|
|
15728
15730
|
checksVerdict: nonEmptyString.optional(),
|
|
15729
15731
|
reviewDecision: nonEmptyString.optional(),
|
|
15730
15732
|
fixAttempts: exports_external.number().int().nonnegative()
|
|
@@ -16935,7 +16937,7 @@ async function bootLegionClaim(host2) {
|
|
|
16935
16937
|
// package.json
|
|
16936
16938
|
var package_default = {
|
|
16937
16939
|
name: "@sjawhar/pi-legion",
|
|
16938
|
-
version: "10.
|
|
16940
|
+
version: "10.22.0",
|
|
16939
16941
|
license: "Apache-2.0",
|
|
16940
16942
|
type: "module",
|
|
16941
16943
|
omp: {
|
|
@@ -16947,7 +16949,7 @@ var package_default = {
|
|
|
16947
16949
|
]
|
|
16948
16950
|
},
|
|
16949
16951
|
legion: {
|
|
16950
|
-
daemonApiVersion:
|
|
16952
|
+
daemonApiVersion: 36
|
|
16951
16953
|
},
|
|
16952
16954
|
repository: {
|
|
16953
16955
|
type: "git",
|
|
@@ -17579,7 +17581,7 @@ function createLegionTool(deps) {
|
|
|
17579
17581
|
label: "legion",
|
|
17580
17582
|
description: legionToolDescription({
|
|
17581
17583
|
envoyPublish: "envoy_publish",
|
|
17582
|
-
after: PHASE_OP_SENTENCES.requestBackwardMove + "sign_off is the architect's acceptance of an issue in accepting, moving it to retro; the daemon refuses it in every " + "other phase (SIGNOFF_OUTSIDE_ACCEPTING). " + "The controller's read_state returns the daemon's whole state and set_status moves an issue to todo, backlog or icebox. "
|
|
17584
|
+
after: PHASE_OP_SENTENCES.requestBackwardMove + "sign_off is the architect's acceptance of an issue in accepting, moving it to retro; the daemon refuses it in every " + "other phase (SIGNOFF_OUTSIDE_ACCEPTING) and refuses it SIGNOFF_CHILD_IN_WORKFLOW while any issue beneath it is still in " + "the workflow, naming each of them: accept once the last of them is done, or take one out with park_child. " + "The controller's read_state returns the daemon's whole state and set_status moves an issue to todo, backlog or icebox. "
|
|
17583
17585
|
}),
|
|
17584
17586
|
defaultInactive: true,
|
|
17585
17587
|
parameters: toolSchema(pi),
|
|
@@ -250,11 +250,12 @@ stays out of the workflow.
|
|
|
250
250
|
## 3. Children complete
|
|
251
251
|
|
|
252
252
|
No notice marks the last child's close as the end-game: each closure arrives as its own
|
|
253
|
-
`child-closed
|
|
253
|
+
`child-closed`. Today's daemon does not order the root's
|
|
254
254
|
own phases after its children: when the design gate opens it starts every admitted issue of the
|
|
255
|
-
tree, the root included, so the root's tester and reviewer
|
|
256
|
-
before any child merges;
|
|
257
|
-
|
|
255
|
+
tree, the root included, so the root's tester and reviewer run and the root reaches `accepting`
|
|
256
|
+
before any child merges; the daemon holds it there — it refuses a `sign_off` that runs ahead of
|
|
257
|
+
the issues beneath it (section 6), and a merged root's close waits for the tree (section 7).
|
|
258
|
+
To get parent integration evidence against the repository's default branch your addressing line names
|
|
258
259
|
after the last child merges, file the parent's integration check as a final child whose acceptance
|
|
259
260
|
is every parent criterion proven on that default branch, release it with `release_children` only
|
|
260
261
|
once every other child has closed, and accept the root (section 6) only after it closes. This holds
|
|
@@ -262,23 +263,21 @@ until the redesign's
|
|
|
262
263
|
integrating phase ships (dispatch://LEGION-223). When that check's tester fails, the daemon sends
|
|
263
264
|
the child back to its implementer; a failure whose fix belongs in other work becomes a new
|
|
264
265
|
corrective child wave you create and release, and the tree returns to children-in-flight. Do not
|
|
265
|
-
downgrade the parent criterion or silently carry the failure forward.
|
|
266
|
-
through its linger with a child still in the workflow
|
|
267
|
-
root of its own (`todo` with
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
`held
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
says. The child stays its own tree after hand-over; reopening the old root later does not take it
|
|
281
|
-
back. Park a child first when its work should stop with the root.
|
|
266
|
+
downgrade the parent criterion or silently carry the failure forward. A root that stays `done`
|
|
267
|
+
through its linger with a child still in the workflow — a person's close now (section 7) — has
|
|
268
|
+
that child handed to Legion as a root of its own (`todo` with
|
|
269
|
+
the `legion` label), its phase, gate, phase records and pull request carried into a new generation
|
|
270
|
+
and that phase's role started. Its architect's catch-up says `handed_over` and `carried`: it
|
|
271
|
+
resumes from the phase and never registers a spec or asks for approval again. Without a gate to
|
|
272
|
+
carry the catch-up says `handed_over` alone; its workers already run their phases, so it
|
|
273
|
+
decomposes nothing and reads the root's phase from the catch-up (for a root `held`, the phase its
|
|
274
|
+
`held` notice names). Before `awaiting_merge` it registers its spec, the gate its READY waits for,
|
|
275
|
+
and starts nothing on that gate's `design-approved`; a child carried in `accepting` is yours to
|
|
276
|
+
accept (section 6). In `awaiting_merge` the READY is through, so it registers none. `awaiting_merge` can
|
|
277
|
+
move back — a red or conflicting head returns the root to `implementing` and its next READY reads
|
|
278
|
+
the gate again — so a later `ready-refused` is when it registers, as that row says. The child
|
|
279
|
+
stays its own tree; reopening the old root does not take it back. Park a child first when its
|
|
280
|
+
work should stop with the root.
|
|
282
281
|
|
|
283
282
|
## 4. Integration verification
|
|
284
283
|
|
|
@@ -314,12 +313,15 @@ The reviewer's approval of the head moves the issue to `accepting` (its Dispatch
|
|
|
314
313
|
notice with an `APPROVED` review is the cue. Read the evidence against every acceptance line of the
|
|
315
314
|
spec and the plan: the tester's handoff (`.legion/<issue>/test.json` at the approved head), the PR
|
|
316
315
|
body's `E2E (implementer)` and `E2E (tester)` lines, the reviewer's approval of that head by SHA
|
|
317
|
-
(`gh api repos/<o>/<r>/pulls/<n>/reviews`)
|
|
318
|
-
|
|
319
|
-
|
|
316
|
+
(`gh api repos/<o>/<r>/pulls/<n>/reviews`). Accept only when scope is fully met, integration
|
|
317
|
+
evidence is current, corrective work is complete, review is clean, and no necessary work was
|
|
318
|
+
silently deferred. Post the acceptance as a
|
|
320
319
|
`dispatch comment` explicit about that evidence, then accept with `sign_off`, which runs only in
|
|
321
320
|
`accepting` (refused `SIGNOFF_OUTSIDE_ACCEPTING` elsewhere) and moves the issue to `retro` with the
|
|
322
|
-
status `retro
|
|
321
|
+
status `retro`. The daemon keeps that order: `sign_off` is refused 409
|
|
322
|
+
`SIGNOFF_CHILD_IN_WORKFLOW` while any issue beneath this one is still in the workflow, naming each
|
|
323
|
+
of them; accept once the last of them is done, or take one out with `park_child`. Only what is
|
|
324
|
+
*beneath* counts: a child with nothing beneath it is accepted while its own root runs.
|
|
323
325
|
|
|
324
326
|
```text
|
|
325
327
|
dispatch comment --issue LEGION-40 --body-file - <<'EOF'
|
|
@@ -334,8 +336,7 @@ fails; the daemon starts that phase's role with the reason in its task and count
|
|
|
334
336
|
as a reviewer's request for changes does. A `handoff_complete` from you in `accepting` is refused
|
|
335
337
|
(`ACCEPTANCE_IS_SIGN_OFF`). CI red at the head in `accepting` returns the issue to `implementing`
|
|
336
338
|
by the daemon's table, as in review. No `Production:` line is asked here: the production check is
|
|
337
|
-
the implementer's after the
|
|
338
|
-
yours.
|
|
339
|
+
the implementer's, after the close (section 7).
|
|
339
340
|
|
|
340
341
|
The daemon keeps this order from its fixed table; you start none of its steps:
|
|
341
342
|
|
|
@@ -368,9 +369,8 @@ The daemon keeps this order from its fixed table; you start none of its steps:
|
|
|
368
369
|
back to `reviewing`, the move's reason naming the code and what it found, so the reviewer runs a
|
|
369
370
|
new round. A READY refused because the head still carries `.legion/<issue>/` goes back to
|
|
370
371
|
`retro`, so the implementer's retro removes it.
|
|
371
|
-
5. the merge lands under the repository's rules and closes the issue
|
|
372
|
-
|
|
373
|
-
nothing for it, and a defect it finds is a `dispatch ask`, never an issue.
|
|
372
|
+
5. the merge lands under the repository's rules and closes the issue — a root's close only once
|
|
373
|
+
the rest of its tree has left the workflow (section 7).
|
|
374
374
|
|
|
375
375
|
What returns the tree to review: a changed diff — a commit above the approved head that
|
|
376
376
|
touches anything outside `docs/solutions/` and the issue's `.legion/<issue>/`, or a
|
|
@@ -409,22 +409,22 @@ role topic, or its review body); the reviewer resolves none.
|
|
|
409
409
|
|
|
410
410
|
The merge closes the issue: when GitHub reports the pull request merged while the issue is in
|
|
411
411
|
`awaiting_merge` (a merge before that closes it the moment it gets there), the daemon writes
|
|
412
|
-
`done`. A
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
412
|
+
`done`. A root is the exception: a merge landing while another issue of its tree is still in the
|
|
413
|
+
workflow is recorded and the root stays in `awaiting_merge` — nothing suspended, no linger, slot
|
|
414
|
+
and workers kept — until the last of them is done or parked, when it closes. One `pr-merged`
|
|
415
|
+
notice names what it waits for and none follows as they depart, so read the pull request on
|
|
416
|
+
GitHub. A child's close reaches you as `child-closed`: the daemon retires five of its six claims
|
|
417
|
+
and suspends its implementer, session kept and Sandbox `Suspended` with its volume, since it still
|
|
418
|
+
owes the production check handed to it at the merge — work after the close that holds no pod while
|
|
419
|
+
it waits for the deploy (`skill://legion-worker/references/merge-gate.md`, *The production check
|
|
420
|
+
after the close*). A root's close writes `done`, frees the tree's admission slot and lingers the
|
|
421
|
+
tree for `linger_hours` with every member suspended; a child still in the workflow at the linger's
|
|
422
|
+
end is handed over (section 3), and the tree's close waits for a member's open check.
|
|
423
|
+
|
|
424
|
+
Nothing of the check is yours. Its record lives on the pull request alone, since Dispatch refuses
|
|
425
|
+
a write on a closed issue; a defect it finds is a `dispatch ask`, never an issue; and
|
|
424
426
|
`read_record` shows a closed issue's open check (`check`: the pull request, the merge commit, the
|
|
425
|
-
looks, what woke the last one and when the next is due).
|
|
426
|
-
every child is `done` (section 3): accepted earlier it would merge before its children, the state
|
|
427
|
-
dispatch://LEGION-703 has the daemon rule out.
|
|
427
|
+
looks, what woke the last one and when the next is due).
|
|
428
428
|
|
|
429
429
|
## Wake routing
|
|
430
430
|
|
|
@@ -446,7 +446,7 @@ active phase worker.
|
|
|
446
446
|
| `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 section 1 says; stay parked; the gate is closed. |
|
|
447
447
|
| `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 section 1's full condition: 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 section 1 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. |
|
|
448
448
|
| `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, 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 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, as section 6 does for a conflict there. 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 -`, released with `release_children` once this issue closes. 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 (section 1), 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` (section 6) 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. |
|
|
449
|
-
| `pr-merged` |
|
|
449
|
+
| `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 (section 7). Either way you start nothing and no further notice follows. |
|
|
450
450
|
| `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. |
|
|
451
451
|
| `merge-refused` | Its `reason` names the pull request, READY's head, what the daemon asked GitHub to do (merge, enqueue, or enable auto-merge for it), and GitHub's refusal, which a retry would meet again (squash merges disabled, a permission the implement App lacks). The daemon has stopped submitting that merge; the issue stays in `awaiting_merge`. The cause is a repository setting or an App permission, a person's to change: ask one with `dispatch ask`, naming the refusal and the pull request at READY's head, which that person merges under the repository's rules once it is fixed. The merge closes the issue. Never bypass a rule. |
|
|
452
452
|
| `merge-stalled` | Its `reason` names the pull request, the state and head GitHub shows (closed, or another head), since when GitHub has shown it, and READY's head, which the record still awaits. GitHub's last change to the pull request is more than 10 minutes old and the record never heard of it, so its webhook never reached the daemon: the daemon has stopped submitting that merge, and the issue stays in `awaiting_merge`. Read the pull request on GitHub. Open at READY's head, GitHub has caught up: a person merges it under the repository's rules, and the merge closes the issue. Closed, or at another head the daemon never recorded: ask a person with `dispatch ask`, naming the pull request, what GitHub shows, and READY's head. Never bypass a rule. |
|
|
@@ -463,7 +463,9 @@ addressing` line names: a child that looks independent of the tree (the controll
|
|
|
463
463
|
whether to re-file it as a root of its own, and does so only on their yes), capacity, and a
|
|
464
464
|
cross-tree conflict. A problem outside the tree is a `dispatch ask` you open, never an issue you
|
|
465
465
|
file (`skill://legion-worker/references/findings.md`). Handle everything else in the
|
|
466
|
-
tree. A
|
|
466
|
+
tree. A tool a worker cannot get is yours to schedule (`skill://legion-worker/references/tools.md`):
|
|
467
|
+
the image change is a pull request against this repository, never something a worker conjures.
|
|
468
|
+
A product, scope, or design decision that needs the human, yours or one a worker escalated,
|
|
467
469
|
is a decision block you write (section 1 says what one does to the root spec's gate). A standalone
|
|
468
470
|
human to-do may use `dispatch ask`; workers may reach the human directly with it the same way. Do not
|
|
469
471
|
create a wait loop for any wake source.
|
|
@@ -28,6 +28,7 @@ with the `read` tool: a read by filesystem path stops at 300 lines.
|
|
|
28
28
|
| You review or approve, build the READY packet, or check the merge in production | `skill://legion-worker/references/merge-gate.md` |
|
|
29
29
|
| The issue renames a repository, package, or URL across the codebase | `skill://legion-worker/references/systematic-rename.md` |
|
|
30
30
|
| The issue deletes code, a command, or documentation | `skill://legion-worker/references/cleanup-deletion.md` |
|
|
31
|
+
| Your phase needs a tool — a client, a linter, a CLI — that the image may not carry | `skill://legion-worker/references/tools.md` |
|
|
31
32
|
| You find a problem outside your issue, or the production check fails | `skill://legion-worker/references/findings.md` |
|
|
32
33
|
|
|
33
34
|
## Identity, scope, and role
|
|
@@ -136,12 +137,11 @@ committed predecessor handoffs in lifecycle order, with the `read` tool from
|
|
|
136
137
|
4. `review.json`
|
|
137
138
|
|
|
138
139
|
Read only files that precede the assigned phase. Each was checked against its phase's rules when
|
|
139
|
-
its phase completed (the completion gate below): fields the phase does not declare passed
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
transcript, the committed file wins: it is the copy that survived.
|
|
140
|
+
its phase completed (the completion gate below): fields the phase does not declare passed untouched
|
|
141
|
+
and reach the next worker. Write the phase-specific fields the next phase and the architect need,
|
|
142
|
+
consistent with what predecessor phases already wrote. The durable copy lives in
|
|
143
|
+
`$LEGION_WORKSPACE/.legion/<issue>/<phase>.json`. If a committed handoff conflicts with memory or a
|
|
144
|
+
prior transcript, the committed file wins: it is the copy that survived.
|
|
145
145
|
|
|
146
146
|
If your task, prompt, or a message since your last turn begins `Your workspace was recreated…`, read
|
|
147
147
|
your phase's committed handoff and any `.legion/<issue>/workspace-recovered.json`, and reconcile
|
|
@@ -218,9 +218,12 @@ reviewing until you finish.
|
|
|
218
218
|
|
|
219
219
|
- **The merge closes the issue; the production check is the implementer's work after the close.**
|
|
220
220
|
When the pull request merges, the daemon writes `done` and, in the implementer's session, still
|
|
221
|
-
live, hands it the check task.
|
|
222
|
-
|
|
223
|
-
|
|
221
|
+
live, hands it the check task. On a root whose tree still runs the close, and so the check task,
|
|
222
|
+
comes later: the merge is recorded, the root waits in `awaiting_merge` until the last other
|
|
223
|
+
issue of its tree is done or parked, and the check opens with that close. Its first line is
|
|
224
|
+
`Production check of <KEY>: <title>.`, and it names the repository, the pull request, the merge
|
|
225
|
+
commit, the look's number, what woke the look (`the merge`, for the first) and whether it is the
|
|
226
|
+
last. A resumed session tells the check task
|
|
224
227
|
from its last phase task by that first line. The check carries no phase and no handoff: a look
|
|
225
228
|
ends with `handoff_complete` carrying a `verdict` and a `summary`, no commit, and the daemon
|
|
226
229
|
refuses a report on an issue with no open check (`CHECK_NOT_OPEN`), on a look already ended
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
# Getting the tool your work needs
|
|
2
|
+
|
|
3
|
+
Part of `skill://legion-worker`. Read it the moment your phase needs a tool the worker image may
|
|
4
|
+
not carry — a database client, a linter, a one-off CLI — before you install, download, or vendor
|
|
5
|
+
anything. Every path it cites is in sjawhar/legion.
|
|
6
|
+
|
|
7
|
+
## The five clients the image already carries
|
|
8
|
+
|
|
9
|
+
Check this list first: the worker image carries these five from the change that added them onward,
|
|
10
|
+
and they are on a role's `PATH` once its deployment pins a digest at or after that change — a role
|
|
11
|
+
still on an older digest finds none of them. Run `command -v psql redis-cli sqlite3 nc dig` to see
|
|
12
|
+
which you have; those need no runner.
|
|
13
|
+
|
|
14
|
+
- `psql` — the Dispatch browser suite (`bun run e2e`) resets its database for every spec through
|
|
15
|
+
`packages/dispatch/e2e/psql.ts`, which `seed.ts` calls, and the local stack (`bun run
|
|
16
|
+
dispatch:local`, `packages/dispatch/e2e/local-instance.ts`) refuses to start without it on
|
|
17
|
+
`PATH` — so a role whose change owes that suite can run it.
|
|
18
|
+
- `redis-cli` — no repository of the project speaks Redis today; it is carried anyway, at about a
|
|
19
|
+
megabyte, because a second image roll costs more than a megabyte.
|
|
20
|
+
- `sqlite3` — a repository's own SQLite fixture or database has no other way to open from a shell,
|
|
21
|
+
and Oh My Pi's own profile databases (`~/.omp/profiles/legion/agent/agent.db`, `models.db`,
|
|
22
|
+
`history.db`) are SQLite too. Your session is not one of them: it is `results.jsonl`,
|
|
23
|
+
`state.json` and `title` under `XDG_STATE_HOME`.
|
|
24
|
+
- `nc` — the reachability check you would otherwise write as an inline Python loop. The name comes
|
|
25
|
+
from the alternatives system, so ask for `nc`, never `nc.openbsd`.
|
|
26
|
+
- `dig` — how a role whose connection to a service name fails tells "the name does not resolve"
|
|
27
|
+
from "the packets are dropped".
|
|
28
|
+
|
|
29
|
+
## Why a sixth is an image change, not an install
|
|
30
|
+
|
|
31
|
+
Your container is an unprivileged user with no writable system prefix: `/usr/local/bin` and `/opt`
|
|
32
|
+
are read-only, `/home` is read-only too, and `apt-get` cannot take dpkg's lock even to download.
|
|
33
|
+
`$HOME` — `/home/legion`, the one writable directory under that read-only `/home` — is yours to
|
|
34
|
+
write.
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
$ apt-get install -d -y postgresql-client
|
|
38
|
+
E: Could not open lock file /var/lib/dpkg/lock-frontend - open (13: Permission denied)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
So that writable `$HOME`, and the issue's disk, are where an install for yourself would go, and on
|
|
42
|
+
the mechanics the answer is not a flat no: `redis-cli`, `sqlite3` and `dig` did run from an
|
|
43
|
+
extraction prefix under `$HOME` with `LD_LIBRARY_PATH` set, and `psql` did not — Debian's
|
|
44
|
+
`/usr/bin/psql` is a Perl wrapper that loads `PgCommon.pm` through Perl's own search path, which no
|
|
45
|
+
extraction prefix is on. What rules such an attempt out is not the unpacking: it is the next
|
|
46
|
+
section's test ("The test: where it lands, and what it leaves resolving"), and an extraction under
|
|
47
|
+
`$HOME` fails that test's first condition whatever you scope it to, because `/home/legion` is the
|
|
48
|
+
container's own filesystem rather than the issue's volume — the placement `roleEnvironment`
|
|
49
|
+
(`packages/daemon/internal/runtime/sandbox/manifest.go`) exists to move writes off. The sanctioned
|
|
50
|
+
extraction is the harness's: onto the issue's volume, and exported around its own commands and no
|
|
51
|
+
further (the Dispatch e2e stack, below, is the worked case). For everything the five do not cover,
|
|
52
|
+
the one documented step is a pinned run through one of the three package runners the image already
|
|
53
|
+
carries — plus `uv run --with` when what you need is a library rather than a command — and a tool
|
|
54
|
+
no runner reaches is an image change ("When no runner reaches it", below).
|
|
55
|
+
|
|
56
|
+
## What the withheld capability is, and what it is not
|
|
57
|
+
|
|
58
|
+
The withheld row reads `installs or configures its own dependencies in the pod: Legion owns its
|
|
59
|
+
dependencies` (`packages/daemon/internal/capabilities/capabilities.go`, ruling
|
|
60
|
+
dispatch://LEGION-200). Read as a sentence on its own, that wording would also forbid `uvx`,
|
|
61
|
+
`bunx --package` and `go run`, each of which fetches a package and writes a cache. It does not, and
|
|
62
|
+
three artefacts in this repository are what draw the line — not a claim about what the ruling meant:
|
|
63
|
+
|
|
64
|
+
- **What the image ships for a role to use.** `packages/daemon/docker/worker.Dockerfile` installs
|
|
65
|
+
`uv` and `uvx`, `bun` with its `bunx` link, and the Go toolchain, and the `toolchain` capability
|
|
66
|
+
row (`packages/daemon/internal/capabilities/capabilities.go`) grants them by name. A runner put
|
|
67
|
+
in the image deliberately and granted by a capability row is not a route around a withheld one.
|
|
68
|
+
- **What the pod configures for those runners to write.** A role container's environment relocates
|
|
69
|
+
`UV_CACHE_DIR` and `UV_PYTHON_INSTALL_DIR` (`podkind.go`) and `BUN_INSTALL_CACHE_DIR`, `GOCACHE`
|
|
70
|
+
and `GOMODCACHE` (`roleEnvironment`, `packages/daemon/internal/runtime/sandbox/manifest.go`)
|
|
71
|
+
onto the issue's volume, shared by the issue's roles. Legion provisions the caches a pinned run
|
|
72
|
+
fills; filling them is what they are for.
|
|
73
|
+
- **What Legion itself does at launch.** `installBlock`
|
|
74
|
+
(`packages/daemon/internal/runtime/sandbox/plugins.go`) renders a `bun add` under
|
|
75
|
+
`/usr/bin/flock` into that shared bun install cache on every installing launch. Fetching a pinned
|
|
76
|
+
package at run time is something Legion does, not something it withholds.
|
|
77
|
+
|
|
78
|
+
So the withheld row is about a dependency **the pod carries**: something that outlives the command
|
|
79
|
+
that created it, and that a later launch or another role's process then resolves *without asking
|
|
80
|
+
for it*.
|
|
81
|
+
|
|
82
|
+
### The test: where it lands, and what it leaves resolving
|
|
83
|
+
|
|
84
|
+
An install of your own — an interpreter, a browser, a second toolchain, an extraction prefix — is
|
|
85
|
+
on the sanctioned side when both of these hold: what it writes lands on the issue's own volume,
|
|
86
|
+
under `/legion`, which outlives the pod and is shared by the issue's roles — whether through a
|
|
87
|
+
variable the runtime sets for you there or under a path on that volume your harness names
|
|
88
|
+
itself — but outside the issue's workspace (`$LEGION_WORKSPACE`, under `/legion/workspaces`) and
|
|
89
|
+
the runtime's own directories (`/legion/repos`, `/legion/feed`, `/legion/sessions`); and after
|
|
90
|
+
the command exits nothing else resolves differently. The workspace is excluded because its jj
|
|
91
|
+
working copy tracks every new file, so a root written there is added to the change every role
|
|
92
|
+
shares; the one write into it that is sanctioned is a repository's own dependency install into
|
|
93
|
+
its ignored directories, below. The variables the runtime sets on the volume, where a tool's own
|
|
94
|
+
cache already goes, are
|
|
95
|
+
`XDG_CACHE_HOME` (`/legion/scratch/cache`), `XDG_DATA_HOME`, `XDG_STATE_HOME`, `TMPDIR`, `GOCACHE`,
|
|
96
|
+
`GOMODCACHE`, `BUN_INSTALL_CACHE_DIR`, `UV_CACHE_DIR` (`/legion/uv/cache`) and
|
|
97
|
+
`UV_PYTHON_INSTALL_DIR` (`/legion/uv/python/<issue>`). (`XDG_CONFIG_HOME` is set too and is not
|
|
98
|
+
one of them: it is the pod's shared in-memory volume.) `$HOME` and `/tmp` are not on the volume:
|
|
99
|
+
both are the container's own filesystem.
|
|
100
|
+
|
|
101
|
+
It is the withheld capability when either condition fails — anything that writes into a
|
|
102
|
+
system prefix, and anything that leaves a toolchain, shim or binary which a later launch or
|
|
103
|
+
another role's process will resolve without asking for it. Between two installs that both land in
|
|
104
|
+
the right place, the test is resolution, not disk, and it reads as scope: a second toolchain
|
|
105
|
+
exported for the commands of your own run — by absolute path, or on a `PATH` your harness's own
|
|
106
|
+
script sets around its own invocations — is sanctioned; the same toolchain left where a later
|
|
107
|
+
launch, another role or a subagent picks it up without naming it is operator setup, whoever typed
|
|
108
|
+
the command.
|
|
109
|
+
|
|
110
|
+
What each shape leaves resolving:
|
|
111
|
+
|
|
112
|
+
- A pinned runner run: nothing. `uvx --from httpie@3.2.4 http --version` leaves a cache entry
|
|
113
|
+
addressed by that coordinate and that version, which the next command reaches only by writing the
|
|
114
|
+
same pin; no `PATH`, no interpreter and no library search of any other process changes. That is
|
|
115
|
+
the sanctioned side, and it is the side the forms below are on.
|
|
116
|
+
- A repository's own dependency set: nothing either, which is why the caches are provisioned at
|
|
117
|
+
all. `uv sync` or `bun install` in your workspace materialises what that project already
|
|
118
|
+
declares, into that project's own directory, which a process reaches by asking for that project
|
|
119
|
+
and no other way — and the runtime puts `UV_PYTHON_INSTALL_DIR` and the workspace itself on the
|
|
120
|
+
issue's volume exactly so a later pod reuses it.
|
|
121
|
+
- An extraction prefix: whatever you leave resolving through it, which is yours to decide. Unpack
|
|
122
|
+
Debian packages under `$HOME`, export `LD_LIBRARY_PATH` in the shell you go on working in, and
|
|
123
|
+
you change what every later command in that shell — and every subagent that inherits the
|
|
124
|
+
environment — resolves `psql` or its libraries to, asked for or not, on a prefix nothing records,
|
|
125
|
+
pins or rebuilds: that fails both conditions, and it is the withheld capability. The harness
|
|
126
|
+
below is the shape that passes. The opposite shape is in the tree too, and superseded: telling
|
|
127
|
+
every subagent to prepend a directory to `PATH` because a subagent's shell starts without it
|
|
128
|
+
hands over an environment to processes that never named it. That is the difference between
|
|
129
|
+
standing a harness up and getting yourself a CLI.
|
|
130
|
+
|
|
131
|
+
The test is about what a command leaves for other processes, never about getting the same bytes
|
|
132
|
+
twice — because a pinned line does not promise the same bytes. What a pin binds is the named
|
|
133
|
+
distribution at that version; its dependency closure is resolved when the command runs. With
|
|
134
|
+
`UV_CACHE_DIR` emptied, `uvx -v --from httpie@3.2.4 http --version` logs `Selecting:
|
|
135
|
+
requests==2.34.2 [compatible]` and resolves `urllib3` and `certifi` from ranges, where the warm run
|
|
136
|
+
prints `3.2.4` at once off the cache that first run filled. When your work needs the closure pinned
|
|
137
|
+
too — a comparison that has to hold across days or across pods — that is a project's own lockfile
|
|
138
|
+
with `uv run` or `bun install` in a workspace, not a one-off runner line.
|
|
139
|
+
|
|
140
|
+
A pinned run whose tool then installs something of its own — an interpreter, a browser, a second
|
|
141
|
+
toolchain — is where the first condition does the work. `uv run --with pyyaml==6.0.2 --no-project`
|
|
142
|
+
passes both: the wheel lands in `UV_CACHE_DIR`, under `wheels-v6/pypi/pyyaml/6.0.2-…`, and
|
|
143
|
+
`UV_PYTHON_INSTALL_DIR` is never created, because the image's own `python3` served the run. The
|
|
144
|
+
interpreter is the other run: a project whose `.python-version` names an interpreter uv must fetch
|
|
145
|
+
puts about 70 MB under `UV_PYTHON_INSTALL_DIR`, where the issue's next pod finds it, while the
|
|
146
|
+
cache holds no wheel. A browser download passes too, and for Playwright — the browser driver this
|
|
147
|
+
repository's own Dispatch suite installs (`playwright install`, `packages/dispatch`'s
|
|
148
|
+
`e2e:install`) — it passes as it stands, with nothing pointed anywhere: playwright-core resolves
|
|
149
|
+
its browser registry under `XDG_CACHE_HOME`, which the runtime has already put on the issue's
|
|
150
|
+
volume, so the issue's roles share one registry under the exclusive install lock that
|
|
151
|
+
`docs/kubernetes.md` writes down in "Anatomy of a Sandbox pod", the paragraph on sharing the cache
|
|
152
|
+
home — worth reading before an install, for the two things that follow from sharing. Pointing
|
|
153
|
+
`PLAYWRIGHT_BROWSERS_PATH` elsewhere is what costs you: that shared registry, and the lock that
|
|
154
|
+
makes a second role installing at the same time wait rather than race. The condition is for a tool
|
|
155
|
+
whose cache the runtime does not already place — Puppeteer is one, resolving its browser cache to
|
|
156
|
+
`join(homedir(), '.cache', 'puppeteer')` and so to the container's own filesystem whatever
|
|
157
|
+
`XDG_CACHE_HOME` says. For that one, point the tool's own cache variable (`PUPPETEER_CACHE_DIR`
|
|
158
|
+
here) at a directory under one of the paths above yourself, since an unset one leaves the download
|
|
159
|
+
wherever the tool defaults, which is nowhere this runtime chose.
|
|
160
|
+
|
|
161
|
+
Where it lands is what you pay. Under one of those variables it is on the issue's volume: it
|
|
162
|
+
outlives the pod, and every role of the issue shares it — under every one of those variables but
|
|
163
|
+
`XDG_STATE_HOME`, which is `/legion/scratch/state/<role>`, "the one directory of the issue's
|
|
164
|
+
scratch that is the role's own rather than the issue's" (`roleEnvironment`), because Oh My Pi
|
|
165
|
+
names its browser broker's lock from the state root and an abstract socket name is one name across
|
|
166
|
+
every container of the pod — and a later pod of the issue reuses it. Under `$HOME` —
|
|
167
|
+
`/home/legion`, which no volume is mounted on — it is the container's own filesystem: one copy per
|
|
168
|
+
role, per pod, gone when the role relaunches, and charged to the container's ephemeral-storage
|
|
169
|
+
limit, which is per role and smaller than the volume. Moving exactly these writes off that
|
|
170
|
+
filesystem is what `roleEnvironment` (`packages/daemon/internal/runtime/sandbox/manifest.go`)
|
|
171
|
+
exists for, after GKE Autopilot evicted issue pods that charged their caches to it — so an install
|
|
172
|
+
under `$HOME` fails the first condition and is outside the sanctioned form, however little else it
|
|
173
|
+
changes. The three artefacts above are the whole of that line's ground. Disk is not: every cache is
|
|
174
|
+
already relocated onto the issue's volume, which has the room for them (`docs/kubernetes.md`, "The
|
|
175
|
+
tools a role's own work needs", carries the figure), and a sanctioned install is paid once per
|
|
176
|
+
issue, on the issue's own disk, by the issue's roles together.
|
|
177
|
+
|
|
178
|
+
**The worked case, and the one a role actually meets: the Dispatch e2e stack.** A change to
|
|
179
|
+
`packages/dispatch` is proved by driving its browser suite, which needs a Postgres with pgvector;
|
|
180
|
+
the image carries a `psql` and no server and no pgvector, and no runner reaches either. The way
|
|
181
|
+
that stack is stood up here is Debian packages unpacked with `dpkg-deb -x` into one root on the
|
|
182
|
+
issue's volume — the five packages and the start sequence are in
|
|
183
|
+
`docs/solutions/testing/the-real-dispatch-stack-runs-inside-a-legion-pod-with-no-postgres-no-docker-and-no-root-INFRA-42.md`,
|
|
184
|
+
and
|
|
185
|
+
`docs/solutions/testing/a-browser-harness-in-an-issue-pod-lives-on-the-issue-volume-INFRA-43.md`
|
|
186
|
+
puts it on the volume, moves the caches with it, and writes it as one idempotent script the next
|
|
187
|
+
role re-runs. That passes both conditions, and the second is the one to get right: export the
|
|
188
|
+
root's `PATH` and `LD_LIBRARY_PATH` around the harness's own commands — `initdb`, `pg_ctl`, the
|
|
189
|
+
server, the Playwright run — rather than into the shell you go on working in. Handing the harness
|
|
190
|
+
to the next role is the same test passed again, and
|
|
191
|
+
`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`
|
|
192
|
+
is the illustration: it leaves the server running and records the `LD_LIBRARY_PATH`, `PATH` and
|
|
193
|
+
`DATABASE_URL` in its handoff's `trickyParts`, so what crosses to the next role is a note that
|
|
194
|
+
role sets deliberately rather than an environment it inherits. The suite shells out to `psql`,
|
|
195
|
+
and `psql` being in the image is what makes that harness work now without your extracting a
|
|
196
|
+
client for yourself at all.
|
|
197
|
+
|
|
198
|
+
## The three pinned runner forms
|
|
199
|
+
|
|
200
|
+
- `uvx --from <package>@<version> <command>`, for a Python tool: the distribution you pin and the
|
|
201
|
+
command you run are named separately, because they are usually different names.
|
|
202
|
+
`uvx --from httpie@3.2.4 http --version` prints `3.2.4`. Where the project's own name *is* its
|
|
203
|
+
command's — checked once, never assumed — the short `uvx <name>@<version>` runs it:
|
|
204
|
+
`uvx ruff@0.14.0 --version` prints `ruff 0.14.0`, in 1.7 s with the uv cache empty and 0.15 s
|
|
205
|
+
warm. A version that does not exist fails closed, exit 1, naming it: `uvx ruff@99.99.99
|
|
206
|
+
--version` prints `error: No solution found when resolving tool dependencies`, with `cause:
|
|
207
|
+
Because there is no version of ruff==99.99.99 …`.
|
|
208
|
+
- `bunx --package <name>@<version> <binary>`, for an npm tool, naming the package and the binary
|
|
209
|
+
separately because they differ. `bunx --package @biomejs/biome@2.6.3 biome --version` prints
|
|
210
|
+
`Version: 2.6.3`, in 0.3 s with bun's cache warm; `bunx --package @biomejs/biome@99.99.99 biome
|
|
211
|
+
--version` fails closed, exit 1, with `error: No version matching "99.99.99" found for specifier
|
|
212
|
+
"@biomejs/biome" (but package exists)`.
|
|
213
|
+
- `go run <module>@<version>`, for a Go tool. `go run github.com/nats-io/natscli/nats@v0.1.5
|
|
214
|
+
--version` prints `v0.1.5`, in 7.2 s with the module cache warm and 103 s on a cold module cache
|
|
215
|
+
— budget for the cold one the first time an issue runs a Go tool. `go run
|
|
216
|
+
github.com/nats-io/natscli/nats@v99.99.99 --version` fails closed, exit 1, with
|
|
217
|
+
`go: github.com/nats-io/natscli/nats@v99.99.99: invalid version: unknown revision nats/v99.99.99`.
|
|
218
|
+
|
|
219
|
+
## When you need a library, not a command
|
|
220
|
+
|
|
221
|
+
The three forms above run somebody else's command. When what you need is a package available to
|
|
222
|
+
code of your own — PyYAML to read a file, not a CLI — the form is `uv run --with
|
|
223
|
+
<package>==<version> --no-project` ahead of the program you are running:
|
|
224
|
+
|
|
225
|
+
```text
|
|
226
|
+
$ uv run --with pyyaml==6.0.2 --no-project python -c "import yaml; print(yaml.__version__)"
|
|
227
|
+
Installed 1 package in 12ms
|
|
228
|
+
6.0.2
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
That whole run is 0.597 s wall, the install included. `--no-project` keeps it off whatever
|
|
232
|
+
`pyproject.toml` happens to sit above your working directory, so it behaves the same wherever you
|
|
233
|
+
type it, and `--with` takes a requirement specifier (`==<version>`), not a runner coordinate
|
|
234
|
+
(`@<version>`). It is on the sanctioned side for the same reason a `uvx` run is: it fills
|
|
235
|
+
`UV_CACHE_DIR` and leaves nothing another process resolves. Reaching for a command runner instead
|
|
236
|
+
is the mistake the next section's `uvx pyyaml@6.0.2 --version` shows — uv installs the project and
|
|
237
|
+
then finds no executable, because a library has none to find.
|
|
238
|
+
|
|
239
|
+
## Always write the exact coordinate and the exact version
|
|
240
|
+
|
|
241
|
+
A pin has two halves, and a runner resolves what you asked for only when both are right:
|
|
242
|
+
|
|
243
|
+
1. **The coordinate** — the registry name the project itself publishes, scope and all:
|
|
244
|
+
`@biomejs/biome`, not `biome`. The command's name is no evidence of the package's. Take the
|
|
245
|
+
coordinate from the project's own published installation instructions — its documentation site
|
|
246
|
+
or its repository's README, reached independently of the registry rather than through the
|
|
247
|
+
registry entry of a name you guessed. That is the step, and it is the only one that establishes
|
|
248
|
+
identity. Do it once, before the tool's first use.
|
|
249
|
+
2. **The version** on that coordinate.
|
|
250
|
+
|
|
251
|
+
Everything a registry can tell you afterwards is corroboration, and it is worth knowing which kind
|
|
252
|
+
each piece is.
|
|
253
|
+
|
|
254
|
+
**Fields the publisher writes.** `npm view <name> version description repository.url homepage`
|
|
255
|
+
reads what the publisher put in its own `package.json`. npm verifies none of it: a squatter closes
|
|
256
|
+
this check by copying two strings, and a legitimate package that sets no `homepage` reads like a
|
|
257
|
+
squat. Evidence, never proof.
|
|
258
|
+
|
|
259
|
+
**A check whose evidence the subject controls is not a check.** Carry that past npm: every line
|
|
260
|
+
below the next prompt was written by the party whose identity is in question.
|
|
261
|
+
|
|
262
|
+
```text
|
|
263
|
+
$ npm view biome version description repository.url homepage
|
|
264
|
+
version = '0.3.3'
|
|
265
|
+
description = 'A simple way to manage environment variables on a per-project basis'
|
|
266
|
+
$ npm view @biomejs/biome version description repository.url homepage
|
|
267
|
+
version = '2.6.3'
|
|
268
|
+
description = 'Biome is a toolchain for the web: formatter, linter and more'
|
|
269
|
+
repository.url = 'git+https://github.com/biomejs/biome.git'
|
|
270
|
+
homepage = 'https://biomejs.dev'
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Those two answers are *consistent with* `@biomejs/biome` being the project and `biome` being
|
|
274
|
+
someone else's package. What settles it is biomejs.dev telling you to install `@biomejs/biome` —
|
|
275
|
+
the site you reached without npm's help.
|
|
276
|
+
|
|
277
|
+
**What a registry can attest.** npm verifies a provenance attestation when it is published, so it
|
|
278
|
+
is the one part of an entry a publisher cannot forge by writing a string. What it proves is *build
|
|
279
|
+
origin*: the attestation binds the artifact to the repository and workflow that built it. Presence
|
|
280
|
+
is not identity — a squat published from its own public Actions workflow carries a genuine
|
|
281
|
+
attestation of its own repository — so the check is to read whose build it names, and compare that
|
|
282
|
+
with the repository the project's own documentation gave you:
|
|
283
|
+
|
|
284
|
+
```text
|
|
285
|
+
$ npm view @biomejs/biome dist.attestations.url
|
|
286
|
+
https://registry.npmjs.org/-/npm/v1/attestations/@biomejs%2fbiome@2.6.3
|
|
287
|
+
$ curl -s "$(npm view @biomejs/biome dist.attestations.url)" \
|
|
288
|
+
| jq -r '.attestations[] | select(.predicateType=="https://slsa.dev/provenance/v1") | .bundle.dsseEnvelope.payload' \
|
|
289
|
+
| base64 -d | jq -r '.predicate.buildDefinition.externalParameters.workflow.repository'
|
|
290
|
+
https://github.com/biomejs/biome
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
That field is the SLSA v1 predicate's `buildDefinition.externalParameters.workflow`, which here
|
|
294
|
+
reads `{"ref": "refs/heads/main", "repository": "https://github.com/biomejs/biome", "path":
|
|
295
|
+
".github/workflows/release.yml"}` — the repository biomejs.dev sent you to, building on its own
|
|
296
|
+
main branch. The comparison is what makes the attestation worth anything: on its own it says only
|
|
297
|
+
that *some* repository built these bytes. (`jq` in your bash tool is the harness's builtin, and a
|
|
298
|
+
script gets `/usr/bin/jq`; both print this.)
|
|
299
|
+
|
|
300
|
+
Its limit runs the other way: most packages publish no provenance at all — `npm view biome
|
|
301
|
+
dist.attestations` prints nothing — so an absent attestation is no evidence of a squat.
|
|
302
|
+
|
|
303
|
+
PyPI publishes attestations only for projects that release through a trusted publisher, and widely
|
|
304
|
+
used tools do without: for `ruff 0.14.0`, `https://pypi.org/pypi/ruff/0.14.0/json` carries no
|
|
305
|
+
`provenance` on any of its files and `https://pypi.org/integrity/ruff/0.14.0/<file>/provenance`
|
|
306
|
+
answers 404. For a Python tool, the project's own documentation is the whole of the check.
|
|
307
|
+
|
|
308
|
+
A Go module path is a domain-scoped URL, so the coordinate *is* the repository path, and you check
|
|
309
|
+
it by going to that path. `sum.golang.org` pins a version's bytes once they are published, which
|
|
310
|
+
stops the content changing under you later and says nothing about whether you picked the right
|
|
311
|
+
module path in the first place.
|
|
312
|
+
|
|
313
|
+
Pinning the version alone does not save you: it pins the squatter at a version, and obeys "write
|
|
314
|
+
the version" to the letter while doing it.
|
|
315
|
+
|
|
316
|
+
```text
|
|
317
|
+
$ bunx biome --version
|
|
318
|
+
0.3.3
|
|
319
|
+
$ bunx biome@0.3.3 --version
|
|
320
|
+
0.3.3
|
|
321
|
+
$ bunx --package @biomejs/biome@2.6.3 biome --version
|
|
322
|
+
Version: 2.6.3
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Only the third runs the formatter, and what separates it from the second is the *name*, not the
|
|
326
|
+
version. No runner exempts you from the coordinate half; each one's syntax just makes a different
|
|
327
|
+
way of getting it wrong easy to type.
|
|
328
|
+
|
|
329
|
+
- `bunx <binary>@<version>` resolves the token as an npm package name, which is why
|
|
330
|
+
`--package <name>@<version> <binary>` exists: a bare `bunx biome@0.3.3` is a coordinate nobody
|
|
331
|
+
checked.
|
|
332
|
+
- `uvx <command>@<version>` does the same on PyPI, and carries it one step further: uv resolves
|
|
333
|
+
the token as the **project**, installs that project, and only then looks for a command of that
|
|
334
|
+
name.
|
|
335
|
+
|
|
336
|
+
```text
|
|
337
|
+
$ uvx pyyaml@6.0.2 --version
|
|
338
|
+
Installed 1 package in 23ms
|
|
339
|
+
Package `pyyaml` does not provide any executables.
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Nothing there is about the version — `pyyaml` is a real project at a real version, and it is not
|
|
343
|
+
what you meant by typing a command's name. `uvx --help` is where the distinction lives:
|
|
344
|
+
`--from <FROM> Use the given package to provide the command`. Hence
|
|
345
|
+
`uvx --from shellcheck-py@0.10.0.1 shellcheck --version`, which prints `ShellCheck - shell
|
|
346
|
+
script analysis tool` and `version: 0.10.0`: the distribution and the command share no name at
|
|
347
|
+
all.
|
|
348
|
+
- `go run <module>@<version>` takes the coordinate as that domain-scoped URL, so a path you
|
|
349
|
+
invented usually fails to resolve rather than running someone else's code — but a module path
|
|
350
|
+
guessed from a binary's name is still a guess, and the checksum database will pin the wrong
|
|
351
|
+
module's bytes as faithfully as the right one's.
|
|
352
|
+
|
|
353
|
+
Nothing refuses an unpinned command: no guard, no lint and no check stops you typing a bare `bunx
|
|
354
|
+
biome`. What you are given is a correct form and a tool that is already there, not a gate — so both
|
|
355
|
+
halves are yours to type.
|
|
356
|
+
|
|
357
|
+
## When no runner reaches it
|
|
358
|
+
|
|
359
|
+
A tool that is neither in the image nor reachable through one of the three runners is an image
|
|
360
|
+
change: a pull request against sjawhar/legion that carries the same name through every site that
|
|
361
|
+
spells it out, and those sites are not all held by a test.
|
|
362
|
+
|
|
363
|
+
Do not work from a list of those sites. This page has carried one twice and it was wrong both
|
|
364
|
+
times, so take them from the repository instead: grep for a client the image already carries,
|
|
365
|
+
under both of its names — the command and the Debian package that provides it are spelled
|
|
366
|
+
differently, and different sites spell each — and carry the new name through everything the two
|
|
367
|
+
find.
|
|
368
|
+
|
|
369
|
+
```text
|
|
370
|
+
$ grep -rn redis-cli
|
|
371
|
+
$ grep -rn redis-tools
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
Two kinds of hit want more than a substitution. The toolchain smoke step in
|
|
375
|
+
`packages/daemon/docker/worker.Dockerfile` runs each tool once, so a new one needs its own
|
|
376
|
+
invocation there (`nc` and `dig` take no `--version`); and the `THIRD_PARTY_NOTICES` intro string
|
|
377
|
+
in the same file is a licence file that hand-lists the Debian packages the image installs,
|
|
378
|
+
assembled into the image at `/usr/share/doc/legion/THIRD_PARTY_NOTICES`.
|
|
379
|
+
|
|
380
|
+
Four of the files the grep finds are in Go, and those four are the gated ones: the `toolchain`
|
|
381
|
+
name list in `packages/daemon/internal/capabilities/image.go` makes the probe require it; that
|
|
382
|
+
row's summary in `packages/daemon/internal/capabilities/capabilities.go` has to name it, a test
|
|
383
|
+
holding the summary to the list; the stub binaries in
|
|
384
|
+
`packages/daemon/internal/capabilities/capabilitiestest/capabilitiestest.go` have to include it, or
|
|
385
|
+
the capability and `probe-image` tests fail on an image that does not carry it; and
|
|
386
|
+
`packages/daemon/internal/capabilities/image_test.go` spells the expected detail string out again.
|
|
387
|
+
|
|
388
|
+
Miss one of those four and the suite is red, which is what makes them easy to find. Nothing holds
|
|
389
|
+
a Dockerfile comment, the notices string or the smoke step to anything today, so a change that
|
|
390
|
+
stops at the gated sites opens green and ships a licence file that does not name the package it
|
|
391
|
+
installs. That is a fact about this build rather than a property: dispatch://INFRA-255 generates
|
|
392
|
+
the package list from `dpkg-query` at build time and drops the hand-kept string, and once it lands
|
|
393
|
+
the notices half of this warning goes with it while the smoke step and the comments stay yours to
|
|
394
|
+
carry. It reaches a role only once a deployment pins the new image digest, as every Legion change
|
|
395
|
+
does — never within your own phase.
|
|
396
|
+
|
|
397
|
+
So when your work needs such a tool, say so to the architect that owns your issue (`envoy_publish`
|
|
398
|
+
to its role topic), with the tool, what your phase needs it for, and the runner forms you already
|
|
399
|
+
ruled out. What is not the step is improvising a CLI for your own convenience — a vendored binary,
|
|
400
|
+
a hand-rolled substitute, or an extraction prefix dropped on your own `PATH`: the architect decides
|
|
401
|
+
whether the image change is work to schedule. An extraction a harness builds for its own runs is
|
|
402
|
+
not improvising and is not what this rules out (the Dispatch e2e stack, above): the two cases part
|
|
403
|
+
on "The test: where it lands, and what it leaves resolving", which the convenience case fails.
|
|
404
|
+
|
|
405
|
+
The operator's side of this — why the image carries those five clients rather than anything else,
|
|
406
|
+
the five priced options with their measurements, and what the clients cost the image — is in
|
|
407
|
+
`docs/kubernetes.md`, the subsection "The tools a role's own work needs".
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sjawhar/pi-legion",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.22.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": 36
|
|
16
16
|
},
|
|
17
17
|
"repository": {
|
|
18
18
|
"type": "git",
|