@ucsandman/legcli 0.14.0 → 0.15.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/CHANGELOG.md +99 -0
- package/README.md +51 -13
- package/bin/leg.mjs +104 -62
- package/docs/DECISIONS.md +8 -0
- package/docs/DEVIATIONS.md +33 -0
- package/docs/ERRORS.md +1 -1
- package/docs/ROADMAP-v2.md +12 -1
- package/docs/concepts.md +52 -14
- package/docs/configuration.md +5 -3
- package/docs/review-2026-09-18.md +172 -0
- package/fixtures/verified.json +1 -1
- package/package.json +1 -1
- package/src/accounts.mjs +67 -7
- package/src/attach.mjs +184 -48
- package/src/board/board.js +1 -1
- package/src/board/floor.js +1 -1
- package/src/bundle.mjs +33 -11
- package/src/digest.mjs +197 -0
- package/src/git.mjs +97 -0
- package/src/handoff.mjs +20 -2
- package/src/history/common.mjs +3 -1
- package/src/history/providers/claude.mjs +5 -1
- package/src/history/worktrees.mjs +105 -25
- package/src/launcher.mjs +1 -1
- package/src/limits.mjs +19 -1
- package/src/scheduler-status.mjs +22 -0
- package/src/scheduler.mjs +3 -14
- package/src/server.mjs +171 -38
- package/src/session-detail.mjs +25 -2
- package/src/sessions.mjs +18 -3
- package/src/synthesis.mjs +23 -4
- package/src/usage.mjs +27 -1
package/docs/DECISIONS.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Durable product and design decisions that the code does not explain on its own. One entry per decision, newest first.
|
|
4
4
|
|
|
5
|
+
## 2026-09-18: a second claude login shares the conversation store, so an account switch keeps the conversation
|
|
6
|
+
|
|
7
|
+
- **What.** `LAYOUT.claude.share` gains `projects`: `leg accounts add claude <name>` junctions Claude Code's conversation store into the account directory beside the harness directories, and `refreshAccount()` adds a missing junction to an account made before this before every launch. `keepsConversation()` in `src/usage.mjs` is the one rule that decides `--resume` over the bundle, for the terminal's switch and the board picker's `keeps_conversation` alike: a same-login downshift (as before), or another login of the same agent under which the transcript file exists. The history index skips a junctioned `projects` so a conversation is listed once.
|
|
8
|
+
- **Why share rather than copy.** Wes runs two 20x logins and wants a Fable or weekly wall on one to continue on the other without re-explaining anything. Copying the one transcript at hand-off would have to write into `~/.claude/projects` on the way back, and that directory is on the README's "read, never written" list. A junction inside `$LEG_HOME/accounts/` keeps that promise: Claude Code writes its own store, Leg reads it. It also carries the auto-memory under `projects/<cwd>/memory/`, which is the same human's and should follow them.
|
|
9
|
+
- **Why the rule checks the file.** A `--resume` into a login that cannot see the transcript opens an empty conversation and loses the bundle too. So the rule asks whether the file exists under the destination home at the moment of the switch, and says bundle when it does not. A false answer costs one bundle prime; a wrong true would cost the context.
|
|
10
|
+
- **Why a cross-login upshift resumes when a same-login upshift does not.** The same-login rule stays as decided on 2026-09-17: the re-read at the stronger model's rate is paid on the login that is already low. Across logins the destination is a fresh window, which is the reason the human bought it.
|
|
11
|
+
- **What this rules out.** codex across logins: `codex resume` under a second `CODEX_HOME` is unobserved, so codex rungs keep taking the bundle and claim nothing.
|
|
12
|
+
|
|
5
13
|
## 2026-09-18: usage is polled by the board, once per login; a rung's model is a catalog entry validated by shape
|
|
6
14
|
|
|
7
15
|
- **What.** The board process runs one usage poller per login (`src/usage-poll.mjs`): 60s, doubling to ten minutes on any refusal, one status line when a login's reading fails and one when it is back. Terminals poll nothing and receive the windows on their session record. `GET /api/models` lists what each installed CLI can run, read from claude's aliases, codex's `models_cache.json` and `config.toml`, and the `models` commands of agy and grok, cached an hour under `<LEG_HOME>/models/`. A rung's `model` is validated by shape (lower-case id characters) for every provider and by membership for claude only.
|
package/docs/DEVIATIONS.md
CHANGED
|
@@ -181,3 +181,36 @@ tests. Rows for the shape changes a later reader would otherwise wonder about.
|
|
|
181
181
|
| 2026-09-14 | src/resume.mjs idle pointer | a checkpoint bundle was described as "the last hand-off", and the per-session file was named unconditionally | `lineage.to` decides hand-off vs checkpoint, and the file is named only when it exists on disk | seen in the live pointer for this repo: it claimed a hand-off that never happened and pointed at a `RESUME-<id>.md` that was never written, which is the same class of lie the module exists to stop |
|
|
182
182
|
| 2026-09-14 | src/board/sessions.js `renderDrawer()` | the whole panel is rebuilt every 3 s poll so relative timestamps stay honest | still rebuilt, but every scrollable box carries a stable `data-scroll-key` and its offset is carried across | the rebuild reset the task box, each message and the timeline to the top every three seconds, so a long message could not be read at all |
|
|
183
183
|
| 2026-09-17 | src/board/sessions.js `verdictLines()` | spec A.5 lists "a model bucket came back and a terminal is still downshifted" below the two branches that print a standing percentage | the came-back branch is tested before them, right after the model-walled branch | as written it can never fire: "one login carries every live terminal" and "several logins carry work" both return for any login that has a figure at all, so the only state change worth telling a downshifted reader about was unreachable |
|
|
184
|
+
|
|
185
|
+
## 2026-09-18: product, performance and architecture pass (0.15.0)
|
|
186
|
+
|
|
187
|
+
Measured first (`scratchpad/prof/BASELINE.md`, reproduction commands in the
|
|
188
|
+
CHANGELOG entry), then changed. Rows for the shape changes a later reader
|
|
189
|
+
would otherwise wonder about; the designs not built are in
|
|
190
|
+
`docs/review-2026-09-18.md`.
|
|
191
|
+
|
|
192
|
+
| date | file | old shape | new shape | why |
|
|
193
|
+
|------|------|-----------|-----------|-----|
|
|
194
|
+
| 2026-09-18 | src/accounts.mjs, src/usage.mjs, src/attach.mjs, src/server.mjs | a hand-off to a second claude login always took the bundle; only a same-login downshift kept the conversation | the account junctions claude's `projects` store; one rule, `keepsConversation()`, decides for the terminal and the picker, and checks the transcript file is reachable under the destination home at the switch | a second 20x login should continue the conversation, not re-read it from a bundle; the junction keeps "~/.claude/projects: read, never written" true |
|
|
195
|
+
| 2026-09-18 | src/history/providers/claude.mjs | every claude home was scanned, junction or not | a home whose `projects` is a link is skipped and reported `shared: true` | the same transcript listed twice from two logins |
|
|
196
|
+
| 2026-09-18 | src/digest.mjs, bin/leg.mjs, src/server.mjs | nothing read the records back as an answer to a person; the audit trail was a flat list | `leg digest [--since]` and `GET /api/digest` (owner only): volume first, what needs you, per repository, walls standing now | the question after eight hours away had no reader; a window with nothing in it must print its counts, not a blank |
|
|
197
|
+
| 2026-09-18 | bin/leg.mjs | 26 static imports; the agent dispatch first; `parseTarget` synchronous | one `await import()` per command group, inside its branch; the agent dispatch after every named group; `parseTarget` async and loads buckets/accounts only for a two-part target | `leg --version` loaded 68 modules for one readFileSync: 117 ms wall, 79 ms CPU, 254 fs calls; now 1 module, 52 ms, 4 fs calls |
|
|
198
|
+
| 2026-09-18 | src/limits.mjs | `SIGNALS = loadSignals()` at import (24 fixture reads, one RegExp each) | a Proxy over the same array that loads on first property access | every command paid for the fixture tree; test/ and scripts/limits-table.mjs read `SIGNALS.length` at their own top level, so a loader function was not an option |
|
|
199
|
+
| 2026-09-18 | src/scheduler-status.mjs (new), src/scheduler.mjs, src/launcher.mjs | `pidfile`, `schedulerStatus`, `MAX_CONCURRENT` lived in scheduler.mjs | a leaf module, re-exported from scheduler.mjs | the launcher pulled orchestrator, land, mergequeue, stations, chain, runner and limits to answer "is the scheduler running" |
|
|
200
|
+
| 2026-09-18 | src/git.mjs (new), src/attach.mjs, src/bundle.mjs | six git processes per poll round (three `rev-parse`, `status --porcelain`, `@{upstream}`, `rev-list --count`), each wrapper private, no timeout, no maxBuffer | one `status --porcelain=v2 --branch` carries head, branch, upstream, ahead/behind and the dirty list; `aheadFromStatus` uses `# branch.ab` or a one-pair head cache; 20 s timeout, 8 MB buffer | an idle terminal spawned 59.5 git processes a minute and blocked its own loop 4.4 to 10.3 s/min; now 10.9/min and 2.1 s/min, CPU 1.7 to 0.1 s/min |
|
|
201
|
+
| 2026-09-18 | src/git.mjs parseStatus | a rename printed the old and new names with the quotes half-stripped | the new name | the file on disk is the one a human recognises; pinned in test/git-status.test.mjs |
|
|
202
|
+
| 2026-09-18 | src/attach.mjs ensureBoard | polled /api/health every 200 ms for up to 15 s before the agent got its first instruction | `wait: false` for a terminal: the agent starts at once, the wait runs behind it, `claimBoardBeforeExit` (2 s at most) still writes the pidfile before a sub-second session exits; `leg share on|off` keeps the blocking form | the one terminal of the day that starts the board waited ~790 ms doing nothing (to-agent 1,280 to 261 ms); a board without a pidfile is one `leg down` cannot stop |
|
|
203
|
+
| 2026-09-18 | src/attach.mjs installedAgents | a `--version` subprocess per bare-name agent per launch, 8 s timeout each | `$LEG_HOME/installed.json`, one day per resolved bin; a `*_BIN` override is asked every time and never cached | a stub pointed at on purpose must be probed; a real CLI on PATH answers the same for a day |
|
|
204
|
+
| 2026-09-18 | src/bundle.mjs, src/handoff.mjs, src/attach.mjs | the periodic checkpoint ran `chb save` synchronously inside the poll tick | `saveSessionBundleAsync` (execFile), one at a time through `checkpointGate`, awaited before the hand-off save; warning, limit and hand-off saves stay synchronous | a python subprocess with a 120 s timeout froze limit detection and every board button for its whole run |
|
|
205
|
+
| 2026-09-18 | src/handoff.mjs | `scrub` imported from runner.mjs | from redact.mjs | the terminal path pulled the card runner's whole graph for one function |
|
|
206
|
+
| 2026-09-18 | src/sessions.mjs updateSession | `withFileLock` defaults: past 1.2 s the read-modify-write ran unlocked | `retries: 250, staleMs: 10000`, run.json's budget | the most-written file in Leg (runner poll, claude hook, usage poller, board actions, every `leg` command); losing a patch is the race the lock exists for |
|
|
207
|
+
| 2026-09-18 | src/sessions.mjs, src/attach.mjs | `rmSync(control.json)` at exit | `clearControl` under `.control.lock` | a bare unlink could delete a board request mid-write |
|
|
208
|
+
| 2026-09-18 | src/attach.mjs | the 12-leg stop said "run leg again in this directory to continue from the bundle" | names the bundle path and `leg resume`; says a fresh `leg <agent>` does not load it | nothing reloads a bundle on a fresh launch; the line promised a hand-off that never happened |
|
|
209
|
+
| 2026-09-18 | src/server.mjs health tick | `pushSessions()` unconditionally every 10 s per client | the stat fingerprint decides; a liveness pass (`reapLost`) runs on the tick because a dead runner moves no file; a card entering or leaving a human-waiting status forces one push | an idle board with 43 terminals did 7,294 fs calls, 8 git processes and 1.1 CPU s a minute for nothing; the first cut of this lost the dead-runner case, and the test that kills a real child now pins it |
|
|
210
|
+
| 2026-09-18 | src/server.mjs sessionsView | `readUsage` per rung per terminal, again for `capacity`, again for the accounts payload (143 reads per answer) | one reader per view | a login is read at most once per answer, and every part of the answer comes from the same reading |
|
|
211
|
+
| 2026-09-18 | src/synthesis.mjs | three `existsSync` per terminal per view | one `readdirSync` per checkout per view (`sessionFileIndex`) | 172 of 512 fs calls per answer, for files that mostly do not exist |
|
|
212
|
+
| 2026-09-18 | src/server.mjs, src/history/worktrees.mjs | `/api/worktrees` ran up to 40 git processes synchronously on the board's loop (9 to 25 s cold) | `listWorktreesAsync` (execFile, four in flight), one refresh per query, the last list served while a refresh runs | the worst `/api/health` during a cold call fell from 11.8 s to 3.2 s; the rest is `listHistory` inside `gather()`, still synchronous |
|
|
213
|
+
| 2026-09-18 | src/server.mjs /api/trunk | every card ledger read on every 2 s floor poll | a 15 s cache keyed by the window, cleared by any card change and by a landing | 27.5 to 0.5 ms p50, 113 to 15 fs calls per request |
|
|
214
|
+
| 2026-09-18 | src/session-detail.mjs | one `git ls-files` per file the terminal touched, every 3 s the drawer is open | one `git ls-files -z -- <paths>` for the list | 7 to 2 processes per detail answer, 1,247 to 305 ms |
|
|
215
|
+
| 2026-09-18 | src/server.mjs rows | every field of session.json on every push | `argv`, `runner_pid`, `head_at_start`, `checkpoints`, `agent_sessions`, `runtime_capabilities`, `files_touched` dropped from the row; `GET /api/sessions/<id>` still hands over the record | nothing under src/board/ read them; 8% off every push |
|
|
216
|
+
| 2026-09-18 | src/history/worktrees.mjs | the git env built by a helper | `MSYS_NO_PATHCONV: '1'` spelled out at each spawn site | test/lessons.test.mjs checks every git spawn line in src/ for it, and the helper hid it from that check |
|
package/docs/ERRORS.md
CHANGED
|
@@ -21,7 +21,7 @@ and "CI green" after a push means the run, not the publish; `npm view
|
|
|
21
21
|
**Fixed by deploying from the repo root with a root `.vercelignore` and
|
|
22
22
|
`--archive=tgz`; the recipe is in DECISIONS.md under "the site deploys itself
|
|
23
23
|
from git".** The `--yes` also auto-created an empty Vercel project named `leg`
|
|
24
|
-
that still needs `vercel project rm leg
|
|
24
|
+
that still needs `vercel project rm leg` (2026-09-18 later: `vercel project ls` no longer lists a `leg` project, so it is gone). The lesson: on a project with a Root
|
|
25
25
|
Directory set, the CLI must run from the repo root, and `--yes` is a consent to
|
|
26
26
|
create projects, not only to skip a confirmation.
|
|
27
27
|
|
package/docs/ROADMAP-v2.md
CHANGED
|
@@ -86,7 +86,18 @@ event names its actor, a board that reads only the ledger, and an auth/bind seam
|
|
|
86
86
|
The same probe shape applies to naming the terminal tab: whether codex, agy
|
|
87
87
|
and grok leave an OSC 2 title alone once the child starts drawing is
|
|
88
88
|
assumed, not known, which is why the tab title is the browser's and not the
|
|
89
|
-
terminal's.
|
|
89
|
+
terminal's. 0.15.0 made the claude half of this real across logins too (a
|
|
90
|
+
second login shares the conversation store, so a login switch keeps the
|
|
91
|
+
conversation): the codex probe now also covers a second `CODEX_HOME` with
|
|
92
|
+
its `sessions` store shared the same way.
|
|
93
|
+
7. **`stalled` and `repeating` on the row.** Two derived states from data the
|
|
94
|
+
record already holds: no turn, file write or commit for N minutes while not
|
|
95
|
+
waiting on a human; the same test red across two legs of one card. Printed
|
|
96
|
+
only, never acted on; a `looping` state waits for a false-positive study on
|
|
97
|
+
real transcripts. Design in `docs/review-2026-09-18.md`.
|
|
98
|
+
8. **The digest on the board.** `leg digest` and `/api/digest` shipped in
|
|
99
|
+
0.15.0; the panel above Terminals on the first load of the day is the
|
|
100
|
+
human surface it still lacks. Same review doc.
|
|
90
101
|
|
|
91
102
|
Also on the list: per-station prompt templates editable from the board, lease
|
|
92
103
|
suggestions from the diff of the previous leg, and a floor view that shows
|
package/docs/concepts.md
CHANGED
|
@@ -19,7 +19,7 @@ Each session gets a directory under `$LEG_HOME/sessions/<id>/`
|
|
|
19
19
|
|
|
20
20
|
| file | what it holds |
|
|
21
21
|
|------|---------------|
|
|
22
|
-
| `session.json` | the live record the board renders
|
|
22
|
+
| `session.json` | the live record the board renders. The runner owns it, and the board's usage poller, Claude Code's hooks and a board action patch fields on it too; every write is one atomic replace under `.session.lock` (`updateSession`), so no writer can lose another's field |
|
|
23
23
|
| `events.jsonl` | the timeline (see [VOCABULARY.md](VOCABULARY.md#session-event-types)) |
|
|
24
24
|
| `control.json` | requests from the board to the runner, for example `{ handoff: true }` |
|
|
25
25
|
| `hook.log` | what Claude Code's hooks sent, claude sessions only |
|
|
@@ -70,12 +70,17 @@ override, so agy stays one account.
|
|
|
70
70
|
Your harness is shared into an extra account, never copied into a fork that
|
|
71
71
|
drifts: the directories are junctions back to the real home (claude: `hooks`,
|
|
72
72
|
`skills`, `agents`, `commands`, `plugins`, `rules`, `scripts`,
|
|
73
|
-
`output-styles`, `tools`; codex: `skills`, `prompts`, `rules`,
|
|
74
|
-
`agents`, `hooks`, `memories`, `superpowers`), and the settings
|
|
75
|
-
copied fresh before every launch (claude: `settings.json`,
|
|
73
|
+
`output-styles`, `tools`, `projects`; codex: `skills`, `prompts`, `rules`,
|
|
74
|
+
`plugins`, `agents`, `hooks`, `memories`, `superpowers`), and the settings
|
|
75
|
+
files are copied fresh before every launch (claude: `settings.json`,
|
|
76
76
|
`settings.local.json`, `CLAUDE.md`, `keybindings.json`, `statusline.ps1`,
|
|
77
77
|
`statusline-combined.ps1`; codex: `config.toml`, `AGENTS.md`). Only the login
|
|
78
|
-
itself lives in the account directory.
|
|
78
|
+
itself lives in the account directory. claude's `projects` is its
|
|
79
|
+
conversation store, shared so that a hand-off to the second login can
|
|
80
|
+
`--resume` the transcript the first one was writing (see Handoff, step 3) and
|
|
81
|
+
so the same human's auto-memory follows them; `refreshAccount()` adds a
|
|
82
|
+
junction an older account is missing before every launch, and the history
|
|
83
|
+
index skips a junctioned store so a conversation is listed once.
|
|
79
84
|
|
|
80
85
|
`leg accounts add <claude|codex> <name>` creates one and prints the single
|
|
81
86
|
line to paste to log in. `leg accounts rm` removes the junctions as links,
|
|
@@ -161,7 +166,10 @@ in that picker ("the next option in the order") is still a press of the button.
|
|
|
161
166
|
`context-handoff-bundle save --repo-local --slug leg-<session id>`, with
|
|
162
167
|
`--update <slug>` after the first time, so one bundle per session is updated
|
|
163
168
|
in place. A checkpoint runs about every two minutes while the session has
|
|
164
|
-
turns, and at every warning, limit and hand-off.
|
|
169
|
+
turns, and at every warning, limit and hand-off. The periodic checkpoint runs
|
|
170
|
+
beside the terminal's poll rather than inside it, one at a time, so a slow
|
|
171
|
+
save never stops the terminal noticing a limit or a board button; a warning,
|
|
172
|
+
a limit and a hand-off save are still finished before the next leg starts.
|
|
165
173
|
2. **Choose.** `candidates()` lists the other accounts of the same agent first,
|
|
166
174
|
then walks the ladder, and after each rung, the other accounts of that
|
|
167
175
|
rung's agent. `evaluateLadder()` walks that list top to bottom and takes the
|
|
@@ -191,14 +199,22 @@ in that picker ("the next option in the order") is still a press of the button.
|
|
|
191
199
|
ladder for an active terminal; the wrapper reads it again at the transition
|
|
192
200
|
and during all-out waiting. Machine Settings is copied only when a new
|
|
193
201
|
terminal starts.
|
|
194
|
-
3. **Switch.** The agent process is stopped and the terminal restored.
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
+
3. **Switch.** The agent process is stopped and the terminal restored. Two
|
|
203
|
+
claude moves keep the conversation instead of taking the bundle, decided by
|
|
204
|
+
one rule both the terminal and the board's picker read
|
|
205
|
+
(`keepsConversation()` in `src/usage.mjs`): a same-login move to a weaker
|
|
206
|
+
model (a downshift, by the `fable, opus, sonnet, haiku` order), which runs
|
|
207
|
+
`claude --resume <agent_session_id> --model <alias>`; and a move to another
|
|
208
|
+
claude login whose home can see the transcript through its `projects`
|
|
209
|
+
junction, which runs `claude --resume <agent_session_id>` under that
|
|
210
|
+
login's `CLAUDE_CONFIG_DIR` (with `--model` only when the rung names one).
|
|
211
|
+
Both need the agent's own session id on the record; the second also needs
|
|
212
|
+
the transcript file to exist under the destination home, checked at the
|
|
213
|
+
switch, so a login made before the junction existed takes the bundle until
|
|
214
|
+
its next launch adds it. No bundle is written into a prompt, and the
|
|
215
|
+
ledger says `kept the conversation`. Every other rung, including a
|
|
216
|
+
same-login upshift back to a stronger model or a different agent, takes
|
|
217
|
+
the bundle: the `context-handoff-bundle load <id>` output
|
|
202
218
|
(with the `## Synthesis` section prepended if
|
|
203
219
|
`.leg/SYNTHESIS-<session-id>.md` is present) is written to
|
|
204
220
|
`.leg/RESUME-<session-id>.md` and copied to `.leg/RESUME.md`, and the next
|
|
@@ -314,6 +330,28 @@ on:
|
|
|
314
330
|
`leg share off` puts the board back on `127.0.0.1` and every link stops
|
|
315
331
|
working; `leg share rotate <name>` replaces one.
|
|
316
332
|
|
|
333
|
+
## The digest
|
|
334
|
+
|
|
335
|
+
`leg digest [--since 8h|2d|<iso>] [--json]` and `GET /api/digest?since=`
|
|
336
|
+
(owner only) answer the question a person asks after eight hours away
|
|
337
|
+
(`src/digest.mjs`). It reads the session records, their `events.jsonl`, the
|
|
338
|
+
cards' ledgers, `landings.jsonl` and the usage records, and writes nothing.
|
|
339
|
+
The record is `{ since, until, volume, attention, repos, walls }`: `volume`
|
|
340
|
+
is what was read (terminals, cards, landings and events in the window, and
|
|
341
|
+
the sessions and cards on disk), printed on the first line so a quiet night
|
|
342
|
+
and an empty home cannot read the same; `attention` is what needs a person,
|
|
343
|
+
ranked (a live terminal waiting on a question, a card in `waiting_human`,
|
|
344
|
+
`needs_approval` or `paused`, a card that failed in the window, a terminal
|
|
345
|
+
that was lost in the window); `repos` groups every terminal that moved or is
|
|
346
|
+
live, every card that moved or is live, and every landing, by repository;
|
|
347
|
+
`walls` is every login and every model that is out right now, soonest reset
|
|
348
|
+
first. A terminal counts when it moved in the window or is live now, and its
|
|
349
|
+
entry carries the events worth a line (`limit`, `handoff`, `all_out`,
|
|
350
|
+
`lost`, `ended`, `landed`, `bounced`, `continued`, `harness_blocked`), never
|
|
351
|
+
the running commentary. The audit trail ([Share](#share-more-than-one-human))
|
|
352
|
+
is the same data as a flat list of actors; the digest is the same data
|
|
353
|
+
grouped by what to do about it.
|
|
354
|
+
|
|
317
355
|
## Cards, stations and pipelines
|
|
318
356
|
|
|
319
357
|
A **card is a terminal you are not sitting at.** It has the same register
|
package/docs/configuration.md
CHANGED
|
@@ -35,7 +35,7 @@ These apply to `leg claude|codex|agy|grok`.
|
|
|
35
35
|
| `LEG_WAIT_TICK_MS` | `1000` | how often the all-out countdown redraws and re-checks Ctrl-C / End while waiting for the first reset | `src/attach.mjs` |
|
|
36
36
|
| `LEG_USAGE_POLL_MS` | `60000` | how often the board asks each login's usage source (one poller per login, not one per terminal); Claude uses its usage endpoint, Codex read-only app-server rate limits, Grok its billing proxy | `src/usage-poll.mjs` |
|
|
37
37
|
| `LEG_USAGE_POLL_MAX_MS` | `600000` | the longest the board waits between readings for one login: a refusal (429, timeout, no login) doubles the wait up to this, and the first good reading puts it back to `LEG_USAGE_POLL_MS` | `src/usage-poll.mjs` |
|
|
38
|
-
| `LEG_ATTACH_POLL_MS` | `2000` | how often the session loop re-reads the taps; git is re-read every third poll | `src/attach.mjs` |
|
|
38
|
+
| `LEG_ATTACH_POLL_MS` | `2000` | how often the session loop re-reads the taps; git is re-read every third poll, as one `git status --porcelain=v2 --branch` (`src/git.mjs`) that carries the head, the branch, the dirty list and the upstream's own ahead count | `src/attach.mjs` |
|
|
39
39
|
| `LEG_CLAUDE_USAGE_URL` | `https://api.anthropic.com/api/oauth/usage` | the usage endpoint, for a test double | `src/taps/claude-usage.mjs` |
|
|
40
40
|
| `LEG_CLAUDE_ARGS`, `LEG_CODEX_ARGS`, `LEG_AGY_ARGS` | (none) | space-separated extra arguments for a leg Leg starts on its own after a hand-off (your own `leg <agent> …` args never apply to the next agent); e.g. `LEG_CODEX_ARGS="-m gpt-5.3-codex-spark"` keeps a test chain on cheap models | `src/attach.mjs` |
|
|
41
41
|
| `LEG_LIVE_DIR` | `fixtures/live/` in a dev clone, else `~/.leg/live/` | where the first real limit payload per agent and signal is kept, secrets scrubbed (`src/live-capture.mjs`); a `leg sessions simulate-limit` payload is never kept | `src/live-capture.mjs`, `scripts/live-limits.mjs` |
|
|
@@ -133,7 +133,7 @@ permission, so it is always on.
|
|
|
133
133
|
|
|
134
134
|
| variable | default | meaning | read in |
|
|
135
135
|
|----------|---------|---------|---------|
|
|
136
|
-
| `LEG_HOME` | `~/.leg` | where sessions, usage, accounts, cards, runs
|
|
136
|
+
| `LEG_HOME` | `~/.leg` | where sessions, usage, accounts, cards, runs, the pidfiles and `installed.json` (which agent CLIs answered `--version`, remembered for a day per resolved bin) live | `src/store.mjs`, `src/ledger.mjs`, `src/runner.mjs`, `src/worktree.mjs`, `src/cards.mjs`, `src/sessions.mjs`, `src/usage.mjs`, `src/accounts.mjs` |
|
|
137
137
|
| `LEG_PORT` | `4747` | board server port | `src/server.mjs`, `src/launcher.mjs`, `bin/leg.mjs` (`open`) |
|
|
138
138
|
| `LEG_BIND` | `127.0.0.1` | board server bind address | `src/server.mjs`, `src/launcher.mjs` |
|
|
139
139
|
| `LEG_TOKEN` | (none) | bearer token required for `/api/*` and the event stream once set | `src/server.mjs`, `src/auth.mjs` |
|
|
@@ -205,7 +205,9 @@ home. `src/accounts.mjs` `LAYOUT` is the whole definition.
|
|
|
205
205
|
accounts.json the named accounts per agent
|
|
206
206
|
accounts/claude/<name>/
|
|
207
207
|
hooks/ skills/ agents/ commands/ plugins/ rules/ scripts/
|
|
208
|
-
output-styles/ tools/
|
|
208
|
+
output-styles/ tools/ projects/ junctions back to ~/.claude (projects is
|
|
209
|
+
the conversation store: a hand-off to this
|
|
210
|
+
login can --resume the same transcript)
|
|
209
211
|
settings.json settings.local.json CLAUDE.md keybindings.json
|
|
210
212
|
statusline.ps1 statusline-combined.ps1
|
|
211
213
|
copies, refreshed before every launch
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Review notes, 2026-09-18: product, performance and architecture pass
|
|
2
|
+
|
|
3
|
+
A requested first-principles pass over Leg as a product and as a program.
|
|
4
|
+
Working file, not published to the docs site. What shipped from it is in
|
|
5
|
+
`CHANGELOG.md` (0.15.0); what did not is designed here so the next pass can
|
|
6
|
+
pick it up without re-deriving it. Measurements are from
|
|
7
|
+
`scratchpad/prof/BASELINE.md` (kept out of the repo) and are summarised in
|
|
8
|
+
the CHANGELOG entry with their reproduction commands.
|
|
9
|
+
|
|
10
|
+
## What Leg is, and what it should become
|
|
11
|
+
|
|
12
|
+
Today Leg is a usage-wall relay: the same terminal keeps going when a login
|
|
13
|
+
stops. Around that wedge a lot arrived in one week: rungs of (agent, login,
|
|
14
|
+
model), a board, cards and pipelines, a machine-wide history index, a
|
|
15
|
+
portable harness, a shared board with roles and an audit trail. The wedge is
|
|
16
|
+
still the identity, and the reviews agree on the two things that make it
|
|
17
|
+
more than a relay:
|
|
18
|
+
|
|
19
|
+
1. **The terminal should never lose its conversation.** A same-login
|
|
20
|
+
downshift already kept it. 0.15.0 keeps it across logins too (the
|
|
21
|
+
`projects` junction plus one `keepsConversation` rule). The remaining gap
|
|
22
|
+
is codex across logins, unprobed.
|
|
23
|
+
2. **Leg holds the record of everything every agent did on this machine**,
|
|
24
|
+
and until 0.15.0 nothing read it back as an answer to a person. `leg
|
|
25
|
+
digest` is the first reader. The audit trail was the second-last.
|
|
26
|
+
|
|
27
|
+
Where it should go, in one sentence: from "a relay that keeps the terminal
|
|
28
|
+
alive" to "the layer that knows what every agent is doing, what it cost, and
|
|
29
|
+
what needs a person, and never loses the thread". Not an orchestrator that
|
|
30
|
+
invents work; a control layer over work the human started.
|
|
31
|
+
|
|
32
|
+
## Gaps found, ranked
|
|
33
|
+
|
|
34
|
+
| gap | evidence | status |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| a second login of the same vendor lost the conversation | `isDownshift` required the same account | **shipped 0.15.0** |
|
|
37
|
+
| nothing summarised what happened while you were away | audit trail is a flat list; ledger counts on the board | **shipped 0.15.0** (`leg digest`) |
|
|
38
|
+
| an idle terminal spent ~60 git processes a minute | `gitInfo` + `aheadCount` every 6 s | **shipped 0.15.0** (one `status --porcelain=v2 --branch`) |
|
|
39
|
+
| the board rebuilt its whole view every 10 s for nothing | health timer bypassed the fingerprint | **shipped 0.15.0** |
|
|
40
|
+
| every command loaded 68 modules, `--version` included | eager imports in `bin/leg.mjs`, two gratuitous edges | **shipped 0.15.0** |
|
|
41
|
+
| the checkpoint bundle froze limit detection for the length of a python spawn | sync `chb save` inside the poll interval | **shipped 0.15.0** |
|
|
42
|
+
| `.leg/DONE` is self-reported; a card is `completed` on the agent's word | `classify()` rule 6 | designed below (verify the claim) |
|
|
43
|
+
| a wedged agent looks like a thinking one; a loop burns the ladder | no stall/loop signal beyond `quiet Nm` | designed below |
|
|
44
|
+
| every new session re-learns the repository | SYNTHESIS dies with its session | designed below (project brain) |
|
|
45
|
+
| cards and terminals are two substrates for one idea | two stores, two status vocabularies, two hand-off loops | designed below (one leg primitive) |
|
|
46
|
+
| every ledger write is a Node process (85–114 ms) | `execFileSync(node, ledger.mjs)` per event | designed below |
|
|
47
|
+
| `codex resume <id> -m <model>` and a second `CODEX_HOME` unprobed | roadmap item 6 | still open |
|
|
48
|
+
|
|
49
|
+
## Designs for what was not built
|
|
50
|
+
|
|
51
|
+
### Verify the claim (cards)
|
|
52
|
+
|
|
53
|
+
`classify()` rule 6 says exit 0 plus `.leg/DONE` is `completed`. Add a
|
|
54
|
+
`verify` policy on the card (`test|none`, default `test` for pipelines with
|
|
55
|
+
no `test` station, else `none` because the station already runs it): on
|
|
56
|
+
`completed`, run `resolveTestCommand(repo)` in the worktree with the land
|
|
57
|
+
timeout, record `{ verified: 'green'|'red'|'untested', command, tail, at }`
|
|
58
|
+
on the run, and treat `red` as `incomplete` (hands off with the tail in the
|
|
59
|
+
bundle's Open questions). A DONE with an empty diff stays `completed`: an
|
|
60
|
+
analysis card legitimately changes nothing, so a zero diff is not evidence
|
|
61
|
+
of a lie. Surface: the card row's sentence (`claimed done; tests red (3
|
|
62
|
+
failures)`), `leg card show`. Tests: a fake adapter writing DONE with green,
|
|
63
|
+
red and no test command. Not built now because the plain `build` preset is
|
|
64
|
+
the one place it applies and the `build-land` preset already verifies.
|
|
65
|
+
|
|
66
|
+
### Stalled, looping, repeating
|
|
67
|
+
|
|
68
|
+
Three derived states, printed only from measured facts:
|
|
69
|
+
|
|
70
|
+
- `stalled`: live, not waiting on a human, and no turn, no file write and no
|
|
71
|
+
commit for N minutes (N per agent, default 12). Data: `last_activity`,
|
|
72
|
+
`files_touched`, `head`. The row says `stalled 14m` where it says `quiet
|
|
73
|
+
14m` today; the head count includes it.
|
|
74
|
+
- `looping`: the same (tool, path) or the same normalised error line three
|
|
75
|
+
times in the last 40 transcript-tail entries (`messagesFromLines` exists).
|
|
76
|
+
Row: `looping: Edit src/server.mjs 4x in 6m`. Never automatic action; a
|
|
77
|
+
Hand off now button already sits beside it.
|
|
78
|
+
- `repeating`: the same test name red across two legs of one card
|
|
79
|
+
(`bounce_reason` history). Stops the chain instead of spending the next
|
|
80
|
+
rung, the way `LEG_MAX_LAND_ATTEMPTS` already caps landings.
|
|
81
|
+
|
|
82
|
+
Not built now: `looping` needs a false-positive study on real transcripts
|
|
83
|
+
before it can print anything ("print nothing rather than a wrong number").
|
|
84
|
+
`stalled` and `repeating` are a day each and should come first.
|
|
85
|
+
|
|
86
|
+
### The project brain
|
|
87
|
+
|
|
88
|
+
`<repo>/.leg/BRAIN.md`, written by Leg's code and never by an agent turn:
|
|
89
|
+
decisions (dated, with who decided), ruled-out approaches, invariants (from
|
|
90
|
+
`CLAUDE.md`/`AGENTS.md`), hot files (from `files_touched` frequency) and open
|
|
91
|
+
questions. At hand-off the session's SYNTHESIS delta merges into it; every
|
|
92
|
+
first prompt of a leg and every card contract opens with its invariants and
|
|
93
|
+
ruled-out list before the bundle dump. `leg brain` prints it, `leg brain
|
|
94
|
+
--check` reports staleness like `leg resume --check`, `leg decide "<text>"`
|
|
95
|
+
appends. It subsumes `.leg/SYNTHESIS-<id>.md` as the per-session delta. Not
|
|
96
|
+
built now because it is a new file in the human's repository with a format
|
|
97
|
+
that has to be right the first time; it deserves its own pass with the
|
|
98
|
+
synthesis schema's authors.
|
|
99
|
+
|
|
100
|
+
### One leg primitive
|
|
101
|
+
|
|
102
|
+
The terminal loop (`src/attach.mjs`) and the card loop
|
|
103
|
+
(`src/orchestrator.mjs` + `src/chain.mjs`) do the same four steps: bundle,
|
|
104
|
+
choose next, switch, wait. What differs is real: the terminal chooses from
|
|
105
|
+
live usage at hand-off time and keeps one TTY; the card walks a static chain
|
|
106
|
+
and classifies post-mortem. The extractable pieces with no semantic change
|
|
107
|
+
are `bundle → prompt` and `prepare harness → proceed?` as shared functions,
|
|
108
|
+
and a per-agent tap table (`PER_AGENT[agent].poll(session, ctx) → patch`)
|
|
109
|
+
so a fifth agent is a table row. Making a card's chain a ladder evaluated at
|
|
110
|
+
hand-off time is the product decision that would unify the rest; it belongs
|
|
111
|
+
in the roadmap, not in a refactor.
|
|
112
|
+
|
|
113
|
+
### In-process ledger writes
|
|
114
|
+
|
|
115
|
+
`src/store.mjs` spawns `node src/ledger.mjs` per event (85 ms append, 114 ms
|
|
116
|
+
update; a scheduler tick that newly blocks six cards spends 487 ms in six of
|
|
117
|
+
them). The single-writer rule is a file lock (`.card.lock`), not a process
|
|
118
|
+
boundary, so the functions can be exported and called in-process. Three
|
|
119
|
+
things make it a refactor: `die()` exits, `assertNoSecrets` exits on a
|
|
120
|
+
secret-looking summary (in-process that must throw, never take the board
|
|
121
|
+
down), and `syncNotify` is async. Also `writeActive()` rescans every
|
|
122
|
+
`card.json` on every write and should be debounced. Not built now: medium
|
|
123
|
+
risk on the card path, which is the v0.1 extra, and the terminal path never
|
|
124
|
+
touches it.
|
|
125
|
+
|
|
126
|
+
### One event schema
|
|
127
|
+
|
|
128
|
+
Sessions have ~24 event types, cards 28 (`ledger.mjs EVENT_TYPES`), with
|
|
129
|
+
different actor shapes. `audit.mjs` and `digest.mjs` both merge the two by
|
|
130
|
+
hand. One `{ v, ts, subject:{kind,id}, actor, type, summary, measured }`
|
|
131
|
+
line format would let the audit, the digest, a future `leg events --follow`
|
|
132
|
+
and an outside subscriber read one stream. Do it when a third reader
|
|
133
|
+
appears.
|
|
134
|
+
|
|
135
|
+
## Rejected, with the reason
|
|
136
|
+
|
|
137
|
+
- **Answering a permission prompt from the board.** `stdio: 'inherit'` is
|
|
138
|
+
the architecture; a PTY between the human and their agent breaks "the same
|
|
139
|
+
agent you always run".
|
|
140
|
+
- **Routing by headroom instead of rung order.** "Rung 1 first, every time"
|
|
141
|
+
is the contract the ladder editor teaches; a sort by headroom makes the
|
|
142
|
+
next destination unpredictable. A `when: below:N` rung already expresses
|
|
143
|
+
the honest version.
|
|
144
|
+
- **Inferred workflows and automatic undo.** A recipe guessed from three
|
|
145
|
+
observations is a wrong number with a button; undoing a leg destroys work
|
|
146
|
+
that may be the only copy.
|
|
147
|
+
- **Marking a DONE with no diff as a false claim.** An analysis task changes
|
|
148
|
+
nothing and is still done.
|
|
149
|
+
- **Pooling a team's logins as one capacity market.** The README's terms
|
|
150
|
+
section draws the same-vendor line at an explicit human act.
|
|
151
|
+
|
|
152
|
+
## What Leg must never do without a human
|
|
153
|
+
|
|
154
|
+
Spend credits (`may_spend` off); push or open a PR; write a settings file
|
|
155
|
+
outside `leg harness enable`'s owned regions; move a credential; delete a
|
|
156
|
+
worktree with uncommitted work or an unmerged branch; pick a second login of
|
|
157
|
+
the same vendor the human did not add; read message bodies wholesale into an
|
|
158
|
+
index; send anything off the machine; answer a permission prompt; print a
|
|
159
|
+
number it does not have.
|
|
160
|
+
|
|
161
|
+
## Next three moves
|
|
162
|
+
|
|
163
|
+
1. **Probe codex across logins**: `codex resume <id> -m <model>` under a
|
|
164
|
+
second `CODEX_HOME` with the `sessions` store junctioned the way
|
|
165
|
+
`projects` now is for claude. One live run settles whether the second
|
|
166
|
+
`keepsConversation` case can include codex.
|
|
167
|
+
2. **`stalled` and `repeating`** as above: two derived states from data
|
|
168
|
+
already on the record, each with a fixture-driven test, no automatic
|
|
169
|
+
action.
|
|
170
|
+
3. **A digest panel on the board** above Terminals on the first load of the
|
|
171
|
+
day, dismissible, fed by `/api/digest`; the CLI and the route exist, the
|
|
172
|
+
board is the human surface.
|
package/fixtures/verified.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"_comment": "The single source of truth for every dated, numbered claim Leg makes in public. scripts/check-claims.mjs asserts that each surface listed here states exactly these values, and --strict additionally compares the pinned CLI versions against what those CLIs currently publish. Re-verify on a real machine, then edit this file and every surface in the same commit.",
|
|
3
3
|
"verifiedOn": "2026-09-11",
|
|
4
4
|
"verifiedOnLong": "11 September 2026",
|
|
5
|
-
"tests":
|
|
5
|
+
"tests": 955,
|
|
6
6
|
"clis": {
|
|
7
7
|
"claude": {
|
|
8
8
|
"label": "Claude Code",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ucsandman/legcli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Usage-limit monitor and automatic handoff for Claude Code, Codex, agy and Grok. Type leg claude|codex|agy|grok and get the same interactive agent with a board alongside, auto-approve on by default, usage tracking per agent and account, a live context handoff bundle, and at the limit the next agent continuing in the same terminal. 14-day free trial, then $79 once, 30-day money-back guarantee.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
package/src/accounts.mjs
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
// back into it with directory junctions, and the settings file is refreshed
|
|
7
7
|
// from the real home before every launch. Only the login lives in the
|
|
8
8
|
// account dir. agy 1.2.0 has no config-dir override, so it stays one account.
|
|
9
|
-
import { existsSync, mkdirSync, readFileSync, copyFileSync, readdirSync, lstatSync, symlinkSync, rmSync } from 'node:fs'
|
|
10
|
-
import { join } from 'node:path'
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, copyFileSync, readdirSync, lstatSync, symlinkSync, rmSync, realpathSync } from 'node:fs'
|
|
10
|
+
import { join, relative, dirname, basename, resolve, isAbsolute } from 'node:path'
|
|
11
11
|
import { homedir } from 'node:os'
|
|
12
12
|
import { home } from './store.mjs'
|
|
13
13
|
import { writeJsonAtomic } from './fsx.mjs'
|
|
@@ -19,7 +19,12 @@ export const LAYOUT = {
|
|
|
19
19
|
claude: {
|
|
20
20
|
env: 'CLAUDE_CONFIG_DIR',
|
|
21
21
|
home: () => process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude'),
|
|
22
|
-
|
|
22
|
+
// `projects` is the conversation store (projects/<encoded cwd>/<id>.jsonl,
|
|
23
|
+
// and the auto-memory beside it). Shared, not copied, so a hand-off to the
|
|
24
|
+
// second login can run `claude --resume <id>` on the transcript the first
|
|
25
|
+
// login was writing, and the same human's memory follows them. Claude Code
|
|
26
|
+
// does the writing; Leg reads that directory and never writes it.
|
|
27
|
+
share: ['hooks', 'skills', 'agents', 'commands', 'plugins', 'rules', 'scripts', 'output-styles', 'tools', 'projects'],
|
|
23
28
|
copy: ['settings.json', 'settings.local.json', 'CLAUDE.md', 'keybindings.json', 'statusline.ps1', 'statusline-combined.ps1'],
|
|
24
29
|
login: (dir) => `$env:CLAUDE_CONFIG_DIR='${dir}'; claude auth login`,
|
|
25
30
|
},
|
|
@@ -73,6 +78,45 @@ export function envFor(agent, account) {
|
|
|
73
78
|
return { [l.env]: accountDir(agent, account) }
|
|
74
79
|
}
|
|
75
80
|
|
|
81
|
+
// The config directory a login runs from: the CLI's own home for `default`,
|
|
82
|
+
// the account directory for anything else. Null for an agent Leg does not know.
|
|
83
|
+
export function homeFor(agent, account = 'default') {
|
|
84
|
+
const l = LAYOUT[agent]
|
|
85
|
+
if (!l) return null
|
|
86
|
+
return account === 'default' ? l.home() : accountDir(agent, account)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Whether the conversation file one login was writing is visible to another
|
|
90
|
+
// login of the same agent: the transcript's path relative to the source home
|
|
91
|
+
// must exist under the destination home. True through the `projects` junction
|
|
92
|
+
// an account carries; false for an account made before that junction existed
|
|
93
|
+
// (until its next launch adds it), for a transcript outside the source home,
|
|
94
|
+
// and for an agent with no per-account home at all. A false answer means the
|
|
95
|
+
// bundle, never a `--resume` that would open an empty conversation.
|
|
96
|
+
export function transcriptReachable(agent, transcriptPath, { from = 'default', to = 'default' } = {}) {
|
|
97
|
+
if (!transcriptPath || from === to || !LAYOUT[agent]?.env) return false
|
|
98
|
+
const dst = homeFor(agent, to)
|
|
99
|
+
if (!dst) return false
|
|
100
|
+
const where = realDir(dirname(transcriptPath))
|
|
101
|
+
// the transcript's place inside whichever home really holds it: the source
|
|
102
|
+
// login's, the destination's, or the CLI's own. A path a CLI reported through
|
|
103
|
+
// an account's junction resolves to the real home, and a record that kept
|
|
104
|
+
// the first login's path while the terminal moved on is still one file.
|
|
105
|
+
for (const account of new Set([from, to, 'default'])) {
|
|
106
|
+
const h = homeFor(agent, account)
|
|
107
|
+
if (!h) continue
|
|
108
|
+
const rel = relative(realDir(h), where)
|
|
109
|
+
if (rel.startsWith('..') || isAbsolute(rel)) continue
|
|
110
|
+
return existsSync(join(dst, rel, basename(transcriptPath)))
|
|
111
|
+
}
|
|
112
|
+
return false
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// A directory's real path, for comparing a transcript's location against a
|
|
116
|
+
// home that may itself be reached through a junction; the path as given when
|
|
117
|
+
// it does not exist.
|
|
118
|
+
function realDir(p) { try { return realpathSync.native(p) } catch { return resolve(p) } }
|
|
119
|
+
|
|
76
120
|
function junction(target, link) {
|
|
77
121
|
if (existsSync(link)) return false
|
|
78
122
|
if (!existsSync(target)) return false
|
|
@@ -80,6 +124,22 @@ function junction(target, link) {
|
|
|
80
124
|
return true
|
|
81
125
|
}
|
|
82
126
|
|
|
127
|
+
// The shared directories, junctioned into the account dir when they are not
|
|
128
|
+
// there yet. Runs at creation and before every launch, so an account made by
|
|
129
|
+
// an older Leg picks up a directory added to `share` since (the `projects`
|
|
130
|
+
// store, for one) the next time it starts.
|
|
131
|
+
export function ensureShared(agent, name) {
|
|
132
|
+
if (name === 'default') return []
|
|
133
|
+
const l = LAYOUT[agent]
|
|
134
|
+
if (!l?.env) return []
|
|
135
|
+
const dir = accountDir(agent, name)
|
|
136
|
+
if (!existsSync(dir)) return []
|
|
137
|
+
const src = l.home()
|
|
138
|
+
const shared = []
|
|
139
|
+
for (const d of l.share) if (junction(join(src, d), join(dir, d))) shared.push(d)
|
|
140
|
+
return shared
|
|
141
|
+
}
|
|
142
|
+
|
|
83
143
|
// Create the account dir, junction the shared harness in, copy the settings.
|
|
84
144
|
export function addAccount(agent, name) {
|
|
85
145
|
if (!NAME_RE.test(name) || name === 'default') throw new Error(`invalid account name "${name}" (letters, digits, - and _; not "default")`)
|
|
@@ -88,9 +148,7 @@ export function addAccount(agent, name) {
|
|
|
88
148
|
if (!l.env) throw new Error(`${agent} has no config-dir override in the installed version; extra accounts are not possible`)
|
|
89
149
|
const dir = accountDir(agent, name)
|
|
90
150
|
mkdirSync(dir, { recursive: true })
|
|
91
|
-
const
|
|
92
|
-
const shared = []
|
|
93
|
-
for (const d of l.share) if (junction(join(src, d), join(dir, d))) shared.push(d)
|
|
151
|
+
const shared = ensureShared(agent, name)
|
|
94
152
|
refreshAccount(agent, name)
|
|
95
153
|
const acc = readAccounts()
|
|
96
154
|
if (!acc[agent]) acc[agent] = ['default']
|
|
@@ -98,11 +156,13 @@ export function addAccount(agent, name) {
|
|
|
98
156
|
return { dir, shared, login: l.login(dir), env: l.env }
|
|
99
157
|
}
|
|
100
158
|
|
|
101
|
-
// Before each launch: bring the copied files up to date with the real home
|
|
159
|
+
// Before each launch: bring the copied files up to date with the real home,
|
|
160
|
+
// and add any shared directory the account is still missing.
|
|
102
161
|
export function refreshAccount(agent, name) {
|
|
103
162
|
if (name === 'default') return []
|
|
104
163
|
const l = LAYOUT[agent]
|
|
105
164
|
const dir = accountDir(agent, name)
|
|
165
|
+
ensureShared(agent, name)
|
|
106
166
|
const copied = []
|
|
107
167
|
for (const f of l.copy) {
|
|
108
168
|
const s = join(l.home(), f)
|