@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.
Files changed (184) hide show
  1. package/CHANGELOG.md +560 -0
  2. package/LICENSE +58 -0
  3. package/NOTICE +4 -0
  4. package/README.md +558 -0
  5. package/bin/fake-agent.mjs +143 -0
  6. package/bin/leg.mjs +449 -0
  7. package/docs/DECISIONS.md +119 -0
  8. package/docs/DEMO.md +214 -0
  9. package/docs/DEVIATIONS.md +182 -0
  10. package/docs/ERRORS.md +326 -0
  11. package/docs/README.md +40 -0
  12. package/docs/REUSE.md +289 -0
  13. package/docs/ROADMAP-v2.md +55 -0
  14. package/docs/VOCABULARY.md +243 -0
  15. package/docs/adapters.md +300 -0
  16. package/docs/board-guide.md +579 -0
  17. package/docs/cli-contracts.md +401 -0
  18. package/docs/concepts.md +426 -0
  19. package/docs/configuration.md +220 -0
  20. package/docs/faq.md +206 -0
  21. package/docs/getting-started.md +251 -0
  22. package/docs/real-run.md +80 -0
  23. package/docs/review-2026-09-14.md +72 -0
  24. package/docs/screenshots/board-400px.png +0 -0
  25. package/docs/screenshots/board-details-open.png +0 -0
  26. package/docs/screenshots/board-done.png +0 -0
  27. package/docs/screenshots/board-drawer.png +0 -0
  28. package/docs/screenshots/board-empty.png +0 -0
  29. package/docs/screenshots/board-handoff.png +0 -0
  30. package/docs/screenshots/board-running.png +0 -0
  31. package/docs/screenshots/demo-1-claude-running.png +0 -0
  32. package/docs/screenshots/demo-2-limit-hit.png +0 -0
  33. package/docs/screenshots/demo-3-handoff-bundle.png +0 -0
  34. package/docs/screenshots/demo-4-codex-running.png +0 -0
  35. package/docs/screenshots/demo-5-done.png +0 -0
  36. package/docs/screenshots/floor-final-1280.png +0 -0
  37. package/docs/screenshots/floor-final-400.png +0 -0
  38. package/docs/screenshots/floor-landing.png +0 -0
  39. package/docs/screenshots/floor.png +0 -0
  40. package/docs/screenshots/share-guest-1280.png +0 -0
  41. package/docs/screenshots/share-owner-1280.png +0 -0
  42. package/docs/screenshots/terminals-1280.png +0 -0
  43. package/docs/screenshots/thumb-done.jpg +0 -0
  44. package/docs/screenshots/thumb-handoff-details.jpg +0 -0
  45. package/docs/screenshots/thumb-limit-hit.jpg +0 -0
  46. package/docs/screenshots/youtube-thumbnail.jpg +0 -0
  47. package/fixtures/limits/agy/agy-resource-exhausted.json +11 -0
  48. package/fixtures/limits/claude/claude-budget-limit.json +11 -0
  49. package/fixtures/limits/claude/claude-max-turns.json +11 -0
  50. package/fixtures/limits/claude/claude-model-limit.json +11 -0
  51. package/fixtures/limits/claude/claude-session-limit.json +11 -0
  52. package/fixtures/limits/claude/claude-weekly-limit.json +11 -0
  53. package/fixtures/limits/codex/codex-quota-exceeded.json +11 -0
  54. package/fixtures/limits/codex/codex-rate-limit-exceeded.json +11 -0
  55. package/fixtures/limits/codex/codex-skills-notice.json +11 -0
  56. package/fixtures/limits/codex/codex-usage-limit-reached.json +11 -0
  57. package/fixtures/limits/codex/codex-usage-limit.json +11 -0
  58. package/fixtures/limits/generic/generic-429.json +11 -0
  59. package/fixtures/limits/generic/generic-overloaded.json +11 -0
  60. package/fixtures/limits/generic/generic-quota.json +11 -0
  61. package/fixtures/limits/generic/generic-rate-limit.json +11 -0
  62. package/fixtures/limits/generic/generic-resource-exhausted.json +11 -0
  63. package/fixtures/limits/generic/generic-usage-limit.json +11 -0
  64. package/fixtures/limits/grok/grok-not-logged-in.json +11 -0
  65. package/fixtures/limits/negative/auth-source-set.json +11 -0
  66. package/fixtures/limits/negative/compile-error.json +11 -0
  67. package/fixtures/limits/negative/empty-stdout-exit-0.json +11 -0
  68. package/fixtures/live/agy/attempt-1-scratch-workspace.out.log +1 -0
  69. package/fixtures/live/agy/cmd.txt +1 -0
  70. package/fixtures/live/agy/err.log +0 -0
  71. package/fixtures/live/agy/out.log +1 -0
  72. package/fixtures/live/agy/parsed.json +5 -0
  73. package/fixtures/live/agy/run.json +18 -0
  74. package/fixtures/live/agy/supervisor.log +2 -0
  75. package/fixtures/live/claude/cmd.txt +1 -0
  76. package/fixtures/live/claude/err.log +0 -0
  77. package/fixtures/live/claude/limit-rate_limit.json +21 -0
  78. package/fixtures/live/claude/out.log +1 -0
  79. package/fixtures/live/claude/parsed.json +11 -0
  80. package/fixtures/live/claude/run.json +18 -0
  81. package/fixtures/live/claude/supervisor.log +2 -0
  82. package/fixtures/live/codex/cmd.txt +1 -0
  83. package/fixtures/live/codex/err.log +1 -0
  84. package/fixtures/live/codex/last.md +2 -0
  85. package/fixtures/live/codex/limit-usage_limit_exceeded.json +25 -0
  86. package/fixtures/live/codex/out.log +8 -0
  87. package/fixtures/live/codex/parsed.json +15 -0
  88. package/fixtures/live/codex/run.json +18 -0
  89. package/fixtures/live/codex/supervisor.log +2 -0
  90. package/fixtures/live/grok/cmd.txt +1 -0
  91. package/fixtures/live/grok/err.log +32 -0
  92. package/fixtures/live/grok/out.log +7 -0
  93. package/fixtures/live/grok/parsed.json +5 -0
  94. package/fixtures/live/grok/run.json +18 -0
  95. package/fixtures/live/grok/supervisor.log +2 -0
  96. package/fixtures/verified.json +34 -0
  97. package/package.json +68 -0
  98. package/scripts/board-shots.mjs +69 -0
  99. package/scripts/build-docs-site.mjs +308 -0
  100. package/scripts/check-claims.mjs +150 -0
  101. package/scripts/clean-clone-check.sh +42 -0
  102. package/scripts/license-sign.mjs +23 -0
  103. package/scripts/limits-table.mjs +38 -0
  104. package/scripts/live-limits.mjs +36 -0
  105. package/scripts/privacy-check.mjs +63 -0
  106. package/scripts/probe.mjs +92 -0
  107. package/scripts/seed-fake-cards.mjs +34 -0
  108. package/scripts/seed-floor-board.mjs +69 -0
  109. package/scripts/seed-wes-board.mjs +91 -0
  110. package/scripts/stripe-setup.mjs +99 -0
  111. package/scripts/vercel-env.mjs +40 -0
  112. package/src/accounts.mjs +119 -0
  113. package/src/adapters/agy.mjs +56 -0
  114. package/src/adapters/claude.mjs +54 -0
  115. package/src/adapters/codex.mjs +73 -0
  116. package/src/adapters/common.mjs +32 -0
  117. package/src/adapters/fake.mjs +48 -0
  118. package/src/adapters/grok.mjs +51 -0
  119. package/src/adapters/index.mjs +27 -0
  120. package/src/adapters/resolve.mjs +40 -0
  121. package/src/attach.mjs +592 -0
  122. package/src/auth.mjs +85 -0
  123. package/src/board/board.css +543 -0
  124. package/src/board/board.js +1307 -0
  125. package/src/board/favicon.svg +1 -0
  126. package/src/board/floor.html +127 -0
  127. package/src/board/floor.js +670 -0
  128. package/src/board/fonts/OFL-atkinson-hyperlegible-next.txt +93 -0
  129. package/src/board/fonts/OFL-azeret-mono.txt +94 -0
  130. package/src/board/fonts/atkinson-board.woff2 +0 -0
  131. package/src/board/fonts/azeret-board.woff2 +0 -0
  132. package/src/board/index.html +251 -0
  133. package/src/board/sessions.js +1451 -0
  134. package/src/bundle.mjs +103 -0
  135. package/src/cards.mjs +114 -0
  136. package/src/chain.mjs +264 -0
  137. package/src/commands.mjs +79 -0
  138. package/src/contract.mjs +73 -0
  139. package/src/env.mjs +19 -0
  140. package/src/fsx.mjs +72 -0
  141. package/src/git-snapshot.mjs +152 -0
  142. package/src/handoff.mjs +171 -0
  143. package/src/hook.mjs +49 -0
  144. package/src/land.mjs +106 -0
  145. package/src/launcher.mjs +272 -0
  146. package/src/leases.mjs +86 -0
  147. package/src/ledger.mjs +369 -0
  148. package/src/license.mjs +168 -0
  149. package/src/limits.mjs +97 -0
  150. package/src/live-capture.mjs +69 -0
  151. package/src/mergequeue.mjs +225 -0
  152. package/src/orchestrator.mjs +365 -0
  153. package/src/pipeline.mjs +100 -0
  154. package/src/preferences.mjs +47 -0
  155. package/src/presets.mjs +23 -0
  156. package/src/ratelimit.mjs +49 -0
  157. package/src/redact.mjs +47 -0
  158. package/src/resume.mjs +354 -0
  159. package/src/runner.mjs +438 -0
  160. package/src/scheduler.mjs +118 -0
  161. package/src/server.mjs +845 -0
  162. package/src/session-detail.mjs +129 -0
  163. package/src/sessions.mjs +202 -0
  164. package/src/share.mjs +163 -0
  165. package/src/stations/agent.mjs +42 -0
  166. package/src/stations/build.mjs +9 -0
  167. package/src/stations/human.mjs +5 -0
  168. package/src/stations/land.mjs +6 -0
  169. package/src/stations/plan.mjs +8 -0
  170. package/src/stations/pr.mjs +31 -0
  171. package/src/stations/review.mjs +8 -0
  172. package/src/stations/test.mjs +27 -0
  173. package/src/store.mjs +90 -0
  174. package/src/sync/dashclaw.mjs +70 -0
  175. package/src/sync/index.mjs +54 -0
  176. package/src/sync/workboard.mjs +54 -0
  177. package/src/taps/agy.mjs +64 -0
  178. package/src/taps/claude-usage.mjs +67 -0
  179. package/src/taps/claude.mjs +170 -0
  180. package/src/taps/codex.mjs +286 -0
  181. package/src/trust.mjs +323 -0
  182. package/src/usage.mjs +179 -0
  183. package/src/wait.mjs +30 -0
  184. package/src/worktree.mjs +207 -0
@@ -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.