@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
package/docs/faq.md ADDED
@@ -0,0 +1,206 @@
1
+ # FAQ
2
+
3
+ Real questions, short answers, sourced from the code and the other docs in
4
+ this directory.
5
+
6
+ **Why subscription CLIs only, never a per-token API?**
7
+ Leg spawns each CLI's own logged-in session (`claude`, `codex`, `agy`).
8
+ Every child process has `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`,
9
+ `ANTHROPIC_BASE_URL`, `ANTHROPIC_CUSTOM_HEADERS`, `OPENAI_API_KEY`,
10
+ `OPENAI_BASE_URL`, `OPENAI_API_BASE`, `GEMINI_API_KEY`, `GOOGLE_API_KEY`,
11
+ `GOOGLE_GEMINI_BASE_URL`, `GOOGLE_GENAI_USE_VERTEXAI`,
12
+ `GOOGLE_GENAI_USE_ENTERPRISE`, `GOOGLE_CLOUD_PROJECT`,
13
+ `GOOGLE_CLOUD_LOCATION`, `GOOGLE_APPLICATION_CREDENTIALS`, `CLAUDECODE`,
14
+ `CLAUDE_CODE_*`, `CLAUDE_EFFORT`, and `CLAUDE_PLUGIN_DATA` stripped out
15
+ (`src/env.mjs`), then gets `CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS=0` for a
16
+ detached Claude print session. A stray API key in your shell cannot silently
17
+ take over billing or shadow the subscription login. If stderr ever says
18
+ "another auth source is set", that leg is classified `auth_failed`, a failed
19
+ launch, and does not count as a usage limit.
20
+
21
+ **Why does Leg poll an endpoint for claude's usage instead of reading the
22
+ status line?**
23
+ Because Claude Code 2.1.268 did not run a custom status line from a settings
24
+ file Leg controls when this was tried on 2026-09-11 (recorded in
25
+ [DEVIATIONS.md](DEVIATIONS.md)); hooks from the same `--settings` file did
26
+ fire. So the numbers come from
27
+ `GET api.anthropic.com/api/oauth/usage` with the login Claude Code already
28
+ stored, which is the same data `/usage` shows. Leg still writes the
29
+ `statusLine` entry, so the endpoint poll becomes a fallback the moment a build
30
+ honours it, and your own status-line command is chained first either way. See
31
+ [adapters.md](adapters.md#claude) and `src/taps/claude-usage.mjs`.
32
+
33
+ **Why does codex get no hook when claude does?**
34
+ Because injecting one would put a prompt in your way. codex asks you to review
35
+ new hooks before it runs them, so a hook per Leg session would mean a
36
+ review prompt per Leg session. It is not needed: an interactive codex writes
37
+ the whole thread to a rollout file under `~/.codex/sessions/YYYY/MM/DD/`, and
38
+ flushes it per event. Leg finds the rollout whose `session_meta` cwd is the
39
+ session's directory and tails it for the rate limits, the prompts and the
40
+ edited files.
41
+
42
+ **Why does agy show no percentage?**
43
+ Because agy exposes none. agy 1.2.0 is a closed Go binary; its own status line
44
+ fetches a quota summary from the backend and writes it nowhere on disk. The
45
+ board shows "no % from agy" rather than an empty bar. The wall itself is still
46
+ caught: Leg passes `--log-file` per session and watches for
47
+ `RESOURCE_EXHAUSTED`, "it resets in …" and "out of quota". Those strings are
48
+ present in `agy.exe`. A real `RESOURCE_EXHAUSTED` wall was caught live on
49
+ 2026-09-11 and handed the session off; the fixture in `fixtures/limits/agy/`
50
+ is still tagged docs-only in [cli-contracts.md](cli-contracts.md#agy-tap)
51
+ because the payload itself was never captured to `fixtures/live/agy/`.
52
+
53
+ **Am I allowed to add a second account?**
54
+ That is your call, and the terms are quoted in full in the README under
55
+ "Second accounts, and what the terms say" (`leg accounts terms` prints the
56
+ same summary). The short version: owning two paid subscriptions is not named
57
+ as prohibited by Anthropic or OpenAI, but rotating to a second account of the
58
+ same vendor because the first is rate-limited sits close to OpenAI's
59
+ "circumvent any rate limits" wording and Anthropic's "circumvent product
60
+ guardrails". Leg's default chain switches vendors (claude, codex, agy),
61
+ which is plainly fine. Same-vendor rotation only happens after you run
62
+ `leg accounts add`.
63
+
64
+ **What does `leg uninstall` remove?**
65
+ `~/.leg` and nothing else: sessions, usage files, the extra account
66
+ directories with their junctions, the v0.1 cards and runs, and the board
67
+ pidfile. It removes the junctions as links, never following them into your
68
+ real `~/.claude` or `~/.codex`. It does not touch any file of yours, any repo,
69
+ or the agent CLIs themselves. Run `leg uninstall` with no flag to print what
70
+ would go, `--yes` to do it; then `npm rm -g legcli` if you want the
71
+ package gone too.
72
+
73
+ **Can I run `leg claude` inside a Claude Code shell?**
74
+ Yes. A parent Claude Code session exports `CLAUDECODE` and `CLAUDE_CODE_*`
75
+ markers that make a nested Claude refuse to start; `sanitizeEnv`
76
+ (`src/env.mjs`) strips them along with the API-key variables, so the child
77
+ starts normally. It becomes its own session with its own card on the board,
78
+ unrelated to the parent's.
79
+
80
+ **Why are `--dangerously-skip-permissions` and similar flags never
81
+ available?**
82
+ Each adapter's `argv()` calls `assertAllowed()`
83
+ (`src/adapters/common.mjs`), which checks the requested mode against that
84
+ adapter's `allowed` list and rejects any flag on its `forbiddenFlags` list,
85
+ before anything spawns. Requesting `bypassPermissions`, `--yolo`,
86
+ `--full-auto`, `danger-full-access`, or similar throws immediately; nothing
87
+ ever runs with permission checks off.
88
+
89
+ **What actually happens when a pipeline leg hits a usage limit?**
90
+ (For an interactive session, see [concepts.md](concepts.md#handoff-interactive).)
91
+ `src/limits.mjs` classifies the leg's exit code, output and diff evidence as
92
+ `limit`. Leg writes a handoff bundle in the same worktree (task, done so
93
+ far, the diff, open findings) via `context-handoff-bundle`, then starts the
94
+ next adapter in that station's chain from the bundle's resume text. If the
95
+ chain has no next adapter, the card fails. See
96
+ [concepts.md](concepts.md#outcomes-and-the-classifier).
97
+
98
+ **Can two agents edit the same files at the same time?**
99
+ Not inside one card: a station runs one chain leg at a time. Across cards,
100
+ the scheduler only starts a queued card when its declared leases (path
101
+ globs) do not overlap any running card's leases (`src/leases.mjs`); the
102
+ default lease is `**` (the whole repo), so two cards with no leases set
103
+ always serialize. The overlap check is a deliberate approximation biased
104
+ toward false positives: an unnecessary serialization costs minutes, a
105
+ wrongly parallel card can corrupt a merge.
106
+
107
+ For terminals: a second `leg <agent>` in a checkout that already has a live
108
+ session gets its own worktree and branch, so the two never write over each
109
+ other's files. Whoever presses Land first fast-forwards trunk; the second one
110
+ rebases onto it, or bounces with the conflicting files named on the card.
111
+ `--no-worktree` shares the checkout when that is what you want.
112
+
113
+ **Can someone else watch my board?**
114
+ Only if you run `leg share on`, which is off by default. It binds your
115
+ Tailscale or LAN address and prints one link per human (`leg share add
116
+ <name>`), each with its own token. A guest sees the terminals lane read-only
117
+ and nothing a terminal has said, read or written: no prompt, no file names, no
118
+ paths, no bundle, no events, no logs, and none of the pipeline side. The
119
+ limit line keeps only the reason and the reset time, never the raw limit
120
+ text. The one thing they can do on your terminal is ask for a hand-off, which
121
+ you approve or dismiss on the card. `leg share off` ends it and every link
122
+ stops working. There is no TLS, so keep it on Tailscale or a network you trust.
123
+
124
+ **What if the CLI I want isn't installed?**
125
+ `node bin/leg.mjs up --dry` prints a preflight table with `ok`/`missing`
126
+ per adapter. A missing adapter only matters if a card's chain names it: that
127
+ leg fails to launch (`launch_failed`), which does not advance the chain,
128
+ install and log in, then press Rerun. A chain only needs the adapters it
129
+ names; leave a missing one out.
130
+
131
+ **How do I resume after a reboot or a crash?**
132
+ Every run's `run.json` under `$LEG_HOME/cards/<id>/runs/<n>/` is the
133
+ source of truth, not process memory. `leg down` kills active agents but
134
+ each run's supervisor still writes its final verdict; the next `leg up`
135
+ (or the scheduler) finds any unsettled run with no live driver and
136
+ re-attaches to apply that verdict, logging a `re-attached to run N` event.
137
+ Nothing manual to do beyond starting Leg again.
138
+
139
+ **Does Leg push to GitHub, or open pull requests?**
140
+ No. The default `land_mode: ff` only rebases and fast-forwards the local
141
+ trunk branch inside the repo you gave it; there is no remote write anywhere
142
+ in `src/worktree.mjs` or `src/mergequeue.mjs`. `land_mode: pr` builds a real
143
+ `gh pr create` argv but is stub-only: it refuses to run unless
144
+ `LEG_GH_BIN` points at a real `gh` (or a test stub), and even then it
145
+ never pushes or creates a remote for you, that is out of scope for this
146
+ build.
147
+
148
+ **Is there a hosted version of Leg?**
149
+ No. Leg is local-first: the board binds `127.0.0.1` by default, every
150
+ card's state lives in files under `LEG_HOME`, and there is no service to
151
+ sign into. See [configuration.md](configuration.md#network-exposure) for
152
+ what changes if you deliberately bind it to a shared address.
153
+
154
+ **How are secrets handled?**
155
+ `src/redact.mjs` holds one pattern list (API keys, bearer tokens, GitHub and
156
+ AWS tokens, `key=value` secrets); `scrub()` rewrites matches to
157
+ `[REDACTED]` in every log line the launcher prints and in handoff bundle
158
+ text, and the ledger's own writer refuses to record an event whose summary
159
+ or body matches a secret pattern at all. Every adapter's `env()` strips the
160
+ API-key/base-URL variables from the child process. `npm test` and the
161
+ pre-commit hook both run `scripts/privacy-check.mjs`, which additionally
162
+ scans the whole tree for a short list of strings specific to the private
163
+ codebase Leg's runner/ledger were ported from.
164
+
165
+ **What happens if I close the terminal instead of Ctrl-C?**
166
+ The board server and any running agents keep running as detached processes.
167
+ Run `node bin/leg.mjs down` (or `npm run stop`) from another terminal to
168
+ stop them cleanly, or just start `leg up` again later: it re-attaches to
169
+ any run left in progress rather than launching a duplicate.
170
+
171
+ **Can I run a card without the board?**
172
+ Yes: `node bin/leg.mjs card run <card-id>` drives one card through the
173
+ orchestrator directly and exits when it reaches a waiting or terminal
174
+ state, printing the final status. `node bin/leg.mjs card show <id>` and
175
+ `card events <id>` work without the server running too, since they read the
176
+ same on-disk ledger the board reads.
177
+
178
+ **What does `LEG_NO_SCHEDULER=1` do, and why would I set it?**
179
+ It boots the board server without its embedded scheduler, so no queued card
180
+ starts automatically, useful when you want to drive every card by hand
181
+ with `card run` (for example, inside a test) while still watching it on the
182
+ board.
183
+
184
+ **Is there a limit to how long a chain can be, or how many legs a card can
185
+ take?**
186
+ No fixed limit; the chain array can be as long as you like. A card fails
187
+ only when a leg's outcome needs to hand off and the chain has no next entry
188
+ left, or (separately) after `LEG_MAX_LAND_ATTEMPTS` test/land bounces.
189
+
190
+ **Windows vs macOS/Linux: what's actually verified?**
191
+ Built and tested on Windows (every worktree, git and taskkill path in the
192
+ source has Windows-specific handling: `MSYS_NO_PATHCONV=1`, `taskkill /T
193
+ /F`, native `.exe` resolution). CI (`.github/workflows/ci.yml`) runs the
194
+ full test suite on both `ubuntu-latest` and `windows-latest` on every push
195
+ and pull request.
196
+
197
+ **How do I report a bug?**
198
+ Open a GitHub issue using the bug report template
199
+ (`.github/ISSUE_TEMPLATE/bug_report.md`). For a security vulnerability, do
200
+ not open a public issue, see [SECURITY.md](../SECURITY.md) instead.
201
+
202
+ **Where do I find the exact word Leg uses for a given status, event, or
203
+ button?**
204
+ [VOCABULARY.md](VOCABULARY.md): one table per category (statuses, outcomes,
205
+ station kinds, event types, actor types, human actions, bounce reasons),
206
+ pulled straight from the source identifiers.
@@ -0,0 +1,251 @@
1
+ # Getting started
2
+
3
+ For a developer setting up Leg for the first time on their own machine. The
4
+ first four sections get an interactive agent running with the board alongside
5
+ it. The v0.1 pipeline flow is section 7 onward.
6
+
7
+ ## 1. Prerequisites
8
+
9
+ - Node 22 or newer (`package.json` sets `"engines": {"node": ">=22"}`).
10
+ - git.
11
+ - Python 3 with pip, for the `context-handoff-bundle` CLI. This is what writes
12
+ and reads the handoff bundles.
13
+ - At least one coding-agent CLI, logged in: `claude`, `codex` or `agy`. You do
14
+ not need all three. With one installed you get the board and the usage
15
+ tracking; with two or more you also get the hand-off.
16
+
17
+ ## 2. Install
18
+
19
+ ```
20
+ npm install -g @ucsandman/legcli
21
+ pip install -U context-handoff-bundle
22
+ ```
23
+
24
+ The source repository is private, so there is no clone to install from. The
25
+ source you run ships in the package: `$(npm root -g)/legcli/src`, plain
26
+ `.mjs`, nothing bundled. Read it before you trust it.
27
+
28
+ ## 3. Run an agent
29
+
30
+ ```
31
+ cd <any repo>
32
+ leg claude
33
+ ```
34
+
35
+ That is the whole setup. `leg claude` runs the real Claude Code in this
36
+ terminal with your own settings, hooks and skills. Anything after the agent
37
+ name passes straight through:
38
+
39
+ ```
40
+ leg claude --model haiku
41
+ leg codex -m gpt-5.3-codex-spark
42
+ leg agy
43
+ ```
44
+
45
+ The first `leg <agent>` starts the board on http://127.0.0.1:4747 and opens
46
+ it once in your browser. Later sessions reuse the same board. Set
47
+ `LEG_NO_OPEN=1` to skip the browser.
48
+
49
+ Leg strips `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`,
50
+ `ANTHROPIC_BASE_URL`, `ANTHROPIC_CUSTOM_HEADERS`, `OPENAI_API_KEY`,
51
+ `OPENAI_BASE_URL`, `OPENAI_API_BASE`, `GEMINI_API_KEY`, `GOOGLE_API_KEY`,
52
+ `GOOGLE_GEMINI_BASE_URL`, `GOOGLE_GENAI_USE_VERTEXAI`,
53
+ `GOOGLE_GENAI_USE_ENTERPRISE`, `GOOGLE_CLOUD_PROJECT`,
54
+ `GOOGLE_CLOUD_LOCATION`, `GOOGLE_APPLICATION_CREDENTIALS`, `CLAUDECODE`,
55
+ `CLAUDE_CODE_*`, `CLAUDE_EFFORT`, and `CLAUDE_PLUGIN_DATA` from the agent's
56
+ environment, then sets `CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS=0` for a detached
57
+ Claude print session. The subscription login is always what runs. Nothing in
58
+ `~/.claude`, `~/.codex` or agy's home is edited.
59
+
60
+ ## 4. What you get while it runs
61
+
62
+ - A card on the board for this terminal: agent, account, repo@branch, the first
63
+ prompt, turns, the files being touched, 5h and 7d usage.
64
+ - A warning at 85 % of either window (`LEG_WARN_PCT`): amber card, an event,
65
+ one terminal bell.
66
+ - For Codex, the board and active attach poll the read-only app-server
67
+ `account/rateLimits/read` response every 60 seconds. Leg maps the returned
68
+ 300- and 10080-minute durations to 5h and 7d, shows `<n>% used`, and labels
69
+ old readings stale. Only an explicit backend available answer clears a prior
70
+ wall; no model turn or hardcoded quota is used.
71
+ - A `context-handoff-bundle` for this session, refreshed every two minutes and
72
+ at every warning, limit and hand-off.
73
+ - At the limit: the bundle is saved, the agent is stopped, and the next option
74
+ starts in the same terminal from `.leg/RESUME-<session-id>.md`, copied to
75
+ `.leg/RESUME.md`. Order is other logins of
76
+ the same agent first, then the remaining agents (claude, codex, agy). When
77
+ every option is out, Leg prints each reset time, soonest first, waits with
78
+ a countdown, and starts the first one back from the bundle. Ctrl-C quits.
79
+
80
+ Force a hand-off at any time with the **Hand off now** button on the card, or
81
+ `leg sessions handoff <id>`.
82
+
83
+ ## Is the resume file still true?
84
+
85
+ ```powershell
86
+ leg resume --check # exit 0 current, 1 stale or unstamped, 3 none here
87
+ leg resume # the same verdict, then the pointer itself
88
+ ```
89
+
90
+ Leg stamps every resume file with the commit, the working tree and the
91
+ terminals it was written against, and recomputes freshness from git when you
92
+ read it. A commit landing, the tree moving, or the terminal it described going
93
+ away all make it stale; the terminal card's drawer shows the same verdict under
94
+ "What happens next". A session ending rewrites `RESUME.md` to say nothing is in
95
+ flight, and the board does the same at start for a terminal that crashed, so
96
+ nothing is left describing work that has moved on.
97
+
98
+ ## 5. Watching and steering from the CLI
99
+
100
+ ```
101
+ leg sessions ls every session and its usage
102
+ leg sessions show <id>
103
+ leg sessions events <id>
104
+ leg sessions handoff <id> same as the board button
105
+ leg sessions end <id>
106
+ leg sessions rm <id> forget an ended session
107
+ leg sessions simulate-limit <id> drive the real limit path without a real wall (claude, agy)
108
+ leg accounts ls logins and their 5h/7d usage
109
+ leg open | down | status the board
110
+ ```
111
+
112
+ On the board, **Remove** safely prunes an ended session only when its worktree
113
+ is clean and its branch is already merged into the base. For a session that
114
+ needs to be removed from the board while preserving its work, use the visible
115
+ **Remove record** button, read its confirmation, and keep the worktree, branch,
116
+ unmerged commits, and dirty files intact.
117
+
118
+ ## 6. Where files live
119
+
120
+ Everything Leg writes goes under `LEG_HOME` (default `~/.leg`):
121
+
122
+ ```
123
+ ~/.leg/
124
+ sessions/<session-id>/
125
+ session.json the live record the board renders
126
+ events.jsonl the timeline
127
+ control.json board to runner requests
128
+ hook.log what Claude Code's hooks sent
129
+ claude-settings.json the per-session --settings file
130
+ agy.log agy's --log-file, agy sessions only
131
+ usage/<agent>--<account>.json
132
+ accounts/<agent>/<name>/ extra logins (see configuration.md)
133
+ landings.jsonl every Land, for the landed-on-trunk list
134
+ board.log the board server's output
135
+ ```
136
+
137
+ In the repo you run in, Leg writes `.leg/` (session notes, `RESUME.md` and
138
+ one `RESUME-<session-id>.md` per hand-off) and `.context-handoffs/` (the
139
+ bundles). Both are added to `.git/info/exclude`, so they never show up in
140
+ `git status`.
141
+
142
+ `leg uninstall --yes` removes `~/.leg` and nothing else.
143
+
144
+ ## 7. Pipelines (extras)
145
+
146
+ Version 0.1 worked the other way round: you dropped a task card on the board
147
+ and Leg ran the agents headless in a git worktree, one per card, with a
148
+ fallback chain, path leases, a scheduler and a merge queue. All of that still
149
+ works and lives below the Terminals lane. It is no longer the way in.
150
+
151
+ ### Preflight
152
+
153
+ ```
154
+ leg up --dry
155
+ ```
156
+
157
+ One row per dependency (node, git, `context-handoff-bundle`, each registered
158
+ adapter) with `ok` or `missing`, then exit without spawning anything. A
159
+ `missing` adapter only matters if a card's chain names it.
160
+
161
+ ### Start the board with the scheduler
162
+
163
+ ```
164
+ npm start
165
+ ```
166
+
167
+ `npm start` runs the same preflight, boots the board server on
168
+ `http://127.0.0.1:4747`, opens it, and streams prefixed, redacted logs. Ctrl-C
169
+ stops the server and any agent it started. From another terminal: `npm run
170
+ stop` (which runs `leg down`).
171
+
172
+ ### A card from the board
173
+
174
+ Click **New card**. The form asks for:
175
+
176
+ - **Repo path**: an absolute path to a local git repository (at least one
177
+ commit).
178
+ - **Task**: the text prompt every agent leg gets.
179
+ - **Pipeline**: `factory`, `build`, `build-land`, or `custom JSON`.
180
+ - **Chain**: one row per adapter, in fallback order.
181
+ - **Leases** (comma separated path globs), **Trunk branch** (default `main`),
182
+ **Land mode** (`ff` or `pr`), **Test command**, **Title**.
183
+ - **Queue immediately**: leave it checked to start the card right away.
184
+
185
+ ### A card from the CLI
186
+
187
+ ```
188
+ leg card add --repo <path-to-a-git-repo> --task "Add a LICENSE file" --chain claude --queue
189
+ ```
190
+
191
+ Other flags `card add` accepts: `--pipeline <preset|file>`, `--mode
192
+ <adapter>=<mode>`, `--max-turns <adapter>=<n>`, `--leases <glob,glob>`,
193
+ `--trunk <branch>`, `--land-mode ff|pr`, `--test-command "<cmd>"`, `--title
194
+ "<text>"`, `--slug <id>`, `--approve <adapter,...>`.
195
+
196
+ The command prints the new card id. Show it, or run it directly:
197
+
198
+ ```
199
+ leg card show <card-id>
200
+ leg card run <card-id>
201
+ leg card events <card-id>
202
+ ```
203
+
204
+ ### Try a pipeline with no real agent
205
+
206
+ The `fake` adapter drives `bin/fake-agent.mjs`, a stand-in CLI, so you can see
207
+ a full run without a subscription login:
208
+
209
+ ```
210
+ leg card add --repo <path-to-a-git-repo> --task "demo" --chain fake --fake-mode fake=limit --queue
211
+ ```
212
+
213
+ `FAKE_MODE` (set per adapter with `--fake-mode <adapter>=<mode>`) picks its
214
+ behaviour: `success`, `incomplete`, `limit`, `stall`, `auth`, `crash`,
215
+ `no_progress`, and the land-demo modes `break-test` / `fix-test` /
216
+ `resolve-rebase`. A chain of two fake adapters (`--chain
217
+ fake-claude,fake-codex --fake-mode "fake-claude=limit,fake-codex=success"`)
218
+ shows a full handoff; see [DEMO.md](DEMO.md) for the walkthrough with
219
+ screenshots.
220
+
221
+ ### Where a card's files live
222
+
223
+ ```
224
+ ~/.leg/
225
+ cards/<card-id>/
226
+ card.json the card's current state
227
+ events-<actor-key>.jsonl one append-only file per writer
228
+ driver.lock the process driving the card, only while it runs
229
+ unsynced.jsonl DashClaw records to retry (`ledger sync`), if any
230
+ runs/<n>/
231
+ run.json this run's status, outcome, exit code
232
+ prompt.txt the exact prompt sent to the adapter
233
+ out.log the adapter's stdout
234
+ err.log the adapter's stderr
235
+ supervisor.log the runner's own log for this run
236
+ locks/land-<hash>.json the merge queue's turn, one per repo root, while a land runs
237
+ ACTIVE.md generated summary of open cards
238
+ leg.pid written by `leg up`, removed on stop
239
+ scheduler.pid written by `leg scheduler start`
240
+ ```
241
+
242
+ A card's own git worktree lives in the repo it targets, not under
243
+ `LEG_HOME`: `<repo>/.leg-worktrees/<card-id>` on branch `leg/<card-id>`.
244
+
245
+ ## Next
246
+
247
+ - [concepts.md](concepts.md): sessions, accounts, usage windows, the
248
+ interactive hand-off, then cards, stations, chains, leases and the land
249
+ station.
250
+ - [board-guide.md](board-guide.md): every element of the board.
251
+ - [configuration.md](configuration.md): every environment variable.
@@ -0,0 +1,80 @@
1
+ # Real run: claude → codex, no fakes
2
+
3
+ One task, two real logged-in CLIs, the real handoff path. Run on 2026-09-10 on
4
+ a scratch repo (`toy-real`: README, package.json with `"test": "node --test"`,
5
+ one commit). Evidence: `fixtures/real-run/` (run records, logs, bundle `show`,
6
+ events; home paths scrubbed to `~`).
7
+
8
+ ## Command
9
+
10
+ ```
11
+ node bin/leg.mjs card add --repo <toy-real> --chain claude,codex --max-turns claude=2 --mode codex=workspace-write --pipeline build --title "calc module (real run)" --task "Create src/calc.mjs exporting add, sub, mul and div (div throws on division by zero). Create test/calc.test.mjs using node:test covering all four functions including the divide-by-zero case. Run node --test and fix until green. Then write .leg/DONE."
12
+ node bin/leg.mjs card run card-20260911-0232-real-calc
13
+ ```
14
+
15
+ `--max-turns claude=2` is the handoff trigger: the task needs more than two
16
+ turns, so the first leg ends before the DONE marker.
17
+
18
+ ## Timeline (UTC, from `fixtures/real-run/events.jsonl`)
19
+
20
+ | time | event | detail |
21
+ |------|-------|--------|
22
+ | 02:32:50 | card_created | actor human:local, chain claude > codex |
23
+ | 02:32:51 | leg_started | adapter `claude`, run 1, `mode=default`, `--max-turns 2` |
24
+ | 02:33:23 | leg_exited | **outcome `failed`**, exit 1, signal `claude-max-turns` (32 s) |
25
+ | 02:33:24 | handoff_written | bundle `20260911-023324-baton-card-20260911-0232-real-calc-build-leg0`, quality **strong (0.65)** |
26
+ | 02:33:25 | leg_started | adapter `codex`, run 2, `-s workspace-write` |
27
+ | 02:35:39 | leg_exited | **outcome `completed`**, exit 0 (2 m 14 s) |
28
+ | 02:35:40 | station_done, done | |
29
+
30
+ Wall time: 2 m 50 s. `grep -c fake fixtures/real-run/events.jsonl` = 0.
31
+
32
+ ## Leg 1 (claude)
33
+
34
+ The result JSON on stdout (`fixtures/real-run/leg1/out.log`) is the signal that
35
+ was docs-only until this run and is now observed-live:
36
+
37
+ ```
38
+ {"type":"result","subtype":"error_max_turns","is_error":true,"stop_reason":"tool_use","terminal_reason":"max_turns","num_turns":3, …}
39
+ ```
40
+
41
+ Exit code 1, empty stderr, no `.leg/DONE`; the diff evidence saw one changed
42
+ path (`.dashclaw-local/`, written by the machine's Claude Code hooks, not the
43
+ task). Classification: `budget` signal `claude-max-turns` + non-zero exit →
44
+ `failed`, handoff. (A budget cap is Leg's own setting, not a usage limit; it
45
+ still hands off.)
46
+
47
+ ## The bundle
48
+
49
+ `fixtures/real-run/bundle-show.txt`: Findings carry "Done so far", "Diff since
50
+ leg start", the touched path; Open questions carry the outcome and exit code.
51
+ Leg 2 picked the handoff up: its first message is "Proceeding from the last
52
+ agent state" and its second tool call reads `.leg/CONTRACT.md` and the
53
+ handoff file (`fixtures/real-run/leg2/out.excerpt.log`, items 1-3).
54
+
55
+ ## Leg 2 (codex)
56
+
57
+ `fixtures/real-run/leg2/out.excerpt.log` (JSONL): codex read the resume,
58
+ wrote `src/calc.mjs` and `test/calc.test.mjs`, ran the tests itself (its
59
+ sandbox allowed `node --test` here; LESSONS 07-13 recorded a policy block on a
60
+ different machine setup), wrote `.leg/PROGRESS.md` and `.leg/DONE`, and
61
+ printed a summary. Verified inside leg 2
62
+ (`fixtures/real-run/leg2/out.excerpt.log`, item 20), which ran the repo's
63
+ `npm test` -> `node --test` in the worktree:
64
+
65
+ ```
66
+ tests 5 pass 5 fail 0
67
+ ```
68
+
69
+ The worktree holds `src/calc.mjs`, `test/calc.test.mjs` and
70
+ `.leg/{CONTRACT.md, DONE, PROGRESS.md, handoff-build-leg0.md}`.
71
+
72
+ ## What this proves
73
+
74
+ - The subscription logins were used: neither leg's stderr carries an
75
+ "another auth source" warning (`leg1/err.log` is empty, `leg2/err.log` is
76
+ one stdin notice), and `sanitizeEnv` strips ANTHROPIC_API_KEY and
77
+ OPENAI_API_KEY before any adapter starts (test/redact.test.mjs).
78
+ - The handoff is CLI-agnostic: the bundle written after a Claude leg was
79
+ consumed by a Codex leg through the same `load` + contract prompt.
80
+ - The DONE-marker contract classified both legs without parsing prose.
@@ -0,0 +1,72 @@
1
+ # Review notes, 2026-09-14
2
+
3
+ This is a requested thorough-review deliverable. It records confirmed fixes
4
+ and the local evidence captured before publication. Current release outcomes
5
+ are tracked on npm; release contents are in the packaged `CHANGELOG.md`.
6
+
7
+ ## Confirmed fixes
8
+
9
+ | area | confirmed behavior | source and regression references |
10
+ |---|---|---|
11
+ | Codex quota | The board and an active Codex attach make a read-only app-server `account/rateLimits/read` request every 60 seconds. Durations identify windows: 300 minutes is 5h and 10080 minutes is 7d. An explicit available response, rather than a percentage, clears an old wall. | `src/server.mjs` `refreshCodexAccounts`, `src/attach.mjs` `refreshCodexUsage`, `src/taps/codex.mjs` `readCodexUsage`, `src/usage.mjs` `recordUsage`; `test/attach-core.test.mjs`, `test/server.test.mjs` |
12
+ | quota display | Account bars read `<n>% used`; readings older than five minutes are stale. No model turn and no hardcoded quota supply the reading. | `src/board/sessions.js`, `src/usage.mjs`; `test/server.test.mjs` |
13
+ | session removal | Normal removal is a safe prune. **Remove record** is a separate confirmed board action that preserves the worktree, branch, unmerged commits, and dirty files after its confirmation. | `src/board/sessions.js`, `src/server.mjs`, `src/worktree.mjs`; `test/server.test.mjs` |
14
+ | concurrent terminal control | End and Hand off write through the session file lock, preventing competing control-file updates. | `src/sessions.mjs` `requestControl`, `src/server.mjs`; `test/attach-core.test.mjs`, `test/attach-e2e.test.mjs` |
15
+ | landing | Landing rejects a wrong base branch, proves a branch is merged before safe pruning, and reruns tests after a retry rebase when trunk moves. | `src/land.mjs` `landSession` and `pruneSessionWorktree`, `src/mergequeue.mjs` `land`, `src/worktree.mjs`; `test/land.test.mjs`, `test/mergequeue.test.mjs`, `test/worktree.test.mjs` |
16
+ | card recovery | An orphaned run can recover a card left in `handing_off` instead of occupying the scheduler indefinitely. | `src/orchestrator.mjs` `orphanedRun`, `src/scheduler.mjs`, `src/runner.mjs`; `test/runner.test.mjs`, `test/scheduler.test.mjs` |
17
+ | board freshness | Guest-to-owner token changes restore pipeline controls; valid guest changes retain the terminal stream. Older card-list, drawer, log, Floor, and retired SSE responses cannot overwrite newer state. | `src/board/board.js`, `src/board/floor.js`; `test/board-updates.test.mjs` |
18
+ | loopback authorization | Tokenless owner access requires a loopback peer and a loopback hostname. An attacker-controlled hostname that resolves or rebinds to `127.0.0.1` is refused on reads and writes even when its `Origin` matches its `Host`. | `src/auth.mjs` `isLoopbackRequest`, `src/server.mjs`; `test/dns-rebinding.test.mjs` |
19
+ | Team refresh | A signed Team key is required for refresh. An expired signed key can refresh when its subscription remains active; this is subscription-level behavior, not individual-seat revocation. | `site/api/key.js`, `site/api/_lib.js`; `test/site-api.test.mjs`, `test/license.test.mjs` |
20
+ | webhook delivery | Stripe webhook verification accepts multiple rotated `v1` signatures. Resend is given a stable event-and-delivery idempotency key; Resend retains that key for 24 hours, so this is not a claim of permanent deduplication. | `site/api/webhook.js`, `site/api/_lib.js`; `test/site-api.test.mjs` |
21
+
22
+ ## Evidence already observed
23
+
24
+ - The final independent `npm test` run exited 0: 391 tests, 390 passed, none
25
+ failed or cancelled, and one POSIX-permission test was skipped on Windows.
26
+ The privacy check reported 0 findings across 256 files. Final lint and
27
+ `git diff --check` both exited 0.
28
+ - The board-focused suite added deferred behavioral cases for guest/owner token
29
+ changes and stale card, drawer, log, and Floor responses. It reported 26
30
+ passing tests before this documentation change.
31
+ - A live Codex app-server probe returned a weekly-only window as `primary`,
32
+ confirming that positional primary-to-5h mapping was wrong. The check did
33
+ not start a model turn; account-specific readings are omitted here.
34
+ The same weekly-only fixture failed against an archived `HEAD` copy because
35
+ the old parser put the 10080-minute object in `five_hour`; it passes now with
36
+ `five_hour=null` and the object in `seven_day`.
37
+ - The DNS-rebinding regression failed against `HEAD` with `200 !== 401`, then
38
+ passed with the fix for both a read and a state-changing request. The focused
39
+ auth/share suite reported 35 passing tests.
40
+ - After a board-only restart, `/api/health` returned 200. Frontend verification
41
+ passed `/` and `/floor` with no console errors or failed requests; the live
42
+ Codex pill rendered the weekly percentage from the app-server source and no
43
+ 5h value. The New card form opened and Escape dismissed it without creating
44
+ data.
45
+ - An independent security review passed the loopback authorization fix for the
46
+ normal API, SSE reauthorization, and the companion listener. Custom-host
47
+ bearer and share tokens remained valid while the attacker Host returned 401.
48
+
49
+ ## Review conclusions and rejected findings
50
+
51
+ - Account labels expose intentional shared metadata, not an additional account
52
+ privacy defect.
53
+ - Removing a landing session after the landing operation completes is not a
54
+ concurrent-removal bug if the final verifier confirms the operation boundary.
55
+ - Resend retains an idempotency key for 24 hours while Stripe can retry live
56
+ webhooks for longer. A delayed retry can therefore resend the same license
57
+ email; it cannot create a duplicate charge or entitlement. Durable delivery
58
+ storage was not added because it would introduce new production state and
59
+ infrastructure beyond this local audit.
60
+
61
+ ## Retro
62
+
63
+ Green baseline checks did not expose every race or operator-facing safety gap.
64
+ Inspecting actual records and running deferred behavioral reproductions did.
65
+ The permanent behavioral cases now cover the observed guest/owner and stale
66
+ response failures so they do not rely on a future review to be rediscovered.
67
+
68
+ ## Deviation log
69
+
70
+ The approved test update required a small refresh-auth protocol change so a
71
+ valid guest token reconnects the sessions stream while pipeline controls remain
72
+ hidden.
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file