@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 +76 -0
- package/README.md +40 -2
- package/bin/leg.mjs +23 -1
- package/docs/ERRORS.md +134 -0
- package/docs/README.md +3 -1
- package/docs/VOCABULARY.md +1 -0
- package/docs/board-guide.md +20 -1
- package/docs/cli-contracts.md +14 -0
- package/docs/configuration.md +1 -0
- package/docs/history.md +172 -0
- package/docs/runtime-tap.md +156 -0
- package/package.json +1 -1
- package/scripts/build-docs-site.mjs +7 -0
- package/src/accounts.mjs +5 -2
- package/src/attach.mjs +50 -4
- package/src/board/board.css +23 -1
- package/src/board/board.js +14 -2
- package/src/board/history.js +377 -0
- package/src/board/index.html +33 -0
- package/src/board/sessions.js +20 -6
- package/src/history/cli.mjs +159 -0
- package/src/history/common.mjs +119 -0
- package/src/history/index.mjs +429 -0
- package/src/history/providers/agy.mjs +91 -0
- package/src/history/providers/claude.mjs +161 -0
- package/src/history/providers/codex.mjs +133 -0
- package/src/history/providers/copilot.mjs +94 -0
- package/src/history/providers/grok.mjs +138 -0
- package/src/history/worktrees.mjs +116 -0
- package/src/redact.mjs +23 -5
- package/src/server.mjs +186 -8
- package/src/sessions.mjs +9 -0
- package/src/taps/claude.mjs +11 -4
- package/src/taps/mod.mjs +340 -0
- package/src/worktree.mjs +1 -1
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,
|
|
340
|
-
|
|
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
|
-
|
|
|
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
|
package/docs/VOCABULARY.md
CHANGED
|
@@ -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 |
|
package/docs/board-guide.md
CHANGED
|
@@ -317,7 +317,7 @@ before saving.
|
|
|
317
317
|
|
|
318
318
|
## The ledger
|
|
319
319
|
|
|
320
|
-
Below the terminals,
|
|
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.`
|
package/docs/cli-contracts.md
CHANGED
|
@@ -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
|
package/docs/configuration.md
CHANGED
|
@@ -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
|
|
package/docs/history.md
ADDED
|
@@ -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.
|