@ucsandman/legcli 0.9.0 → 0.10.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 CHANGED
@@ -1,5 +1,81 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.0 (2026-09-17)
4
+
5
+ - **The board is responsive again, `leg` starts straight away, and the board
6
+ opens every time.** One running terminal was enough to saturate the board
7
+ server: it rewrites its record every few seconds and takes a control lock
8
+ about once a second, and the watcher over the sessions directory rebuilt the
9
+ whole terminals view — over a second of `git` — for every one of those
10
+ touches, including the lock files and the temp files atomic writes leave
11
+ behind. The server spent all its time in that loop, so it answered a
12
+ stylesheet in fourteen seconds and `/api/health` in seven. Four consequences,
13
+ all fixed:
14
+ - The watcher now drops locks and temp files by name, and checks the rest
15
+ against a stat fingerprint of the files the view is actually built from,
16
+ because a lock taken inside a session directory changes that directory's
17
+ own mtime and arrives as an event naming nothing else. What survives is
18
+ floored to one rebuild per interval. `canLand` is cached per record
19
+ revision with staggered expiries, so twenty worktrees are never re-read in
20
+ one pass. The terminals view went from 2,000 ms to 115 ms; on a board with
21
+ 66 sessions `/api/health` went from 14.2 s to 2.4 ms and `/board.css` from
22
+ 14.2 s to 1.5 ms.
23
+ - `leg claude` treated a board too busy to answer a health probe as no board
24
+ at all, spawned a second server that could only die of `EADDRINUSE`, then
25
+ polled the dead child for fifteen seconds. A listener on the port is now a
26
+ board: `leg` attaches to it.
27
+ - The board is opened whether or not this terminal is the one that started
28
+ it. Previously the browser was only opened on the path that spawned the
29
+ server, so with a board already running nothing ever opened.
30
+ - One push rebuilt the terminals grid three times (two listeners registered
31
+ for `leg:sessions` and one for the legacy alias, with the payload parsed
32
+ twice per push and one dispatch outside the staleness guard). Every button
33
+ was destroyed and recreated several times a second. One push now rebuilds
34
+ once, and an open confirm row is left alone until it is answered.
35
+
36
+ - **Remove, Remove record, End and Land work.** Confirming any of them did
37
+ nothing at all. `confirmRow` clears `pendingConfirm` and then calls its
38
+ callback, and the callback read `pendingConfirm.action` — off the variable it
39
+ had just cleared. Every Yes on the Terminals panel threw
40
+ `TypeError: Cannot read properties of null (reading 'action')` into the
41
+ console and never reached the request. The callback now closes over a
42
+ snapshot taken before the row is built.
43
+
44
+ - **Every conversation on this machine (`leg history`, `leg worktrees`, the
45
+ board's Conversations cell).** One read-only index over the conversations
46
+ Claude Code, Codex, Grok, Antigravity and Copilot CLI keep in their own
47
+ stores, plus Leg's own sessions, deduplicated (a session Leg started and
48
+ the same conversation in the agent's store are one row, marked `leg`; the
49
+ rest are `external`). List, filter by agent, repository or text, open one
50
+ (its last messages, read from the tail of the transcript only when asked),
51
+ and `leg history continue <id>` to start a supervised leg on it where the
52
+ installed CLI documents resume-by-id (claude, codex, grok, agy; copilot
53
+ lists and reads but is not an agent Leg runs). Nothing in an agent's store
54
+ is moved or written; Leg writes only `~/.leg/history/index.json`, refreshed
55
+ incrementally by size and mtime, and never opens a SQLite file. Every
56
+ checkout Leg can see, git's and its own and the ones conversations ran in,
57
+ with owner, uncommitted count, orphaned, stale and missing verdicts, read
58
+ only. On a shared board the whole group is the owner's. Redaction learned
59
+ the shapes another agent's transcript carries (Stripe, Google, xAI, npm,
60
+ GitLab, Hugging Face keys, JWTs, private-key blocks, basic auth, URL
61
+ credentials, `password=`), and the board token joined the values it strips.
62
+ Support matrix and every file read: docs/history.md.
63
+ - **History, after its review.** The board's Conversations cell pages past
64
+ 150 rows (the total no longer shrank with each cursor page); the board
65
+ refreshes a stale index in a child process instead of its own event loop;
66
+ a cwd on an unreachable network share no longer stalls a refresh; an
67
+ Antigravity retitle or a touched presence lock is seen by an incremental
68
+ refresh, and only that conversation is re-read; an older Leg session id
69
+ still opens the conversation it was one leg of; `leg history` prints a
70
+ failed refresh whatever the last index still lists, `refresh` fails cleanly
71
+ when another Leg holds the lock, `--limit 0` is a usage error (`--all`),
72
+ `--offset` works, a Leg-only row's id prints whole; a malformed id on
73
+ `/api/history/<id>` is a 400; an account name that is a path is ignored;
74
+ titles are cut to 200 as documented and go through the held-value
75
+ redaction too; the Copilot chip has a colour. Redaction no longer eats
76
+ `Basic authentication/authorization`, `cache_key = ...` or
77
+ `refresh_token: string`, and a value never crosses a line break.
78
+
3
79
  ## 0.9.0 (2026-09-16)
4
80
 
5
81
  - **The portable harness (`leg harness`), off by default.** A hand-off can now
