@sjawhar/pi-legion 10.29.2 → 10.30.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
@@ -14445,6 +14445,7 @@ var SEARCH_QUERY_HINT = "search with a short phrase of a few words, not a passag
14445
14445
  var SEARCH_KIND_DEPTH = 100;
14446
14446
  var PROJECT_KEY_PATTERN = /^[A-Z][A-Z0-9]{1,9}$/;
14447
14447
  var PROJECT_KEY_RULE = "an uppercase letter, then 1 to 9 uppercase letters or digits, such as CORE";
14448
+ var ARCHITECTURE_SOURCE_REPO_PATTERN = /^[A-Za-z0-9-]+\/(?!\.\.?$)[A-Za-z0-9._-]+$/;
14448
14449
  var ISSUE_STATUSES = [
14449
14450
  "triage",
14450
14451
  "icebox",
@@ -14810,7 +14811,7 @@ var dispatchToolSpecs = [
14810
14811
  {
14811
14812
  name: "dispatch_issues",
14812
14813
  example: { project: "PROJ", limit: 250, offset: 250 },
14813
- description: "List a project's issues for a roadmap or backlog pass: every issue in one project, each carrying " + "its status, priority, parent, labels, open-ask count, progress (its spec's task-list items and " + "its direct children, each done of total, as `tasks 3/7 \xB7 children 2/5`, each only when it has " + "any), and route with whether it reaches anyone, " + "so you can see backlog shape without opening every issue. Optionally filter by status, parent, " + "label, priority, route status, or how recently it changed; priority takes one or more of 0-3 " + "(P0-P3) and null for an issue with no priority, so an owner's P0/P1 audit is priority [0, 1]. " + 'route_status "no_holder" lists every open issue whose route names a role nobody holds or a ' + "session that is not running at the moment of the read, whatever its priority. A restarting " + "session is absent for minutes, so an issue is unowned only when a read ten minutes later agrees. " + "Do not use it to search by keyword or phrase; dispatch search remains the keyword surface. " + "Dispatch pages the list: limit sets the page size (default " + `${DEFAULT_ISSUE_PAGE_LIMIT}, max ${MAX_ISSUE_PAGE_LIMIT}) and offset selects where it starts ` + "(default 0), and the answer names how many issues match, so repeat with the next offset to " + "walk every matching issue. A walk is exact only while the list does not change: an issue " + "that enters or leaves what the filters match, or whose status or rank changes, between two " + "pages shifts rows across a page boundary, so one issue can come back twice and another never.",
14814
+ description: "List a project's issues for a roadmap or backlog pass: every issue in one project, each carrying " + "its status, priority, parent, labels, open-ask count, progress (its spec's task-list items and " + "its direct children, each done of total, as `tasks 3/7 \xB7 children 2/5`, each only when it has " + "any), and route with whether it reaches anyone, " + "so you can see backlog shape without opening every issue. Optionally filter by status, parent, " + "label, priority, route status, or how recently it changed; priority takes one or more of 0-3 " + "(P0-P3) and null for an issue with no priority, so an owner's P0/P1 audit is priority [0, 1]. " + 'route_status "no_holder" lists every open issue whose route names a role nobody holds or a ' + "session that is not running at the moment of the read, whatever its priority. A restarting " + "session is absent for minutes, so an issue is unowned only when a read ten minutes later agrees. " + "Do not use it to search by keyword or phrase; dispatch search remains the keyword surface. " + "Dispatch pages the list: limit sets the page size (default " + `${DEFAULT_ISSUE_PAGE_LIMIT}, max ${MAX_ISSUE_PAGE_LIMIT}) and offset selects where it starts ` + "(default 0), and the answer names how many issues match, so repeat with the next offset to " + "walk every matching issue. A walk is exact only while the list does not change: an issue " + "that enters or leaves what the filters match, or whose status or rank changes, between two " + "pages shifts rows across a page boundary, so one issue can come back twice and another never. " + "all takes the whole filtered list in one read instead, for a caller that must not miss or " + "repeat a row while other writers change the list, and especially for one whose own writes " + "move issues out of what it filtered on.",
14814
14815
  arguments: (z2) => ({
14815
14816
  project: z2.string().describe("Project key to list issues from."),
14816
14817
  status: z2.enum(ISSUE_STATUSES).describe("Optional lifecycle status filter.").optional(),
@@ -14820,7 +14821,8 @@ var dispatchToolSpecs = [
14820
14821
  updated_since: z2.string().describe("Optional RFC3339 timestamp; only issues updated at or after it.").optional(),
14821
14822
  route_status: z2.enum(ISSUE_ROUTE_STATUSES).describe("Optional: only open issues whose route is in this state. live: a running session holds " + "the role or is the routed session. no_holder: nobody running holds the role, or the " + "session is not running, right now. unknown: the Envoy listener did not answer.").optional(),
14822
14823
  limit: z2.number({ int: true, min: 1, max: MAX_ISSUE_PAGE_LIMIT }).describe(`Maximum rows, 1-${MAX_ISSUE_PAGE_LIMIT}; default ${DEFAULT_ISSUE_PAGE_LIMIT}.`).optional(),
14823
- offset: z2.number({ int: true, min: 0 }).describe("Rows to skip before the page; nonnegative integer; default 0.").optional()
14824
+ offset: z2.number({ int: true, min: 0 }).describe("Rows to skip before the page; nonnegative integer; default 0.").optional(),
14825
+ all: z2.boolean().describe("Take every matching issue in one unpaged read, so no row is missed or repeated while " + "the list changes. Refused with limit or offset.").optional()
14824
14826
  })
14825
14827
  },
14826
14828
  {
@@ -14842,11 +14844,19 @@ var dispatchToolSpecs = [
14842
14844
  },
14843
14845
  {
14844
14846
  name: "dispatch_architecture_sync",
14845
- example: { project: "CORE" },
14846
- description: "Import a project's architecture model from its configured source repository now, instead of " + "waiting for the server's five-minute schedule. Returns the imported commit and component " + "count, or the recorded error when the model was rejected (the previous model stays up). " + "The source itself is configured by a human in Settings; 404 SOURCE_NOT_FOUND without one.",
14847
+ example: { project: "CORE", repo: "acme/infra" },
14848
+ description: "Import a project's architecture model from its source repositories now, instead of waiting " + "for the server's five-minute schedule. A project may have several sources, one per " + "repository, and its model is every source's components together. Without repo every " + "source imports and each answers on its own line; with repo only that source does. Each " + "line names the imported commit, or the recorded error when that source's model was " + "rejected (its previous components stay up, and every other source is untouched). Sources " + "are saved in Settings or with PUT /api/v1/projects/{key}/architecture-sources/{owner}/{repo}; " + "404 SOURCE_NOT_FOUND when the project has none, or none for that repository.",
14847
14849
  arguments: (z2) => ({
14848
- project: z2.string().describe("Project key whose architecture source to sync, such as CORE.")
14850
+ project: z2.string().describe("Project key whose architecture sources to sync, such as CORE."),
14851
+ repo: z2.string().describe("One source repository of the project to sync alone, owner/name; omit it to sync every source.").optional()
14849
14852
  }),
14853
+ validation: {
14854
+ check: (value) => {
14855
+ const { repo } = value;
14856
+ return typeof repo !== "string" || ARCHITECTURE_SOURCE_REPO_PATTERN.test(repo);
14857
+ },
14858
+ message: "repo is not a repository: give it as owner/name, such as acme/infra."
14859
+ },
14850
14860
  strict: true
14851
14861
  },
14852
14862
  {
@@ -15063,6 +15073,9 @@ function asErrorShape(value) {
15063
15073
  function isJson(response) {
15064
15074
  return response.headers.get("content-type")?.includes("application/json") ?? false;
15065
15075
  }
15076
+ function isNoRoute(error48) {
15077
+ return error48 instanceof DispatchServiceError && error48.status === 404 && error48.code === "NOT_FOUND" && error48.message.startsWith("no route for ");
15078
+ }
15066
15079
  var ISSUE_EVENTS_PAGE_MAX = 200;
15067
15080
  var DISPATCH_TOOL_DEADLINE_MS = 60000;
15068
15081
  function requestSignal(signal) {
@@ -15090,6 +15103,18 @@ function whyNotAPage(answer) {
15090
15103
  }
15091
15104
  return `${what}. Retrying will not help: the same request gets the same answer until that ` + "Dispatch is upgraded or fixed.";
15092
15105
  }
15106
+ function whyNotAnArray(answer) {
15107
+ if (typeof answer === "string")
15108
+ return `text that is not JSON, ${GATEWAY_PAGE}, so a retry may succeed`;
15109
+ if (answer === null)
15110
+ return "null";
15111
+ if (typeof answer !== "object")
15112
+ return `a ${typeof answer}`;
15113
+ if ("issues" in answer && Array.isArray(answer.issues)) {
15114
+ return "a page, so this Dispatch pages a request that carries no limit and no offset";
15115
+ }
15116
+ return "an object that is not the listing";
15117
+ }
15093
15118
  var GATEWAY_PAGE = "which looks like a proxy or gateway page rather than Dispatch's own answer";
15094
15119
  function gatewayAdvice(method, status) {
15095
15120
  if (!transientStatus(status)) {
@@ -15165,6 +15190,14 @@ class DispatchClient {
15165
15190
  return answer;
15166
15191
  throw new Error(`GET ${this.#url(path2, query)} asked for a page ({issues, total, limit, offset}) and got ` + refusal);
15167
15192
  }
15193
+ async listAllIssues(options) {
15194
+ const path2 = ["api", "v1", "issues"];
15195
+ const query = { ...options };
15196
+ const answer = await this.#json("GET", path2, undefined, query);
15197
+ if (Array.isArray(answer))
15198
+ return answer;
15199
+ throw new Error(`GET ${this.#url(path2, query)} asked for the unpaged listing and got ${whyNotAnArray(answer)}.`);
15200
+ }
15168
15201
  async listProjectArtifacts(project, unlinked = false) {
15169
15202
  return this.#json("GET", ["api", "v1", "projects", project, "artifacts"], undefined, unlinked ? { unlinked: "true" } : undefined);
15170
15203
  }
@@ -15253,19 +15286,69 @@ class DispatchClient {
15253
15286
  async listProjects() {
15254
15287
  return this.#json("GET", ["api", "v1", "projects"]);
15255
15288
  }
15256
- async syncArchitectureSource(project) {
15257
- return this.#json("POST", ["api", "v1", "projects", project, "architecture-source", "sync"]);
15258
- }
15259
- async getArchitectureSource(project) {
15289
+ async listArchitectureSources(project) {
15290
+ try {
15291
+ return await this.#json("GET", ["api", "v1", "projects", project, "architecture-sources"]);
15292
+ } catch (error48) {
15293
+ if (!isNoRoute(error48))
15294
+ throw error48;
15295
+ }
15260
15296
  try {
15261
- return await this.#json("GET", ["api", "v1", "projects", project, "architecture-source"]);
15297
+ const source = await this.#json("GET", [
15298
+ "api",
15299
+ "v1",
15300
+ "projects",
15301
+ project,
15302
+ "architecture-source"
15303
+ ]);
15304
+ return source === null ? [] : [source];
15262
15305
  } catch (error48) {
15263
15306
  if (error48 instanceof DispatchServiceError && error48.status === 404 && error48.code === "SOURCE_NOT_FOUND") {
15264
- return null;
15307
+ return [];
15265
15308
  }
15266
15309
  throw error48;
15267
15310
  }
15268
15311
  }
15312
+ async syncArchitectureSources(project) {
15313
+ try {
15314
+ return await this.#json("POST", [
15315
+ "api",
15316
+ "v1",
15317
+ "projects",
15318
+ project,
15319
+ "architecture-sources",
15320
+ "sync"
15321
+ ]);
15322
+ } catch (error48) {
15323
+ if (!isNoRoute(error48))
15324
+ throw error48;
15325
+ }
15326
+ const source = await this.#json("POST", [
15327
+ "api",
15328
+ "v1",
15329
+ "projects",
15330
+ project,
15331
+ "architecture-source",
15332
+ "sync"
15333
+ ]);
15334
+ return [source];
15335
+ }
15336
+ async syncArchitectureSource(project, repo2) {
15337
+ const [owner, name, ...rest] = repo2.split("/");
15338
+ if (!owner || !name || rest.length > 0) {
15339
+ throw new Error(`repository must be owner/name, got ${JSON.stringify(repo2)}`);
15340
+ }
15341
+ return this.#json("POST", [
15342
+ "api",
15343
+ "v1",
15344
+ "projects",
15345
+ project,
15346
+ "architecture-sources",
15347
+ owner,
15348
+ name,
15349
+ "sync"
15350
+ ]);
15351
+ }
15269
15352
  async resolveAsk(id, input) {
15270
15353
  return this.#json("POST", ["api", "v1", "asks", id, "resolve"], input);
15271
15354
  }
@@ -16956,7 +17039,7 @@ async function bootLegionClaim(host2) {
16956
17039
  // package.json
16957
17040
  var package_default = {
16958
17041
  name: "@sjawhar/pi-legion",
16959
- version: "10.29.2",
17042
+ version: "10.30.0",
16960
17043
  license: "Apache-2.0",
16961
17044
  type: "module",
16962
17045
  omp: {
@@ -16968,7 +17051,7 @@ var package_default = {
16968
17051
  ]
16969
17052
  },
16970
17053
  legion: {
16971
- daemonApiVersion: 36
17054
+ daemonApiVersion: 37
16972
17055
  },
16973
17056
  repository: {
16974
17057
  type: "git",
@@ -120,8 +120,9 @@ The merge closes the issue: when GitHub reports the pull request merged while th
120
120
  `pr-merged` notice names what it waits for and none follows as they depart, so read the pull
121
121
  request on GitHub.
122
122
  - A child's close reaches you as `child-closed`. The daemon retires five of its six claims and
123
- suspends its implementer, session and volume kept, because it still owes the production check
124
- (`skill://legion-worker/references/merge-gate.md`, *The production check after the close*).
123
+ leaves the implementer running, because it still owes the production check. When the issue's
124
+ pod and Sandbox go is in `skill://legion-worker/references/merge-gate.md`, *The production check
125
+ after the close*.
125
126
  - A root's close writes `done`, frees the tree's admission slot and lingers the tree for
126
127
  `linger_hours` with every member suspended. The linger's end, the tree's last step, waits until
127
128
  every member's open production check has closed.
@@ -61,17 +61,21 @@ repositories with your gh's read-only access (`-R owner/repo`).
61
61
 
62
62
  ## Architecture components
63
63
 
64
- A project's architecture source is the repository whose `.dispatch/architecture/` files Dispatch
65
- imports as its components (`skill://dispatch`, "Architecture components").
64
+ A project's architecture sources are the repositories whose `.dispatch/architecture/` files
65
+ Dispatch imports as its components (`skill://dispatch`, "Architecture components").
66
66
 
67
67
  - **The components exist:** attach the root before you decompose, so every child inherits it:
68
68
  `dispatch issue-update --issue <root> --components-json '{"mode":"explicit","ids":[…]}'`.
69
69
  - **They do not exist yet (bootstrap):** in the root's first PR (the implement worker pushes it),
70
70
  write `.dispatch/architecture/<id>.md` files for the planned components — front matter `title`,
71
- `parent`, `depends_on`, `external`; no `paths` yet. The importer reads the source branch's head,
72
- so leave the root on `inherit` until that PR has merged: an attach before then answers
71
+ `parent`, `depends_on`, `external`; no `paths` yet. A project may have several sources, one per
72
+ repository, and its model is all of their components together: the file goes in the branch of
73
+ the source whose repository holds it, `parent` and `depends_on` name components of that same
74
+ source only, and its id must be one no other source of the project uses (a repeated id fails that
75
+ source's import, naming the repository that has it). The importer reads the source branch's
76
+ head, so leave the root on `inherit` until that PR has merged: an attach before then answers
73
77
  `400 COMPONENTS_INPUT`, since the component does not exist. After the merge, run
74
- `dispatch architecture-sync --project <KEY>` and attach the root; children created before then
75
- inherit it.
78
+ `dispatch architecture-sync --project <KEY>` (every source; `--repo <owner/name>` syncs that one
79
+ alone) and attach the root; children created before then inherit it.
76
80
  - Give a child its own `--components-json` only when it changes a narrower set, and
77
81
  `{"mode":"none","reason":"…"}` when it changes no code or file a component describes.
@@ -11,7 +11,7 @@ You work one phase of one issue in a jj workspace every role of the issue shares
11
11
 
12
12
  ## Every phase, in this order
13
13
 
14
- 1. **Read first.** Read the issue, then each committed handoff before your phase, with `read` from `$LEGION_WORKSPACE/.legion/<issue>/`, in order: `plan.json`, `implement.json`, `test.json`, `review.json`. Where one disagrees with your memory or a transcript, the file wins: it is the copy that survived. At each later assignment read them again, your own included, since other phases may have committed since. After a `Your workspace was recreated…` message, read `.legion/<issue>/workspace-recovered.json` too, once.
14
+ 1. **Read first.** Read the issue, then each committed handoff before your phase, with `read` from `$LEGION_WORKSPACE/.legion/<issue>/`, in order: `plan.json`, `implement.json`, `test.json`, `review.json`. Where one disagrees with your memory or a transcript, the file wins: it is the copy that survived. At each later assignment read them again, your own included, since other phases may have committed since. After a `Your workspace was recreated…` message, anything unpushed is gone, and no file in the workspace says so.
15
15
  2. **Commit only your phase's work**, with `jj -R "$LEGION_WORKSPACE" split -m "<message>" <paths…>`, each commit holding only your phase's paths, so the next phase sees what you changed.
16
16
  3. **Put back what you copied in.** Before you write the handoff, run `jj -R "$LEGION_WORKSPACE" status` and remove or restore every file you copied or fetched in that is not your phase's work, such as another branch's test rig. With jj the working copy is the commit: the next role's first commit builds on what you left, and its push ships it.
17
17
  4. **Write the handoff** your role prompt names. Each `proof` entry, the implementer's or the tester's, is an object of six non-empty strings: `criterion` (the acceptance line it proves), `surface` (where a user reaches the change), `command`, `observed`, `headSha` (the commit it ran at) and `negativeControl` (a deliberately broken input and the refusal seen); `skill://legion-worker/references/pr-body.md#what-a-proof-is` defines each. To add to a handoff already on disk, `read` it, edit it, and `write` it whole with `completed` set to now.
@@ -251,16 +251,17 @@ reviewing until you finish.
251
251
  topic, and file no issue. It names this issue, the pull request, the merge commit, what you
252
252
  drove and what failed. Name the ask (`dispatch://<REPORT KEY>/ask/<id>`) in the `Production:`
253
253
  line and in the summary.
254
- - **Nothing waits in the session for a deploy.** A `pending` ends the look: the daemon suspends
255
- your claim with its session and the issue's volume kept, the pod goes, and the daemon wakes the
256
- check (the pod comes back and your session resumes with a new check task) on Dispatch's
254
+ - **Nothing waits in the session for a deploy.** A `pending` ends the look. Where no wake is
255
+ queued yet, the daemon suspends your claim with its session kept, and the issue's Sandbox goes
256
+ with its pod and disk; where a wake arrived while the look was open, the next look starts at once
257
+ on the same pod. Either way the daemon wakes the check (after a suspension a new pod comes back
258
+ on a fresh disk, and your session resumes with a new check task) on Dispatch's
257
259
  `issue.deployed` event for the pull request; when the project's `legion.yaml` names the
258
260
  repository's deploy workflow (`projects.<KEY>.deploys`), at each completed run of that workflow
259
261
  on the default branch whose named step (such as `Apply`) succeeded after the merge, once per such
260
262
  job; or, for `sjawhar/legion`, on a Legion daemon's boot on a build containing the merge, on
261
263
  Dispatch's `server.started` naming such a build, or on the build the daemon read from Dispatch at
262
- its boot. A wake while a look is open is kept, and a `pending` then starts the next look at once.
263
- The woken look's task names the event (`Dispatch's issue.deployed: run <id> completed <t>`,
264
+ its boot. The woken look's task names the event (`Dispatch's issue.deployed: run <id> completed <t>`,
264
265
  `the deploy workflow's run: <repo> <workflow> run <id> attempt <n>, job <job name>: <step> succeeded at <t>`,
265
266
  `the Legion daemon's boot on build <sha>`, `Dispatch's start on build <sha>`,
266
267
  `Dispatch's build <sha>`), and its `Production:` line names it too, so the pull request carries
@@ -279,5 +280,6 @@ reviewing until you finish.
279
280
  on a closed issue (`ISSUE_CLOSED`), so the deployment instructions' "on the pull request and the
280
281
  issue" is the pull request for a closed issue, and a defect reaches Dispatch as the ask above.
281
282
  The architect accepted the issue before the merge and is asked nothing after it. `pass` or
282
- `fail` closes the check: your claim is retired and, for a child, the issue's Sandbox and volume
283
- are released.
283
+ `fail` closes the check: your claim is retired and the issue's Sandbox goes with its pod and
284
+ disk, for a root as for a child. Neither ending retires or suspends anything of an issue back in
285
+ the workflow (a root reopened inside its linger): its claim and pod are the new run's.
@@ -86,11 +86,11 @@ for it*.
86
86
 
87
87
  An install of your own — an interpreter, a browser, a second toolchain, an extraction prefix — is
88
88
  on the sanctioned side when both of these hold: what it writes lands on the issue's own volume,
89
- under `/legion`, which outlives the pod and is shared by the issue's roles — whether through a
90
- variable the runtime sets for you there or under a path on that volume your harness names
91
- itself — but outside the issue's workspace (`$LEGION_WORKSPACE`, under `/legion/workspaces`) and
92
- the runtime's own directories (`/legion/repos`, `/legion/feed`, `/legion/sessions`); and after
93
- the command exits nothing else resolves differently. The workspace is excluded because its jj
89
+ under `/legion`, which outlives a role's relaunch in its pod and is shared by the issue's roles —
90
+ whether through a variable the runtime sets for you there or under a path on that volume your
91
+ harness names itself — but outside the issue's workspace (`$LEGION_WORKSPACE`, under
92
+ `/legion/workspaces`) and the runtime's own directories (`/legion/repos`, `/legion/feed`); and
93
+ after the command exits nothing else resolves differently. The workspace is excluded because its jj
94
94
  working copy tracks every new file under jj's `snapshot.max-new-file-size` (1 MiB in a role
95
95
  container), so 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
@@ -122,7 +122,7 @@ What each shape leaves resolving:
122
122
  all. `uv sync` or `bun install` in your workspace materialises what that project already
123
123
  declares, into that project's own directory, which a process reaches by asking for that project
124
124
  and no other way — and the runtime puts `UV_PYTHON_INSTALL_DIR` and the workspace itself on the
125
- issue's volume exactly so a later pod reuses it.
125
+ issue's volume exactly so every role of the issue's pod reuses it.
126
126
  - An extraction prefix: whatever you leave resolving through it, which is yours to decide. Unpack
127
127
  Debian packages under `$HOME`, export `LD_LIBRARY_PATH` in the shell you go on working in, and
128
128
  you change what every later command in that shell — and every subagent that inherits the
@@ -147,8 +147,8 @@ toolchain — is where the first condition does the work. `uv run --with pyyaml=
147
147
  passes both: the wheel lands in `UV_CACHE_DIR`, under `wheels-v6/pypi/pyyaml/6.0.2-…`, and
148
148
  `UV_PYTHON_INSTALL_DIR` is never created, because the image's own `python3` served the run. The
149
149
  interpreter is the other run: a project whose `.python-version` names an interpreter uv must fetch
150
- puts about 70 MB under `UV_PYTHON_INSTALL_DIR`, where the issue's next pod finds it, while the
151
- cache holds no wheel. A browser download passes too, and for Playwright — the browser driver this
150
+ puts about 70 MB under `UV_PYTHON_INSTALL_DIR`, where every role of the issue's pod finds it, while
151
+ the cache holds no wheel. A browser download passes too, and for Playwright — the browser driver this
152
152
  repository's own Dispatch suite installs (`playwright install`, `packages/dispatch`'s
153
153
  `e2e:install`) — it passes as it stands, with nothing pointed anywhere: playwright-core resolves
154
154
  its browser registry under `XDG_CACHE_HOME`, which the runtime has already put on the issue's
@@ -164,11 +164,13 @@ here) at a directory under one of the paths above yourself, since an unset one l
164
164
  wherever the tool defaults, which is nowhere this runtime chose.
165
165
 
166
166
  Where it lands is what you pay. Under one of those variables it is on the issue's volume: it
167
- outlives the pod, and every role of the issue shares it — under every one of those variables but
168
- `XDG_STATE_HOME`, which is `/legion/scratch/state/<role>`, "the one directory of the issue's
169
- scratch that is the role's own rather than the issue's" (`roleEnvironment`), because Oh My Pi
170
- names its browser broker's lock from the state root and an abstract socket name is one name across
171
- every container of the pod — and a later pod of the issue reuses it. Under `$HOME` —
167
+ outlives a role's relaunch in its pod, and every role of the issue shares it — under every one of
168
+ those variables but `XDG_STATE_HOME`, which is `/legion/scratch/state/<role>`, "the one directory
169
+ of the issue's scratch that is the role's own rather than the issue's" (`roleEnvironment`),
170
+ because Oh My Pi names its browser broker's lock from the state root and an abstract socket name
171
+ is one name across every container of the pod — though not the pod itself: the issue's disk is
172
+ the pod's own and is deleted with it, so a later pod of the issue starts without it. Under
173
+ `$HOME` —
172
174
  `/home/legion`, which no volume is mounted on — it is the container's own filesystem: one copy per
173
175
  role, per pod, gone when the role relaunches, and charged to the container's ephemeral-storage
174
176
  limit, which is per role and smaller than the volume. Moving exactly these writes off that
@@ -178,7 +180,7 @@ under `$HOME` fails the first condition and is outside the sanctioned form, howe
178
180
  changes. The three artefacts above are the whole of that line's ground. Disk is not: every cache is
179
181
  already relocated onto the issue's volume, which has the room for them (`docs/kubernetes.md`, "The
180
182
  tools a role's own work needs", carries the figure), and a sanctioned install is paid once per
181
- issue, on the issue's own disk, by the issue's roles together.
183
+ issue pod, on the issue's own disk, by the issue's roles together.
182
184
 
183
185
  **The worked case, and the one a role actually meets: the Dispatch e2e stack.** A change to
184
186
  `packages/dispatch` is proved by driving its browser suite, which needs a Postgres with pgvector;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-legion",
3
- "version": "10.29.2",
3
+ "version": "10.30.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": 36
15
+ "daemonApiVersion": 37
16
16
  },
17
17
  "repository": {
18
18
  "type": "git",