@ucsandman/legcli 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +560 -0
- package/LICENSE +58 -0
- package/NOTICE +4 -0
- package/README.md +558 -0
- package/bin/fake-agent.mjs +143 -0
- package/bin/leg.mjs +449 -0
- package/docs/DECISIONS.md +119 -0
- package/docs/DEMO.md +214 -0
- package/docs/DEVIATIONS.md +182 -0
- package/docs/ERRORS.md +326 -0
- package/docs/README.md +40 -0
- package/docs/REUSE.md +289 -0
- package/docs/ROADMAP-v2.md +55 -0
- package/docs/VOCABULARY.md +243 -0
- package/docs/adapters.md +300 -0
- package/docs/board-guide.md +579 -0
- package/docs/cli-contracts.md +401 -0
- package/docs/concepts.md +426 -0
- package/docs/configuration.md +220 -0
- package/docs/faq.md +206 -0
- package/docs/getting-started.md +251 -0
- package/docs/real-run.md +80 -0
- package/docs/review-2026-09-14.md +72 -0
- package/docs/screenshots/board-400px.png +0 -0
- package/docs/screenshots/board-details-open.png +0 -0
- package/docs/screenshots/board-done.png +0 -0
- package/docs/screenshots/board-drawer.png +0 -0
- package/docs/screenshots/board-empty.png +0 -0
- package/docs/screenshots/board-handoff.png +0 -0
- package/docs/screenshots/board-running.png +0 -0
- package/docs/screenshots/demo-1-claude-running.png +0 -0
- package/docs/screenshots/demo-2-limit-hit.png +0 -0
- package/docs/screenshots/demo-3-handoff-bundle.png +0 -0
- package/docs/screenshots/demo-4-codex-running.png +0 -0
- package/docs/screenshots/demo-5-done.png +0 -0
- package/docs/screenshots/floor-final-1280.png +0 -0
- package/docs/screenshots/floor-final-400.png +0 -0
- package/docs/screenshots/floor-landing.png +0 -0
- package/docs/screenshots/floor.png +0 -0
- package/docs/screenshots/share-guest-1280.png +0 -0
- package/docs/screenshots/share-owner-1280.png +0 -0
- package/docs/screenshots/terminals-1280.png +0 -0
- package/docs/screenshots/thumb-done.jpg +0 -0
- package/docs/screenshots/thumb-handoff-details.jpg +0 -0
- package/docs/screenshots/thumb-limit-hit.jpg +0 -0
- package/docs/screenshots/youtube-thumbnail.jpg +0 -0
- package/fixtures/limits/agy/agy-resource-exhausted.json +11 -0
- package/fixtures/limits/claude/claude-budget-limit.json +11 -0
- package/fixtures/limits/claude/claude-max-turns.json +11 -0
- package/fixtures/limits/claude/claude-model-limit.json +11 -0
- package/fixtures/limits/claude/claude-session-limit.json +11 -0
- package/fixtures/limits/claude/claude-weekly-limit.json +11 -0
- package/fixtures/limits/codex/codex-quota-exceeded.json +11 -0
- package/fixtures/limits/codex/codex-rate-limit-exceeded.json +11 -0
- package/fixtures/limits/codex/codex-skills-notice.json +11 -0
- package/fixtures/limits/codex/codex-usage-limit-reached.json +11 -0
- package/fixtures/limits/codex/codex-usage-limit.json +11 -0
- package/fixtures/limits/generic/generic-429.json +11 -0
- package/fixtures/limits/generic/generic-overloaded.json +11 -0
- package/fixtures/limits/generic/generic-quota.json +11 -0
- package/fixtures/limits/generic/generic-rate-limit.json +11 -0
- package/fixtures/limits/generic/generic-resource-exhausted.json +11 -0
- package/fixtures/limits/generic/generic-usage-limit.json +11 -0
- package/fixtures/limits/grok/grok-not-logged-in.json +11 -0
- package/fixtures/limits/negative/auth-source-set.json +11 -0
- package/fixtures/limits/negative/compile-error.json +11 -0
- package/fixtures/limits/negative/empty-stdout-exit-0.json +11 -0
- package/fixtures/live/agy/attempt-1-scratch-workspace.out.log +1 -0
- package/fixtures/live/agy/cmd.txt +1 -0
- package/fixtures/live/agy/err.log +0 -0
- package/fixtures/live/agy/out.log +1 -0
- package/fixtures/live/agy/parsed.json +5 -0
- package/fixtures/live/agy/run.json +18 -0
- package/fixtures/live/agy/supervisor.log +2 -0
- package/fixtures/live/claude/cmd.txt +1 -0
- package/fixtures/live/claude/err.log +0 -0
- package/fixtures/live/claude/limit-rate_limit.json +21 -0
- package/fixtures/live/claude/out.log +1 -0
- package/fixtures/live/claude/parsed.json +11 -0
- package/fixtures/live/claude/run.json +18 -0
- package/fixtures/live/claude/supervisor.log +2 -0
- package/fixtures/live/codex/cmd.txt +1 -0
- package/fixtures/live/codex/err.log +1 -0
- package/fixtures/live/codex/last.md +2 -0
- package/fixtures/live/codex/limit-usage_limit_exceeded.json +25 -0
- package/fixtures/live/codex/out.log +8 -0
- package/fixtures/live/codex/parsed.json +15 -0
- package/fixtures/live/codex/run.json +18 -0
- package/fixtures/live/codex/supervisor.log +2 -0
- package/fixtures/live/grok/cmd.txt +1 -0
- package/fixtures/live/grok/err.log +32 -0
- package/fixtures/live/grok/out.log +7 -0
- package/fixtures/live/grok/parsed.json +5 -0
- package/fixtures/live/grok/run.json +18 -0
- package/fixtures/live/grok/supervisor.log +2 -0
- package/fixtures/verified.json +34 -0
- package/package.json +68 -0
- package/scripts/board-shots.mjs +69 -0
- package/scripts/build-docs-site.mjs +308 -0
- package/scripts/check-claims.mjs +150 -0
- package/scripts/clean-clone-check.sh +42 -0
- package/scripts/license-sign.mjs +23 -0
- package/scripts/limits-table.mjs +38 -0
- package/scripts/live-limits.mjs +36 -0
- package/scripts/privacy-check.mjs +63 -0
- package/scripts/probe.mjs +92 -0
- package/scripts/seed-fake-cards.mjs +34 -0
- package/scripts/seed-floor-board.mjs +69 -0
- package/scripts/seed-wes-board.mjs +91 -0
- package/scripts/stripe-setup.mjs +99 -0
- package/scripts/vercel-env.mjs +40 -0
- package/src/accounts.mjs +119 -0
- package/src/adapters/agy.mjs +56 -0
- package/src/adapters/claude.mjs +54 -0
- package/src/adapters/codex.mjs +73 -0
- package/src/adapters/common.mjs +32 -0
- package/src/adapters/fake.mjs +48 -0
- package/src/adapters/grok.mjs +51 -0
- package/src/adapters/index.mjs +27 -0
- package/src/adapters/resolve.mjs +40 -0
- package/src/attach.mjs +592 -0
- package/src/auth.mjs +85 -0
- package/src/board/board.css +543 -0
- package/src/board/board.js +1307 -0
- package/src/board/favicon.svg +1 -0
- package/src/board/floor.html +127 -0
- package/src/board/floor.js +670 -0
- package/src/board/fonts/OFL-atkinson-hyperlegible-next.txt +93 -0
- package/src/board/fonts/OFL-azeret-mono.txt +94 -0
- package/src/board/fonts/atkinson-board.woff2 +0 -0
- package/src/board/fonts/azeret-board.woff2 +0 -0
- package/src/board/index.html +251 -0
- package/src/board/sessions.js +1451 -0
- package/src/bundle.mjs +103 -0
- package/src/cards.mjs +114 -0
- package/src/chain.mjs +264 -0
- package/src/commands.mjs +79 -0
- package/src/contract.mjs +73 -0
- package/src/env.mjs +19 -0
- package/src/fsx.mjs +72 -0
- package/src/git-snapshot.mjs +152 -0
- package/src/handoff.mjs +171 -0
- package/src/hook.mjs +49 -0
- package/src/land.mjs +106 -0
- package/src/launcher.mjs +272 -0
- package/src/leases.mjs +86 -0
- package/src/ledger.mjs +369 -0
- package/src/license.mjs +168 -0
- package/src/limits.mjs +97 -0
- package/src/live-capture.mjs +69 -0
- package/src/mergequeue.mjs +225 -0
- package/src/orchestrator.mjs +365 -0
- package/src/pipeline.mjs +100 -0
- package/src/preferences.mjs +47 -0
- package/src/presets.mjs +23 -0
- package/src/ratelimit.mjs +49 -0
- package/src/redact.mjs +47 -0
- package/src/resume.mjs +354 -0
- package/src/runner.mjs +438 -0
- package/src/scheduler.mjs +118 -0
- package/src/server.mjs +845 -0
- package/src/session-detail.mjs +129 -0
- package/src/sessions.mjs +202 -0
- package/src/share.mjs +163 -0
- package/src/stations/agent.mjs +42 -0
- package/src/stations/build.mjs +9 -0
- package/src/stations/human.mjs +5 -0
- package/src/stations/land.mjs +6 -0
- package/src/stations/plan.mjs +8 -0
- package/src/stations/pr.mjs +31 -0
- package/src/stations/review.mjs +8 -0
- package/src/stations/test.mjs +27 -0
- package/src/store.mjs +90 -0
- package/src/sync/dashclaw.mjs +70 -0
- package/src/sync/index.mjs +54 -0
- package/src/sync/workboard.mjs +54 -0
- package/src/taps/agy.mjs +64 -0
- package/src/taps/claude-usage.mjs +67 -0
- package/src/taps/claude.mjs +170 -0
- package/src/taps/codex.mjs +286 -0
- package/src/trust.mjs +323 -0
- package/src/usage.mjs +179 -0
- package/src/wait.mjs +30 -0
- package/src/worktree.mjs +207 -0
package/docs/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).
|