package/README.md CHANGED
@@ -95,6 +95,7 @@ yourself.
95
95
  - [Two sessions in one repo](#two-sessions-in-one-repo)
96
96
  - [More than one human](#more-than-one-human)
97
97
  - [The board](#the-board)
98
+ - [Every conversation on this machine](#every-conversation-on-this-machine)
98
99
  - [Second accounts, and what the terms say](#second-accounts-and-what-the-terms-say)
99
100
  - [What is and is not touched](#what-is-and-is-not-touched)
100
101
  - [CLI reference](#cli-reference)
@@ -336,12 +337,40 @@ one sam's; sam's board showed wes's card with the prompt hidden and only
336
337
  - **Buttons**, in a fixed order that never reflows: Land, Hand off now,
337
338
  Details, End. Once a session has ended, Remove and Remove record take End's
338
339
  place. Details opens an expansion in flow under the panel.
339
- - Below it, optional **Background tasks** an agent runs in a separate worktree
340
- without joining the terminal conversation (see below), then **Settings**.
340
+ - Below it, **Conversations**: every coding-agent conversation on this machine
341
+ as a count that opens, the ones Leg started and the ones Claude Code, Codex,
342
+ Grok, Antigravity and Copilot keep in their own stores. Filter by agent,
343
+ search, repository, or only what Leg started; a row opens its last messages
344
+ in place, with the `leg history continue` command to copy where the agent
345
+ can resume by id. The same drawer lists every checkout Leg can see. Then
346
+ optional **Background tasks** an agent runs in a separate worktree without
347
+ joining the terminal conversation (see below), then **Settings**.
341
348
 
342
349
  The board reads `~/.leg/sessions/*/session.json` over server-sent events; a
343
350
  session whose runner process is gone is marked `lost`, never shown as live.
344
351
 
352
+ ## Every conversation on this machine
353
+
354
+ Claude Code, Codex, Grok, Antigravity and Copilot each keep their history in
355
+ their own place. `leg history` is one list over all of them, whether or not
356
+ Leg started the conversation, and `leg worktrees` is one list over every
357
+ checkout: git's, Leg's own, and the ones those conversations ran in.
358
+
359
+ ```
360
+ leg history newest first, every agent; --provider, --repo, --search, --json
361
+ leg history show claude:0fc5 where it ran, its last messages, whether Leg can continue it
362
+ leg history continue claude:0fc5 leg claude --resume <id> in that folder, supervised like any session
363
+ leg worktrees path, repo, branch, exists, uncommitted, owner, conversations, stale
364
+ ```
365
+
366
+ Nothing moves: each agent's store stays where it was, Leg writes only its own
367
+ `~/.leg/history/index.json`, reads transcripts from their head and tail, and
368
+ opens messages only when you open a conversation. A session Leg started and
369
+ the same conversation in the agent's store are one row, marked `leg`; the rest
370
+ are `external`. On a shared board the whole group is the owner's. The support
371
+ matrix (which agents list, show messages, continue) and every file read are in
372
+ [docs/history.md](docs/history.md).
373
+
345
374
  ## Second accounts, and what the terms say
346
375
 
347
376
  Optional. `leg accounts add claude work` creates
@@ -387,6 +416,10 @@ happens after you run `leg accounts add`; that is your call.
387
416
  `~/.codex/config.toml`, agy's files, your repo's settings. Claude Code gets
388
417
  hooks through a per-session `--settings` file under `~/.leg`; codex and
389
418
  agy get nothing injected.
419
+ - **Read, never written**: each agent's own history (`~/.claude/projects`,
420
+ `~/.codex/sessions`, `~/.grok/sessions`, `~/.gemini/antigravity-cli`,
421
+ `~/.copilot/session-state`) for `leg history`; the index it builds lives
422
+ under `~/.leg/history/`, and no SQLite file is ever opened.
390
423
  - **Written only after `leg harness enable`** ([the portable harness](docs/harness.md),
391
424
  off by default): the destination client's global rules file
392
425
  (`~/.codex/AGENTS.md`, `~/.gemini/GEMINI.md`, `~/.claude/leg-rules.md` plus
@@ -429,6 +462,10 @@ leg sessions show|events <id>
429
462
  leg sessions handoff|end <id> same as the board buttons
430
463
  leg sessions rm <id> forget an ended session
431
464
  leg sessions simulate-limit <id> the real limit path without a real wall (claude, agy, grok)
465
+ leg history [ls] [--provider p] [--repo r] [--search q] [--managed|--external] [--live] [--all] [--json]
466
+ every conversation on this machine, Leg's own and the agents' own (read only)
467
+ leg history show <id> [--messages n] [--json] | continue <id> [agent args…] | refresh [--full] | providers
468
+ leg worktrees [--repo <path>] [--no-dirty] [--json] every checkout: git's, Leg's, the conversations' (read only)
432
469
  leg accounts ls logins and their 5h/7d usage
433
470
  leg accounts add <claude|codex|grok> <name> | rm <agent> <name> | terms
434
471
  leg harness status|inspect|check|explain|history [--json] the portable harness, read-only
@@ -534,6 +571,7 @@ More in [docs/faq.md](docs/faq.md).
534
571
  | [Board guide](docs/board-guide.md) | every word, number and button on the board explained |
535
572
  | [Configuration](docs/configuration.md) | environment variables and options |
536
573
  | [Portable harness](docs/harness.md) | carrying rules, hooks, skills, agents, commands and MCP servers to the agent a hand-off lands on: what moves, what does not, policies, ownership, secrets |
574
+ | [History](docs/history.md) | `leg history` and `leg worktrees`: every conversation and checkout on this machine across agents, the support matrix, what is read and what is written |
537
575
  | [Adapters](docs/adapters.md) | what each CLI exposes and how Leg attaches to it |
538
576
  | [CLI contracts](docs/cli-contracts.md) | exact argv per CLI and the limit-signal table with sources |
539
577
  | [FAQ](docs/faq.md) | a question the others did not answer |
package/bin/leg.mjs CHANGED
@@ -27,6 +27,7 @@ import { home } from '../src/store.mjs'
27
27
  import { entitlement, allows, describe as describeLicense, activate as activateLicense, deactivate as deactivateLicense, refresh as refreshLicense, licensePath, BUY_URL } from '../src/license.mjs'
28
28
  import { resumeVerdict, bodyOf, ago } from '../src/resume.mjs'
29
29
  import { harnessCommand } from '../src/harness/cli.mjs'
30
+ import { historyCommand, worktreesCommand } from '../src/history/cli.mjs'
30
31
 
31
32
  const SRC = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'src')
32
33
  // one source of truth for the version, so the help text cannot drift from the package
@@ -329,6 +330,21 @@ async function main() {
329
330
  const code = await harnessCommand(cmd, args, { out, die })
330
331
  process.exit(code)
331
332
  }
333
+ if (group === 'history' || group === 'worktrees') {
334
+ // Every conversation on this machine, Leg's own and the ones the agents'
335
+ // stores hold: a read-only index (src/history/index.mjs). `continue`
336
+ // starts a normal supervised leg on one of them. `leg history --json` is
337
+ // `leg history ls --json`: a leading flag names no verb.
338
+ const isHelp = cmd === '--help' || cmd === '-h' || cmd === 'help' || args.help || args.h
339
+ const bare = typeof cmd === 'string' && cmd.startsWith('--')
340
+ const verb = isHelp ? 'help' : (bare ? 'ls' : cmd)
341
+ const a = bare ? parseArgs([cmd, ...rest]) : args
342
+ // `continue <id> [agent args...]`: what follows the id goes to the agent
343
+ // untouched, the way `leg claude [args...]` passes its argv straight through
344
+ const raw = bare ? [cmd, ...rest] : rest
345
+ const code = group === 'history' ? await historyCommand(verb, a, { out, die, raw }) : worktreesCommand(verb, a, { out, die })
346
+ process.exit(code)
347
+ }
332
348
  if (group === 'license') {
333
349
  // The paid gate. Keys verify offline against the public key in
334
350
  // src/license.mjs; nothing here talks to the network except refresh.
@@ -454,13 +470,19 @@ async function main() {
454
470
  out(openBoard(url) ? `opened ${url}` : `could not open a browser; visit ${url}`)
455
471
  return
456
472
  }
457
- if (group && group !== '--help' && group !== 'help') die(2, `unknown command "${group}" (claude|codex|agy|grok|sessions|resume|accounts|harness|license|share|up|down|status|open|card|scheduler|uninstall)`)
473
+ if (group && group !== '--help' && group !== 'help') die(2, `unknown command "${group}" (claude|codex|agy|grok|sessions|history|worktrees|resume|accounts|harness|license|share|up|down|status|open|card|scheduler|uninstall)`)
458
474
  out(`leg ${VERSION}, your coding agents, with a board alongside and a handoff when one hits its limit
459
475
  claude|codex|agy|grok [args...] the normal interactive agent in this terminal; args pass straight through
460
476
  the board opens once, the session shows as a card, usage is tracked, a limit hands off
461
477
  a second live session in one checkout gets its own worktree (--no-worktree to share)
462
478
  auto-approve mode (--no-auto-approve to opt out)
463
479
  sessions ls|show|events|handoff|end|rm|simulate-limit <id>
480
+ history [ls] [--provider p] [--repo r] [--search q] [--json]
481
+ every conversation on this machine: Leg's own, and the ones Claude Code, Codex,
482
+ Grok, Antigravity and Copilot keep in their own stores (read only, nothing moved)
483
+ history show|continue <id> | refresh | providers
484
+ one conversation, or start leg <agent> on it where the agent can resume by id
485
+ worktrees [--repo r] [--json] every checkout Leg can see: git's, its own, the ones conversations worked in
464
486
  resume [--check] [--json] [--path <dir>] the hand-off waiting in this checkout, and whether it is still true
465
487
  freshness is recomputed from git at read time; --check prints only the verdict
466
488
  exit 0 current, 1 stale or unstamped, 3 no pointer here
package/docs/ERRORS.md CHANGED
@@ -3,6 +3,86 @@
3
3
  What broke, why, and what fixed it. One entry per failure, newest first. A first
4
4
  occurrence has to be written down or a repeat is never countable.
5
5
 
6
+ ## 2026-09-17: one running terminal saturated the board's event loop, and four separate symptoms came out of it
7
+
8
+ **Fixed in `src/server.mjs` (watcher filter, stat fingerprint, push floor, cached
9
+ `canLand`), `src/attach.mjs` (a taken port is a board; always open it) and
10
+ `src/board/{board,sessions}.js` (one render per push, hold an open confirm row).
11
+ Regression tests: `test/board-responsiveness.test.mjs`.**
12
+
13
+ Reported as four bugs: `leg claude` took a long time to start, the board never
14
+ opened by itself, the board was slow to load and to react, and Remove under
15
+ Terminals did nothing. All four were one cause. The board server had burned
16
+ 17,080 seconds of CPU — it answered `/api/health` in 7 to 14 seconds and a
17
+ 40 KB stylesheet in 14. A live agent rewrites its session record every six
18
+ seconds or so and takes a control lock about once a second; the recursive
19
+ watcher over the sessions directory answered every one of those — including
20
+ `.control.lock`, `.session.lock` and the `session.json.*.tmp` files atomic
21
+ writes leave behind — with a full rebuild of the terminals view, which costs
22
+ about two seconds (1.1 s of it `canLand` shelling out to git across twenty
23
+ worktrees). 105 filesystem events in 30 seconds against a 300 ms debounce and a
24
+ 2,000 ms rebuild: the queue could never drain.
25
+
26
+ What that one stall produced:
27
+
28
+ - **Slow start.** `ensureBoard`'s health probe times out at 2 s, so a merely
29
+ busy board read as no board. `leg` spawned a second server, which died of
30
+ `EADDRINUSE` (the evidence was already in `~/.baton/board.log`), then polled
31
+ the dead child every 200 ms for the full 15 s.
32
+ - **The board never opened.** `openBoard(url)` sat only on the branch that
33
+ successfully started a server. With one already running, or on the 15 s
34
+ give-up path, nothing was ever opened.
35
+ - **Slow to react.** `board.js` dispatched `leg:sessions` and `baton:sessions`
36
+ per push, parsing the quarter-megabyte payload twice, with the second
37
+ dispatch outside the `state.es === es` staleness guard (missing braces on a
38
+ one-line `if`). `sessions.js` was registered on `leg:sessions` twice and on
39
+ the alias once. One push rebuilt the whole grid three times.
40
+ - **Slow to react, part two.** `renderSessions` starts with
41
+ `grid.textContent = ''`, so with three rebuilds per push every button was
42
+ destroyed and recreated several times a second.
43
+
44
+ **Remove was a second, independent bug, and fixing the first did not fix it.**
45
+ Reported again as "it's still not removing them" after all of the above
46
+ shipped. `confirmRow` does `pendingConfirm = null; onYes(yes)`, and the call
47
+ site passed `(btn) => act(s.session_id, pendingConfirm.action, btn)` — a
48
+ closure over the module variable, not over its value. Every Yes on the
49
+ Terminals panel threw `TypeError: Cannot read properties of null (reading
50
+ 'action')` before reaching `act()`, so Remove, Remove record, End and Land had
51
+ never worked from the confirm row; the exception went to the console and
52
+ nowhere the reader would see it. Fixed by snapshotting
53
+ `const pending = pendingConfirm` before building the row. Caught by driving the
54
+ real page: the click produced zero network requests, which pointed at the
55
+ handler rather than at anything server-side. The instrument that found it was
56
+ five lines — wrap `window.fetch`, listen for `window.onerror`, click, read
57
+ both.
58
+
59
+ Three lessons. First, a name filter is not enough to classify a filesystem event:
60
+ taking a lock inside a directory changes that directory's own mtime, and the
61
+ event arrives naming only the directory. The watcher now treats an event as a
62
+ hint and compares a stat fingerprint of the files the view is really built from
63
+ — sub-millisecond, against a 2 s rebuild. Second, the CPU counter on a
64
+ long-lived process names a stall in one command, before any code is read.
65
+
66
+ Third, and the one that cost a round trip: several symptoms reported together
67
+ are not thereby one bug. Three of these four were, and finding that cause made
68
+ the fourth *look* explained — the render churn is a real way to drop a click,
69
+ so "Remove doesn't work" fit the story. It was a different bug in a different
70
+ file, and it was still there after the fix shipped. A symptom is only closed
71
+ once it has been driven and watched, not once a plausible cause for it has been
72
+ found and fixed. Clicking Remove in the real page would have taken two minutes
73
+ and would have shown zero network requests and a `TypeError` in the console.
74
+
75
+ Verified by running the new tests against `da453b7`: 7 of 8 fail there, 8 of 8
76
+ pass after, and the busy-board test takes 21 s before versus 4 s after. On the
77
+ live board, same 66 sessions: `/api/health` 14.19 s → 2.4 ms, `/board.css`
78
+ 14.24 s → 1.5 ms, `/api/sessions` 15.64 s → 109 ms, view rebuild 2,049 ms →
79
+ 115 ms, renders per push 3 → 1.
80
+
81
+ Also found while verifying: `npm run lint` reported "ESLint: No issues found"
82
+ with `node_modules/.bin` empty and eslint not installed — a wrapper was printing
83
+ it. Run `npm install` first and check that ESLint really ran; a lint result from
84
+ a tool that is not on disk is not a lint result.
85
+
6
86
  ## 2026-09-16: a hand-edited `package.json` version left `package-lock.json` behind, and the publish gate refused 0.9.0
7
87
 
8
88
  **Fixed by bumping the lockfile root version; the gate is `scripts/npm-publish-gate.mjs`.**
@@ -395,3 +475,57 @@ it), and check whether a board was listening on 4747 at the time.
395
475
  module that reads the environment at import time cannot be imported from a
396
476
  helper that runs before the environment is set, no matter how small the thing
397
477
  you wanted from it.
478
+
479
+ ## A 3,000-line feature landed with red CI on Windows and no docs page (2026-09-16)
480
+
481
+ - **Symptom.** Commit `c7f24a0` (the history index, committed by the agy
482
+ session that took over after a Claude usage limit) went green on Ubuntu and
483
+ macOS and red on Windows in two `history.test.mjs` cases, and the `docs` job
484
+ failed with "site/ is out of date". `/docs/history` did not exist and every
485
+ link to `history.md` pointed at the npm package page.
486
+ - **Root cause, tests.** GitHub's Windows runner hands out an 8.3 short TEMP
487
+ path (`RUNNER~1`) while git reports the long form (`runneradmin`). The two
488
+ assertions compared a `mkdtemp` path with a path that had been through git.
489
+ The repo already had the seam for this (`canonPath` in `src/fsx.mjs`,
490
+ used the same way in `handoff-order.test.mjs`); the new tests did not use it.
491
+ It cannot be seen on a developer machine with 8.3 names disabled.
492
+ - **Root cause, docs.** `scripts/build-docs-site.mjs` builds from a hard-coded
493
+ `PAGES` list. A new `docs/*.md` that is not in it gets no page, and
494
+ `rewriteHref` sends its links to the npm URL by design. Nobody ran
495
+ `npm run docs` before pushing, so the docs job was the first to notice.
496
+ - **Fix.** `ebda794`: both sides of the four path assertions through
497
+ `canonPath`; reproduced locally by pointing `TEMP` at a case-altered path
498
+ (seen failing, then 16/16); `history` registered in `PAGES`; site rebuilt.
499
+ - **The lesson that generalises.** A test that compares two filesystem paths
500
+ on Windows compares two spellings of the same folder unless both go through
501
+ `canonPath`. And a new `docs/*.md` is two edits, the file and the `PAGES`
502
+ entry, then `npm run docs`; the docs job exists because the second and third
503
+ get forgotten.
504
+
505
+ ## The review that was in flight when the limit hit never ran; the feature shipped with 38 defects (2026-09-17)
506
+
507
+ - **Symptom.** `c7f24a0` (history discovery) was committed and pushed by the
508
+ agent that took over after a Claude usage limit. The nine-lens adversarial
509
+ review the Claude session had launched died with that session. Re-run the
510
+ next day it confirmed 38 findings, 3 high: the board's Conversations cell
511
+ could never page past 150 rows (the total was counted after the cursor
512
+ slice), an unreachable UNC cwd blocked a refresh for seconds per record,
513
+ and an Antigravity retitle was invisible to every incremental refresh
514
+ (the directory's mtime was the cache key; an in-place rewrite never moves
515
+ it).
516
+ - **Root cause.** The hand-off bundle carried "what was established" and
517
+ "next moves" but not "what was in flight": a background workflow is not a
518
+ file, so the next agent saw a clean-looking task and shipped it. Nothing
519
+ in the ship path asks whether the change was reviewed.
520
+ - **Fix.** `582dfe5`: 20 findings closed, each with a test that fails on the
521
+ previous source (proved on a detached worktree of HEAD before the fix).
522
+ - **Left open, by choice.** Coverage-only findings (live markers, the
523
+ write-scope guard, the flag surfaces, extra accounts), a low-severity
524
+ dedup edge (a conversation split over two Claude transcript files when Leg
525
+ recorded only the older path), the `sessions ls` row format, and the
526
+ pretty-printed index file.
527
+ - **The lesson that generalises.** A review is part of the change, not a
528
+ step after it: launch it before the feature is committable and record it
529
+ in the hand-off as in flight, or the next agent ships without it. And a
530
+ cache keyed on a directory's mtime sees files added and removed, never a
531
+ file rewritten in place.
package/docs/README.md CHANGED
@@ -7,6 +7,7 @@
7
7
  - [board-guide.md](board-guide.md): the instrument head (a row per login, two window rails each, the 85 percent post), the Terminals panels, overlap flags, Landed on main, Background tasks, Settings, and the floor view.
8
8
  - [configuration.md](configuration.md): every environment variable, the accounts layout, `.env`, network exposure, card-level options.
9
9
  - [harness.md](harness.md): the portable harness, off by default: what moves between agents and what does not, the first run, policies, ownership and backups, secrets, the evidence trail, and how Leg relates to the Agnostic AI engine it embeds.
10
+ - [history.md](history.md): `leg history` and `leg worktrees`, one read-only index over every agent's own conversation store and every checkout, the support matrix per provider, dedup against Leg's own sessions, privacy, and how to add a provider.
10
11
  - [adapters.md](adapters.md): what Leg reads from each CLI in an interactive session, each adapter's headless argv, modes, forbidden flags, gotchas, and how to add a new one.
11
12
  - [faq.md](faq.md): short answers to real questions (the status line, codex's missing hook, agy's missing percentage, second accounts, uninstall, limits, secrets, Windows support).
12
13
 
@@ -25,7 +26,8 @@
25
26
  | interactive sessions | `src/attach.mjs` (the `leg <agent>` runner), `src/sessions.mjs` (the session store), `src/usage.mjs` (usage windows and the chooser), `src/accounts.mjs` (extra logins), `src/bundle.mjs` (the per-session bundle), `src/hook.mjs` (what Claude Code's hooks run) |
26
27
  | portable harness | `src/harness/index.mjs` (capture, compare, apply, status, the hand-off decision), `src/harness/registry.mjs` (which clients, where their files are), `src/harness/fingerprint.mjs`, `src/harness/cli.mjs` (`leg harness`), `src/harness/vendor/agnostic-ai/` (the engine, verbatim; `scripts/sync-harness-engine.mjs` is the only writer) |
27
28
  | taps | `src/taps/claude.mjs`, `src/taps/claude-usage.mjs`, `src/taps/codex.mjs`, `src/taps/agy.mjs` |
28
- | board | `src/server.mjs`, `src/board/sessions.js` (Terminals lane), `src/board/board.js` and `src/board/floor.js` (pipelines) |
29
+ | history | `src/history/index.mjs` (the index, dedup, resume), `src/history/providers/*.mjs` (one per agent store, read only), `src/history/worktrees.mjs` (the unified checkout view), `src/history/cli.mjs` (`leg history`, `leg worktrees`) |
30
+ | board | `src/server.mjs`, `src/board/sessions.js` (Terminals lane), `src/board/history.js` (Conversations), `src/board/board.js` and `src/board/floor.js` (pipelines) |
29
31
  | pipelines | `src/orchestrator.mjs`, `src/scheduler.mjs`, `src/chain.mjs`, `src/pipeline.mjs`, `src/runner.mjs`, `src/ledger.mjs`, `src/leases.mjs`, `src/mergequeue.mjs`, `src/adapters/*.mjs` |
30
32
 
31
33
  ## History
@@ -47,6 +47,7 @@ Source: the `appendEvent`/`updateSession` call sites in `src/attach.mjs`,
47
47
  | `handoff` | the switch happened: from, to, reason, bundle id |
48
48
  | `all_out` | every option is walled; the resets are printed and the terminal waits for the first one (`ended` with "quit while waiting" if Ctrl-C or End cuts the wait short, exit 3) |
49
49
  | `agent_exit` | the agent process exited, with its code |
50
+ | `continued` | the session was started by `leg history continue` on a conversation the agent's own store holds; `session.json` carries `continued_from: { id, provider, native_id }` |
50
51
  | `ended` | the session ended |
51
52
  | `lost` | the runner pid is gone; the session was marked `lost` |
52
53
  | `error` | a spawn error, a tap error, a failed bundle checkpoint, or an error the agent reported |
@@ -317,7 +317,7 @@ before saving.
317
317
 
318
318
  ## The ledger
319
319
 
320
- Below the terminals, three counts sitting on the ground with no panel, because a
320
+ Below the terminals, four counts sitting on the ground with no panel, because a
321
321
  raised surface here would compete with the terminals that are live. Each is a
322
322
  heading, a line of detail and a button that opens the detail below the row.
323
323
 
@@ -334,6 +334,25 @@ heading, a line of detail and a button that opens the detail below the row.
334
334
  visual weight as the live terminals, so the loudest thing on the page was a
335
335
  commit from eleven days ago. With nothing landed it reads `Nothing landed yet`
336
336
  and explains what Land does.
337
+ - **N conversations**, every coding-agent conversation on this machine, `799
338
+ claude, 198 codex, 92 agy, 14 grok, 5 copilot; looked 2m ago`: the sessions
339
+ Leg started and the ones the agents keep in their own stores, read from
340
+ there and never moved ([history.md](history.md)). **Browse N** opens a
341
+ drawer with filters (agent, search, repository, only what Leg started), a
342
+ page of fifty rows newest first and **Show 50 more**. A row is the title as a
343
+ button, then the agent chip, `leg` or `external` (`, live` when a process is
344
+ still on it), `repo on branch`, the prompt count and the short id. The title
345
+ opens the conversation in place: agent, who started it, folder, repository,
346
+ worktree, times, prompts, transcript path, id, the `leg history continue
347
+ <id>` command with **Copy the continue command** where the agent can resume
348
+ by id (or the reason it cannot), **Copy folder path**, and the last eight
349
+ messages newest first, each redacted the way the terminal drawer's are. Below
350
+ the rows, **Checkouts**: every worktree Leg can see with its repo and branch,
351
+ owner (the checkout itself, a Leg session, a Leg card, or not Leg's), how
352
+ many conversations point at it, and `missing`, `orphaned`, `stale`, `clean`
353
+ or `N uncommitted`. Nothing here is pushed over the live stream; the count
354
+ re-reads itself once a minute while the drawer is closed. On a shared board
355
+ a guest sees `The owner of this machine sees them.` and nothing else.
337
356
  - **N background tasks**, the card runtime below, as a count with **View N
338
357
  cards**, plus **New card**. With none it reads `Nothing is queued. Leg starts
339
358
  the next login only when a terminal hands off.`
@@ -391,6 +391,20 @@ in [VOCABULARY.md](VOCABULARY.md). Board routes: `GET /api/sessions`,
391
391
  `POST /api/sessions/:id/end`, `DELETE /api/sessions/:id`, with the list pushed
392
392
  as the SSE `sessions` event (source: src/server.mjs, src/board/sessions.js).
393
393
 
394
+ ### History index
395
+
396
+ `<LEG_HOME>/history/index.json` is the one file `leg history` writes: per
397
+ provider, per transcript, size, mtime and a small scrubbed record (source:
398
+ src/history/index.mjs). What each provider reads from its CLI's own store,
399
+ with the observed file and field names, is the header of each
400
+ `src/history/providers/<name>.mjs`; the support matrix and the resume
401
+ commands quoted from each CLI's `--help` (observed-live 2026-09-16: `claude
402
+ --resume <id>`, `codex resume <id>`, `grok --resume <id>`, `agy --conversation
403
+ <id>`; copilot has none Leg has verified) are in [history.md](history.md).
404
+ Board routes, owner only: `GET /api/history`, `GET /api/history/:id`,
405
+ `GET /api/history/providers`, `POST /api/history/refresh`,
406
+ `GET /api/worktrees` (source: src/server.mjs, src/board/history.js).
407
+
394
408
  ## Limit signals
395
409
 
396
410
  Recorded in `fixtures/limits/<group>/<id>.json` and classified by
@@ -200,6 +200,7 @@ Not meant for normal use; documented for completeness.
200
200
  |----------|---------|---------|
201
201
  | `LEG_SKIP_KILL` | set to `1` to make the supervisor skip actually killing a leg's process (used by tests that need an "unkillable agent") | `src/runner.mjs` |
202
202
  | `LEG_SERVER_SCRIPT` | override the server script path the launcher spawns | `src/launcher.mjs` |
203
+ | `COPILOT_HOME` | where `leg history` looks for the Copilot CLI store instead of `~/.copilot` (Claude, Codex and Grok use their own `CLAUDE_CONFIG_DIR`, `CODEX_HOME`, `GROK_HOME`; Antigravity has no variable and is found under the OS home) | `src/history/providers/copilot.mjs` |
203
204
 
204
205
  ## Network exposure
205
206
 
@@ -0,0 +1,172 @@
1
+ # History: every conversation on this machine
2
+
3
+ `leg history` lists the coding-agent conversations on this machine in one
4
+ place: the sessions Leg started itself, and the ones Claude Code, Codex, Grok,
5
+ Antigravity and GitHub Copilot CLI keep in their own stores, whether or not
6
+ Leg was involved. `leg worktrees` does the same for checkouts. The board's
7
+ fourth ledger cell, **Conversations**, is the same index with a search box.
8
+
9
+ Nothing moves. Claude keeps Claude's history where it always was, Codex
10
+ keeps Codex's, and so on. Leg reads those stores, writes one index file of
11
+ its own, and points back.
12
+
13
+ ## What you get
14
+
15
+ ```
16
+ leg history newest first, every agent, 50 rows
17
+ leg history --provider codex,grok one or more agents
18
+ leg history --repo leg a repository by name, or by path
19
+ leg history --search "drainer" title, repo, branch, folder or id
20
+ leg history --managed | --external only what Leg started, or only what it found
21
+ leg history --live only conversations with a process still on them
22
+ leg history --all --json everything, as JSON
23
+ leg history show <id> one conversation: where, when, its last messages, whether it can continue
24
+ leg history continue <id> start leg <agent> on it, in its folder, supervised like any other session
25
+ leg history refresh [--full] re-stat every store now; --full drops the index and re-reads everything
26
+ leg history providers the support matrix below, from the code that implements it
27
+ leg worktrees [--repo <path>] [--json] [--no-dirty]
28
+ ```
29
+
30
+ An id is `<provider>:<the agent's own id>`, for example
31
+ `claude:0fc54b51-6cfa-40cf-9823-a5a36d1ca3f3`. A unique prefix of the agent's
32
+ id, four characters or more, is enough: `leg history show 0fc5`. A Leg session
33
+ id (`s-2026…`) works too.
34
+
35
+ A row says which agent, whether the conversation is **leg** (Leg started it,
36
+ its session id follows) or **external** (found in the agent's store), whether
37
+ it is **live** (a process is still on it, where the agent leaves a marker),
38
+ the repository and branch, when it was last active, and its title.
39
+
40
+ ## Support matrix
41
+
42
+ Verified against the stores on a real machine on 2026-09-16 (Claude Code
43
+ 2.1.273, codex-cli 0.154.0, Grok CLI with `chat_format_version` 1,
44
+ Antigravity 1.2.0, Copilot CLI 1.0.80). Each provider's file header in
45
+ `src/history/providers/` names the exact files and fields it reads.
46
+
47
+ | provider | list | title | branch | live marker | transcript | continue | store read |
48
+ |---|---|---|---|---|---|---|---|
49
+ | Claude Code | yes | custom title, else the AI title, else the first prompt | yes | `sessions/<pid>.json`, pid checked | yes | `claude --resume <id>` | `~/.claude/projects/*/<id>.jsonl`, `history.jsonl`, `sessions/` |
50
+ | Codex | yes | `session_index.jsonl`, else the first prompt | yes (the `git` block) | no | yes | `codex resume <id>` | `~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl`, `session_index.jsonl`, `history.jsonl` |
51
+ | Grok | yes | `generated_title`, else the summary, else the first prompt | yes | `active_sessions.json` | yes | `grok --resume <id>` | `~/.grok/sessions/<cwd>/<id>/summary.json`, `chat_history.jsonl`, `prompt_history.jsonl` |
52
+ | Antigravity | yes | `annotations/<id>.pbtxt`, else the first prompt | no | no | **no** (SQLite) | `agy --conversation <id>` | `~/.gemini/antigravity-cli/history.jsonl`, `annotations/`, `presence/` |
53
+ | Copilot CLI | yes | the session name | yes | no | yes | **no** | `~/.copilot/session-state/<id>/workspace.yaml`, `events.jsonl` |
54
+
55
+ Extra logins made with `leg accounts add` are scanned too (their `CLAUDE_CONFIG_DIR`,
56
+ `CODEX_HOME` or `GROK_HOME` directory); a row from one carries the account
57
+ name.
58
+
59
+ **Continue** is offered only where the installed CLI's own help documents
60
+ resume-by-id, quoted in the provider file. Antigravity keeps its transcripts
61
+ in SQLite, which Leg does not open, so it lists and continues but cannot show
62
+ messages. Copilot is discoverable without being an agent Leg supervises, so it
63
+ lists and shows messages but cannot be continued through Leg; that split (a
64
+ provider Leg can *find* versus an agent Leg can *run*) is deliberate, and the
65
+ registry in `src/history/index.mjs` is separate from `src/adapters/`.
66
+
67
+ Looked at and left unsupported, each for one reason: Cursor (its chat lives
68
+ in `state.vscdb`, SQLite), opencode (`opencode.db`, SQLite), Pi (documented
69
+ under `~/.pi/agent/sessions/`, not present on the verification machine, the
70
+ cwd encoding undocumented), Hermes, aider and Windsurf (no session store).
71
+
72
+ ## How discovery works
73
+
74
+ - **One index file.** `$LEG_HOME/history/index.json` holds, per provider, per
75
+ transcript, the file's size and mtime and a small record: ids, cwd, repo,
76
+ branch, times, a scrubbed title cut to 200 characters. No message body is
77
+ ever cached. A refresh stats every file and re-reads only the ones whose
78
+ size or mtime changed; deleted files drop out.
79
+ - **Bounded reads.** Metadata comes from the first 256 KB and the last 256 KB
80
+ of a transcript, never the whole file (a busy machine has gigabytes of
81
+ them). Messages are read only when a conversation is opened, from the last
82
+ 4 MB, and every string is passed through the same redaction as the rest of
83
+ the board plus the values of the well-known key variables the process holds.
84
+ - **Repository resolution without git.** A cwd is walked up to its `.git`; a
85
+ `.git` *file* names a linked worktree and its main repository, so a
86
+ conversation that ran in a worktree groups under the repository it belongs
87
+ to. When the folder is gone, what the agent itself recorded (Claude's
88
+ `worktree-state`, Grok's `git_root_dir`) is used instead.
89
+ - **Dedup.** A Leg session records the agent's own id (`agent_session_id`)
90
+ and transcript path; a discovered record with the same id or path is the
91
+ same conversation and is shown once, marked managed, with the Leg session id
92
+ and status. A Leg session whose agent id Leg never learned still lists,
93
+ under `leg:<session id>`. `leg history continue` seeds the new session with
94
+ the id up front, so the continued conversation dedups from its first turn.
95
+ - **Hidden by default.** Subagent threads (Codex `source.subagent`, Grok
96
+ `session_kind: subagent`, Claude sidechains and the `<id>/subagents/`
97
+ directory) and sessions Claude Code itself hides (`history-suppression`)
98
+ are left out unless `--subagents` (the API's `hidden=1`) asks for them.
99
+ - **A provider failing is contained.** Each provider scans inside its own
100
+ try; a throw keeps that provider's last good entries, records the error on
101
+ its stats line, and the others still index.
102
+ - **The listing refreshes itself** when the index is older than a minute;
103
+ `leg history refresh` forces it. The CLI refreshes inline; the board hands
104
+ the refresh to a child `leg history refresh` so its own event loop keeps
105
+ serving (a scan stats thousands of files and walks every cwd). A refresh
106
+ that fails or cannot take the index lock keeps the last index, and the
107
+ listing says so.
108
+ - **A folder on a network share** (`\host\share\...`) is listed as the agent
109
+ recorded it and never probed: an unreachable host blocks every synchronous
110
+ file-system call for seconds, so no refresh touches one.
111
+ - **Antigravity's titles and activity marks** live one small file per
112
+ conversation (`annotations/<id>.pbtxt`, `presence/<id>.lock`). Each is
113
+ stat'ed on every pass and read again only when its own mtime moved; a
114
+ retitle rewrites the file in place and does not change the directory, so
115
+ the directory is no signal.
116
+
117
+ ## Worktrees
118
+
119
+ `leg worktrees` merges three sources into one list: what `git worktree list`
120
+ says for every repository Leg knows (from its sessions, its cards and the
121
+ discovered conversations), the worktrees Leg's sessions and cards recorded
122
+ (even when git no longer lists them), and the worktrees discovered
123
+ conversations ran in. Each row carries the repository, branch, whether the
124
+ directory exists, how many files are uncommitted (git status, on up to 40
125
+ checkouts per call; `null` past that or when `--no-dirty`), who owns it (the
126
+ checkout itself, a Leg session, a Leg card, or nobody Leg knows), which
127
+ conversations point at it and when the last one was active, **orphaned**
128
+ (a `.leg-worktrees/` directory no session or card records), **stale**
129
+ (exists, no live owner, nothing touched it in 14 days) and **missing** (git
130
+ or a record names it, the directory is gone).
131
+
132
+ It is a view. Removing a worktree is still `leg sessions rm`, `leg card rm`
133
+ or the board's Remove, which keep their guards.
134
+
135
+ ## Privacy
136
+
137
+ - The whole `/api/history` and `/api/worktrees` group is the owner's. On a
138
+ shared board a guest gets 403 from every route in it, the same gate as the
139
+ pipeline routes; nothing history knows is pushed over SSE.
140
+ - The API takes an id, never a path. The transcript a record names is read
141
+ only when it sits inside a provider home, an extra account's directory or
142
+ Leg's own `sessions/`; an index edited by hand to point elsewhere reads
143
+ nothing.
144
+ - Discovery writes only `$LEG_HOME/history/index.json`, its lock and its
145
+ temporary sibling, and refuses to write when `LEG_HOME` sits inside a
146
+ provider's store. No SQLite file is ever opened. Junctions and symlinks
147
+ under a store are not followed.
148
+ - `leg history continue` validates the id against the provider's own shape
149
+ (a UUID) before it becomes an argument, and the folder before it becomes
150
+ the working directory: it must exist, be a directory, and not sit under
151
+ `LEG_HOME`. The continued session shares its checkout (no worktree is cut):
152
+ the conversation's files are where it left them.
153
+
154
+ ## Adding a provider
155
+
156
+ One file in `src/history/providers/`, registered in `src/history/index.mjs`:
157
+
158
+ - `name`, `label`, `transcript` (`'supported' | 'unsupported'`), `ID_RE`
159
+ - `root(homes)`: the store's directory (from the CLI's own environment variable
160
+ when it has one; `homes` overrides it in tests)
161
+ - `scan({ home, prev })`: `{ entries, aux, scanned, parsed }` where `entries`
162
+ maps a stable key (the transcript path) to `{ mtime, size, record }`, reusing
163
+ `prev.entries[key]` when the file is unchanged; `record` carries
164
+ `native_id, cwd, branch, title, started_at, updated_at, transcript_path,
165
+ size_bytes, turns, live, native`
166
+ - `messages(record, limit)`: the last messages from a bounded tail, or `null`
167
+ - `resume(record)`: `{ supported, agent, args }` or `{ supported: false, reason }`,
168
+ with the CLI's help text quoted beside it
169
+ - optionally `liveIds(home)`
170
+
171
+ `test/history-fixture.mjs` shows the shapes each existing provider expects;
172
+ a new provider gets a builder there and a row in the matrix above.