@ucsandman/legcli 0.7.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 +560 -0
- package/LICENSE +58 -0
- package/NOTICE +4 -0
- package/README.md +558 -0
- package/bin/fake-agent.mjs +143 -0
- package/bin/leg.mjs +449 -0
- package/docs/DECISIONS.md +119 -0
- package/docs/DEMO.md +214 -0
- package/docs/DEVIATIONS.md +182 -0
- package/docs/ERRORS.md +326 -0
- package/docs/README.md +40 -0
- package/docs/REUSE.md +289 -0
- package/docs/ROADMAP-v2.md +55 -0
- package/docs/VOCABULARY.md +243 -0
- package/docs/adapters.md +300 -0
- package/docs/board-guide.md +579 -0
- package/docs/cli-contracts.md +401 -0
- package/docs/concepts.md +426 -0
- package/docs/configuration.md +220 -0
- package/docs/faq.md +206 -0
- package/docs/getting-started.md +251 -0
- package/docs/real-run.md +80 -0
- package/docs/review-2026-09-14.md +72 -0
- package/docs/screenshots/board-400px.png +0 -0
- package/docs/screenshots/board-details-open.png +0 -0
- package/docs/screenshots/board-done.png +0 -0
- package/docs/screenshots/board-drawer.png +0 -0
- package/docs/screenshots/board-empty.png +0 -0
- package/docs/screenshots/board-handoff.png +0 -0
- package/docs/screenshots/board-running.png +0 -0
- package/docs/screenshots/demo-1-claude-running.png +0 -0
- package/docs/screenshots/demo-2-limit-hit.png +0 -0
- package/docs/screenshots/demo-3-handoff-bundle.png +0 -0
- package/docs/screenshots/demo-4-codex-running.png +0 -0
- package/docs/screenshots/demo-5-done.png +0 -0
- package/docs/screenshots/floor-final-1280.png +0 -0
- package/docs/screenshots/floor-final-400.png +0 -0
- package/docs/screenshots/floor-landing.png +0 -0
- package/docs/screenshots/floor.png +0 -0
- package/docs/screenshots/share-guest-1280.png +0 -0
- package/docs/screenshots/share-owner-1280.png +0 -0
- package/docs/screenshots/terminals-1280.png +0 -0
- package/docs/screenshots/thumb-done.jpg +0 -0
- package/docs/screenshots/thumb-handoff-details.jpg +0 -0
- package/docs/screenshots/thumb-limit-hit.jpg +0 -0
- package/docs/screenshots/youtube-thumbnail.jpg +0 -0
- package/fixtures/limits/agy/agy-resource-exhausted.json +11 -0
- package/fixtures/limits/claude/claude-budget-limit.json +11 -0
- package/fixtures/limits/claude/claude-max-turns.json +11 -0
- package/fixtures/limits/claude/claude-model-limit.json +11 -0
- package/fixtures/limits/claude/claude-session-limit.json +11 -0
- package/fixtures/limits/claude/claude-weekly-limit.json +11 -0
- package/fixtures/limits/codex/codex-quota-exceeded.json +11 -0
- package/fixtures/limits/codex/codex-rate-limit-exceeded.json +11 -0
- package/fixtures/limits/codex/codex-skills-notice.json +11 -0
- package/fixtures/limits/codex/codex-usage-limit-reached.json +11 -0
- package/fixtures/limits/codex/codex-usage-limit.json +11 -0
- package/fixtures/limits/generic/generic-429.json +11 -0
- package/fixtures/limits/generic/generic-overloaded.json +11 -0
- package/fixtures/limits/generic/generic-quota.json +11 -0
- package/fixtures/limits/generic/generic-rate-limit.json +11 -0
- package/fixtures/limits/generic/generic-resource-exhausted.json +11 -0
- package/fixtures/limits/generic/generic-usage-limit.json +11 -0
- package/fixtures/limits/grok/grok-not-logged-in.json +11 -0
- package/fixtures/limits/negative/auth-source-set.json +11 -0
- package/fixtures/limits/negative/compile-error.json +11 -0
- package/fixtures/limits/negative/empty-stdout-exit-0.json +11 -0
- package/fixtures/live/agy/attempt-1-scratch-workspace.out.log +1 -0
- package/fixtures/live/agy/cmd.txt +1 -0
- package/fixtures/live/agy/err.log +0 -0
- package/fixtures/live/agy/out.log +1 -0
- package/fixtures/live/agy/parsed.json +5 -0
- package/fixtures/live/agy/run.json +18 -0
- package/fixtures/live/agy/supervisor.log +2 -0
- package/fixtures/live/claude/cmd.txt +1 -0
- package/fixtures/live/claude/err.log +0 -0
- package/fixtures/live/claude/limit-rate_limit.json +21 -0
- package/fixtures/live/claude/out.log +1 -0
- package/fixtures/live/claude/parsed.json +11 -0
- package/fixtures/live/claude/run.json +18 -0
- package/fixtures/live/claude/supervisor.log +2 -0
- package/fixtures/live/codex/cmd.txt +1 -0
- package/fixtures/live/codex/err.log +1 -0
- package/fixtures/live/codex/last.md +2 -0
- package/fixtures/live/codex/limit-usage_limit_exceeded.json +25 -0
- package/fixtures/live/codex/out.log +8 -0
- package/fixtures/live/codex/parsed.json +15 -0
- package/fixtures/live/codex/run.json +18 -0
- package/fixtures/live/codex/supervisor.log +2 -0
- package/fixtures/live/grok/cmd.txt +1 -0
- package/fixtures/live/grok/err.log +32 -0
- package/fixtures/live/grok/out.log +7 -0
- package/fixtures/live/grok/parsed.json +5 -0
- package/fixtures/live/grok/run.json +18 -0
- package/fixtures/live/grok/supervisor.log +2 -0
- package/fixtures/verified.json +34 -0
- package/package.json +68 -0
- package/scripts/board-shots.mjs +69 -0
- package/scripts/build-docs-site.mjs +308 -0
- package/scripts/check-claims.mjs +150 -0
- package/scripts/clean-clone-check.sh +42 -0
- package/scripts/license-sign.mjs +23 -0
- package/scripts/limits-table.mjs +38 -0
- package/scripts/live-limits.mjs +36 -0
- package/scripts/privacy-check.mjs +63 -0
- package/scripts/probe.mjs +92 -0
- package/scripts/seed-fake-cards.mjs +34 -0
- package/scripts/seed-floor-board.mjs +69 -0
- package/scripts/seed-wes-board.mjs +91 -0
- package/scripts/stripe-setup.mjs +99 -0
- package/scripts/vercel-env.mjs +40 -0
- package/src/accounts.mjs +119 -0
- package/src/adapters/agy.mjs +56 -0
- package/src/adapters/claude.mjs +54 -0
- package/src/adapters/codex.mjs +73 -0
- package/src/adapters/common.mjs +32 -0
- package/src/adapters/fake.mjs +48 -0
- package/src/adapters/grok.mjs +51 -0
- package/src/adapters/index.mjs +27 -0
- package/src/adapters/resolve.mjs +40 -0
- package/src/attach.mjs +592 -0
- package/src/auth.mjs +85 -0
- package/src/board/board.css +543 -0
- package/src/board/board.js +1307 -0
- package/src/board/favicon.svg +1 -0
- package/src/board/floor.html +127 -0
- package/src/board/floor.js +670 -0
- package/src/board/fonts/OFL-atkinson-hyperlegible-next.txt +93 -0
- package/src/board/fonts/OFL-azeret-mono.txt +94 -0
- package/src/board/fonts/atkinson-board.woff2 +0 -0
- package/src/board/fonts/azeret-board.woff2 +0 -0
- package/src/board/index.html +251 -0
- package/src/board/sessions.js +1451 -0
- package/src/bundle.mjs +103 -0
- package/src/cards.mjs +114 -0
- package/src/chain.mjs +264 -0
- package/src/commands.mjs +79 -0
- package/src/contract.mjs +73 -0
- package/src/env.mjs +19 -0
- package/src/fsx.mjs +72 -0
- package/src/git-snapshot.mjs +152 -0
- package/src/handoff.mjs +171 -0
- package/src/hook.mjs +49 -0
- package/src/land.mjs +106 -0
- package/src/launcher.mjs +272 -0
- package/src/leases.mjs +86 -0
- package/src/ledger.mjs +369 -0
- package/src/license.mjs +168 -0
- package/src/limits.mjs +97 -0
- package/src/live-capture.mjs +69 -0
- package/src/mergequeue.mjs +225 -0
- package/src/orchestrator.mjs +365 -0
- package/src/pipeline.mjs +100 -0
- package/src/preferences.mjs +47 -0
- package/src/presets.mjs +23 -0
- package/src/ratelimit.mjs +49 -0
- package/src/redact.mjs +47 -0
- package/src/resume.mjs +354 -0
- package/src/runner.mjs +438 -0
- package/src/scheduler.mjs +118 -0
- package/src/server.mjs +845 -0
- package/src/session-detail.mjs +129 -0
- package/src/sessions.mjs +202 -0
- package/src/share.mjs +163 -0
- package/src/stations/agent.mjs +42 -0
- package/src/stations/build.mjs +9 -0
- package/src/stations/human.mjs +5 -0
- package/src/stations/land.mjs +6 -0
- package/src/stations/plan.mjs +8 -0
- package/src/stations/pr.mjs +31 -0
- package/src/stations/review.mjs +8 -0
- package/src/stations/test.mjs +27 -0
- package/src/store.mjs +90 -0
- package/src/sync/dashclaw.mjs +70 -0
- package/src/sync/index.mjs +54 -0
- package/src/sync/workboard.mjs +54 -0
- package/src/taps/agy.mjs +64 -0
- package/src/taps/claude-usage.mjs +67 -0
- package/src/taps/claude.mjs +170 -0
- package/src/taps/codex.mjs +286 -0
- package/src/trust.mjs +323 -0
- package/src/usage.mjs +179 -0
- package/src/wait.mjs +30 -0
- package/src/worktree.mjs +207 -0
package/docs/concepts.md
ADDED
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
# Concepts
|
|
2
|
+
|
|
3
|
+
Two halves. The first four sections are the 0.2 way in: `leg claude` runs an
|
|
4
|
+
interactive agent and Leg watches it. The rest is the v0.1 pipeline, which
|
|
5
|
+
still works and now sits below the Terminals lane on the board. Read
|
|
6
|
+
[getting-started.md](getting-started.md) first if you have not run anything
|
|
7
|
+
yet.
|
|
8
|
+
|
|
9
|
+
## Sessions
|
|
10
|
+
|
|
11
|
+
A **session** is one terminal running one agent under Leg. `leg claude`,
|
|
12
|
+
`leg codex` and `leg agy` each create one. Leg spawns the real CLI with
|
|
13
|
+
stdio inherited, so the agent's own TUI, prompts, permissions, hooks and skills
|
|
14
|
+
are what you see; every argument after the agent name is passed through
|
|
15
|
+
unchanged.
|
|
16
|
+
|
|
17
|
+
Each session gets a directory under `$LEG_HOME/sessions/<id>/`
|
|
18
|
+
(`src/sessions.mjs`):
|
|
19
|
+
|
|
20
|
+
| file | what it holds |
|
|
21
|
+
|------|---------------|
|
|
22
|
+
| `session.json` | the live record the board renders; the runner is its only writer, and every write is one atomic replace |
|
|
23
|
+
| `events.jsonl` | the timeline (see [VOCABULARY.md](VOCABULARY.md#session-event-types)) |
|
|
24
|
+
| `control.json` | requests from the board to the runner, for example `{ handoff: true }` |
|
|
25
|
+
| `hook.log` | what Claude Code's hooks sent, claude sessions only |
|
|
26
|
+
| `claude-settings.json` | the per-session `--settings` file, claude sessions only |
|
|
27
|
+
| `agy.log` | agy's `--log-file`, agy sessions only |
|
|
28
|
+
| `land.json` | the last Land of a session with its own worktree (landing, landed, noop, bounced); the board server is its only writer |
|
|
29
|
+
| `requests.json` | hand-off requests from another human on a shared board (`{ by, at, state }`); the board server is its only writer |
|
|
30
|
+
|
|
31
|
+
A session's status is one of `starting`, `running`, `warning`, `limit`,
|
|
32
|
+
`handing_off`, `waiting`, `handed_off`, `ended`, `lost`. `lost` means the runner process
|
|
33
|
+
that owned the terminal is gone (closed window, crash); the board never shows
|
|
34
|
+
it as live.
|
|
35
|
+
|
|
36
|
+
The board is started detached on `127.0.0.1:4747` by the first session that
|
|
37
|
+
finds it down, and opened once. Later sessions reuse it.
|
|
38
|
+
|
|
39
|
+
## Accounts
|
|
40
|
+
|
|
41
|
+
An **account** is one login for one agent. `default` is the CLI's own home
|
|
42
|
+
(`~/.claude`, `~/.codex`). An extra account is a directory under
|
|
43
|
+
`$LEG_HOME/accounts/<agent>/<name>/` that the CLI is pointed at with its
|
|
44
|
+
config-directory variable: `CLAUDE_CONFIG_DIR` for claude, `CODEX_HOME` for
|
|
45
|
+
codex (`src/accounts.mjs` `LAYOUT`). agy 1.2.0 has no config-directory
|
|
46
|
+
override, so agy stays one account.
|
|
47
|
+
|
|
48
|
+
Your harness is shared into an extra account, never copied into a fork that
|
|
49
|
+
drifts: the directories are junctions back to the real home (claude: `hooks`,
|
|
50
|
+
`skills`, `agents`, `commands`, `plugins`, `rules`, `scripts`,
|
|
51
|
+
`output-styles`, `tools`; codex: `skills`, `prompts`, `rules`, `plugins`,
|
|
52
|
+
`agents`, `hooks`, `memories`, `superpowers`), and the settings files are
|
|
53
|
+
copied fresh before every launch (claude: `settings.json`,
|
|
54
|
+
`settings.local.json`, `CLAUDE.md`, `keybindings.json`, `statusline.ps1`,
|
|
55
|
+
`statusline-combined.ps1`; codex: `config.toml`, `AGENTS.md`). Only the login
|
|
56
|
+
itself lives in the account directory.
|
|
57
|
+
|
|
58
|
+
`leg accounts add <claude|codex> <name>` creates one and prints the single
|
|
59
|
+
line to paste to log in. `leg accounts rm` removes the junctions as links,
|
|
60
|
+
never following them, and deletes the directory. `leg accounts terms` prints
|
|
61
|
+
what both vendors' terms say about a second account; the quotes are in the
|
|
62
|
+
README.
|
|
63
|
+
|
|
64
|
+
## Usage windows
|
|
65
|
+
|
|
66
|
+
Every agent exposes two rolling windows: a 5-hour one and a 7-day one. Leg
|
|
67
|
+
keeps the latest reading per (agent, account) in
|
|
68
|
+
`$LEG_HOME/usage/<agent>--<account>.json` (`src/usage.mjs`):
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
{ five_hour: {pct, resets_at}, seven_day: {pct, resets_at},
|
|
72
|
+
limited_until, limited_reason, source, updated_at }
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Where each number comes from is per agent, and is in
|
|
76
|
+
[adapters.md](adapters.md). The rules on top of them are shared:
|
|
77
|
+
|
|
78
|
+
- **Warning** at `WARN_PCT`, default 85, settable with `LEG_WARN_PCT`. The
|
|
79
|
+
highest percentage across the known windows is the pressure; the hottest
|
|
80
|
+
window names the warning.
|
|
81
|
+
- **Wall.** `markLimited()` records `limited_until` from the reset time the CLI
|
|
82
|
+
itself reported. With no reset time it uses the soonest known window reset,
|
|
83
|
+
and with neither it assumes five hours.
|
|
84
|
+
- **Clearing.** A usage reading that arrives after `limited_until` has passed
|
|
85
|
+
clears the wall.
|
|
86
|
+
|
|
87
|
+
## Handoff (interactive)
|
|
88
|
+
|
|
89
|
+
When a session hits its limit, or you press **Hand off now**, Leg does four
|
|
90
|
+
things in order (`src/attach.mjs`, `src/bundle.mjs`):
|
|
91
|
+
|
|
92
|
+
1. **Bundle.** `sessionNotes()` writes the six sections
|
|
93
|
+
`context-handoff-bundle` parses (Scope, Projects mentioned, Findings,
|
|
94
|
+
Opportunities, Open questions, Evidence anchors) from the task, the last
|
|
95
|
+
messages in the transcript, `git diff --stat`, the dirty files, the files
|
|
96
|
+
edited this session, recent commits and why it stopped. The CLI is called as
|
|
97
|
+
`context-handoff-bundle save --repo-local --slug leg-<session id>`, with
|
|
98
|
+
`--update <slug>` after the first time, so one bundle per session is updated
|
|
99
|
+
in place. A checkpoint runs about every two minutes while the session has
|
|
100
|
+
turns, and at every warning, limit and hand-off.
|
|
101
|
+
2. **Choose.** `candidates()` lists the other accounts of the same agent first,
|
|
102
|
+
then every other agent in the terminal's saved order. That order is an
|
|
103
|
+
absolute priority list, not a rotation anchored on the agent running now:
|
|
104
|
+
an agent placed last is tried last whichever agent the terminal started on,
|
|
105
|
+
and every option is still tried once. The default order is claude, codex,
|
|
106
|
+
agy. `chooseNext()` skips a missing CLI or an option whose wall has not
|
|
107
|
+
reset. The board can save a new order for an active terminal; the wrapper reads
|
|
108
|
+
it again at the transition and during all-out waiting. Machine Settings is
|
|
109
|
+
copied only when a new terminal starts.
|
|
110
|
+
3. **Switch.** The agent process is stopped and the terminal restored. The
|
|
111
|
+
bundle's `context-handoff-bundle load <id>` output is written to
|
|
112
|
+
`.leg/RESUME-<session-id>.md` and copied to `.leg/RESUME.md`, and the next
|
|
113
|
+
agent starts in the same terminal with a short pointer prompt as its first
|
|
114
|
+
positional argument: `claude "<prompt>"`, `codex "<prompt>"`,
|
|
115
|
+
`agy -i "<prompt>"`. The prompt names the per-session file, and says to check
|
|
116
|
+
`git status` and `git diff`, continue, and not ask the human to restate the
|
|
117
|
+
task.
|
|
118
|
+
4. **All out.** If every option is walled, Leg prints each one with its reset
|
|
119
|
+
time, soonest first, then waits in the terminal with a one-line countdown
|
|
120
|
+
(`src/wait.mjs`) and starts the first option back from the bundle when its
|
|
121
|
+
reset passes; if that option is walled again meanwhile it re-picks and
|
|
122
|
+
waits again. The card records `session.all_out` and `session.waiting`
|
|
123
|
+
(`{ agent, account, resets_at, since }`) and shows status `waiting`. Ctrl-C
|
|
124
|
+
in the terminal, or End on the card, quits with exit 3.
|
|
125
|
+
|
|
126
|
+
## The resume pointer
|
|
127
|
+
|
|
128
|
+
`.leg/RESUME.md` is the file humans and other agents open by habit, so Leg
|
|
129
|
+
owns it and keeps it from describing a picture that is no longer true.
|
|
130
|
+
|
|
131
|
+
Every resume file starts with a stamp, an HTML comment that renders as nothing:
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
<!-- leg-resume {"v":1,"kind":"handoff","session":"s-…","head":"cf27986…",
|
|
135
|
+
"branch":"main","dirty":{"count":12,"hash":"0a4c4f34ee93"},
|
|
136
|
+
"live":[{"id":"s-…","agent":"claude"}],"bundle":"…","written_at":"…"} -->
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The stamp says what was true when the file was written. It is never read as a
|
|
140
|
+
verdict. `leg resume --check` asks git what is true now and reports the
|
|
141
|
+
difference, so a file cannot lie about HEAD to a reader who re-asks git:
|
|
142
|
+
|
|
143
|
+
| state | when | exit |
|
|
144
|
+
| --- | --- | --- |
|
|
145
|
+
| current | the repository still matches the stamp, and the terminals it names are the ones that are live | 0 |
|
|
146
|
+
| stale | a commit landed, the working tree moved, the terminal it describes is gone, or another one appeared | 1 |
|
|
147
|
+
| unstamped | no Leg wrote this file, so nothing can be checked | 1 |
|
|
148
|
+
| missing | there is no `.leg/RESUME.md` from here up to the filesystem root | 3 |
|
|
149
|
+
|
|
150
|
+
The working-tree fingerprint is a count and a short hash of the sorted paths,
|
|
151
|
+
never the names: a shared board must not leak what someone is working on.
|
|
152
|
+
Leg's own directories (`.leg/`, `.context-handoffs/`) are left out of it, so
|
|
153
|
+
Leg's bookkeeping never reads as the human's work moving on.
|
|
154
|
+
|
|
155
|
+
Two things rewrite `RESUME.md` besides a hand-off. A session ending replaces it
|
|
156
|
+
with a "nothing in flight" pointer naming the last hand-off, its date and the
|
|
157
|
+
per-session file that still holds its full text. The board, at start, does the
|
|
158
|
+
same for any checkout whose pointer describes a terminal that is gone or that no
|
|
159
|
+
Leg stamped, the case where a terminal crashed instead of exiting. A terminal
|
|
160
|
+
that is genuinely still running keeps its own hand-off text; only the terminal
|
|
161
|
+
that owns a pointer may replace it.
|
|
162
|
+
|
|
163
|
+
`leg resume` prints the body, with a loud banner and a non-zero exit when it
|
|
164
|
+
is stale: a stale hand-off still beats nothing when a human chooses to read it,
|
|
165
|
+
and the exit code is what a script or a hook keys on. The terminal drawer's
|
|
166
|
+
"What happens next" section shows the same verdict, recomputed every poll.
|
|
167
|
+
|
|
168
|
+
`LEG_NO_HANDOFF=1` keeps the warning and the record but never switches.
|
|
169
|
+
|
|
170
|
+
## Share (more than one human)
|
|
171
|
+
|
|
172
|
+
`leg share` is off until you run it (`src/share.mjs`). On, it writes
|
|
173
|
+
`$LEG_HOME/share.json`: where the board listens, who is on it, and one
|
|
174
|
+
sha256 hash per person's token (the token itself is printed once). From then
|
|
175
|
+
on:
|
|
176
|
+
|
|
177
|
+
- Every `/api` request names a human: their token, or a browser on the board's
|
|
178
|
+
own machine, which is the owner.
|
|
179
|
+
- A terminal belongs to the human who started it (`LEG_PERSON`, else the
|
|
180
|
+
owner). Only they, and an owner, can read or control it.
|
|
181
|
+
- Everyone else sees the card without anything the terminal has said, read or
|
|
182
|
+
written, and one button: Request handoff. The request lands in the session's
|
|
183
|
+
`requests.json`; the owner approves it on the card, and the runner is told
|
|
184
|
+
who it was for.
|
|
185
|
+
- The pipeline side of the board is the owner's alone (403 for a guest).
|
|
186
|
+
|
|
187
|
+
`leg share off` puts the board back on `127.0.0.1` and every link stops
|
|
188
|
+
working; `leg share rotate <name>` replaces one.
|
|
189
|
+
|
|
190
|
+
## Cards, stations and pipelines
|
|
191
|
+
|
|
192
|
+
Everything from here down is the v0.1 pipeline: headless agents in a git
|
|
193
|
+
worktree, one per card. It has not changed since 0.2.0 and is not the way in.
|
|
194
|
+
|
|
195
|
+
A **card** is one task moving through a **pipeline**: an ordered list of
|
|
196
|
+
**stations**. A station has a `kind`:
|
|
197
|
+
|
|
198
|
+
| kind | what runs |
|
|
199
|
+
|------|-----------|
|
|
200
|
+
| `agent` | the station's chain (one or more adapters, in fallback order) with that station's prompt |
|
|
201
|
+
| `human` | nothing automatic; the card parks in `waiting_human` for a button press |
|
|
202
|
+
| `test` | the repo's test command; red bounces the card back to the nearest earlier `build` agent station |
|
|
203
|
+
| `land` | the merge queue: rebase, test, fast-forward trunk (see [Land station](#the-land-station)) |
|
|
204
|
+
|
|
205
|
+
Three presets ship in `src/presets.mjs`:
|
|
206
|
+
|
|
207
|
+
| preset | stations |
|
|
208
|
+
|--------|----------|
|
|
209
|
+
| `build` | `build` (agent) |
|
|
210
|
+
| `build-land` | `build` (agent) → `test` → `land` |
|
|
211
|
+
| `factory` | `plan` (agent) → `build` (agent) → `review` (agent) → `test` → `land` |
|
|
212
|
+
|
|
213
|
+
A custom pipeline is a JSON array of stations passed as `--pipeline <file>`
|
|
214
|
+
or the board's "custom JSON" option. A `land` station must be last, and a
|
|
215
|
+
pipeline may have at most one.
|
|
216
|
+
|
|
217
|
+
## Chains and legs
|
|
218
|
+
|
|
219
|
+
A station's `chain` is an ordered list of adapters: the fallback order for
|
|
220
|
+
that station. Each entry is one **leg**. When a leg ends without finishing
|
|
221
|
+
(a limit, a stall, an incomplete exit, a failure), Leg writes a handoff
|
|
222
|
+
bundle and starts the next entry in the chain as the next leg, in the same
|
|
223
|
+
worktree. If the chain is exhausted, the card fails.
|
|
224
|
+
|
|
225
|
+
## Adapters and modes
|
|
226
|
+
|
|
227
|
+
Every adapter spawns its CLI as argv, never a shell, with its own permission
|
|
228
|
+
mode. Leg never passes a bypass/YOLO flag; requesting one throws before
|
|
229
|
+
anything spawns.
|
|
230
|
+
|
|
231
|
+
| adapter | default mode | allowed modes |
|
|
232
|
+
|---------|---------------|----------------|
|
|
233
|
+
| `claude` | `acceptEdits` | acceptEdits, auto, plan, manual, dontAsk |
|
|
234
|
+
| `codex` | `workspace-write` | read-only, workspace-write |
|
|
235
|
+
| `agy` | `accept-edits` | accept-edits, plan |
|
|
236
|
+
| `fake` (and `fake-claude`/`fake-codex`/`fake-agy`/`fake-nostdin`) | `acceptEdits` | acceptEdits, plan, workspace-write, read-only, accept-edits, auto_edit |
|
|
237
|
+
| `grok` (built, not registered) | `acceptEdits` | default, acceptEdits, auto, dontAsk, plan |
|
|
238
|
+
|
|
239
|
+
See [adapters.md](adapters.md) for each adapter's exact argv, forbidden
|
|
240
|
+
flags, and gotchas.
|
|
241
|
+
|
|
242
|
+
## The DONE marker contract
|
|
243
|
+
|
|
244
|
+
Every leg gets the same contract, regardless of which CLI runs it
|
|
245
|
+
(`src/contract.mjs`): a file written to `.leg/CONTRACT.md` in the
|
|
246
|
+
worktree, stating the task, the station's goal and deliverables, and the
|
|
247
|
+
finish rule:
|
|
248
|
+
|
|
249
|
+
> When the task is finished and verified, write the file `.leg/DONE`
|
|
250
|
+
> containing one line that summarizes what you did.
|
|
251
|
+
|
|
252
|
+
Agents are also asked to keep `.leg/PROGRESS.md` updated as they go, one
|
|
253
|
+
line per step. Without a fresh `.leg/DONE`, Leg treats the leg as
|
|
254
|
+
unfinished and hands it to the next agent in the chain, no matter what the
|
|
255
|
+
CLI printed.
|
|
256
|
+
|
|
257
|
+
## Outcomes and the classifier
|
|
258
|
+
|
|
259
|
+
`src/limits.mjs` `classify()` turns one leg's raw result (exit code, stdout,
|
|
260
|
+
stderr, the parsed result JSON, whether `.leg/DONE` exists, and the git or
|
|
261
|
+
filesystem diff since the leg started) into one outcome. It checks, in this
|
|
262
|
+
order, stopping at the first match:
|
|
263
|
+
|
|
264
|
+
1. a spawn error → `launch_failed`
|
|
265
|
+
2. stderr says "another auth source is set" → `auth_failed`
|
|
266
|
+
3. an adapter-specific or generic `auth` signal → `auth_failed`
|
|
267
|
+
4. killed from the board → `killed`
|
|
268
|
+
5. the kill timer fired → `stalled`
|
|
269
|
+
6. exit 0 and `.leg/DONE` present → `completed`
|
|
270
|
+
7. an adapter-specific or generic `limit` signal → `limit`
|
|
271
|
+
8. a `launch` signal → `launch_failed`
|
|
272
|
+
9. exit 0, changes present, no DONE marker → `incomplete`
|
|
273
|
+
10. exit 0, no DONE marker, no changes → `no_progress`
|
|
274
|
+
11. anything else (non-zero exit, no recognized signal) → `failed`
|
|
275
|
+
|
|
276
|
+
`completed`, `auth_failed` and `killed` never hand off. Every other outcome
|
|
277
|
+
(`limit`, `incomplete`, `no_progress`, `stalled`, `failed`) hands the card to
|
|
278
|
+
the next chain entry, or fails the card if the chain is exhausted.
|
|
279
|
+
`auth_failed` and `launch_failed` never advance the chain either way: a
|
|
280
|
+
human fixes the environment and presses Rerun. The signal fixtures behind
|
|
281
|
+
this table are in `fixtures/limits/` and documented per-CLI in
|
|
282
|
+
[cli-contracts.md](cli-contracts.md).
|
|
283
|
+
|
|
284
|
+
## Handoff bundles
|
|
285
|
+
|
|
286
|
+
When a leg needs to hand off, `src/handoff.mjs` calls the
|
|
287
|
+
`context-handoff-bundle` CLI as an argv subprocess (never re-implementing
|
|
288
|
+
its format): it writes a structured notes file, saves the bundle
|
|
289
|
+
repo-local in the worktree (`.context-handoffs/`), and validates it. The
|
|
290
|
+
notes carry six sections in the bundle's own vocabulary:
|
|
291
|
+
|
|
292
|
+
- **Scope**: the task, and which card/station/leg/adapter stopped with which
|
|
293
|
+
outcome.
|
|
294
|
+
- **Projects mentioned**: the card id.
|
|
295
|
+
- **Findings**: `.leg/PROGRESS.md`'s lines, the previous agent's last
|
|
296
|
+
message, the diff summary, the touched files.
|
|
297
|
+
- **Opportunities**: read `.leg/PROGRESS.md` and `.leg/CONTRACT.md`,
|
|
298
|
+
continue from the last done step, then write `.leg/DONE`.
|
|
299
|
+
- **Open questions**: the outcome, the exit code, any bounce reason.
|
|
300
|
+
- **Evidence anchors**: the touched files, `.leg/PROGRESS.md`,
|
|
301
|
+
`.leg/CONTRACT.md`.
|
|
302
|
+
|
|
303
|
+
The next leg's prompt starts with the bundle's `load` output (the resume
|
|
304
|
+
text) followed by the same contract.
|
|
305
|
+
|
|
306
|
+
## Worktrees
|
|
307
|
+
|
|
308
|
+
Every card runs in its own git worktree: `<repo>/.leg-worktrees/<card-id>`
|
|
309
|
+
on branch `leg/<card-id>` (`src/worktree.mjs`). The repo root is never
|
|
310
|
+
touched by an agent directly. Every git call sets `MSYS_NO_PATHCONV=1` so
|
|
311
|
+
Git Bash on Windows does not rewrite absolute path arguments. Leg never
|
|
312
|
+
pushes, opens a remote, or removes a path outside
|
|
313
|
+
`<repo>/.leg-worktrees/`.
|
|
314
|
+
|
|
315
|
+
Terminal sessions use the same layout when they would collide. A `leg
|
|
316
|
+
<agent>` started in a checkout where another session is live gets
|
|
317
|
+
`<repo>/.leg-worktrees/<session-id>` on `leg/<session-id>`, cut from the
|
|
318
|
+
branch the checkout has out (`isolate` in `src/attach.mjs`); its `repo` stays
|
|
319
|
+
the checkout, so the board groups it with the others. Its Land button runs the
|
|
320
|
+
same merge queue as a card's land station, with one difference: the checkout
|
|
321
|
+
is a live terminal and may have local changes of its own. Those are left alone,
|
|
322
|
+
and a fast-forward that would overwrite one bounces `dirty-trunk` naming the
|
|
323
|
+
files. `~/.leg/landings.jsonl` records every landing (session, agent, who
|
|
324
|
+
pressed Land, the commits) for the landed-on-trunk list.
|
|
325
|
+
|
|
326
|
+
## Leases and the scheduler
|
|
327
|
+
|
|
328
|
+
A card can declare **leases**: path globs it claims for the duration of its
|
|
329
|
+
run (default `**`, meaning the whole repo). `src/leases.mjs` decides whether
|
|
330
|
+
two cards' leases could touch the same files; the check is a deliberate
|
|
331
|
+
approximation biased toward false positives, because a wrongly serialized
|
|
332
|
+
card costs minutes and a wrongly parallel card can corrupt a merge.
|
|
333
|
+
|
|
334
|
+
`src/scheduler.mjs` ticks once a second by default: it reads every card's
|
|
335
|
+
`card.json` (never in-memory state), starts queued cards whose leases do not
|
|
336
|
+
overlap any running card's leases, up to `LEG_MAX_CONCURRENT` (default 2)
|
|
337
|
+
running at once, and records one `blocked_by` ledger event whenever a
|
|
338
|
+
card's blocker changes.
|
|
339
|
+
|
|
340
|
+
## The land station
|
|
341
|
+
|
|
342
|
+
A pipeline ending in a `land` station lands continuously. `src/mergequeue.mjs`
|
|
343
|
+
runs one land at a time per repo root, FIFO, and does, in order:
|
|
344
|
+
|
|
345
|
+
1. checks the repo root is on the trunk branch and clean; otherwise bounces
|
|
346
|
+
`dirty-trunk` without touching the root;
|
|
347
|
+
2. commits whatever the agents left uncommitted in the worktree, then
|
|
348
|
+
rebases the card's branch onto trunk; a conflict aborts the rebase and
|
|
349
|
+
bounces `rebase-conflict` with the conflicting file list (a rebase git
|
|
350
|
+
refuses for any other reason, a hook say, bounces `rebase-failed` with
|
|
351
|
+
git's own words);
|
|
352
|
+
3. runs the test command (the card's `test_command`, else `npm test` from
|
|
353
|
+
`package.json`, else `pytest` when there is a `pyproject.toml`, else it
|
|
354
|
+
lands untested with a `land_warning`); red bounces `tests-red` with the
|
|
355
|
+
last lines;
|
|
356
|
+
4. fast-forwards trunk from the repo root (`git merge --ff-only`); if trunk
|
|
357
|
+
moved while the tests ran, it rebases once more and retries, then
|
|
358
|
+
bounces `trunk-moved`;
|
|
359
|
+
5. on success, records a `landed` event with the sha, files, and line
|
|
360
|
+
counts.
|
|
361
|
+
|
|
362
|
+
A bounce sends the card back to the nearest earlier `build` agent station
|
|
363
|
+
(or the first agent station) with the failure written into the next
|
|
364
|
+
handoff bundle's Open questions. `LEG_MAX_LAND_ATTEMPTS` (default 3) is a
|
|
365
|
+
shared cap: the `test` station's own bounces and the `land` station's
|
|
366
|
+
bounces both increment `land_attempts`, so a card that never goes green
|
|
367
|
+
cannot loop forever.
|
|
368
|
+
|
|
369
|
+
## The ledger and actors
|
|
370
|
+
|
|
371
|
+
`src/ledger.mjs` is the only writer of a card's on-disk state
|
|
372
|
+
(`$LEG_HOME/cards/<id>/`). Every event names an **actor**: `{type:
|
|
373
|
+
'agent', adapter}`, `{type: 'human', id}`, or `{type: 'baton'}`. Each actor
|
|
374
|
+
writes to its own `events-<actor-key>.jsonl` file (append-only); reading a
|
|
375
|
+
card's events merges every writer's file, sorted by timestamp. The board and
|
|
376
|
+
CLI read only these files; there is no separate in-memory state to fall out
|
|
377
|
+
of sync with a restart.
|
|
378
|
+
|
|
379
|
+
## Card status state diagram
|
|
380
|
+
|
|
381
|
+
Generated from `src/chain.mjs` `TRANSITIONS`. Two rules are not drawn as
|
|
382
|
+
per-state arrows because they apply broadly: `kill` moves any non-terminal
|
|
383
|
+
status (`backlog`, `queued`, `running`, `handing_off`, `waiting_human`,
|
|
384
|
+
`needs_approval`, `paused`) straight to `killed`, and `rerun` moves any
|
|
385
|
+
terminal status (`done`, `failed`, `killed`) back to `queued` (station 0,
|
|
386
|
+
leg 0). `reassign` also applies to any non-terminal status when the current
|
|
387
|
+
station is an agent station, staying in `queued`.
|
|
388
|
+
|
|
389
|
+
```mermaid
|
|
390
|
+
stateDiagram-v2
|
|
391
|
+
[*] --> backlog
|
|
392
|
+
backlog --> queued: enqueue
|
|
393
|
+
queued --> running: start
|
|
394
|
+
running --> queued: leg:completed (next station)
|
|
395
|
+
running --> done: leg:completed (last station)
|
|
396
|
+
running --> waiting_human: leg:completed (next is human)
|
|
397
|
+
running --> handing_off: leg:handoff (chain has a next leg)
|
|
398
|
+
running --> failed: leg:handoff (chain exhausted)
|
|
399
|
+
running --> failed: leg:auth_failed
|
|
400
|
+
running --> failed: leg:launch_failed
|
|
401
|
+
running --> killed: leg:killed
|
|
402
|
+
handing_off --> queued: bundle_written
|
|
403
|
+
handing_off --> needs_approval: bundle_written (next leg approve:true)
|
|
404
|
+
running --> queued: test:green (next station)
|
|
405
|
+
running --> done: test:green (last station)
|
|
406
|
+
running --> queued: test:red (bounce)
|
|
407
|
+
running --> done: land:landed
|
|
408
|
+
running --> queued: land:bounced
|
|
409
|
+
running --> failed: land:failed
|
|
410
|
+
running --> paused: pause
|
|
411
|
+
paused --> queued: resume
|
|
412
|
+
needs_approval --> queued: approve
|
|
413
|
+
waiting_human --> queued: approve (next station)
|
|
414
|
+
waiting_human --> done: approve (last station)
|
|
415
|
+
done --> [*]
|
|
416
|
+
failed --> [*]
|
|
417
|
+
killed --> [*]
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
## See also
|
|
421
|
+
|
|
422
|
+
- [board-guide.md](board-guide.md): what each of these states looks like on
|
|
423
|
+
the board.
|
|
424
|
+
- [adapters.md](adapters.md): the exact CLI shape behind each adapter.
|
|
425
|
+
- [configuration.md](configuration.md): the environment variables named
|
|
426
|
+
above (`LEG_MAX_CONCURRENT`, `LEG_MAX_LAND_ATTEMPTS`, ...).
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# Configuration
|
|
2
|
+
|
|
3
|
+
The user-facing settings Leg reads, the seller settings for its checkout
|
|
4
|
+
site, and the card-level options that configure one card instead of the whole
|
|
5
|
+
install. For a developer who needs to change a default or point Leg at a
|
|
6
|
+
non-default binary.
|
|
7
|
+
|
|
8
|
+
Environment variables use the `LEG_` prefix with automatic fallback to legacy
|
|
9
|
+
`BATON_*` variables if set.
|
|
10
|
+
|
|
11
|
+
## `.env`
|
|
12
|
+
|
|
13
|
+
`npm start` and `npm run dev` run
|
|
14
|
+
`node --env-file-if-exists=.env bin/leg.mjs up`: `npm start` and `npm run
|
|
15
|
+
dev` automatically load a `.env` file in the repo root through that Node flag.
|
|
16
|
+
Inherited shell values are still honored and take precedence. Copy
|
|
17
|
+
`.env.example` to `.env` and edit it; `.env` is gitignored. Running `node
|
|
18
|
+
bin/leg.mjs` directly does not automatically load `.env`; export the
|
|
19
|
+
variables yourself in that case.
|
|
20
|
+
|
|
21
|
+
## Interactive sessions
|
|
22
|
+
|
|
23
|
+
These apply to `leg claude|codex|agy`.
|
|
24
|
+
|
|
25
|
+
| variable | default | meaning | read in |
|
|
26
|
+
|----------|---------|---------|---------|
|
|
27
|
+
| `LEG_ACCOUNT` | `default` | start the session on a named login instead of the CLI's own home | `src/attach.mjs` |
|
|
28
|
+
| `LEG_WARN_PCT` | `85` | the percentage of either usage window that turns the card amber, records a `warning` event and rings the terminal bell once | `src/usage.mjs` |
|
|
29
|
+
| `LEG_NO_HANDOFF` | (unset, hand-off on) | set to `1` to warn and record but never switch agents | `src/attach.mjs` |
|
|
30
|
+
| `LEG_NO_OPEN` | (unset, opens once) | set to `1` to start the board without opening a browser | `bin/leg.mjs` |
|
|
31
|
+
| `LEG_NO_BOARD` | (unset) | set to `1` to run a session with no board at all (the record under `$LEG_HOME/sessions/` is still kept; the test suite uses this) | `src/attach.mjs` |
|
|
32
|
+
| `LEG_WAIT_TICK_MS` | `1000` | how often the all-out countdown redraws and re-checks Ctrl-C / End while waiting for the first reset | `src/attach.mjs` |
|
|
33
|
+
| `LEG_USAGE_POLL_MS` | `60000` | how often an active attach polls its usage source; Claude uses its usage endpoint and Codex uses read-only app-server rate limits | `src/attach.mjs` |
|
|
34
|
+
| `LEG_ATTACH_POLL_MS` | `2000` | how often the session loop re-reads the taps; git is re-read every third poll | `src/attach.mjs` |
|
|
35
|
+
| `LEG_CLAUDE_USAGE_URL` | `https://api.anthropic.com/api/oauth/usage` | the usage endpoint, for a test double | `src/taps/claude-usage.mjs` |
|
|
36
|
+
| `LEG_CLAUDE_ARGS`, `LEG_CODEX_ARGS`, `LEG_AGY_ARGS` | (none) | space-separated extra arguments for a leg Leg starts on its own after a hand-off (your own `leg <agent> …` args never apply to the next agent); e.g. `LEG_CODEX_ARGS="-m gpt-5.3-codex-spark"` keeps a test chain on cheap models | `src/attach.mjs` |
|
|
37
|
+
| `LEG_LIVE_DIR` | `fixtures/live/` in a dev clone, else `~/.leg/live/` | where the first real limit payload per agent and signal is kept, secrets scrubbed (`src/live-capture.mjs`); a `leg sessions simulate-limit` payload is never kept | `src/live-capture.mjs`, `scripts/live-limits.mjs` |
|
|
38
|
+
|
|
39
|
+
| `LEG_PERSON` | the board's owner | whose terminal this is when the board is shared (`leg share`); it is the name on the card and the one that decides who may control it | `src/share.mjs` `whoami` |
|
|
40
|
+
|
|
41
|
+
`LEG_SESSION` is not an input: Leg sets it in the agent's environment to
|
|
42
|
+
the session id, so a hook or a script inside the session can find its own
|
|
43
|
+
record under `$LEG_HOME/sessions/`.
|
|
44
|
+
|
|
45
|
+
`CLAUDE_CONFIG_DIR` and `CODEX_HOME` are set for the child when the session
|
|
46
|
+
runs on a named account; see [Accounts](#accounts) below.
|
|
47
|
+
|
|
48
|
+
## Core
|
|
49
|
+
|
|
50
|
+
| variable | default | meaning | read in |
|
|
51
|
+
|----------|---------|---------|---------|
|
|
52
|
+
| `LEG_HOME` | `~/.leg` | where sessions, usage, accounts, cards, runs and the pidfiles live | `src/store.mjs`, `src/ledger.mjs`, `src/runner.mjs`, `src/worktree.mjs`, `src/cards.mjs`, `src/sessions.mjs`, `src/usage.mjs`, `src/accounts.mjs` |
|
|
53
|
+
| `LEG_PORT` | `4747` | board server port | `src/server.mjs`, `src/launcher.mjs`, `bin/leg.mjs` (`open`) |
|
|
54
|
+
| `LEG_BIND` | `127.0.0.1` | board server bind address | `src/server.mjs`, `src/launcher.mjs` |
|
|
55
|
+
| `LEG_TOKEN` | (none) | bearer token required for `/api/*` and the event stream once set | `src/server.mjs`, `src/auth.mjs` |
|
|
56
|
+
| `LEG_MAX_CONCURRENT` | `2` | how many cards the scheduler runs at once | `src/scheduler.mjs` |
|
|
57
|
+
| `LEG_MAX_LAND_ATTEMPTS` | `3` | shared cap on test-station and land-station bounces before a card fails | `src/chain.mjs`, `src/mergequeue.mjs` (test-run timeout uses a separate variable, below) |
|
|
58
|
+
| `LEG_NO_SCHEDULER` | (unset, scheduler on) | set to `1` to boot the board server without its embedded scheduler | `src/server.mjs` |
|
|
59
|
+
| `LEG_QUIET` | (unset, logs on) | set to `1` to silence the `[leg]`/`[board]` log lines | `src/orchestrator.mjs`, `src/server.mjs` (the launcher always starts its child with `LEG_QUIET=0` so `leg up`'s own log stream is not silenced by an inherited `1`) |
|
|
60
|
+
| `LEG_POLL_MS` | `2000` | how often the orchestrator polls a run's `run.json` while it waits | `src/orchestrator.mjs` |
|
|
61
|
+
| `LEG_TIMERS_MS` | `1800000,5400000` | `<notify-ms>,<kill-ms>[,<kill-verify-ms>]`: when the supervisor logs a "still running" notice, when it kills the leg, and how long it waits before retrying/declaring a kill failed (verify defaults to `30000`) | `src/runner.mjs` |
|
|
62
|
+
| `LEG_COMMIT_VERIFY` | (unset, hooks skipped) | set to `1` to run the repo's git hooks when the land station commits the worktree (default passes `--no-verify`) | `src/mergequeue.mjs` |
|
|
63
|
+
| `LEG_LAND_TEST_TIMEOUT_MS` | `600000` | timeout for the land station's own test run | `src/mergequeue.mjs` |
|
|
64
|
+
| `LEG_HEALTH_TIMEOUT_MS` | `20000` | how long `leg up` waits for `/api/health` before giving up | `src/launcher.mjs` |
|
|
65
|
+
|
|
66
|
+
## Optional syncs
|
|
67
|
+
|
|
68
|
+
Off unless explicitly enabled; a sync failure never blocks or fails a card
|
|
69
|
+
(see [concepts.md](concepts.md) and the README's "Optional syncs" section).
|
|
70
|
+
|
|
71
|
+
| variable | default | meaning | read in |
|
|
72
|
+
|----------|---------|---------|---------|
|
|
73
|
+
| `LEG_SYNC_WORKBOARD` | `0` | set to `1` to mirror cards to the OpenClaw Workboard CLI | `src/sync/workboard.mjs` |
|
|
74
|
+
| `OPENCLAW_BIN` | (none) | path to an `openclaw` `.mjs`/`.cjs`/`.js` entry, overriding resolution | `src/sync/workboard.mjs` |
|
|
75
|
+
| `OPENCLAW_PKG_DIR` | (none) | directory to resolve the `openclaw` npm package from | `src/sync/workboard.mjs` |
|
|
76
|
+
| `LEG_SYNC_DASHCLAW` | `0` | set to `1` (with the two variables below) to record every ledger event as a DashClaw action | `src/sync/dashclaw.mjs` |
|
|
77
|
+
| `DASHCLAW_URL` | (none) | DashClaw API base URL | `src/sync/dashclaw.mjs` |
|
|
78
|
+
| `DASHCLAW_API_KEY` | (none) | DashClaw API key, sent as `x-api-key` | `src/sync/dashclaw.mjs` |
|
|
79
|
+
|
|
80
|
+
## Seller and checkout site
|
|
81
|
+
|
|
82
|
+
These are for the checkout/key-delivery site and its deployment helpers, not
|
|
83
|
+
normal Leg CLI use.
|
|
84
|
+
|
|
85
|
+
| variable | meaning | read in |
|
|
86
|
+
|----------|---------|---------|
|
|
87
|
+
| `LEG_SITE` | buyer-site origin used by the CLI for its purchase link; defaults to `https://legcli.com` | `src/license.mjs` |
|
|
88
|
+
| `LEG_SITE_ORIGIN` | site origin included in checkout and email links; defaults to `https://legcli.com` | `site/api/_lib.js`, `scripts/vercel-env.mjs` |
|
|
89
|
+
| `STRIPE_SECRET_KEY`, `STRIPE_TEST_SECRET_KEY` | live or test Stripe secret key supplied to the deployment helper | `scripts/vercel-env.mjs` |
|
|
90
|
+
| `STRIPE_TEST_WEBHOOK_SECRET`, `STRIPE_LIVE_WEBHOOK_SECRET` | test or live webhook input selected by the deployment helper and deployed as `STRIPE_WEBHOOK_SECRET` | `scripts/vercel-env.mjs` |
|
|
91
|
+
| `STRIPE_WEBHOOK_SECRET` | deployed webhook verification secret used by the webhook handler | `site/api/webhook.js` |
|
|
92
|
+
| `RESEND_API_KEY` | Resend key used to email a paid license key | `site/api/_lib.js` |
|
|
93
|
+
| `LEG_MAIL_FROM`, `LEG_MAIL_REPLY_TO` | runtime site API sender and reply-to overrides; `scripts/vercel-env.mjs` currently deploys its own fixed sender and reply-to values | `site/api/_lib.js`, `scripts/vercel-env.mjs` |
|
|
94
|
+
| `LEG_LICENSE_PRIVATE_KEY` | private signing key for license issuance; never commit it | `site/api/key.js`, `scripts/vercel-env.mjs` |
|
|
95
|
+
| `NPM_TOKEN` | external npm publishing automation only; repository scripts do not read it | (none) |
|
|
96
|
+
|
|
97
|
+
## Adapter binary overrides
|
|
98
|
+
|
|
99
|
+
Each points at a specific CLI binary (or, when it ends `.mjs`/`.cjs`/`.js`,
|
|
100
|
+
a Node entry point run via `node <entry>`), instead of Leg's own
|
|
101
|
+
resolution logic. See [adapters.md](adapters.md) for what each adapter
|
|
102
|
+
resolves to by default.
|
|
103
|
+
|
|
104
|
+
| variable | adapter | read in |
|
|
105
|
+
|----------|---------|---------|
|
|
106
|
+
| `LEG_CLAUDE_BIN` | claude | `src/adapters/claude.mjs` |
|
|
107
|
+
| `LEG_CODEX_BIN` | codex | `src/adapters/codex.mjs` |
|
|
108
|
+
| `LEG_AGY_BIN` | agy | `src/adapters/agy.mjs` |
|
|
109
|
+
| `LEG_GROK_BIN` | grok (not registered by default) | `src/adapters/grok.mjs` |
|
|
110
|
+
| `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` |
|
|
111
|
+
| `LEG_CHB_BIN` | `context-handoff-bundle` | `src/handoff.mjs`; unset, Leg tries `context-handoff-bundle` on PATH, then `python -m context_handoff_bundle` |
|
|
112
|
+
|
|
113
|
+
## Accounts
|
|
114
|
+
|
|
115
|
+
An extra login is a directory under `$LEG_HOME`, never a change to your real
|
|
116
|
+
home. `src/accounts.mjs` `LAYOUT` is the whole definition.
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
~/.leg/
|
|
120
|
+
accounts.json the named accounts per agent
|
|
121
|
+
accounts/claude/<name>/
|
|
122
|
+
hooks/ skills/ agents/ commands/ plugins/ rules/ scripts/
|
|
123
|
+
output-styles/ tools/ junctions back to ~/.claude
|
|
124
|
+
settings.json settings.local.json CLAUDE.md keybindings.json
|
|
125
|
+
statusline.ps1 statusline-combined.ps1
|
|
126
|
+
copies, refreshed before every launch
|
|
127
|
+
.credentials.json the login, written by claude itself
|
|
128
|
+
accounts/codex/<name>/
|
|
129
|
+
skills/ prompts/ rules/ plugins/ agents/ hooks/ memories/ superpowers/
|
|
130
|
+
junctions back to ~/.codex
|
|
131
|
+
config.toml AGENTS.md copies, refreshed before every launch
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
| agent | config-dir variable | default home |
|
|
135
|
+
|-------|---------------------|--------------|
|
|
136
|
+
| claude | `CLAUDE_CONFIG_DIR` | `~/.claude` |
|
|
137
|
+
| codex | `CODEX_HOME` | `~/.codex` |
|
|
138
|
+
| agy | none in 1.2.0, so one account only | `~/.gemini/antigravity-cli` |
|
|
139
|
+
|
|
140
|
+
Commands:
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
leg accounts ls logins and their 5h/7d usage
|
|
144
|
+
leg accounts add <claude|codex> <name>
|
|
145
|
+
leg accounts rm <claude|codex> <name>
|
|
146
|
+
leg accounts terms what both vendors' terms say
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`add` creates the directory, junctions the shared directories in, copies the
|
|
150
|
+
settings files, and prints one line to paste to log in. `rm` removes the
|
|
151
|
+
junctions as links, never following them, then deletes the directory. Nothing
|
|
152
|
+
under your real home is written at any point. Start a session on a named
|
|
153
|
+
account with `LEG_ACCOUNT=<name>`, or let a limit hand off to it.
|
|
154
|
+
|
|
155
|
+
## Test and development seams
|
|
156
|
+
|
|
157
|
+
Not meant for normal use; documented for completeness.
|
|
158
|
+
|
|
159
|
+
| variable | meaning | read in |
|
|
160
|
+
|----------|---------|---------|
|
|
161
|
+
| `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` |
|
|
162
|
+
| `LEG_SERVER_SCRIPT` | override the server script path the launcher spawns | `src/launcher.mjs` |
|
|
163
|
+
|
|
164
|
+
## Network exposure
|
|
165
|
+
|
|
166
|
+
Leg binds `127.0.0.1` by default: no token is required, and only
|
|
167
|
+
processes on the same machine can reach it. Setting `LEG_BIND` to any
|
|
168
|
+
other address without also setting `LEG_TOKEN` makes the server refuse to
|
|
169
|
+
start, exit code `3` (`src/auth.mjs` `checkBind`/`BindRefused`). With a
|
|
170
|
+
token set, every `/api/*` request needs an `Authorization: Bearer <token>`
|
|
171
|
+
header; the event stream (`EventSource`, which cannot set headers) accepts
|
|
172
|
+
the same token as a `?token=` query parameter instead. There is no TLS; keep
|
|
173
|
+
`LEG_BIND` on loopback unless you are using `leg share`, which gives
|
|
174
|
+
each human their own token (see [Share](#share-more-than-one-human)).
|
|
175
|
+
|
|
176
|
+
## Share (more than one human)
|
|
177
|
+
|
|
178
|
+
Off until `leg share on` writes `$LEG_HOME/share.json`. That file is the
|
|
179
|
+
switch and the roster; the env variables below only tune the limits.
|
|
180
|
+
|
|
181
|
+
| field | meaning |
|
|
182
|
+
|-------|---------|
|
|
183
|
+
| `on` | share is on (it also needs a `bind` and at least one person) |
|
|
184
|
+
| `bind`, `bind_kind`, `port` | where the board listens: the Tailscale address by default, `lan`, or one you named |
|
|
185
|
+
| `owner` | the name a terminal belongs to when nothing else says (`LEG_PERSON`), and the name a loopback browser is treated as |
|
|
186
|
+
| `people[]` | `{ name, role: owner\|guest, token_sha256, created_at }`; the token itself is printed once and never stored |
|
|
187
|
+
| `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 |
|
|
188
|
+
|
|
189
|
+
| variable | default | meaning | read in |
|
|
190
|
+
|----------|---------|---------|---------|
|
|
191
|
+
| `LEG_RATE_MAX` | `600` | requests a minute per human (per address for an unnamed one); over it the board answers 429 with `Retry-After` | `src/ratelimit.mjs` |
|
|
192
|
+
| `LEG_RATE_MAX_FAILURES` | `20` | wrong tokens a minute from one address before that address waits the window out; a request with no token at all is not counted | `src/ratelimit.mjs` |
|
|
193
|
+
|
|
194
|
+
`LEG_TOKEN` is the single-token mode and is ignored while share is on.
|
|
195
|
+
|
|
196
|
+
## Card-level options
|
|
197
|
+
|
|
198
|
+
These configure one card, not the whole install (set with `card add` flags
|
|
199
|
+
or the New card form; see [getting-started.md](getting-started.md) and
|
|
200
|
+
[board-guide.md](board-guide.md)):
|
|
201
|
+
|
|
202
|
+
- **leases**: path globs the card claims for scheduling (`src/leases.mjs`).
|
|
203
|
+
- **land_mode**: `ff` (fast-forward, the merge queue) or `pr` (stub;
|
|
204
|
+
requires `LEG_GH_BIN`) (`src/land.mjs`).
|
|
205
|
+
- **test_command**: overrides the land/test stations' auto-detected command
|
|
206
|
+
(`src/mergequeue.mjs` `resolveTestCommand`).
|
|
207
|
+
- **trunk**: the branch a card's worktree is based on and lands onto
|
|
208
|
+
(default `main`).
|
|
209
|
+
- Per-adapter chain options: **mode**, **max-turns**, **model**, and
|
|
210
|
+
**approve** (gate that leg behind a human Approve), set per adapter with
|
|
211
|
+
`--mode <adapter>=<mode>`, `--max-turns <adapter>=<n>`, `--model
|
|
212
|
+
<adapter>=<name>`, `--approve <adapter,...>` (`src/pipeline.mjs`
|
|
213
|
+
`normalizeChainEntry`).
|
|
214
|
+
|
|
215
|
+
## See also
|
|
216
|
+
|
|
217
|
+
- [adapters.md](adapters.md): what each `LEG_<ADAPTER>_BIN` overrides,
|
|
218
|
+
and each adapter's default resolution.
|
|
219
|
+
- [concepts.md](concepts.md): leases, the scheduler, and the land station
|
|
220
|
+
these variables tune.
|