@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/ERRORS.md ADDED
@@ -0,0 +1,326 @@
1
+ # Errors
2
+
3
+ What broke, why, and what fixed it. One entry per failure, newest first. A first
4
+ occurrence has to be written down or a repeat is never countable.
5
+
6
+ ## 2026-09-15: macOS `/var` symlink broke two e2e tests
7
+
8
+ **Fixed in `test/helpers.mjs`.**
9
+
10
+ `initRepo` and `makeHome` returned the path from `mkdtempSync(join(tmpdir(),
11
+ ...))` verbatim. On macOS, `tmpdir()` returns `/var/folders/…` but `/var` is a
12
+ symlink to `/private/var`. Leg stores paths through `canonPath` (which calls
13
+ `realpathSync`), so `s.cwd` was `/private/var/…` while the test's `repo` was
14
+ `/var/…`. Two `attach-e2e` assertions failed: `assert.equal(codex.cwd, repo)`
15
+ and the `.find(s => s.cwd === repo)` that guards the "End from board" test.
16
+
17
+ The fix wraps both helpers in `realpathSync` so the returned path matches what
18
+ baton stores. This is a test-infrastructure bug, not a production-code bug:
19
+ `canonPath` was already doing the right thing.
20
+
21
+ The lesson: any path created from `os.tmpdir()` that will be compared against a
22
+ path stored by production code must be resolved first, because macOS `tmpdir()`
23
+ returns the unresolved symlink form.
24
+
25
+ ## 2026-09-15: Land could not run the tests on macOS
26
+
27
+ **Fixed in `src/adapters/resolve.mjs`.**
28
+
29
+ The first CI run with `macos-latest` in the matrix failed where Linux and
30
+ Windows passed: `cannot resolve npm's JS entry` from `resolveCommand`
31
+ (`src/commands.mjs:22`), taking down `runTests` and with it `landNow` and the
32
+ merge queue. On a Mac, Land could never have run a repo whose test command
33
+ starts with `npm` or `npx`.
34
+
35
+ `resolveNpmCliEntry` looked in three places: `%APPDATA%\npm\node_modules`,
36
+ `/usr/local/lib/node_modules` and `/usr/lib/node_modules`. All three are
37
+ guesses at where someone else put node. The GitHub macOS runner keeps it in
38
+ `~/hostedtoolcache`, so none of them existed and the lookup returned null.
39
+ Linux passed only because `/usr/local/lib/node_modules` happens to exist
40
+ there, the same latent bug, hidden by a coincidence of layout. Homebrew,
41
+ nvm and asdf would all have failed the same way on a real machine.
42
+
43
+ The fix derives the prefix from the running binary instead of guessing:
44
+ POSIX installs put node at `<prefix>/bin/node` and global packages at
45
+ `<prefix>/lib/node_modules`, so `dirname(process.execPath)/../lib/node_modules`
46
+ finds it wherever node actually lives. `execPath` is now an injectable option
47
+ so the layout can be tested without one.
48
+
49
+ The lesson is about the class, not the path: a hardcoded absolute path is a
50
+ guess about someone else's machine, and a test matrix that omits a platform
51
+ does not tell you the code works there, it tells you nothing about it. This
52
+ was latent for as long as the matrix was two platforms wide.
53
+
54
+ Covered by `npm-entry-from-execpath` in `test/lessons.test.mjs`, checked in
55
+ both directions: it fails with the fix reverted and passes with it.
56
+
57
+ ## 2026-09-15: share-security failed once in the full suite and has not repeated
58
+
59
+ **Not fixed. Recorded so a second occurrence is countable.**
60
+
61
+ One full-suite run reported `test\share-security.test.mjs` failing at `:1:1`
62
+ after 1496 ms, with no individual test named. The file passed 19 of 19 in
63
+ isolation in 6.5 s, and passed inside two later full runs of the whole suite.
64
+ No root cause found.
65
+
66
+ What the evidence rules out: parallel contention, because `npm test` runs
67
+ `--test-concurrency=1`. What it points at: the file failed at the top level
68
+ rather than in a named test, and far faster than its healthy 6.5 s, so it died
69
+ during setup, not in an assertion. The file binds several loopback boards on
70
+ ephemeral ports and exercises a rate-limit lockout, and a Leg board was live
71
+ on 4747 for 161 minutes when the failing run started.
72
+
73
+ The cost of the bad reading was real even though the test was fine: the suite
74
+ totalled 454 tests that run instead of 472, because a file that dies at the top
75
+ level never registers its subtests, and 454 was briefly written into
76
+ `fixtures/verified.json` and onto two public pages as the published test count.
77
+ A total from a run with a failure in it is not the suite's size.
78
+
79
+ If it happens again: capture the file's stderr before anything else
80
+ (`node --test --test-reporter=spec` writes it; the default reporter swallowed
81
+ it), and check whether a board was listening on 4747 at the time.
82
+
83
+ ## 2026-09-15: a 3-second rebuild made the board impossible to scroll
84
+
85
+ - **Symptom.** Reported by the operator as "I'm not able to scroll down or up or
86
+ really interact with the new UI layout, and it keeps reverting to where I was
87
+ before scrolling." Intermittent-looking, which is what made it hard to believe.
88
+ - **Root cause.** `src/board/sessions.js`, in `putFocus()`. Every region on the
89
+ board is wiped and rebuilt on a timer, so focus is captured before the wipe and
90
+ restored after it, or a tabbed control is lost every three seconds. The restore
91
+ called `target.focus()`. `focus()` scrolls its element into view unless it is
92
+ passed `{ preventScroll: true }`, so the moment the reader clicked any button,
93
+ every poll dragged the viewport back to that button, forever.
94
+ - **Why it looked random.** With nothing focused, `takeFocus()` returns null and
95
+ the page behaves. The bug only appears after the reader interacts, which is
96
+ exactly when they stop suspecting the page and start suspecting themselves.
97
+ - **Fix.** `target.focus({ preventScroll: true })` on the rebuild path only. A
98
+ focus move the reader asked for (Cancel in a confirm row, the close control of
99
+ the detail region) still scrolls, because there it is the right behaviour.
100
+ - **Measured before and after.** Before: scroll to 1200, click Details, and the
101
+ next seven samples read 106, 106, 106… After: four scenarios held for 20
102
+ seconds each across both the 3s poll and the 15s re-sort, 0px drift on all
103
+ four. `test/board-drawer.test.mjs` now pins both halves of the rule.
104
+ - **The lesson that generalises.** Screenshots and a green suite cannot see "the
105
+ page fights you three seconds later". A live surface needs a hold test: put the
106
+ viewport somewhere, wait through every timer the page owns, and assert it did
107
+ not move. The same applies to focus, scroll position, an open disclosure and a
108
+ text selection.
109
+
110
+ ## 2026-09-15: Leg silently reversed a user's refusal to trust a folder
111
+
112
+ - **Symptom.** None visible. That is the point: it wrote to a file the user owns
113
+ and printed a line that read like a first-time record.
114
+ - **Root cause.** `src/trust.mjs` treated "not `true`" as "not asked yet". A
115
+ `hasTrustDialogAccepted: false` in `~/.claude.json`, which is what clicking
116
+ "No, exit" records, was flipped to `true` on every run in that repo. The agy
117
+ writer had the same shape: any value other than `TRUST_FOLDER`, including a
118
+ deliberate `DO_NOT_TRUST`, fell through to a write. The codex writer was the
119
+ only one of the three that got it right, because it happened to test for
120
+ presence rather than for truth.
121
+ - **Fix.** Only an absent key is an unanswered question. An answer already on
122
+ file is the user's, including "no", and is left alone with a stated reason.
123
+ The imports question is separate: `WarningShown: true` with `Approved: false`
124
+ is a recorded refusal and is not re-approved.
125
+ - **The lesson that generalises.** When code writes a file a human owns, absent
126
+ and negative are different states and must be distinguished in the code, not
127
+ in a comment. `!== true` is not "unset".
128
+
129
+ ## 2026-09-15: the trust record was written under a key Claude Code never reads
130
+
131
+ - **Symptom.** Would have been: the flag is on file, the prompt still appears,
132
+ and nothing says why. Caught before shipping by diffing against a copy of a
133
+ real `~/.claude.json` rather than a synthetic one.
134
+ - **Root cause.** The project key was written in Windows-native form,
135
+ `C:\Projects\foo`. Claude Code stores it as `C:/Projects/foo`: 100 of the 104
136
+ entries in a real config are forward-slash with an upper-case drive letter and
137
+ no trailing separator, and the four that are not are older duplicates of
138
+ projects that also appear in the new form. The wrong spelling creates a second
139
+ entry that is simply ignored.
140
+ - **Fix.** `claudeProjectKey()` normalises to that form. An older spelling
141
+ already present is updated in place, never created, so a stale entry is
142
+ corrected without littering the config.
143
+ - **The lesson that generalises.** When writing into someone else's data file,
144
+ derive the key format from that file's own contents on a real machine. A
145
+ synthetic fixture agrees with whatever the code does and proves nothing.
146
+
147
+ ## 2026-09-15: a walled usage bar was drawn full, then drawn red, before it was drawn at all
148
+
149
+ - **Symptom.** The account at its limit showed a full calm-blue bar beside the
150
+ words `no reading`: the most urgent row on the page looked like the safest.
151
+ - **Root cause.** `board.css` draws a walled account's fill at 100%, but the
152
+ colour stops were computed from the percentage, which is 0 when there is no
153
+ reading, and `fillStops(0)` puts every stop at 100% so the whole bar paints
154
+ calm.
155
+ - **The wrong first fix, and why.** Computing the stops from 100 when walled made
156
+ the bar danger-red. That is the same fabricated number, louder. An independent
157
+ review caught it. The right answer is that a window with no reading paints no
158
+ fill at all, and the wall is carried by the words beside it.
159
+ - **The lesson that generalises.** When a display and its own label disagree,
160
+ fix the display's right to exist, not its colour.
161
+
162
+ ## 2026-09-15: the OG card advertised a commercial product as MIT
163
+
164
+ - **Symptom.** `site/og.html` printed `Claude Code · Codex · agy · local · MIT`
165
+ on the card every shared link renders. Leg ships under the Leg License
166
+ Agreement, all rights reserved; the only MIT in the repo is in `NOTICE`, about
167
+ a borrowed component explicitly re-licensed.
168
+ - **Fix.** The tag line names the price instead. The first correction was longer
169
+ than the string it replaced and pushed `.cmd` into wrapping, so `.cmd` is now
170
+ `flex: none; white-space: nowrap` and the footer measures 0px past its gutter
171
+ with a 15px gap, checked with `getBoundingClientRect` rather than by eye.
172
+ - **The lesson that generalises.** A generated image is code with no test. Any
173
+ copy change inside one needs the render measured afterwards, and legal or
174
+ pricing strings in marketing assets need checking against `LICENSE` and
175
+ `package.json`, which are the only sources that are actually true.
176
+
177
+ ## 2026-09-15: a redesign measured on a board of healthy terminals
178
+
179
+ - **Symptom.** Compact row styling was reported as producing 40px terminal rows.
180
+ Wes's actual screen showed 400px rows and he rejected the result.
181
+ - **Root cause.** The styling was scoped to `.panel.is-running` and measured
182
+ against a board of live, healthy terminals. A real board after a day's work is
183
+ mostly `lost` and `ended` terminals with long absolute temp paths, and none of
184
+ them matched the selector, so the measurement was taken on the minority of rows
185
+ the change actually touched.
186
+ - **Fix.** `scratchpad/seed-wes.mjs` seeds a board with the real shape, 4 live,
187
+ 3 lost, 2 ended, long temp paths, an image tag in a prompt, served on an
188
+ isolated port with a throwaway `LEG_HOME`. Run against it, the *pre-change*
189
+ board reproduced the defect exactly: 3,302px page and 180-277px rows at 1280,
190
+ 6,530px and 307-585px at 400. That is the check being observed failing before
191
+ it was trusted.
192
+ - **The lesson that generalises.** A UI measurement is only worth what its
193
+ fixture is worth. Before trusting a number about a redesign, seed the state the
194
+ operator actually has and confirm the instrument reproduces the complaint.
195
+ Never 4747 for that board: that port is the live one with real sessions on it.
196
+
197
+ ## 2026-09-15: a CSS rewrite silently broke the page that shares the stylesheet
198
+
199
+ - **Symptom.** `/floor` rendered unstyled account rows and threw
200
+ `ReferenceError: narrowQuery is not defined` after the board redesign.
201
+ - **Root cause.** `floor.html` and `floor.js` share `board.css` and carry a
202
+ ported copy of the board's head block. Rewriting `board.css` removed the
203
+ classes that copy emits (`.acct`, `.rail`, `.track`, `.fill`, `.num`, `.tier`),
204
+ and removing helpers from `sessions.js` left the copy in `floor.js` referencing
205
+ names that no longer existed on its own page.
206
+ - **Fix.** The head block is now extracted from `sessions.js` mechanically rather
207
+ than transcribed, so the "character for character" claim in its comment stays
208
+ true; `floor.html` moved onto the new shell; the floor's table styles were
209
+ added to the new system.
210
+ - **The lesson that generalises.** Before rewriting a shared stylesheet, list
211
+ every page that links it and open each one. A grep for the class names would
212
+ also have caught this; rendering the page is what actually did.
213
+
214
+ ## 2026-09-15: two pre-existing CI failures, one hiding behind the other
215
+
216
+ - **Symptom.** Three trust tests failed on the ubuntu job; the windows job showed
217
+ `cancelled`. Fixing ubuntu revealed three *different* failures on windows, in
218
+ resume, that had been there for two commits.
219
+ - **Root cause.** A failing matrix leg cancels its siblings, so windows had not
220
+ run to completion since `1f61414` introduced `test/resume.test.mjs`. Every run
221
+ after that reported `win=cancelled`, which reads like "not the problem" and is
222
+ actually "not measured". Both underlying bugs were the same shape: a function
223
+ describing a path asked the *host* to resolve it. On ubuntu, `resolve()` on
224
+ `C:\cfg` prepended the runner's cwd; on windows, `realPath()` expanded the 8.3
225
+ short name in `tmpdir()`, so `C:\Users\RUNNER~1\...` came back as
226
+ `C:\Users\runneradmin\...` and no longer matched the path the caller passed in.
227
+ - **Fix.** `src/trust.mjs` treats a drive-letter path as already absolute and
228
+ recognises the older Windows spelling by its spelling rather than by asking the
229
+ host to resolve it. `src/resume.mjs` walks with `resolve()` instead of
230
+ `realPath()` and returns the root in the caller's own spelling; two spellings
231
+ of one checkout are reconciled by `canonPath()` where they are compared.
232
+ - **The lesson that generalises.** `cancelled` on a CI matrix leg is not a pass
233
+ and not a failure, it is no information, and it stays that way for as long as
234
+ a sibling keeps failing. Read the per-job conclusions, not the run's, and treat
235
+ a leg that has not completed since a feature landed as unmeasured. Locally,
236
+ `npm test` does not run `npm run lint` here: both are needed before a push.
237
+
238
+ ## 2026-09-15: the site did not deploy, and the repo said it had
239
+
240
+ - **Symptom.** `main` was fast-forwarded with the board and site redesign, every
241
+ check was green, and `legcli.com` still served the old blue page.
242
+ GitHub showed `Vercel, Canceled by Ignored Build Step` with a green tick,
243
+ which reads like a pass.
244
+ - **Root cause.** `site/vercel.json` carried
245
+ `ignoreCommand: git diff --quiet HEAD^ HEAD .`, and with Root Directory `site`
246
+ that asks one question: did the SINGLE most recent commit touch `site/`? The
247
+ push was five commits; `site/` changed in the first of them and the tip was a
248
+ test fix, so the answer was no and the build was skipped. A push is not a
249
+ commit, and `HEAD^ HEAD` cannot see past the tip.
250
+ - **Fix.** `ignoreCommand` removed, so every push to `main` deploys. Vercel's
251
+ built-in "Skip deployments when there are no changes to the root directory"
252
+ is the safe form of the same idea because it compares against the last
253
+ deployment, not against `HEAD^`.
254
+ - **The lesson that generalises.** A skipped step reports as a green tick.
255
+ "Canceled by Ignored Build Step" and a cancelled CI matrix leg are the same
256
+ trap in different clothes: an absence of failure that is not a success. After
257
+ a deploy, load the page and confirm the change is on it, the deploy
258
+ platform's own tick is not evidence that anything shipped.
259
+
260
+ ## 2026-09-15: the new OG card was live and every share still showed the old one
261
+
262
+ - **Symptom.** The site had deployed the dark OG card, and a link shared in a
263
+ messaging app still previewed the blue one, with a tag line naming MIT, which
264
+ had been corrected days earlier.
265
+ - **Root cause.** Nothing was wrong with the deploy: fetching
266
+ `/og.png` returned the new image. Link-preview caches (Telegram, Slack,
267
+ iMessage, X) key on the image URL and most never re-fetch a URL they already
268
+ hold. `og.png` never changed, so every client that had ever crawled the page
269
+ kept its first copy indefinitely.
270
+ - **Fix.** `og:image` and `twitter:image` carry `?v=`, bumped in the same commit
271
+ that changes `og.png`. Already-cached previews need the platform's own
272
+ refresh (Telegram: send the URL to `@WebpageBot`).
273
+ - **The lesson that generalises.** "Deployed" and "what people see" are
274
+ different questions for anything a third party caches by URL. An asset that
275
+ will ever be revised needs a version in its URL from the first ship, not after
276
+ someone notices the old one is still going out.
277
+
278
+ ## The card row was never ported, and nothing noticed for four days (2026-09-15)
279
+
280
+ - **Symptom.** Retaking `docs/screenshots/` showed the Background tasks rows
281
+ rendering as unstyled markup: the card title unreadable, chips welded to the
282
+ text beside them, the four-column layout stacked into blocks. The rest of the
283
+ board had been redesigned on 2026-09-15 and looked right.
284
+ - **Root cause.** `.r1` `.r2` `.r3` `.r4`, the card row's whole layout, had no
285
+ rule in `board.css`. `.row-title` had a rule, but it set only `font-size` and
286
+ `color`, never resetting the native button, so the browser's own
287
+ `rgb(240,240,240)` fill sat under the board's near-white text. Two separate
288
+ misses in the same region, neither of which fails a test, a lint, or a build.
289
+ - **Fix.** Both rules written against `.term-row`, the terminal row they are
290
+ supposed to mirror. `test/board-a11y.test.mjs` now fails if any class that
291
+ reaches the DOM from `board.js`, `sessions.js`, `floor.js`, `index.html` or
292
+ `floor.html` has no rule in `board.css`, with a named exception list for the
293
+ ones queried as selectors only. Verified by deleting the `.r1` rules and
294
+ watching the test name `.r1`.
295
+ - **The lesson that generalises.** A redesign that lands region by region has no
296
+ natural signal for the region nobody opened. CSS has no undefined-variable
297
+ error: an unstyled class renders, it just renders wrong, and it renders wrong
298
+ only where someone looks. The check that would have caught it is cheap and
299
+ mechanical, every class the code emits must resolve to a rule, and it should
300
+ exist before the redesign starts, not after the screenshots expose it.
301
+
302
+ ## Removing the trial turned the test suite red in three unrelated files (2026-09-15)
303
+
304
+ - **Symptom.** After deleting the 14-day trial from `src/license.mjs`,
305
+ `share.test.mjs`, `cards.test.mjs` and `launcher.test.mjs` failed with
306
+ licensing errors, none of which mention licensing in their test names.
307
+ - **Root cause.** The trial opened every gate for free, so ~470 tests had been
308
+ passing the licence check without ever declaring they needed to. Removing it
309
+ made the suite's dependence on it visible all at once.
310
+ - **Fix.** `testEnv()` writes a Team key into each throwaway home, signed by a
311
+ pair generated per run, with `LEG_PUBLIC_KEY_B64` pointing the spawned CLI
312
+ at its public half. `LEG_UNLICENSED=1` opts a test back into the refusal.
313
+ - **A second bug inside the fix.** The first version imported `src/license.mjs`
314
+ from `test/helpers.mjs` to reuse `signLicense`. That pulls in `store.mjs`,
315
+ which reads `LEG_HOME` once at import time, and `helpers.mjs` is imported by
316
+ every test file *before* it sets `LEG_HOME`, so the ledger was pinned to
317
+ whatever home happened to be set, which on a developer machine is the real
318
+ `~/.leg`. Caught because `server.test.mjs` started failing on a card it had
319
+ just written. The key format is now reproduced in `helpers.mjs` instead.
320
+ Checked `~/.leg` afterwards: no test cards, no `license.json`, nothing
321
+ written.
322
+ - **The lesson that generalises.** A permissive default in test setup is load
323
+ bearing and invisible; you find out how much only when you remove it. And a
324
+ module that reads the environment at import time cannot be imported from a
325
+ helper that runs before the environment is set, no matter how small the thing
326
+ you wanted from it.
package/docs/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # Docs index
2
+
3
+ ## Start here
4
+
5
+ - [getting-started.md](getting-started.md): install, run `leg claude`, what you get while it runs, where files live, and the v0.1 pipeline flow as an extra.
6
+ - [concepts.md](concepts.md): sessions, accounts, usage windows and the interactive handoff, then cards, stations, chains, outcomes, leases, the land station and the card status state diagram.
7
+ - [board-guide.md](board-guide.md): the instrument head (a row per login, two window rails each, the 85 percent post), the Terminals panels, overlap flags, Landed on main, Background tasks, Settings, and the floor view.
8
+ - [configuration.md](configuration.md): every environment variable, the accounts layout, `.env`, network exposure, card-level options.
9
+ - [adapters.md](adapters.md): what Leg reads from each CLI in an interactive session, each adapter's headless argv, modes, forbidden flags, gotchas, and how to add a new one.
10
+ - [faq.md](faq.md): short answers to real questions (the status line, codex's missing hook, agy's missing percentage, second accounts, uninstall, limits, secrets, Windows support).
11
+
12
+ ## Reference
13
+
14
+ - [VOCABULARY.md](VOCABULARY.md): the exact identifiers Leg uses for session statuses and session events, and for card statuses, outcomes, event types, actor types, human actions and bounce reasons.
15
+ - [cli-contracts.md](cli-contracts.md): what Leg knows about each coding-agent CLI, the interactive taps and their sources, and the limit-signal table, every fact tagged observed-live or docs-only.
16
+ - [DEMO.md](DEMO.md): a fake-adapter walkthrough of a usage-limit handoff, with screenshots.
17
+ - [real-run.md](real-run.md): one real pipeline run, claude handing off to codex with no fakes involved.
18
+ - [ROADMAP-v2](ROADMAP-v2.md): where the factory-floor shape is headed after v1.
19
+
20
+ ## The code behind the docs
21
+
22
+ | area | modules |
23
+ |------|---------|
24
+ | interactive sessions | `src/attach.mjs` (the `leg <agent>` runner), `src/sessions.mjs` (the session store), `src/usage.mjs` (usage windows and the chooser), `src/accounts.mjs` (extra logins), `src/bundle.mjs` (the per-session bundle), `src/hook.mjs` (what Claude Code's hooks run) |
25
+ | taps | `src/taps/claude.mjs`, `src/taps/claude-usage.mjs`, `src/taps/codex.mjs`, `src/taps/agy.mjs` |
26
+ | board | `src/server.mjs`, `src/board/sessions.js` (Terminals lane), `src/board/board.js` and `src/board/floor.js` (pipelines) |
27
+ | pipelines | `src/orchestrator.mjs`, `src/scheduler.mjs`, `src/chain.mjs`, `src/pipeline.mjs`, `src/runner.mjs`, `src/ledger.mjs`, `src/leases.mjs`, `src/mergequeue.mjs`, `src/adapters/*.mjs` |
28
+
29
+ ## History
30
+
31
+ - [REUSE.md](REUSE.md): what Leg ports from the private team tooling it started from, and what it drops.
32
+ - [ERRORS.md](ERRORS.md): what broke, the root cause and the fix, so a repeat is countable.
33
+ - [DEVIATIONS.md](DEVIATIONS.md): every place Leg's shape differs from that ported source or from the plan, one row per change.
34
+
35
+ ## Also see
36
+
37
+ - [../README.md](../README.md): the top-level overview.
38
+ - [../CHANGELOG.md](../CHANGELOG.md): what changed in each release.
39
+ - [../CONTRIBUTING.md](../CONTRIBUTING.md): dev setup, tests, lint, the privacy hook, PR checklist.
40
+ - [../SECURITY.md](../SECURITY.md): how to report a vulnerability.
package/docs/REUSE.md ADDED
@@ -0,0 +1,289 @@
1
+ # REUSE, what Leg ports from the private team tooling, and what it drops
2
+
3
+ Source: a private ucsandman repository (team tooling for a two-agent protocol
4
+ with a detached Claude Code launcher, a JSONL task ledger, a git snapshot tool,
5
+ and a governed capability runner). Read in full on 2026-09-10 before any Leg
6
+ code was designed. Every ported shape change is one row in `DEVIATIONS.md`.
7
+ NOTICE carries the attribution. No path, id, or chat identifier from the source
8
+ appears anywhere in this tree; `scripts/privacy-check.mjs` enforces that.
9
+
10
+ Legend: **Ported** = kept, with its new home. **Dropped** = removed, with the
11
+ reason. **Why** = the judgment.
12
+
13
+ ## lead-handoff.mjs
14
+
15
+ **Ported** (→ `src/runner.mjs`, subcommands `launch | supervise | sweep`):
16
+ - `launch`: detached supervisor spawn (`detached: true, windowsHide: true`,
17
+ `child.unref()`), the `handoff_already_running` refusal (exit 11) when the
18
+ recorded supervisor pid is alive, archiving a stale record to `*.prev.json`
19
+ and relaunching when it is dead, one-line JSON on stdout.
20
+ - `supervise`: spawn the agent with stdout/stderr redirected to files, write
21
+ the run record on every transition, notify timer (30 min) and kill timer
22
+ (90 min) with the kill-verify retry (30 s) and the UNKILLABLE path (exit 12),
23
+ session-id capture from the result file, ledger writes through the ledger CLI
24
+ via `ledgerSafe` (a ledger failure never crashes the supervisor), stderr tail
25
+ scrubbed of secrets before it is logged (`SECRET_RES`, `scrub`, `errTail`).
26
+ - `killTree`: `taskkill /PID <pid> /T /F` on Windows, process-group SIGKILL
27
+ elsewhere; the `HANDOFF_SKIP_KILL` test seam becomes `LEG_SKIP_KILL`.
28
+ - `sweep`: orphan detection (record says running, supervisor pid dead →
29
+ `orphaned` + ledger error + ORPHANED line naming the child pid).
30
+ - `pidAlive`, `parseArgs`, `need`, `die`, exit codes 0 / 2 / 3 / 11 / 12 / 13.
31
+ - `resolveOpenclawEntry` → `src/adapters/resolve.mjs` `resolveNpmCliEntry(pkg, bin)`:
32
+ resolves an npm package's real JS entry so the runner can `spawn(node, [entry])`
33
+ without a shell (the `.cmd` shim needs one). Codex and the optional
34
+ OpenClaw sync need exactly this.
35
+ - Child environment sanitization before spawn (the "drill launched from inside
36
+ a Claude Code session leaks a session key" lesson) → one exported
37
+ `sanitizeEnv(env)` that every adapter calls; the deleted-key set grows (see
38
+ DEVIATIONS).
39
+ - The timer seam (`HANDOFF_TIMERS_MS` → `LEG_TIMERS_MS`, same
40
+ `notify,kill,kill-verify` shape and defaults).
41
+
42
+ **Dropped:**
43
+ - `telegramTarget`, `sendTelegram`, `.env.handoff`, every `notify(...)` message
44
+ string, and the sweep's delivery canary, Telegram delivery and chat ids
45
+ (privacy rule). The notify timer becomes a ledger `status` event only.
46
+ - `LEAD_ALLOWED_TOOLS`, a hard-coded allowlist naming the team bin scripts and
47
+ two MCPs. Leg passes only the permission mode the chain entry names and
48
+ never disallows the Agent tool.
49
+ - `findReportEvent`, decided "completed" by scanning the lead's ledger for a
50
+ `done` event. Leg uses one CLI-agnostic completion contract (`.leg/DONE`
51
+ in the worktree), phase 5.
52
+ - `--model` / `--effort` / `--safe-mode` argv and the `origin` lookup, model
53
+ and effort move into the chain entry; there is no origin surface.
54
+ - `WORKDIR = ROOT/../..` (the lead ran in the private repo's root), Leg runs
55
+ every leg in the card's worktree.
56
+
57
+ **Why:** the supervise/kill/sweep logic is paid-for behaviour with tests that
58
+ caught real incidents (split-brain relaunch, unkillable lead, orphaned
59
+ supervisor after reboot). Everything dropped is either transport (Telegram),
60
+ identity (allowlist, private paths), or a completion rule that only worked for
61
+ one CLI.
62
+
63
+ ## lead-handoff.test.mjs
64
+
65
+ **Ported** (→ `test/runner.test.mjs`, stub agents → `src/adapters/fake.mjs` +
66
+ `bin/fake-agent.mjs` selected by `FAKE_MODE`):
67
+ - launch writes the run record + prompt file and prints ok JSON
68
+ - launch refuses unknown card (exit 3) and missing prompt file (exit 2)
69
+ - launch exits 11 when the supervisor pid is alive
70
+ - launch archives a stale record (dead pid) and relaunches
71
+ - supervise: fast finish stores the session id, exit code, never fires timers
72
+ - supervise: agent exits without finishing → `failed`-shaped record, exit 13,
73
+ ledger error with a **scrubbed** stderr tail (the `sk-…` assertion stays)
74
+ - supervise: runaway agent → notify event then killed, exit 12
75
+ - supervise: unkillable agent → retry, UNKILLABLE ledger error, exit 12
76
+ - supervise: child environment is sanitized (extended to all seven keys plus
77
+ the `CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS=0` assertion)
78
+ - sweep: dead supervisor → orphaned + ORPHANED line + ledger error
79
+ - sweep: healthy running record left alone
80
+ - sweep: no records → quiet OK line, exit 0
81
+
82
+ **Dropped** (named here so the count is auditable):
83
+ - "supervise: lead logs done -> completed, session id stored, HANDOFF_DONE sent"
84
+ (the completed half is replaced by phase 5's DONE-marker test; the Telegram
85
+ half is gone)
86
+ - "supervise: claude-code origin -> no telegram send at all"
87
+ - "supervise: no TEAM_TELEGRAM_TO configured -> send skipped, still completes"
88
+ - "sweep: no handoffs at all -> ... dry-run-only canary" (canary half)
89
+ - "sweep: broken delivery path -> DELIVERY-CHECK FAILED line"
90
+ - "sweep: no TEAM_TELEGRAM_TO -> DELIVERY-CHECK FAILED line"
91
+
92
+ **Why:** the stub-agent approach (a per-test `.mjs` that plays the CLI) is the
93
+ right seam for a runner that must never touch a real CLI under test; it becomes
94
+ Leg's fake adapter, which phase 4 extends with limit / stall / auth-failure
95
+ modes.
96
+
97
+ ## team-ledger.mjs
98
+
99
+ **Ported** (→ `src/ledger.mjs`, subcommands `create | append | update | sync`):
100
+ - The rule that this file is the ONLY writer of ledger files; per-writer
101
+ append-only JSONL; `ACTIVE.md` regenerated on every create/update with
102
+ corrupt records skipped and warned, never bricking the list.
103
+ - `assertNoSecrets` with `SECRET_PATTERNS` (refuses to log, exit 2).
104
+ - `parseArgs` / `need(args, key, allowed)` validation with the exact
105
+ `invalid --type "gossip" (allowed: …)` error shape.
106
+ - The DashClaw sync: `loadDashclawConfig` (process variables win, then a
107
+ dotenv file), `dashclawRequest` on native `http`/`https` with a 5 s timeout
108
+ and the 409-on-create-is-success rule, `bufferUnsynced` → `unsynced.jsonl`,
109
+ `flushUnsynced` via `sync [--card]` exiting 1 while anything remains.
110
+ - Exit codes 2 (bad args / secret / corrupt record) and 3 (not found).
111
+
112
+ **Dropped:**
113
+ - `AGENTS = ['claude', 'openclaw']`, `RECIPIENTS`, and the `from`/`to` pair -
114
+ replaced by a validated `actor` object plus `card_id`, `station`, `leg`
115
+ (amendment 2 §4).
116
+ - `ORIGINS`, `lead`, `stop_condition`, `max_exchanges`,
117
+ `openclaw_session_key`, two-agent protocol fields with no Leg meaning.
118
+ - The `team-` id prefix and `tasks/` directory (→ `card-`, `cards/`).
119
+ - The DashClaw endpoint paths `/api/team-tasks…`, phase 9 wires the real
120
+ target through config; the transport code stays.
121
+
122
+ **Why:** an append-only, secret-refusing, per-writer ledger with a buffered
123
+ best-effort sync is exactly the multiplayer-ready event store amendment 2 asks
124
+ for. Only the vocabulary changes.
125
+
126
+ ## team-ledger.test.mjs
127
+
128
+ **Ported** (→ `test/ledger.test.mjs`, renamed to card/actor vocabulary):
129
+ - create writes `card.json`, the `card_created` event, and `ACTIVE.md`
130
+ - append adds one line to the writer's own file only
131
+ - update patches status and session id and regenerates `ACTIVE.md`
132
+ - append with an invalid type exits 2 and names the bad value
133
+ - append to a missing card exits 3
134
+ - create rejects a slug that will not produce a valid id
135
+ - create syncs to DashClaw with `x-api-key` (fake server on 127.0.0.1)
136
+ - sync failure buffers to `unsynced.jsonl` and still exits 0
137
+ - `sync` flushes the buffer
138
+ - append refuses secret-looking values with exit 2 and writes nothing
139
+ - update dies cleanly (exit 2) on a corrupt record
140
+ - unknown subcommand exits 2
141
+ - a corrupt record is skipped with a warning and does not brick `ACTIVE.md`
142
+
143
+ **Added in the port:** append without a valid `--actor` exits 2;
144
+ `readEvents(cardId)` merges writer files sorted by `ts`.
145
+
146
+ **Dropped:** none. Assertions on `from`/`to`/`origin`/`lead` become assertions
147
+ on `actor`/`card_id`/`station`/`leg`.
148
+
149
+ **Why:** all thirteen encode behaviour Leg keeps.
150
+
151
+ ## git-snapshot.mjs
152
+
153
+ **Ported** (→ `src/git-snapshot.mjs`, byte-identical apart from the two removals):
154
+ read-only snapshot JSON (`branch`, `head`, `head_subject`, `dirty` counts and
155
+ files with `--max-files` truncation, `submodules_dirty`, `recommendation`) and
156
+ `--diff-since <ref>` (commits, changed files, insertions, deletions). Phase 5
157
+ uses `--diff-since` to classify a leg that exited 0 with no DONE marker and no
158
+ diff as `no_progress`; phase 7 uses the snapshot before landing.
159
+
160
+ **Dropped:** the hard-coded private-path constant and its `dirty-check`
161
+ recommendation branch. Recommendation is `branch` when clean, `worktree` when
162
+ dirty.
163
+
164
+ **Why:** the tool already does what the land station and the no-progress
165
+ check need, and it never mutates the repo.
166
+
167
+ ## git-snapshot.test.mjs
168
+
169
+ **Ported** (→ `test/git-snapshot.test.mjs`): clean repo → `branch`; dirty
170
+ repo → `worktree` with exact counts; `--diff-since` across two commits; non-repo
171
+ dir exits 2; 25 untracked files truncate at 20.
172
+
173
+ **Dropped:** "private-path override … yields recommendation dirty-check", it
174
+ asserts on a machine path that must not exist in this tree.
175
+
176
+ **Why:** five of six tests are portable as-is.
177
+
178
+ ## invoke-capability.mjs
179
+
180
+ **Ported** (as patterns, not code → `src/sync/dashclaw.mjs` in phase 9):
181
+ - The argv-builder shape: a registry entry owns an `argv(input)` function that
182
+ returns a plain array, and `--dry-run` prints the exact command without
183
+ touching anything. Leg's `pr` land mode (`gh pr create` argv, stub-tested
184
+ only) and the DashClaw action recorder use this shape.
185
+ - Fail-closed: no config → do nothing and say so; a 4xx/5xx or a timeout never
186
+ turns into "executed". Leg's optional syncs are off by default and a sync
187
+ failure never changes a card's state.
188
+
189
+ **Dropped:** the capability registry (`post-to-x`, `send-email`), the
190
+ approval-polling loop, agent identities, `REPO_ROOT`-relative wrapper paths,
191
+ and the global `fetch` client (LESSONS 07-12: Node 24 on Windows can crash at
192
+ exit after global fetch; the ledger already uses native `http`/`https`).
193
+
194
+ **Why:** exactly the verdict the plan expected: nothing to port but the two
195
+ shapes. Leg records actions; it does not gate external capabilities.
196
+
197
+ ## LESSONS.md
198
+
199
+ Each applicable line becomes a named test in `test/lessons.test.mjs` (phase 4).
200
+ Lines that only concern X/OAuth, the OpenClaw gateway, Vercel, or the weekly
201
+ drill do not apply and are omitted.
202
+
203
+ | lesson | Leg test name |
204
+ |--------|-----------------|
205
+ | 07-09 MSYS mangles leading-slash args | `lessons: every git spawn carries MSYS_NO_PATHCONV=1 and never a shell` |
206
+ | 07-10 `claude -p --output-format json` prints nothing until the end | `lessons: stall detector reads out/err file mtime, never waits on stdout` |
207
+ | 07-10 acceptEdits silently denies tools outside `--allowedTools` | `lessons: claude argv carries no --allowedTools and never disallows Agent` |
208
+ | 07-11 `bash` under some PATHs is the WSL shim and drops variables | `lessons: no adapter spawn sets shell:true; argv[0] is node or a native exe` |
209
+ | 07-11 every ledger write from your OWN identity only | `lessons: an event always lands in its own actor's events file` |
210
+ | 07-11 a detached session cannot answer permission prompts | `lessons: CONTRACT.md tells the agent no interactive prompt will be answered` |
211
+ | 07-12 Node 24 + global fetch crashes at exit | `lessons: src/ has no global fetch call` |
212
+ | 07-13 codex exec may start read-only despite the flag | `lessons: codex argv carries -s workspace-write and -C <worktree>; phase 3 records the effective sandbox` |
213
+ | 07-13 `node --test <dir>` fails on Node 24/Windows | `lessons: package.json test script is bare node --test` |
214
+ | 07-13 `[hidden]` loses to an author `display:` rule | `lessons: board CSS ships [hidden]{display:none!important}` |
215
+ | 08-10 enumerate the interface from source before building | `lessons: cli-contracts.md carries a producing command for every CLI section` |
216
+ | 08-10 relative doc paths silently no-op | `lessons: README links resolve to files in the tree` |
217
+ | 08-11 two checkouts, wrong path built against | `lessons: card repo is resolved to its git toplevel before a worktree is created` |
218
+
219
+ Thirteen tests. Phase 4 may add more if the port surfaces another line.
220
+
221
+ ## PROTOCOL.md
222
+
223
+ **Ported** (as rules in `docs/cli-contracts.md`, the adapters, and the land
224
+ station):
225
+ - Codex sandbox ladder: `read-only` for review, `workspace-write` only when
226
+ edits are the deliverable, `danger-full-access` and
227
+ `--dangerously-bypass-approvals-and-sandbox` never (a chain entry naming one
228
+ refuses to launch). Blast radius: `-C <narrowest dir>` = the card's worktree.
229
+ Network off by default: `-c sandbox_workspace_write.network_access=false`.
230
+ - Git workflow: snapshot before a leg, snapshot `--diff-since` after; the
231
+ `worktree` recommendation is Leg's only mode (one worktree per card,
232
+ branch `baton/<card-id>`); the diff, never prose, is what review and landing
233
+ trust.
234
+ - Failure rules that carry over: exit 11 means a supervisor is already running,
235
+ never relaunch over it; a failed launch is retried once then the chain moves
236
+ on; slow, dead, or runaway legs are the supervisor's job (30 m notify, 90 m
237
+ kill); no secrets in any ledger field.
238
+ - Model routing: a `claude -p` call that omits `--model` inherits the
239
+ interactive default. Leg lets the chain entry name the model and passes it
240
+ when present; the default is documented per CLI in `cli-contracts.md` rather
241
+ than hard-coded.
242
+
243
+ **Dropped:** the two-agent classifier, lead/specialist roles, envelopes, the
244
+ exchange cap, Telegram transport, DashClaw tiers and approvals (Leg's
245
+ approvals are buttons on the board; DashClaw is an optional recorder), the
246
+ `openclaw agent` transport.
247
+
248
+ **Why:** the containment rules for Codex and the snapshot-then-diff workflow
249
+ were written after real incidents and transfer unchanged. The protocol's
250
+ coordination layer is what Leg replaces with a board.
251
+
252
+ ## context-handoff-bundle (the handoff format)
253
+
254
+ **Reused as-is** (argv subprocess, never re-implemented): `save --title --slug
255
+ --notes <file> --tag --repo-local [--update]`, `load [query] [--json]`,
256
+ `validate <dir>`, `list`, `show`. The `--notes` file is the input Leg
257
+ writes; its parser reads `## Scope`, `## Projects mentioned`, `## Findings`,
258
+ `## Opportunities`, `## Open questions`, `## Evidence anchors` (bullets under
259
+ each). Leg's mapping: Scope = the task and the leg that stopped; Findings =
260
+ what was done (from `.leg/PROGRESS.md` and the diff); Open questions = why
261
+ the leg stopped and what is unverified (a failing test lands here on a
262
+ bounce); Evidence anchors = the changed files, so drift checks flag them.
263
+ Bundles are saved `--repo-local` inside the worktree so the next agent finds
264
+ them in its cwd. Storage: `.context-handoffs/index.json` in the worktree.
265
+ `checkpoint` / `rescue` (hook-driven passive checkpoints) are not used.
266
+
267
+ 0.2.0's interactive sessions reuse the same seam rather than adding a second
268
+ one: `src/bundle.mjs` imports `chb()` and `ensureExcluded()` from
269
+ `src/handoff.mjs`, so the CLI stays the only writer of bundle files. The
270
+ difference is one bundle per session, refreshed with `save --update <slug>`
271
+ instead of one bundle per handoff.
272
+
273
+ **Machine state (verified live 2026-09-10):** the CLI on PATH was 0.1.0 from
274
+ the Store Python; PyPI and the source checkout are 0.4.0 (`--notes`,
275
+ `--repo-local`, `--update`, drift). Phase 5 installed 0.4.0 from PyPI, then
276
+ the local checkout in editable mode because 0.4.0 has no `--version` flag:
277
+ the checkout gained `--version` and `tests/test_version.py` (76 upstream
278
+ tests green, not pushed). `src/handoff.mjs` reads the version through
279
+ `importlib.metadata` when the flag is absent, so a plain `pip install -U
280
+ context-handoff-bundle` still works. Live-verified in phase 5: `save
281
+ --repo-local --notes`, `validate`, `load`, `list --repo-only`, `show`.
282
+
283
+ ## project-launch-button-policy
284
+
285
+ Adopted whole as phase 8's spec: `npm start` = `leg up`; boots, health-checks,
286
+ opens the board, prefixed unified logs, Ctrl+C teardown, `--dry` and `down`
287
+ modes, argv subprocesses only, secret redaction on stdout, refuses dangerous
288
+ misconfiguration (a YOLO flag in a chain entry, a bind address off loopback
289
+ without a token).