@ucsandman/legcli 0.14.0 → 0.15.1

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/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; the runner is its only writer, and every write is one atomic replace |
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`, `plugins`,
74
- `agents`, `hooks`, `memories`, `superpowers`), and the settings files are
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. A
195
- same-login move to a weaker claude model (a downshift, by the
196
- `fable, opus, sonnet, haiku` order) with the agent's own session id on the
197
- record keeps the conversation instead: Leg runs
198
- `claude --resume <agent_session_id> --model <alias>`, no bundle is written
199
- into a prompt, and the ledger says so. Every other rung, including an
200
- upshift back to a stronger model, a different account, or a different
201
- agent, takes the bundle: the `context-handoff-bundle load <id>` output
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
@@ -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 and the pidfiles 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` |
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/ junctions back to ~/.claude
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
package/docs/faq.md CHANGED
@@ -20,14 +20,15 @@ launch, and does not count as a usage limit.
20
20
 
21
21
  **Why does Leg poll an endpoint for claude's usage instead of reading the
22
22
  status line?**
23
- Because Claude Code 2.1.268 did not run a custom status line from a settings
24
- file Leg controls when this was tried on 2026-09-11 (recorded in
23
+ Because Claude Code 2.1.268 and 2.1.278 did not run a custom status line from a settings
24
+ file Leg controls when this was tried on 2026-09-11 and 2026-09-19 (recorded in
25
25
  [DEVIATIONS.md](DEVIATIONS.md)); hooks from the same `--settings` file did
26
26
  fire. So the numbers come from
27
27
  `GET api.anthropic.com/api/oauth/usage` with the login Claude Code already
28
28
  stored, which is the same data `/usage` shows. Leg still writes the
29
29
  `statusLine` entry, so the endpoint poll becomes a fallback the moment a build
30
- honours it, and your own status-line command is chained first either way. See
30
+ honours it, and your own status-line command runs first, its rows above
31
+ Leg's one, either way (since 0.15.1). See
31
32
  [adapters.md](adapters.md#claude) and `src/taps/claude-usage.mjs`.
32
33
 
33
34
  **Why does codex get no hook when claude does?**
@@ -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.
@@ -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": 926,
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.14.0",
3
+ "version": "0.15.1",
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
- share: ['hooks', 'skills', 'agents', 'commands', 'plugins', 'rules', 'scripts', 'output-styles', 'tools'],
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 src = l.home()
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)