@ucsandman/legcli 0.9.0 → 0.11.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 +146 -0
- package/README.md +110 -11
- package/bin/leg.mjs +78 -15
- package/docs/ERRORS.md +187 -0
- package/docs/README.md +3 -1
- package/docs/ROADMAP-v2.md +24 -11
- package/docs/VOCABULARY.md +1 -0
- package/docs/adapters.md +93 -11
- package/docs/board-guide.md +20 -1
- package/docs/cli-contracts.md +50 -17
- package/docs/configuration.md +56 -5
- package/docs/history.md +172 -0
- package/docs/runtime-tap.md +156 -0
- package/fixtures/limits/grok/grok-balance-exhausted.json +11 -0
- package/fixtures/live/grok/cmd.txt +1 -1
- package/fixtures/live/grok/parsed.json +6 -3
- package/fixtures/live/grok/run.json +22 -10
- package/fixtures/verified.json +8 -1
- package/package.json +1 -1
- package/scripts/build-docs-site.mjs +11 -4
- package/scripts/probe.mjs +2 -1
- package/src/accounts.mjs +5 -2
- package/src/adapters/cli.mjs +130 -0
- package/src/adapters/custom.mjs +271 -0
- package/src/adapters/grok.mjs +51 -10
- package/src/adapters/index.mjs +34 -7
- package/src/attach.mjs +85 -13
- package/src/audit.mjs +118 -0
- package/src/board/audit.js +123 -0
- package/src/board/board.css +38 -1
- package/src/board/board.js +14 -2
- package/src/board/history.js +377 -0
- package/src/board/index.html +55 -0
- package/src/board/sessions.js +49 -7
- 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 +272 -28
- package/src/sessions.mjs +9 -0
- package/src/share.mjs +66 -6
- package/src/taps/claude.mjs +11 -4
- package/src/taps/grok.mjs +4 -0
- package/src/taps/mod.mjs +340 -0
- package/src/usage.mjs +21 -5
- package/src/worktree.mjs +1 -1
package/docs/ERRORS.md
CHANGED
|
@@ -3,6 +3,139 @@
|
|
|
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: a new field on the session view leaked the owner's reset times to a guest, through the guest's own terminal
|
|
7
|
+
|
|
8
|
+
**Fixed in `src/server.mjs` (`sessionsView` decides `guest` before the map and
|
|
9
|
+
blanks `handoff_targets[].resets_at` for one). Caught by
|
|
10
|
+
`test/share-security.test.mjs`, which was already asserting it.**
|
|
11
|
+
|
|
12
|
+
The hand-off picker needed each destination's availability, so `handoff_targets`
|
|
13
|
+
went onto every session in `sessionsView` carrying `resets_at`. The obvious
|
|
14
|
+
mental model was "a guest gets `redactSession`, which lists its fields
|
|
15
|
+
explicitly, so a new field is invisible to them". That is only true of someone
|
|
16
|
+
else's terminal. `mine(s)` is true for a guest's **own** terminal, and that path
|
|
17
|
+
spreads the whole object. So a guest's own card carried the exact reset
|
|
18
|
+
timestamp of every account on the machine, including the owner's, which is
|
|
19
|
+
precisely the usage data the share design keeps off a guest's board.
|
|
20
|
+
|
|
21
|
+
The lesson is about where redaction lives, not about this field. There are two
|
|
22
|
+
paths out of `sessionsView`: `redactSession` (allow-list, safe by default) and
|
|
23
|
+
the `mine(s)` spread (deny-list, unsafe by default). **Any field added to a
|
|
24
|
+
session object is visible to whoever owns that session, and a guest owns one.**
|
|
25
|
+
A field that carries machine-level data has to be blanked where it is built, not
|
|
26
|
+
left to a redactor that never sees it.
|
|
27
|
+
|
|
28
|
+
What made this cheap: the security suite already asserted the whole guest
|
|
29
|
+
response text contains no reset time, so the leak failed a test in the same run
|
|
30
|
+
that introduced it. The test was written against the property ("a guest board
|
|
31
|
+
carries no reset time"), not against the fields that existed when it was
|
|
32
|
+
written, which is why it still caught a field invented months later.
|
|
33
|
+
|
|
34
|
+
Two smaller ones from the same change, both worth the line:
|
|
35
|
+
|
|
36
|
+
- Registering `grok` broke three tests that asserted `unknown adapter "grok"`.
|
|
37
|
+
A test that encodes "not supported yet" as an assertion becomes a tripwire on
|
|
38
|
+
the day support lands. Assert the refusal with a name nothing will ever
|
|
39
|
+
provide (`no-such-agent`), so the test outlives the gap it was describing.
|
|
40
|
+
- `names()` started reading `$LEG_HOME/adapters` from disk, and `/api/health`
|
|
41
|
+
calls it once for the list and once per adapter. That put a readdir, a read
|
|
42
|
+
and a JSON parse per spec on the same event loop the terminals lane is pushed
|
|
43
|
+
from, and `/api/health` went over its 1 s budget in
|
|
44
|
+
`test/board-responsiveness.test.mjs`. Cached against the directory's entry
|
|
45
|
+
list with a one-second floor. The board's hot path is `/api/health` plus the
|
|
46
|
+
sessions view; anything new they call has to be counted, not assumed cheap.
|
|
47
|
+
(This is the same event-loop failure as the entry below, from the other end.)
|
|
48
|
+
- `test/board-responsiveness.test.mjs` failed the ship twice at 1055 ms and
|
|
49
|
+
1140 ms against a hard `< 1000 ms`, and passed three times out of three when
|
|
50
|
+
run alone. An absolute millisecond budget on a four-way-concurrent runner
|
|
51
|
+
measures the machine, not the code. It now takes an idle baseline in the same
|
|
52
|
+
process and asserts the busy request is not 20x it, with a 3 s ceiling for the
|
|
53
|
+
symptom the test is named for. **It was only trusted after being made to
|
|
54
|
+
fail**: putting the original shape back (no floor, no fingerprint, a 1.2 s
|
|
55
|
+
blocking view on every watcher event) made it report 4,810 ms and fail both
|
|
56
|
+
assertions, which is the "four to fourteen seconds" the entry below describes.
|
|
57
|
+
A perf test that has never been watched failing is a number, not a guard.
|
|
58
|
+
|
|
59
|
+
## 2026-09-17: one running terminal saturated the board's event loop, and four separate symptoms came out of it
|
|
60
|
+
|
|
61
|
+
**Fixed in `src/server.mjs` (watcher filter, stat fingerprint, push floor, cached
|
|
62
|
+
`canLand`), `src/attach.mjs` (a taken port is a board; always open it) and
|
|
63
|
+
`src/board/{board,sessions}.js` (one render per push, hold an open confirm row).
|
|
64
|
+
Regression tests: `test/board-responsiveness.test.mjs`.**
|
|
65
|
+
|
|
66
|
+
Reported as four bugs: `leg claude` took a long time to start, the board never
|
|
67
|
+
opened by itself, the board was slow to load and to react, and Remove under
|
|
68
|
+
Terminals did nothing. All four were one cause. The board server had burned
|
|
69
|
+
17,080 seconds of CPU — it answered `/api/health` in 7 to 14 seconds and a
|
|
70
|
+
40 KB stylesheet in 14. A live agent rewrites its session record every six
|
|
71
|
+
seconds or so and takes a control lock about once a second; the recursive
|
|
72
|
+
watcher over the sessions directory answered every one of those — including
|
|
73
|
+
`.control.lock`, `.session.lock` and the `session.json.*.tmp` files atomic
|
|
74
|
+
writes leave behind — with a full rebuild of the terminals view, which costs
|
|
75
|
+
about two seconds (1.1 s of it `canLand` shelling out to git across twenty
|
|
76
|
+
worktrees). 105 filesystem events in 30 seconds against a 300 ms debounce and a
|
|
77
|
+
2,000 ms rebuild: the queue could never drain.
|
|
78
|
+
|
|
79
|
+
What that one stall produced:
|
|
80
|
+
|
|
81
|
+
- **Slow start.** `ensureBoard`'s health probe times out at 2 s, so a merely
|
|
82
|
+
busy board read as no board. `leg` spawned a second server, which died of
|
|
83
|
+
`EADDRINUSE` (the evidence was already in `~/.baton/board.log`), then polled
|
|
84
|
+
the dead child every 200 ms for the full 15 s.
|
|
85
|
+
- **The board never opened.** `openBoard(url)` sat only on the branch that
|
|
86
|
+
successfully started a server. With one already running, or on the 15 s
|
|
87
|
+
give-up path, nothing was ever opened.
|
|
88
|
+
- **Slow to react.** `board.js` dispatched `leg:sessions` and `baton:sessions`
|
|
89
|
+
per push, parsing the quarter-megabyte payload twice, with the second
|
|
90
|
+
dispatch outside the `state.es === es` staleness guard (missing braces on a
|
|
91
|
+
one-line `if`). `sessions.js` was registered on `leg:sessions` twice and on
|
|
92
|
+
the alias once. One push rebuilt the whole grid three times.
|
|
93
|
+
- **Slow to react, part two.** `renderSessions` starts with
|
|
94
|
+
`grid.textContent = ''`, so with three rebuilds per push every button was
|
|
95
|
+
destroyed and recreated several times a second.
|
|
96
|
+
|
|
97
|
+
**Remove was a second, independent bug, and fixing the first did not fix it.**
|
|
98
|
+
Reported again as "it's still not removing them" after all of the above
|
|
99
|
+
shipped. `confirmRow` does `pendingConfirm = null; onYes(yes)`, and the call
|
|
100
|
+
site passed `(btn) => act(s.session_id, pendingConfirm.action, btn)` — a
|
|
101
|
+
closure over the module variable, not over its value. Every Yes on the
|
|
102
|
+
Terminals panel threw `TypeError: Cannot read properties of null (reading
|
|
103
|
+
'action')` before reaching `act()`, so Remove, Remove record, End and Land had
|
|
104
|
+
never worked from the confirm row; the exception went to the console and
|
|
105
|
+
nowhere the reader would see it. Fixed by snapshotting
|
|
106
|
+
`const pending = pendingConfirm` before building the row. Caught by driving the
|
|
107
|
+
real page: the click produced zero network requests, which pointed at the
|
|
108
|
+
handler rather than at anything server-side. The instrument that found it was
|
|
109
|
+
five lines — wrap `window.fetch`, listen for `window.onerror`, click, read
|
|
110
|
+
both.
|
|
111
|
+
|
|
112
|
+
Three lessons. First, a name filter is not enough to classify a filesystem event:
|
|
113
|
+
taking a lock inside a directory changes that directory's own mtime, and the
|
|
114
|
+
event arrives naming only the directory. The watcher now treats an event as a
|
|
115
|
+
hint and compares a stat fingerprint of the files the view is really built from
|
|
116
|
+
— sub-millisecond, against a 2 s rebuild. Second, the CPU counter on a
|
|
117
|
+
long-lived process names a stall in one command, before any code is read.
|
|
118
|
+
|
|
119
|
+
Third, and the one that cost a round trip: several symptoms reported together
|
|
120
|
+
are not thereby one bug. Three of these four were, and finding that cause made
|
|
121
|
+
the fourth *look* explained — the render churn is a real way to drop a click,
|
|
122
|
+
so "Remove doesn't work" fit the story. It was a different bug in a different
|
|
123
|
+
file, and it was still there after the fix shipped. A symptom is only closed
|
|
124
|
+
once it has been driven and watched, not once a plausible cause for it has been
|
|
125
|
+
found and fixed. Clicking Remove in the real page would have taken two minutes
|
|
126
|
+
and would have shown zero network requests and a `TypeError` in the console.
|
|
127
|
+
|
|
128
|
+
Verified by running the new tests against `da453b7`: 7 of 8 fail there, 8 of 8
|
|
129
|
+
pass after, and the busy-board test takes 21 s before versus 4 s after. On the
|
|
130
|
+
live board, same 66 sessions: `/api/health` 14.19 s → 2.4 ms, `/board.css`
|
|
131
|
+
14.24 s → 1.5 ms, `/api/sessions` 15.64 s → 109 ms, view rebuild 2,049 ms →
|
|
132
|
+
115 ms, renders per push 3 → 1.
|
|
133
|
+
|
|
134
|
+
Also found while verifying: `npm run lint` reported "ESLint: No issues found"
|
|
135
|
+
with `node_modules/.bin` empty and eslint not installed — a wrapper was printing
|
|
136
|
+
it. Run `npm install` first and check that ESLint really ran; a lint result from
|
|
137
|
+
a tool that is not on disk is not a lint result.
|
|
138
|
+
|
|
6
139
|
## 2026-09-16: a hand-edited `package.json` version left `package-lock.json` behind, and the publish gate refused 0.9.0
|
|
7
140
|
|
|
8
141
|
**Fixed by bumping the lockfile root version; the gate is `scripts/npm-publish-gate.mjs`.**
|
|
@@ -395,3 +528,57 @@ it), and check whether a board was listening on 4747 at the time.
|
|
|
395
528
|
module that reads the environment at import time cannot be imported from a
|
|
396
529
|
helper that runs before the environment is set, no matter how small the thing
|
|
397
530
|
you wanted from it.
|
|
531
|
+
|
|
532
|
+
## A 3,000-line feature landed with red CI on Windows and no docs page (2026-09-16)
|
|
533
|
+
|
|
534
|
+
- **Symptom.** Commit `c7f24a0` (the history index, committed by the agy
|
|
535
|
+
session that took over after a Claude usage limit) went green on Ubuntu and
|
|
536
|
+
macOS and red on Windows in two `history.test.mjs` cases, and the `docs` job
|
|
537
|
+
failed with "site/ is out of date". `/docs/history` did not exist and every
|
|
538
|
+
link to `history.md` pointed at the npm package page.
|
|
539
|
+
- **Root cause, tests.** GitHub's Windows runner hands out an 8.3 short TEMP
|
|
540
|
+
path (`RUNNER~1`) while git reports the long form (`runneradmin`). The two
|
|
541
|
+
assertions compared a `mkdtemp` path with a path that had been through git.
|
|
542
|
+
The repo already had the seam for this (`canonPath` in `src/fsx.mjs`,
|
|
543
|
+
used the same way in `handoff-order.test.mjs`); the new tests did not use it.
|
|
544
|
+
It cannot be seen on a developer machine with 8.3 names disabled.
|
|
545
|
+
- **Root cause, docs.** `scripts/build-docs-site.mjs` builds from a hard-coded
|
|
546
|
+
`PAGES` list. A new `docs/*.md` that is not in it gets no page, and
|
|
547
|
+
`rewriteHref` sends its links to the npm URL by design. Nobody ran
|
|
548
|
+
`npm run docs` before pushing, so the docs job was the first to notice.
|
|
549
|
+
- **Fix.** `ebda794`: both sides of the four path assertions through
|
|
550
|
+
`canonPath`; reproduced locally by pointing `TEMP` at a case-altered path
|
|
551
|
+
(seen failing, then 16/16); `history` registered in `PAGES`; site rebuilt.
|
|
552
|
+
- **The lesson that generalises.** A test that compares two filesystem paths
|
|
553
|
+
on Windows compares two spellings of the same folder unless both go through
|
|
554
|
+
`canonPath`. And a new `docs/*.md` is two edits, the file and the `PAGES`
|
|
555
|
+
entry, then `npm run docs`; the docs job exists because the second and third
|
|
556
|
+
get forgotten.
|
|
557
|
+
|
|
558
|
+
## The review that was in flight when the limit hit never ran; the feature shipped with 38 defects (2026-09-17)
|
|
559
|
+
|
|
560
|
+
- **Symptom.** `c7f24a0` (history discovery) was committed and pushed by the
|
|
561
|
+
agent that took over after a Claude usage limit. The nine-lens adversarial
|
|
562
|
+
review the Claude session had launched died with that session. Re-run the
|
|
563
|
+
next day it confirmed 38 findings, 3 high: the board's Conversations cell
|
|
564
|
+
could never page past 150 rows (the total was counted after the cursor
|
|
565
|
+
slice), an unreachable UNC cwd blocked a refresh for seconds per record,
|
|
566
|
+
and an Antigravity retitle was invisible to every incremental refresh
|
|
567
|
+
(the directory's mtime was the cache key; an in-place rewrite never moves
|
|
568
|
+
it).
|
|
569
|
+
- **Root cause.** The hand-off bundle carried "what was established" and
|
|
570
|
+
"next moves" but not "what was in flight": a background workflow is not a
|
|
571
|
+
file, so the next agent saw a clean-looking task and shipped it. Nothing
|
|
572
|
+
in the ship path asks whether the change was reviewed.
|
|
573
|
+
- **Fix.** `582dfe5`: 20 findings closed, each with a test that fails on the
|
|
574
|
+
previous source (proved on a detached worktree of HEAD before the fix).
|
|
575
|
+
- **Left open, by choice.** Coverage-only findings (live markers, the
|
|
576
|
+
write-scope guard, the flag surfaces, extra accounts), a low-severity
|
|
577
|
+
dedup edge (a conversation split over two Claude transcript files when Leg
|
|
578
|
+
recorded only the older path), the `sessions ls` row format, and the
|
|
579
|
+
pretty-printed index file.
|
|
580
|
+
- **The lesson that generalises.** A review is part of the change, not a
|
|
581
|
+
step after it: launch it before the feature is committable and record it
|
|
582
|
+
in the hand-off as in flight, or the next agent ships without it. And a
|
|
583
|
+
cache keyed on a directory's mtime sees files added and removed, never a
|
|
584
|
+
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/ROADMAP-v2.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Roadmap v2: the software factory
|
|
2
2
|
|
|
3
|
+
**Unreleased (2026-09-17) opened the chain and finished the share story.** grok is
|
|
4
|
+
a registered card adapter; any other CLI becomes one from a JSON spec
|
|
5
|
+
(`leg adapter add`, no code); **Hand off now** can name its destination
|
|
6
|
+
instead of taking the next in the order; `leg share` grew TLS from a
|
|
7
|
+
certificate pair you supply, an `operator` role between owner and guest, and
|
|
8
|
+
an audit trail of who did what across every terminal and every card. That
|
|
9
|
+
closes items 1 and 4 below, and the "hand off now to \<adapter>" picker.
|
|
10
|
+
|
|
3
11
|
**0.3.0 (2026-09-11) shipped item 1 and the first half of continuous landing.**
|
|
4
12
|
A second live session in one checkout gets its own worktree and branch, and
|
|
5
13
|
**Land** on its card sends that branch through the merge queue (rebase, tests,
|
|
@@ -33,23 +41,28 @@ event names its actor, a board that reads only the ledger, and an auth/bind seam
|
|
|
33
41
|
|
|
34
42
|
## Next
|
|
35
43
|
|
|
36
|
-
1.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
1. ~~**Multi-human network access**~~ — done. 0.3.0 shipped `leg share`; the
|
|
45
|
+
unreleased work of 2026-09-17 shipped the three that were left: TLS (`leg share on --tls-cert/--tls-key`,
|
|
46
|
+
or `LEG_TLS_CERT`/`LEG_TLS_KEY`, from a pair you supply — Leg issues none),
|
|
47
|
+
the `operator` role between owner and guest, and the audit trail
|
|
48
|
+
(`/api/audit`, Settings → Audit trail).
|
|
41
49
|
2. **Review station with human reviewers**: a `human` station kind that shows
|
|
42
50
|
the diff, the bundle and the test tail, with Approve / Request changes /
|
|
43
51
|
Reassign as buttons; reviewer identity from the token.
|
|
44
52
|
3. **`pr` land mode live**: `gh pr create` argv is built and stub-tested today;
|
|
45
53
|
run it for real behind an explicit per-card opt-in and a remote allowlist.
|
|
46
|
-
4.
|
|
47
|
-
|
|
48
|
-
|
|
54
|
+
4. ~~**More adapters**~~ — done. grok is registered (2026-09-17: flags read
|
|
55
|
+
from `grok --help` on 1.0.34, envelope read from the shipped binary, probe
|
|
56
|
+
reached the account and returned a real 402 wall that classified `limit`;
|
|
57
|
+
the success path is still unprobed for want of balance). Anything else,
|
|
58
|
+
muse included, is a custom adapter: a JSON spec in
|
|
59
|
+
`$LEG_HOME/adapters/<name>.json`, `leg adapter add`, no code
|
|
60
|
+
([adapters.md](adapters.md#custom-adapters)).
|
|
49
61
|
5. **OpenClaw Workboard mirror** once the bundled plugin is allowed
|
|
50
62
|
(`plugins.allow`): the verb table in `src/sync/workboard.mjs` is the only
|
|
51
63
|
thing to check against `openclaw workboard --help`.
|
|
52
64
|
|
|
53
|
-
Also on the list: per-station prompt templates editable from the board,
|
|
54
|
-
|
|
55
|
-
|
|
65
|
+
Also on the list: per-station prompt templates editable from the board, lease
|
|
66
|
+
suggestions from the diff of the previous leg, and a floor view that shows
|
|
67
|
+
lease contention over time. (The "hand off now to \<adapter>" picker landed
|
|
68
|
+
2026-09-17, unreleased: Details → **Hand off now to**, or `leg sessions handoff <id> --to`.)
|
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/adapters.md
CHANGED
|
@@ -239,25 +239,107 @@ These are what a pipeline card's chain spawns. Unchanged since 0.2.0.
|
|
|
239
239
|
`emulates` to that CLI's name, so the limit classifier applies that CLI's own
|
|
240
240
|
fixtures to it and a demo chain reads like a real fallback.
|
|
241
241
|
|
|
242
|
-
### grok (
|
|
242
|
+
### grok (headless)
|
|
243
243
|
|
|
244
244
|
- **Binary**: `LEG_GROK_BIN`, else `~/.grok/bin/grok.exe`, else `grok` on
|
|
245
245
|
PATH.
|
|
246
|
-
- **Argv**: `grok -
|
|
247
|
-
<mode>`, plus
|
|
246
|
+
- **Argv**: `grok --prompt-file <run>/prompt.txt --output-format json
|
|
247
|
+
--permission-mode <mode> --cwd <worktree>`, plus `--max-turns <n>`,
|
|
248
|
+
`-m <model>`, `-r <session-id>`. Every flag was read from `grok --help` on
|
|
249
|
+
grok 1.0.34 (`3736acbc8658`) on 2026-09-17. The prompt travels by file
|
|
250
|
+
rather than on argv because a hand-off prompt carries the whole bundle
|
|
251
|
+
summary and Windows caps one command line at about 32k; with no prompt file
|
|
252
|
+
the adapter falls back to `-p "<prompt>"`. `--cwd` is passed explicitly
|
|
253
|
+
rather than relying on the spawn's working directory: grok can run against a
|
|
254
|
+
shared leader process (`~/.grok/leader.sock`), and a leg must edit its own
|
|
255
|
+
worktree, not whatever directory the leader started in.
|
|
248
256
|
- **Stdin**: `ignore`.
|
|
249
257
|
- **Modes**: default `acceptEdits`; allowed `default`, `acceptEdits`,
|
|
250
258
|
`auto`, `dontAsk`, `plan`.
|
|
251
259
|
- **Forbidden flags**: `--always-approve`, `bypassPermissions`,
|
|
252
260
|
`--permission-mode=bypassPermissions`.
|
|
253
|
-
- **
|
|
254
|
-
|
|
255
|
-
`
|
|
256
|
-
`
|
|
257
|
-
`
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
+
- **Result**: grok's headless writer emits the Claude Code result envelope.
|
|
262
|
+
The field names (`"type":"result"`, `subtype`, `is_error`, `session_id`,
|
|
263
|
+
`result`, `num_turns`, `stop_reason`, `total_cost`) were read out of the
|
|
264
|
+
shipped `grok.exe` on 2026-09-17. An error is the other envelope,
|
|
265
|
+
`{"type":"error","message":…}`, observed live the same day.
|
|
266
|
+
- **Status**: registered on 2026-09-17. The probe reached the account and came
|
|
267
|
+
back with a real wall — `API error (status 402 Payment Required): Grok Build
|
|
268
|
+
usage balance exhausted`, exit 1, classified `limit` and handed off — so the
|
|
269
|
+
spawn, the argv, the auth and the wall path are all verified live
|
|
270
|
+
(`fixtures/live/grok/`). The **success** path of a grok leg is still
|
|
271
|
+
unprobed: that needs balance on the account. Until it is, a grok leg whose
|
|
272
|
+
envelope does not parse falls back to the `.leg/DONE` marker and the diff,
|
|
273
|
+
which is what every adapter does when `parseResult` returns null. Re-run
|
|
274
|
+
`node scripts/probe.mjs --adapter grok --repo <toy-repo>` with balance to
|
|
275
|
+
close it.
|
|
276
|
+
|
|
277
|
+
## Custom adapters
|
|
278
|
+
|
|
279
|
+
Any other coding-agent CLI becomes a card adapter through a JSON spec in
|
|
280
|
+
`$LEG_HOME/adapters/<name>.json`, with no code in this package. This is the
|
|
281
|
+
open end of the chain: claude, codex, agy and grok ship with taps and a probe,
|
|
282
|
+
and anything else joins as "argv in, JSON out".
|
|
283
|
+
|
|
284
|
+
A custom adapter runs **cards**: headless, in a worktree, handing off like any
|
|
285
|
+
other leg. It is not an interactive `leg <agent>` terminal, because that needs
|
|
286
|
+
a usage tap and a wall signal, which only the four built-ins expose.
|
|
287
|
+
|
|
288
|
+
```
|
|
289
|
+
leg adapter template --name muse > muse.json # a starter spec
|
|
290
|
+
leg adapter add muse.json # validate and install it
|
|
291
|
+
leg adapter check muse # the exact command a leg would run
|
|
292
|
+
leg card add --repo <path> --task "<t>" --chain muse,claude --queue
|
|
293
|
+
leg adapter list | show muse | rm muse
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
The spec:
|
|
297
|
+
|
|
298
|
+
```json
|
|
299
|
+
{
|
|
300
|
+
"name": "muse",
|
|
301
|
+
"bin": "muse",
|
|
302
|
+
"stdin": "ignore",
|
|
303
|
+
"args": ["run", "--json",
|
|
304
|
+
["--dir", "{{cwd}}"],
|
|
305
|
+
["--model", "{{model}}"],
|
|
306
|
+
"--prompt-file", "{{promptFile}}"],
|
|
307
|
+
"modes": { "default": "auto", "allowed": ["auto", "readonly"] },
|
|
308
|
+
"forbiddenFlags": ["--unsafe"],
|
|
309
|
+
"result": { "format": "json", "sessionId": "session_id",
|
|
310
|
+
"message": "result", "stopReason": "stop_reason" }
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
- **`bin`** is argv[0]. It is spawned directly, never through a shell, so it
|
|
315
|
+
cannot contain `< > | & ;`. `LEG_<NAME>_BIN` overrides it. A `.mjs`, `.cjs`
|
|
316
|
+
or `.js` path runs under this Node.
|
|
317
|
+
- **`args`** is a list of strings and groups. Placeholders: `{{prompt}}`,
|
|
318
|
+
`{{promptFile}}`, `{{cwd}}`, `{{mode}}`, `{{model}}`, `{{resume}}`,
|
|
319
|
+
`{{maxTurns}}`, `{{runDir}}`. A bare string is always kept; a **group** (a
|
|
320
|
+
nested array) is dropped whole when a placeholder inside it has no value, so
|
|
321
|
+
`["--model", "{{model}}"]` disappears rather than passing a bare `--model`.
|
|
322
|
+
Something has to carry the prompt: `{{prompt}}`, `{{promptFile}}`, or
|
|
323
|
+
`"stdin": "pipe"`.
|
|
324
|
+
- **`modes`** is validated before anything spawns, the same way a built-in's
|
|
325
|
+
is: a chain entry naming a mode outside `allowed` throws `forbidden flag`.
|
|
326
|
+
- **`forbiddenFlags`** is yours to add to. The flags that turn a supervised
|
|
327
|
+
agent into an unsupervised one are refused whatever the spec says, in `args`,
|
|
328
|
+
in `modes` and at argv time (`NEVER_ALLOWED`, `src/adapters/custom.mjs`).
|
|
329
|
+
- **`result.format`** is `json` (the first parseable object in stdout),
|
|
330
|
+
`jsonl` (the last line carrying the message field) or `text` (no parsing).
|
|
331
|
+
`sessionId`, `message` and `stopReason` are dotted paths, so
|
|
332
|
+
`"sessionId": "thread.id"` reads `{"thread":{"id":…}}`. With `text`, or when
|
|
333
|
+
nothing parses, the leg is judged by its `.leg/DONE` marker and its diff,
|
|
334
|
+
which is what happens for any adapter whose `parseResult` returns null.
|
|
335
|
+
|
|
336
|
+
A broken spec is reported, never thrown: `leg adapter list` names the file and
|
|
337
|
+
the reason, and the board, the scheduler and `leg card add` carry on without
|
|
338
|
+
it. A spec may not take a built-in's name. The directory is re-read whenever
|
|
339
|
+
the list is asked for, keyed on its entries, so a spec added while the board is
|
|
340
|
+
up appears in the New card form without a restart.
|
|
341
|
+
|
|
342
|
+
## How to add a built-in adapter
|
|
261
343
|
|
|
262
344
|
An adapter is a plain object (see `src/adapters/common.mjs` for the shared
|
|
263
345
|
helpers, `src/adapters/fake.mjs` for the simplest full example):
|
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
|
@@ -25,7 +25,7 @@ questions."
|
|
|
25
25
|
| claude | 2.1.268 | exit 0, file yes, DONE yes, 20 s | `src/adapters/claude.mjs` |
|
|
26
26
|
| codex | codex-cli 0.153.4 | exit 0, file yes, DONE yes, 28 s | `src/adapters/codex.mjs` |
|
|
27
27
|
| agy | 1.2.0 | attempt 1 exit 0 but wrote to its scratch workspace; attempt 2 (`--add-dir`) exit 0, file yes, DONE yes, 41 s | `src/adapters/agy.mjs` |
|
|
28
|
-
| grok | 0.
|
|
28
|
+
| grok | 1.0.34 | exit 1, no file, no DONE: the account answered `402 Payment Required: Grok Build usage balance exhausted`, classified `limit` (re-probed 2026-09-17; the 2026-09-11 run on 0.2.51 was not logged in) | `src/adapters/grok.mjs` |
|
|
29
29
|
|
|
30
30
|
The auth-source check: the build shell carried `ANTHROPIC_API_KEY` and
|
|
31
31
|
`OPENAI_API_KEY` (injected by the shell profile). Every adapter's `env()` deletes
|
|
@@ -165,29 +165,48 @@ stderr are 0 bytes, and codex's is the one stdin notice. observed-live.
|
|
|
165
165
|
|
|
166
166
|
## grok
|
|
167
167
|
|
|
168
|
-
- Version grok 0.
|
|
168
|
+
- Version grok 1.0.34 (3736acbc8658) [stable] (source: `grok --version`,
|
|
169
|
+
2026-09-17). The 2026-09-11 sweep saw 0.2.51 on a machine with no login; every
|
|
170
|
+
line below was re-read on 1.0.34.
|
|
169
171
|
- Binary: `~/.grok/bin/grok.exe` (native; also an npm shim on PATH);
|
|
170
172
|
`LEG_GROK_BIN` overrides (source: `where grok`).
|
|
171
|
-
- Headless argv (from cmd.txt): `grok.exe -
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
173
|
+
- Headless argv (from cmd.txt): `grok.exe --prompt-file <run>/prompt.txt
|
|
174
|
+
--output-format json --permission-mode acceptEdits --cwd <worktree>`; stdin
|
|
175
|
+
`ignore`. The file form is used whenever the runner has written a prompt file,
|
|
176
|
+
because a hand-off prompt carries the whole bundle summary and Windows caps a
|
|
177
|
+
command line near 32k; with none, the adapter falls back to `-p "<prompt>"`
|
|
178
|
+
(source: `grok --help`, `-p, --single <PROMPT>` and `--prompt-file <PATH>`).
|
|
179
|
+
`--cwd <CWD>` is passed explicitly rather than trusting the spawn's working
|
|
180
|
+
directory, because grok can run against a shared leader process
|
|
181
|
+
(`~/.grok/leader.sock`) and a leg must edit its own worktree.
|
|
182
|
+
- Output: the Claude Code result envelope. `"type":"result"`, `subtype`,
|
|
183
|
+
`is_error`, `session_id`, `result`, `num_turns`, `stop_reason`, `total_cost`,
|
|
184
|
+
read out of the shipped `grok.exe` on 2026-09-17 alongside the streaming
|
|
185
|
+
types (`assistant`, `system`, `text`, `usage`, `end`, `error`). An error is
|
|
186
|
+
the other envelope, `{"type":"error","message":…}` (observed-live,
|
|
187
|
+
`fixtures/live/grok/out.log`).
|
|
175
188
|
- Exit codes:
|
|
176
189
|
|
|
177
190
|
| exit | meaning | source |
|
|
178
191
|
|------|---------|--------|
|
|
179
|
-
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
192
|
+
| 1 | printed `{"type":"error","message":"Internal error: … API error (status 402 Payment Required): Grok Build usage balance exhausted"}` and did no work: the account had no balance left | observed-live 2026-09-17, fixtures/live/grok/out.log |
|
|
193
|
+
| 0 | printed JSON with `stopReason:"Cancelled"` and did no work: the CLI was not logged in, printed a device-code prompt on stderr and gave up after ~58 s | observed-live 2026-09-11 on 0.2.51 |
|
|
194
|
+
|
|
195
|
+
The 402 is a wall, not a failure: `fixtures/limits/grok/grok-balance-exhausted.json`
|
|
196
|
+
classifies it `limit`, so the card hands off instead of stopping. None of the
|
|
197
|
+
rate-limit strings the tap watched for appear in a 402, which is why an
|
|
198
|
+
exhausted grok terminal used to sit there; `src/taps/grok.mjs` reads it too now.
|
|
199
|
+
- Permission modes: `default`, `acceptEdits`, `auto`, `dontAsk`, `plan`, and a
|
|
200
|
+
bypass mode Leg never passes (source: `grok --help`). Leg: default
|
|
201
|
+
`acceptEdits`; forbidden the bypass mode and `--always-approve`.
|
|
186
202
|
- Login: `grok login` (source: `grok --help` Commands).
|
|
187
|
-
- **Verdict:
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
203
|
+
- **Verdict: registered, wall path verified, success path not.** The probe
|
|
204
|
+
reached the account through the real runner, so the binary resolution, the
|
|
205
|
+
argv, the auth and the limit classification are all observed-live. A leg that
|
|
206
|
+
completes its task has not been seen, because that needs balance on the
|
|
207
|
+
account; until it is, a grok leg whose envelope does not parse is judged by
|
|
208
|
+
its `.leg/DONE` marker and its diff, as every adapter is. Re-run
|
|
209
|
+
`node scripts/probe.mjs --adapter grok --repo <toy>` with balance to close it.
|
|
191
210
|
|
|
192
211
|
## `leg harness` (the portable harness)
|
|
193
212
|
|
|
@@ -391,6 +410,20 @@ in [VOCABULARY.md](VOCABULARY.md). Board routes: `GET /api/sessions`,
|
|
|
391
410
|
`POST /api/sessions/:id/end`, `DELETE /api/sessions/:id`, with the list pushed
|
|
392
411
|
as the SSE `sessions` event (source: src/server.mjs, src/board/sessions.js).
|
|
393
412
|
|
|
413
|
+
### History index
|
|
414
|
+
|
|
415
|
+
`<LEG_HOME>/history/index.json` is the one file `leg history` writes: per
|
|
416
|
+
provider, per transcript, size, mtime and a small scrubbed record (source:
|
|
417
|
+
src/history/index.mjs). What each provider reads from its CLI's own store,
|
|
418
|
+
with the observed file and field names, is the header of each
|
|
419
|
+
`src/history/providers/<name>.mjs`; the support matrix and the resume
|
|
420
|
+
commands quoted from each CLI's `--help` (observed-live 2026-09-16: `claude
|
|
421
|
+
--resume <id>`, `codex resume <id>`, `grok --resume <id>`, `agy --conversation
|
|
422
|
+
<id>`; copilot has none Leg has verified) are in [history.md](history.md).
|
|
423
|
+
Board routes, owner only: `GET /api/history`, `GET /api/history/:id`,
|
|
424
|
+
`GET /api/history/providers`, `POST /api/history/refresh`,
|
|
425
|
+
`GET /api/worktrees` (source: src/server.mjs, src/board/history.js).
|
|
426
|
+
|
|
394
427
|
## Limit signals
|
|
395
428
|
|
|
396
429
|
Recorded in `fixtures/limits/<group>/<id>.json` and classified by
|
package/docs/configuration.md
CHANGED
|
@@ -126,7 +126,8 @@ resolves to by default.
|
|
|
126
126
|
| `LEG_CLAUDE_BIN` | claude | `src/adapters/claude.mjs` |
|
|
127
127
|
| `LEG_CODEX_BIN` | codex | `src/adapters/codex.mjs` |
|
|
128
128
|
| `LEG_AGY_BIN` | agy | `src/adapters/agy.mjs` |
|
|
129
|
-
| `LEG_GROK_BIN` | grok
|
|
129
|
+
| `LEG_GROK_BIN` | grok | `src/adapters/grok.mjs` |
|
|
130
|
+
| `LEG_<NAME>_BIN` | a custom adapter called `<name>` (dashes become underscores, so `my-agent` reads `LEG_MY_AGENT_BIN`) | `src/adapters/custom.mjs` |
|
|
130
131
|
| `LEG_GH_BIN` | the `pr` land-mode stub | `src/stations/pr.mjs`; unset, `land_mode: pr` returns an error rather than running a real `gh` |
|
|
131
132
|
| `LEG_CHB_BIN` | `context-handoff-bundle` | `src/handoff.mjs`; unset, Leg tries `context-handoff-bundle` on PATH, then `python -m context_handoff_bundle` |
|
|
132
133
|
|
|
@@ -200,6 +201,7 @@ Not meant for normal use; documented for completeness.
|
|
|
200
201
|
|----------|---------|---------|
|
|
201
202
|
| `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
203
|
| `LEG_SERVER_SCRIPT` | override the server script path the launcher spawns | `src/launcher.mjs` |
|
|
204
|
+
| `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
205
|
|
|
204
206
|
## Network exposure
|
|
205
207
|
|
|
@@ -209,9 +211,10 @@ other address without also setting `LEG_TOKEN` makes the server refuse to
|
|
|
209
211
|
start, exit code `3` (`src/auth.mjs` `checkBind`/`BindRefused`). With a
|
|
210
212
|
token set, every `/api/*` request needs an `Authorization: Bearer <token>`
|
|
211
213
|
header; the event stream (`EventSource`, which cannot set headers) accepts
|
|
212
|
-
the same token as a `?token=` query parameter instead.
|
|
213
|
-
|
|
214
|
-
|
|
214
|
+
the same token as a `?token=` query parameter instead. Keep `LEG_BIND` on
|
|
215
|
+
loopback unless you are using `leg share`, which gives each human their own
|
|
216
|
+
token (see [Share](#share-more-than-one-human)) and can serve the board over
|
|
217
|
+
TLS.
|
|
215
218
|
|
|
216
219
|
## Share (more than one human)
|
|
217
220
|
|
|
@@ -223,7 +226,8 @@ switch and the roster; the env variables below only tune the limits.
|
|
|
223
226
|
| `on` | share is on (it also needs a `bind` and at least one person) |
|
|
224
227
|
| `bind`, `bind_kind`, `port` | where the board listens: the Tailscale address by default, `lan`, or one you named |
|
|
225
228
|
| `owner` | the name a terminal belongs to when nothing else says (`LEG_PERSON`), and the name a loopback browser is treated as |
|
|
226
|
-
| `people[]` | `{ name, role: owner\|guest, token_sha256, created_at }`; the token itself is printed once and never stored |
|
|
229
|
+
| `people[]` | `{ name, role: owner\|operator\|guest, token_sha256, created_at }`; the token itself is printed once and never stored |
|
|
230
|
+
| `tls` | `{ cert, key }`, the paths to a certificate pair; absent means plain http |
|
|
227
231
|
| `loopback_owner` | default `true`: a browser on this machine is the owner without a token. Set it to `false` to ask for a link even here |
|
|
228
232
|
|
|
229
233
|
| variable | default | meaning | read in |
|
|
@@ -233,6 +237,53 @@ switch and the roster; the env variables below only tune the limits.
|
|
|
233
237
|
|
|
234
238
|
`LEG_TOKEN` is the single-token mode and is ignored while share is on.
|
|
235
239
|
|
|
240
|
+
### Roles
|
|
241
|
+
|
|
242
|
+
| role | terminals | cards | this machine |
|
|
243
|
+
|------|-----------|-------|--------------|
|
|
244
|
+
| `owner` | every one | every one | settings, harness policy, the trunk's repo paths, the history index, the worktree map, the audit trail |
|
|
245
|
+
| `operator` | their own; someone else's is read-only and redacted | adds, runs, approves, reassigns, kills | nothing: `/api/settings`, `/api/history`, `/api/worktrees`, `/api/trunk` and `/api/audit` all answer 403, and `/api/health` omits the home path |
|
|
246
|
+
| `guest` | their own; someone else's is read-only and redacted, with **Request handoff** as the only button | nothing: 403 | nothing |
|
|
247
|
+
|
|
248
|
+
A guest and an operator both see their own terminal's hand-off destinations,
|
|
249
|
+
so they can use the picker on it — but never the reset times behind them,
|
|
250
|
+
which are this machine's usage data. `src/share.mjs` `mayUseCards` and
|
|
251
|
+
`mayUseMachine` are the only place a role is turned into permission.
|
|
252
|
+
|
|
253
|
+
### TLS
|
|
254
|
+
|
|
255
|
+
Off unless a certificate pair is configured. Leg never issues one: a
|
|
256
|
+
self-signed pair teaches everyone on the board to click through a warning,
|
|
257
|
+
which is worse than plaintext on a network that is already private. On
|
|
258
|
+
Tailscale, `tailscale cert <machine>.<tailnet>.ts.net` issues a pair browsers
|
|
259
|
+
already trust.
|
|
260
|
+
|
|
261
|
+
| variable | meaning | read in |
|
|
262
|
+
|----------|---------|---------|
|
|
263
|
+
| `LEG_TLS_CERT` | path to the certificate; wins over `share.json`'s `tls.cert` | `src/share.mjs` `readTls` |
|
|
264
|
+
| `LEG_TLS_KEY` | path to the private key; wins over `share.json`'s `tls.key` | `src/share.mjs` `readTls` |
|
|
265
|
+
|
|
266
|
+
`leg share on --tls-cert <file> --tls-key <file>` writes the pair into
|
|
267
|
+
`share.json` and reads it once, so a bad pair fails there rather than at the
|
|
268
|
+
next board start. With a pair configured the shared address serves https and
|
|
269
|
+
every link `leg share` prints says `https://`. The companion listener on
|
|
270
|
+
`127.0.0.1` — the one that lets this machine's own browser in without a token
|
|
271
|
+
— stays plain http, because the certificate is for the shared name and
|
|
272
|
+
loopback traffic never leaves the machine. Half a pair, a missing file, an
|
|
273
|
+
unreadable file or an empty one stops the board with exit `3` rather than
|
|
274
|
+
falling back to plaintext. A renewed pair is picked up by `leg down && leg up`.
|
|
275
|
+
|
|
276
|
+
### The audit trail
|
|
277
|
+
|
|
278
|
+
`GET /api/audit` (owner only) and Settings → **Audit trail** on the board: one
|
|
279
|
+
list across every terminal and every card, newest first, of the actions a
|
|
280
|
+
person or an agent took — hand-offs, landings, approvals, reassignments,
|
|
281
|
+
kills. It reads the events already on disk and stores nothing new. Query
|
|
282
|
+
parameters: `limit` (default 200, max 1000), `since` (an ISO timestamp), `who`
|
|
283
|
+
(a name), `kind` (`human`, `agent` or `leg`). Every answer carries `scanned`
|
|
284
|
+
— how many terminals, cards and events it read — so an empty trail cannot be
|
|
285
|
+
mistaken for a quiet week.
|
|
286
|
+
|
|
236
287
|
## Card-level options
|
|
237
288
|
|
|
238
289
|
These configure one card, not the whole install (set with `card add` flags
|