@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
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Decisions
|
|
2
|
+
|
|
3
|
+
Durable product and design decisions that the code does not explain on its own. One entry per decision, newest first.
|
|
4
|
+
|
|
5
|
+
## 2026-09-15: the board is dark cobalt, and there is no light mode
|
|
6
|
+
|
|
7
|
+
- **What.** The board ground is a saturated deep cobalt at hue 258, the same hue the marketing site is drenched in, taken to its dark end. Not a neutral near-black: measured in OKLab, `--e0` sits 0.0507 from `#0f1115` at 5.7 times its chroma, so the anti-reference colour `PRODUCT.md` bans is not reachable from this palette.
|
|
8
|
+
- **Why.** The scene decides it. The board is checked dozens of times an hour by a head pointed at a dark terminal that then goes straight back to it; a pale plate beside a dark window is a re-adaptation on every glance, held for hours. The board is not a document the reader reads, it is an instrument the reader checks.
|
|
9
|
+
- **The counter-argument, and the answer.** A pale plate is a different object in peripheral vision, so the eye knows which window it is in before it focuses. This design answers that with shape and luminance range instead of polarity: the terminal is white monospace on near-black at one size; the board is a graduated cobalt field whose single brightest object is a 30px right-aligned numeral above a horizontal rail.
|
|
10
|
+
- **What it costs, as an exclusion and not a deferral.** There is no light mode. A user at a bright desk with a window behind them has no recourse. The tokens are structured so a light variant is an override of `--e0` through `--e4` plus the text ramp, but it is not in scope and it is not shipped.
|
|
11
|
+
|
|
12
|
+
## 2026-09-15: urgency is carried by luminance, and never by colour alone
|
|
13
|
+
|
|
14
|
+
- **What.** Every state prints a word beside its colour: `waiting on you`, `at the wall`, `over 85`, `under 60`, `no reading`, `stale 41m`. Agent identity always prints the agent name beside its hue. Panels that need the reader are raised one elevation step rather than tinted.
|
|
15
|
+
- **Why.** The old board carried its entire hierarchy in one red that meant six different things, so nothing meant anything. Colour as the only carrier also fails WCAG 1.4.1 and fails the glance from four feet.
|
|
16
|
+
- **The rule that follows.** No agent identity hue may sit within 25 degrees of a state hue. That is measured, not asserted; it is what caught codex green reading as success.
|
|
17
|
+
|
|
18
|
+
## 2026-09-15: a window with no reading paints no bar
|
|
19
|
+
|
|
20
|
+
- **What.** A usage rail draws a fill only when a real percentage was computed. A walled account with no reading gets an empty track with its graduations, and the wall is carried by the words `at the wall` and its return time.
|
|
21
|
+
- **Why.** A walled account was drawn full-width because the wall is known. But the numeral beside it said `no reading`, so the loudest object on the page asserted a number the product does not have. The first fix made that bar danger-red, which was worse: it was the same fabricated number, louder. Print nothing rather than a wrong number, and say the wall in words.
|
|
22
|
+
|
|
23
|
+
## 2026-09-15: Leg records the folder-trust answer, and never overrides one already given
|
|
24
|
+
|
|
25
|
+
- **What.** Before starting an agent, Leg writes the folder-trust answer for the repository the user chose by typing `baton <agent>` in it: `hasTrustDialogAccepted` in `~/.claude.json`, `trust_level` in `~/.codex/config.toml`, an entry in `~/.gemini/trustedFolders.json`. `LEG_TRUST=never` turns it off.
|
|
26
|
+
- **Why.** The handoff is the product, and it fires when the limit hits, which is usually when nobody is watching. An agent that had never run in that folder stopped on its first-run trust prompt and waited for a keypress that was not coming, so the bundle was written and the terminal sat idle until morning.
|
|
27
|
+
- **Why writing those files is allowed at all.** `stdio: 'inherit'` in `src/attach.mjs` hands the real terminal to the agent, so Leg cannot watch for the prompt and answer it. Pre-seeding is the only mechanism that does not change Leg's architecture. For Claude Code it is also the documented remedy: its permissions guide prescribes exactly this edit.
|
|
28
|
+
- **The three rules that bound it.** Never create a config file that is not already there. Never rewrite a file to say what it already says. Never override an answer already on file: only an absent key is an unanswered question, so a recorded refusal stays a refusal.
|
|
29
|
+
- **What changed in the marketing claim.** The site said "edits none of your config files"; that is now "leaves your settings files alone", and the README names exactly what is written and where.
|
|
30
|
+
|
|
31
|
+
## 2026-09-14: npm publication follows a successful push to `main`
|
|
32
|
+
|
|
33
|
+
- **What.** Leg publishes from the existing `.github/workflows/ci.yml`; there is no separate release workflow or manual publish step. The publisher waits for the complete Ubuntu and Windows test matrix, then publishes only when the exact package version is absent from npm.
|
|
34
|
+
- **Authentication.** The job uses npm trusted publishing and GitHub OIDC with `contents: read` and `id-token: write`. It has no `NPM_TOKEN`. The one-time npm trusted-publisher binding for `ucsandman/baton` and `ci.yml` was created on 2026-09-14; the first `0.4.2` workflow publication remains the end-to-end proof.
|
|
35
|
+
- **Private source.** The repository stays private. npm accepts trusted publishing from a private repository but cannot generate public provenance for it, so the publish command explicitly uses `--provenance=false`.
|
|
36
|
+
- **Failure behavior.** A validated registry 200 response skips an existing version. An exact-version 404 permits publication only when the candidate is a stable three-part version newer than npm's stable `latest`; this prevents an older queued run from moving the tag backward. Network failures, invalid JSON, unexpected statuses, prereleases, stale versions, and package/lock/repository metadata drift stop the job. Publish concurrency is serialized without cancelling an in-progress release.
|
|
37
|
+
- **Retro.** Git, site deployment, and npm publication are separate release results and must be verified separately. A green skip proves idempotency only; `0.4.2` must actually publish once to prove unattended trusted publishing end to end.
|
|
38
|
+
|
|
39
|
+
## 2026-09-11: the marketing site is a cobalt-drenched static page in `site/`
|
|
40
|
+
|
|
41
|
+
- **What.** One static HTML page (`site/index.html`, `style.css`, `site.js`), self-hosted fonts, deployed to Vercel from the `site/` directory with `vercel.json` headers. No framework, no build step. PRODUCT.md and DESIGN.md at the repo root carry the brief and the tokens so later edits inherit them.
|
|
42
|
+
- **How it was chosen.** A four-concept tournament (light restrained, drenched racing green, committed cobalt, product-led dark terminal) judged against a written rubric. Committed cobalt won and borrowed the DOM-recreated board from the light concept and the typed full-bleed terminal from the product-led one. The scores and disqualifications are recorded in the session notes; the design tokens are in DESIGN.md.
|
|
43
|
+
- **Why cobalt and not the board's own dark palette.** The operator board and the sibling site declick.dev are both near-black; a third near-black surface from the same author would read as one family and as the generic dark AI-tool page. The site's warmth comes only from the agent colors inside product visuals.
|
|
44
|
+
- **What the page promises.** Every number, path, version and date on it is copied from the README as verified on 2026-09-11. The terminal transcript is a labeled sample session whose `[baton]` lines are the strings `src/attach.mjs` prints and whose pointer prompt is the one `src/bundle.mjs` sends. The two-session cards are from the live run the README documents.
|
|
45
|
+
- **Analytics and search.** Vercel Web Analytics is the only script besides `site.js`; nothing on the page depends on it. Search Console and Bing registration state is recorded below this entry when done.
|
|
46
|
+
|
|
47
|
+
### Registration state, 2026-09-11
|
|
48
|
+
|
|
49
|
+
- Google Search Console: URL-prefix property `https://legcli.com/`, verified by the `google-site-verification` meta tag in `site/index.html` (removing the tag revokes it); `sitemap.xml` submitted; home URL inspected and indexing requested.
|
|
50
|
+
- Bing Webmaster Tools: site added manually and verified by the `msvalidate.01` meta tag (account-wide code); `sitemap.xml` submitted.
|
|
51
|
+
- Vercel Web Analytics: enabled on project `legcli` (plan-included tier); the tag is `<script defer src="/_vercel/insights/script.js">` and it is the only script besides `site.js`.
|
|
52
|
+
- Not done: a custom domain (none owned for Leg; Wes buys one 2026-09-18).
|
|
53
|
+
|
|
54
|
+
## 2026-09-15: the private repo gets a public documentation site instead of being opened
|
|
55
|
+
|
|
56
|
+
The repository stays private (see the 2026-09-11 license decision). That left every "read the source", README, FAQ and changelog link on the site and in the package pointing at a 404 for anyone who is not the owner, which is the first click a Hacker News or Reddit reader makes.
|
|
57
|
+
|
|
58
|
+
- **The docs are published, the source is not.** `scripts/build-docs-site.mjs` renders the public subset of `docs/` plus `README.md` and `CHANGELOG.md` to static pages under `site/docs`, committed, because Vercel serves `site/` with no build step. DECISIONS, DEVIATIONS, ERRORS, REUSE, ROADMAP-v2, DEMO and dated review notes are working files and stay out. CI regenerates and fails if the committed output has drifted from `docs/`.
|
|
59
|
+
- **Nothing dangles at a 404.** The clone instructions in `README.md` and `docs/getting-started.md` are replaced by where the source actually is for a buyer (`$(npm root -g)/legcli/src`). Links to files with a public home go there (`LICENSE` → `/license`, `SECURITY.md` → `/support#security`); links to working files that ship inside the package point at the package. `package.json` keeps its `repository` field because npm trusted publishing is bound to it (`.github/workflows/ci.yml` asserts the exact URL), so the npm "Repository" link is the one 404 that has to stay; `bugs.url` points at `/support` instead.
|
|
60
|
+
- **README images are absolute.** npm rewrites relative image paths to `raw.githubusercontent.com`, which 404s for a private repo, so the hero image and the new handoff GIF are absolute URLs on the site. Checked: the raw URL returns 404.
|
|
61
|
+
- **A support page exists.** `/support` carries the one address (`legcli@practicalsystems.io`), the refund terms, license-key troubleshooting, what to send in a bug report, and the security reporting path, because a private repo has no issue tracker to point at.
|
|
62
|
+
- **The claims are machine-checked.** `fixtures/verified.json` pins the verified CLI versions, the verification date and the test count; `scripts/check-claims.mjs` asserts every public surface agrees (run by `npm test`) and, with `--strict`, compares the pins against what those CLIs publish now (weekly, `.github/workflows/tap-drift.yml`). It found on its first run that `llms.txt` said 463 tests and the landing page said 406 while the suite reported 454.
|
|
63
|
+
|
|
64
|
+
## 2026-09-11: Leg is a commercial product; the site stays on the free Vercel address
|
|
65
|
+
|
|
66
|
+
- **License.** Wes: "if we're trying to sell this thing it shouldn't be open source and MIT." The repo stays private and the package ships under the Leg License Agreement (LICENSE): commercial, source readable in the package for inspection and own-use modification, no redistribution, no working around the license check. The FSL option from the pricing research was dropped for the same reason. Versions 0.2.0 and 0.3.0 remain available under MIT.
|
|
67
|
+
- **Pricing.** Personal $79 once with 12 months of releases (Sublime shape); Team $12 per seat per month (adds `leg share`); no trial, and a 30-day money-back guarantee as the risk reversal instead (2026-09-15: a trial suits daily-habit products, and Leg's value is bursty, it pays off in the moment a limit lands, which a fortnight of evaluation does not reliably contain; a buyer already paying for two or three agent subscriptions is not price-sensitive at $79, they are trust-sensitive, and a trial does not answer trust). Keys are Ed25519 tokens signed with a private key that lives only in the seller's `.env` and the site's Vercel env; the public key is in `src/license.mjs`. A Personal key is a window over `RELEASE_DATE`, so every release bumps that constant.
|
|
68
|
+
- **Checkout.** Stripe payment links (live) with automatic tax, `site/api/key` and `site/api/webhook` on Vercel functions, Resend from `legcli@practicalsystems.io`. `scripts/stripe-setup.mjs` is idempotent per site origin; a test-mode purchase was run end to end on 2026-09-11 (checkout, thanks page, key activated in the CLI, webhook 200 twice).
|
|
69
|
+
- **Domain.** A `legcli.com` purchase ($11.25) was started and cancelled at Wes's "just deploy it to a free vercel site"; nothing was bought. The site is https://legcli.com/.
|
|
70
|
+
- **Not a lawyer.** The license text was drafted in-session; a review before the first sale outside the US is Wes's call.
|
|
71
|
+
|
|
72
|
+
## 2026-09-14: the handoff order is a priority list, and a killed agent's terminal is restored in full
|
|
73
|
+
|
|
74
|
+
- **Order.** The saved order (Settings for new terminals, **Change order** per running terminal) is applied as an absolute priority list: drop the agent already running, keep the rest in the saved order. It used to rotate, anchored on the current agent and wrapping, so `codex > claude > agy` handed a Claude terminal to agy first. Wes: "I want agy to always be the last option." A rotation cannot express that; a priority list can, and every option is still tried exactly once. Same-agent alternate accounts still come before any agent switch, because continuing with the same agent on another login loses the least.
|
|
75
|
+
- **Terminal restore.** Leg kills the agent on End, Hand off now and the usage limit, so nothing the agent set is ever unset by the agent itself. Leg now undoes all of it (mouse reporting, bracketed paste, focus events, application keys, autowrap, the scrolling region) instead of only leaving the alternate screen. DECSTBM homes the cursor, so the margin reset sits between DECSC and DECRC, and the erase clears only what is below the cursor.
|
|
76
|
+
|
|
77
|
+
## 2026-09-14: the site deploys itself from git
|
|
78
|
+
|
|
79
|
+
- **Connected.** The `legcli` Vercel project is connected to `github.com/ucsandman/baton`, with Root Directory `site`, so a push to `main` publishes the marketing site and its two functions. A manual `vercel --prod` from `site/` still works and is the fallback.
|
|
80
|
+
- **Root Directory had to change first.** It was `.`, which is correct when deploying from inside `site/` but would have published the repo root on a git build: no `index.html`, and `api/key` and `api/webhook` gone, so a purchase in flight would not have received its key.
|
|
81
|
+
- ~~**Not every push.** `site/vercel.json` carries `ignoreCommand: git diff --quiet HEAD^ HEAD .`, which Vercel maps to the Ignored Build Step. A commit that touches nothing under `site/` cancels the build. The command failing (a shallow clone with no `HEAD^`) exits non-zero, which builds, so the failure mode is a redundant deploy rather than a missed one.~~ **Superseded 2026-09-15: the failure mode was a MISSED deploy.** `HEAD^ HEAD` compares one commit, not the push. Fast-forwarding five commits to main where only the first touched `site/` left the tip with no `site/` change, so Vercel cancelled the build and the redesigned site never went out while the repo said it had. The `ignoreCommand` is removed: every push to `main` deploys the site. Vercel's own "Skip deployments when there are no changes to the root directory" toggle is the safe version of this idea, because it compares against the last deployment rather than against `HEAD^`.
|
|
82
|
+
- **Agent sessions cannot do this part.** `vercel --prod` and `vercel git connect` are both denied by the harness classifier as production deploys, and the CLI auth token cannot be read. Wes ran both.
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
## 2026-09-15: the board is a dark product surface, and DESIGN.md was the defect
|
|
86
|
+
|
|
87
|
+
- **Six rejected rounds had one cause, and it was not taste.** Every attempt was
|
|
88
|
+
built against `DESIGN.md`'s brief, "exact, unhurried, mechanical, the printed
|
|
89
|
+
operator's manual for a piece of test equipment", with a sharp radius stance
|
|
90
|
+
("2px on controls, 3px on plates, never 8px"), hairline rules as the only
|
|
91
|
+
structure, and "no cards as layout". That brief cannot produce the
|
|
92
|
+
consumer-product finish that was asked for from the first message. The brief
|
|
93
|
+
was rewritten before anything else, because leaving it is how the next session
|
|
94
|
+
rebuilds the rejected look.
|
|
95
|
+
- **What replaced it.** Mock B, "dark product surface", with two grafts: mock C's
|
|
96
|
+
headline framing (the verdict names which login everything is on and how much
|
|
97
|
+
is left, rather than restating the number the panel beneath it prints) and
|
|
98
|
+
mock A's staleness sentence (the age of a reading plus which direction it is
|
|
99
|
+
wrong in). Neutral near-black ground, panels raised with a top highlight and a
|
|
100
|
+
shadow, 16px radii, type 13 to 52px, one accent on the primary action.
|
|
101
|
+
- **Size encodes importance.** The login carrying the terminals gets the wide lit
|
|
102
|
+
panel; a login with one fact gets a half panel; a login with no figure draws no
|
|
103
|
+
instrument. Previously every login got an identical row, so the layout said
|
|
104
|
+
nothing before you read it. This is the single decision that does the most work
|
|
105
|
+
on the page.
|
|
106
|
+
- **History leaves the live area.** Finished terminals, landed commits and
|
|
107
|
+
background tasks became three counts on the ground that open on click. On a
|
|
108
|
+
real board they are most of the list, and at equal visual weight they buried
|
|
109
|
+
the one or two terminals that were live.
|
|
110
|
+
- **The site runs on the board's hex values, not an approximation.** A marketing
|
|
111
|
+
page that recreates the product in a different palette is a picture of a
|
|
112
|
+
different product. `site/style.css` carries the same numbers as
|
|
113
|
+
`src/board/board.css`, and both recreations were rebuilt against what the board
|
|
114
|
+
actually renders.
|
|
115
|
+
- **Design agents must be spawned as `general-purpose`.** The lean agent types
|
|
116
|
+
(`sonnet-implementer`, `opus-owner`, `haiku-scout`) have no Skill tool at all,
|
|
117
|
+
so a design subagent spawned as one of them works from a paraphrase in its
|
|
118
|
+
prompt instead of from `frontend-design` and `impeccable`. That was the second,
|
|
119
|
+
independent reason the v3 mocks failed.
|
package/docs/DEMO.md
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# Demo: a usage limit hands the card to the next agent
|
|
2
|
+
|
|
3
|
+
A background card starts on `fake-claude`, hits a recorded Claude usage limit,
|
|
4
|
+
Leg writes a context handoff bundle, and `fake-codex` resumes in the same
|
|
5
|
+
worktree and finishes the work. The two fake adapters replay the shapes the real
|
|
6
|
+
CLIs print and spend no subscription usage. The same sequence against the real
|
|
7
|
+
CLIs is in [real-run.md](real-run.md).
|
|
8
|
+
|
|
9
|
+
The replay uses a throwaway git repo, a throwaway board home and a port of its
|
|
10
|
+
own, so it cannot reach a board you already have running on 4747. At the delay
|
|
11
|
+
set below the card took 53 seconds of wall time: two legs of 20 seconds, plus
|
|
12
|
+
the few seconds it took to read the row and click **Approve** between them.
|
|
13
|
+
|
|
14
|
+
Five stills of the run on 2026-09-15 (03:55 to 03:56 UTC) are in
|
|
15
|
+
`docs/screenshots/`, taken from the live board at 1280x800:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
demo-1-claude-running.png leg 1 running on fake-claude
|
|
19
|
+
demo-2-limit-hit.png limit hit, bundle written, the row waiting on you
|
|
20
|
+
demo-3-handoff-bundle.png the row expanded: bundle path, run signal, timeline
|
|
21
|
+
demo-4-codex-running.png leg 2 running on fake-codex
|
|
22
|
+
demo-5-done.png done, both legs on the chain
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The ledger of that run is `fixtures/demo/events.jsonl`.
|
|
26
|
+
|
|
27
|
+
## Replay it
|
|
28
|
+
|
|
29
|
+
### 1. A throwaway repo
|
|
30
|
+
|
|
31
|
+
PowerShell:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
mkdir C:\baton-demo\toy-demo
|
|
35
|
+
cd C:\baton-demo\toy-demo
|
|
36
|
+
git init -b main
|
|
37
|
+
"# toy" | Out-File -Encoding utf8 README.md
|
|
38
|
+
git add -A
|
|
39
|
+
git commit -m init
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
bash:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
mkdir -p ~/baton-demo/toy-demo && cd ~/baton-demo/toy-demo
|
|
46
|
+
git init -b main
|
|
47
|
+
echo '# toy' > README.md
|
|
48
|
+
git add -A && git commit -m init
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The stills were taken with the repo at `C:\baton-demo\toy-demo`. Any path works;
|
|
52
|
+
a different one only changes the paths the board prints.
|
|
53
|
+
|
|
54
|
+
### 2. A throwaway board on its own port
|
|
55
|
+
|
|
56
|
+
`FAKE_DELAY_MS` is how long each fake agent waits before it acts. At 20000 each
|
|
57
|
+
leg runs for 20 seconds, which is enough time to read the row while it is in
|
|
58
|
+
flight.
|
|
59
|
+
|
|
60
|
+
PowerShell, from the Leg checkout:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
cd C:\Projects\baton
|
|
64
|
+
$env:LEG_HOME = 'C:\baton-demo\home'
|
|
65
|
+
$env:LEG_PORT = '4851'
|
|
66
|
+
$env:FAKE_DELAY_MS = '20000'
|
|
67
|
+
npm start
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
bash:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
cd /path/to/baton
|
|
74
|
+
LEG_HOME=~/baton-demo/home LEG_PORT=4851 FAKE_DELAY_MS=20000 npm start
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
It prints a preflight table, then
|
|
78
|
+
`ready http://127.0.0.1:4851 (scheduler max 2, 0 cards)`, and opens that
|
|
79
|
+
address in your browser. The board polls every three seconds, so nothing below
|
|
80
|
+
needs a reload.
|
|
81
|
+
|
|
82
|
+
The page is one column, read top to bottom: the instrument head with one row per
|
|
83
|
+
login, then **Terminals**, **Landed on main**, **Background tasks**, and
|
|
84
|
+
**Settings**. This demo happens entirely in **Background tasks**. On a fresh
|
|
85
|
+
board home the head reads `no reading` on every rail, because no agent has
|
|
86
|
+
reported usage into this home yet.
|
|
87
|
+
|
|
88
|
+
### 3. Queue the card
|
|
89
|
+
|
|
90
|
+
Click **New card** in the **Background tasks** head. The dialog opens on **New
|
|
91
|
+
background card**.
|
|
92
|
+
|
|
93
|
+
1. **Repo path**: `C:\baton-demo\toy-demo`.
|
|
94
|
+
2. **Task**: `Add a file greeting.txt containing 'hello from baton'`.
|
|
95
|
+
3. Leave **Run now** ticked.
|
|
96
|
+
4. Open **Advanced options**.
|
|
97
|
+
5. Set **Scripted first agent (test/demo only)**, which sits below **Fallback
|
|
98
|
+
agents**, to `fake-claude (test/demo)`. The row under **First-agent
|
|
99
|
+
permissions and limits** is rebuilt for that adapter: it is labelled
|
|
100
|
+
`First: fake-claude` and now ends in a box marked `test behavior`. Type
|
|
101
|
+
`limit` in that box. **First agent** higher up still names a real agent; the
|
|
102
|
+
scripted adapter takes its place for this card. Set the scripted adapter
|
|
103
|
+
before you type in that row, because changing it rebuilds the row.
|
|
104
|
+
6. Under **Fallback agents** Leg has already filled in the real agents it
|
|
105
|
+
found. Click **Remove** on each of those rows.
|
|
106
|
+
7. Click **Add fallback agent**. In the new `Fallback 1` row set the agent to
|
|
107
|
+
`fake-codex (test/demo)`, type `success` in its `test behavior` box, and tick
|
|
108
|
+
**approval before start** so the handoff stops and waits for you.
|
|
109
|
+
8. Click **Create card**.
|
|
110
|
+
|
|
111
|
+
The recorded chain is `fake-claude>fake-codex`.
|
|
112
|
+
|
|
113
|
+
### 4. Leg 1, on fake-claude
|
|
114
|
+
|
|
115
|
+
The card appears as a row in **Background tasks**. The line beside the region
|
|
116
|
+
title reads `1 running`, and the row reads left to right:
|
|
117
|
+
|
|
118
|
+
- `fake-claude`, `build`, `running`
|
|
119
|
+
- the title, and under it the one sentence
|
|
120
|
+
`leg_started: leg started: adapter=fake-claude run=1 mode=acceptEdits`
|
|
121
|
+
followed by the local time
|
|
122
|
+
- `toy-demo@main`, the worktree path in short form, the chain
|
|
123
|
+
`fake-claude · running · fake-codex · pending`, and the lease `**`
|
|
124
|
+
- the elapsed clock, then **Pause**, **Hand off now**, **Reassign**, **Kill**
|
|
125
|
+
|
|
126
|
+
This is `demo-1-claude-running.png`.
|
|
127
|
+
|
|
128
|
+
### 5. The limit, and the bundle
|
|
129
|
+
|
|
130
|
+
Twenty seconds in, `fake-claude` prints the recorded limit line and exits 1.
|
|
131
|
+
Leg records `limit_detected`, writes the handoff bundle, and stops for the
|
|
132
|
+
approval you ticked. Within a second or two the row changes on its own:
|
|
133
|
+
|
|
134
|
+
- the line beside the region title reads `1 waiting on you`
|
|
135
|
+
- the status word is `needs approval` and the agent is now `fake-codex`
|
|
136
|
+
- the sentence is `waiting on you at station build since 11:56 PM`, with the
|
|
137
|
+
local time it stopped
|
|
138
|
+
- the chain reads `fake-claude · handed off · fake-codex · pending`
|
|
139
|
+
- the elapsed clock is `--:--` and the buttons are **Approve**, **Reassign**,
|
|
140
|
+
**Kill**
|
|
141
|
+
|
|
142
|
+
This is `demo-2-limit-hit.png`.
|
|
143
|
+
|
|
144
|
+
### 6. What Leg wrote
|
|
145
|
+
|
|
146
|
+
Click the card title. The detail opens in flow directly under the row, not as an
|
|
147
|
+
overlay, and the row it belongs to stays visible above it. It holds, in order,
|
|
148
|
+
**Task**, **Where**, **Pipeline**, **Runs**, **Timeline** and **Log**:
|
|
149
|
+
|
|
150
|
+
- **Where** prints the full paths, never shortened: `repo`, `trunk`,
|
|
151
|
+
`worktree`, `leases`, and `bundle`, which is the context handoff bundle
|
|
152
|
+
written for this leg, under `.context-handoffs` inside the worktree.
|
|
153
|
+
- **Runs** reads `run 1 fake-claude, limit, signal claude-session-limit, exit 1`.
|
|
154
|
+
`claude-session-limit` is the fixture the fake adapter replayed.
|
|
155
|
+
- **Timeline** is newest first: `approval_needed`, `handoff_written` with the
|
|
156
|
+
bundle id and `after limit; next: fake-codex`, `limit_detected`,
|
|
157
|
+
`leg_started`, `card_created`.
|
|
158
|
+
- **Log** is the last line `fake-claude` printed, which is the recorded Claude
|
|
159
|
+
shape:
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
{"type":"result","subtype":"error","is_error":true,"result":"You've hit your session limit","session_id":"sess-fake"}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
This is `demo-3-handoff-bundle.png`, framed on the bundle path, the run and the
|
|
166
|
+
timeline. Click the title again, or **Close** at the top of the detail, to
|
|
167
|
+
collapse it.
|
|
168
|
+
|
|
169
|
+
### 7. Approve, and leg 2
|
|
170
|
+
|
|
171
|
+
Click **Approve** on the row. `fake-codex` starts in the same worktree, reading
|
|
172
|
+
the bundle Leg wrote. The status word is `running` again, the run is `run=2`,
|
|
173
|
+
and the chain reads `fake-claude · handed off · fake-codex · running`. This is
|
|
174
|
+
`demo-4-codex-running.png`.
|
|
175
|
+
|
|
176
|
+
Twenty seconds later the station finishes. The line beside the region title
|
|
177
|
+
reads `1 finished`, the status word is `done`, the sentence is
|
|
178
|
+
`done after 2 runs, card done: all 1 station(s) complete`, the chain reads
|
|
179
|
+
`fake-claude · handed off · fake-codex · done`, and the buttons are **Rerun**
|
|
180
|
+
and **Remove**. This is `demo-5-done.png`.
|
|
181
|
+
|
|
182
|
+
The worktree named in **Where** now holds the file the fake agent wrote,
|
|
183
|
+
`hello-fake.txt`, and `.leg/DONE`.
|
|
184
|
+
|
|
185
|
+
### 8. Stop
|
|
186
|
+
|
|
187
|
+
Ctrl-C in the terminal running `npm start`. It prints `stopping (SIGINT)` and
|
|
188
|
+
`stopped`, and kills any agent still running. `leg down` from another terminal
|
|
189
|
+
does the same.
|
|
190
|
+
|
|
191
|
+
## The same demo headless
|
|
192
|
+
|
|
193
|
+
`card run` runs the card in the terminal you type it in and exits with its
|
|
194
|
+
status. No board is needed. Run these from the Leg checkout, in a shell with
|
|
195
|
+
`LEG_HOME` set the way step 2 sets it:
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
node bin/leg.mjs card add --repo "C:\baton-demo\toy-demo" --task "Add greeting.txt" --chain fake-claude,fake-codex --fake-mode "fake-claude=limit,fake-codex=success" --fake-target fake-codex=greeting.txt --title "Greeting file"
|
|
199
|
+
node bin/leg.mjs card run <card-id>
|
|
200
|
+
node bin/leg.mjs card events <card-id>
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
`card add` prints the card id, `card run` prints `<card-id> done at build`, and
|
|
204
|
+
`card events` prints eight lines: `card_created`, `leg_started`,
|
|
205
|
+
`limit_detected`, `handoff_written`, `leg_started`, `leg_exited`,
|
|
206
|
+
`station_done`, `done`.
|
|
207
|
+
|
|
208
|
+
Two differences from the click-through. There is no `--approve`, so the card
|
|
209
|
+
does not stop between the legs, and the ledger has no `approval_needed` or
|
|
210
|
+
`approved` in it: the ten-line ledger in `fixtures/demo/events.jsonl` is the
|
|
211
|
+
click-through. And the fake agent writes the file named by
|
|
212
|
+
`FAKE_TARGET`, default `hello-fake.txt`; it never reads the task, which is why
|
|
213
|
+
the headless replay passes `--fake-target fake-codex=greeting.txt` and the
|
|
214
|
+
click-through leaves `hello-fake.txt` in the worktree.
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# DEVIATIONS
|
|
2
|
+
|
|
3
|
+
One row per place Leg's shape differs from the ported source or from the
|
|
4
|
+
plan. Append-only. Newest at the bottom.
|
|
5
|
+
|
|
6
|
+
| date | file | ported shape | baton shape | why |
|
|
7
|
+
|------|------|--------------|-------------|-----|
|
|
8
|
+
| 2026-09-10 | src/handoff/bundle.mjs | context-handoff-bundle imported as a Python module | called as an argv subprocess (`context-handoff-bundle save/load/validate`) | Node vs Python; the bundle format stays the one format, Leg never re-implements the schema |
|
|
9
|
+
| 2026-09-10 | src/adapters/codex.mjs | runner spawns with stdin `pipe` and writes the prompt | `codex exec` spawns with stdin `ignore`, prompt in argv | plan-time probe: codex exec reads stdin when it is not a TTY and hung 170 s ("Reading additional input from stdin...") |
|
|
10
|
+
| 2026-09-10 | src/pipeline.mjs | fixed kanban columns (backlog / running / done) | configurable station pipeline; board columns derive from the config; presets factory / build / build-land | factory north star (Wes, 2026-09-10) |
|
|
11
|
+
| 2026-09-10 | src/scheduler.mjs | one running card at a time | scheduler runs N cards concurrently (`LEG_MAX_CONCURRENT`, default 2), each in its own worktree, with path leases; overlapping leases never start together | factory north star (Wes, 2026-09-10) |
|
|
12
|
+
| 2026-09-10 | src/ledger.mjs | events carry `from`/`to` agent names and `task_id` | events carry `actor` ({type:'agent', adapter, model?} \| {type:'human', id} \| {type:'baton'}), `card_id`, `station`, `leg` | factory north star (Wes, 2026-09-10): multiplayer-ready state from day one |
|
|
13
|
+
| 2026-09-10 | src/server.mjs | (no board in source) | the board reads the ledger on every request, never in-memory state; restart-persistence test | factory north star (Wes, 2026-09-10) |
|
|
14
|
+
| 2026-09-10 | src/server.mjs | (no server in source) | `LEG_BIND` (default 127.0.0.1) + `LEG_TOKEN` seams; binding off loopback without a token refuses to start | factory north star (Wes, 2026-09-10) |
|
|
15
|
+
| 2026-09-10 | src/land.mjs | (no landing in source; humans merged) | land station = merge queue: rebase onto trunk in the worktree, run the repo test command, ff-only trunk if green; red or conflict bounces the card to build with the failure in the bundle; `pr` is an opt-in land mode | factory north star (Wes, 2026-09-10): continuous landing |
|
|
16
|
+
| 2026-09-10 | src/server.mjs | (no board in source) | floor view: running cards, station, agent, leased paths, last event, trunk lane of the last hour | factory north star (Wes, 2026-09-10) |
|
|
17
|
+
| 2026-09-10 | docs/ROADMAP-v2.md | (n/a) | multi-human network access deferred to a later, security-reviewed phase; v1 is single-operator on loopback | factory north star (Wes, 2026-09-10): do not expose a merge queue to the LAN without a review |
|
|
18
|
+
| 2026-09-10 | src/runner.mjs | `findReportEvent` scans the lead's ledger file for a `done`/`result` event to decide "completed" | dropped; completion is the DONE-marker contract (`.leg/DONE` in the worktree) decided in phase 5; phase 2 records `outcome: null` | one CLI-agnostic completion rule instead of parsing each CLI's prose |
|
|
19
|
+
| 2026-09-10 | src/runner.mjs | `telegramTarget`, `sendTelegram`, `.env.handoff`, the sweep's delivery canary | removed; the notify timer writes a ledger `status` event only; the sweep keeps orphan detection | privacy rule: no Telegram delivery or chat ids in Leg |
|
|
20
|
+
| 2026-09-10 | src/runner.mjs | `LEAD_ALLOWED_TOOLS` hard-codes an allowlist that names the team bin scripts and MCPs | no allowlist by default; adapters pass only the mode named in the chain entry; the Agent tool is never disallowed | agents keep their own permission modes; a disallowed Agent tool degrades claude to solo (LESSONS 07-10) |
|
|
21
|
+
| 2026-09-10 | src/runner.mjs | `spawnClaude(model, effort, envelope)` with `--model`/`--effort` argv | `spawnAgent(adapter, opts)`; model/effort live in the chain entry, adapters build argv | one runner for four CLIs |
|
|
22
|
+
| 2026-09-10 | src/runner.mjs | env sanitizer deletes ANTHROPIC_API_KEY, CLAUDECODE, CLAUDE_CODE_*, CLAUDE_EFFORT, CLAUDE_PLUGIN_DATA | also deletes ANTHROPIC_AUTH_TOKEN, ANTHROPIC_BASE_URL, OPENAI_API_KEY; sets CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS=0 | hard constraint: subscription auth only; detached `claude -p` needs the ceiling at 0 |
|
|
23
|
+
| 2026-09-10 | src/ledger.mjs | `ORIGINS` (telegram \| claude-code) on every task | dropped; cards have no origin surface | no Telegram; the board is the only origin |
|
|
24
|
+
| 2026-09-10 | src/ledger.mjs | id format `team-<yyyymmdd>-<hhmm>-<slug>`, dir `tasks/<id>` | `card-<yyyymmdd>-<hhmm>-<slug>`, dir `cards/<id>`, `card.json` | naming |
|
|
25
|
+
| 2026-09-10 | src/git-snapshot.mjs | the hard-coded private-path constant and its `dirty-check` recommendation branch | removed; recommendation is `branch` when clean, `worktree` when dirty | private path; Leg always uses a worktree per card anyway |
|
|
26
|
+
| 2026-09-10 | src/sync/dashclaw.mjs | invoke-capability.mjs governed loop with a capability registry and global `fetch` | dropped except the argv-builder pattern and the fail-closed shape; sync uses native `http`/`https` | DashClaw capability registry is not Leg's concern; global fetch crashes Node 24 on Windows at exit (LESSONS 07-12) |
|
|
27
|
+
| 2026-09-10 | src/env.mjs | `spawnClaude` sanitized the child variables inline | `sanitizeEnv` lives in `src/env.mjs` and is re-exported from `src/runner.mjs`; adapters import it from env.mjs | an adapter importing it from the runner while the runner awaits `main()` is a top-level-await import cycle (Node exits 13 "unsettled top-level await") |
|
|
28
|
+
| 2026-09-10 | src/runner.mjs, src/ledger.mjs | CLI body runs at module top level | CLI body is `main()` behind an `isMain` guard so tests can import `sanitizeEnv`, `readEvents`, `parseActor` | importable and executable from one file |
|
|
29
|
+
| 2026-09-10 | src/runner.mjs | one `handoff/` dir per task with `handoff.json` and `handoff.prev.json` | numbered `runs/<n>/` per card; a stale active record (dead pid) is marked `orphaned` in place and run n+1 starts; exit 11 error is `run_already_active` | a card runs many legs; every leg keeps its own logs |
|
|
30
|
+
| 2026-09-10 | src/runner.mjs | supervisor appends a `result`/`error` event and sets the task `failed` on a dead lead | supervisor appends `leg_started` and `leg_exited` (exit code + scrubbed stderr tail) and never changes the card status | classification (completed / incomplete / limit / …) is the chain's job in phases 4–5; phase-2 spec's "result" event is not in the new EVENT_TYPES |
|
|
31
|
+
| 2026-09-10 | src/runner.mjs | `launch --task --envelope-file --model --effort --safe-mode` | `launch --card --adapter --prompt-file [--cwd] [--mode] [--max-turns] [--resume]`; supervise takes `--run <n>` | adapter/card vocabulary; the worktree is `--cwd` |
|
|
32
|
+
| 2026-09-10 | src/ledger.mjs | `create --slug --instruction --origin --lead` | `create --slug --task --repo --chain <json> [--title] [--actor <json>]`, default actor `{type:'human', id:'local'}`; `update` accepts `--status --session-id --station --leg` | card vocabulary; phase 5 adds pipeline/leases |
|
|
33
|
+
| 2026-09-10 | .gitattributes | (none) | `* text=auto eol=lf` | stop CRLF churn on a Windows checkout |
|
|
34
|
+
| 2026-09-10 | src/adapters/*.mjs | phase-3 spec shape: `argv() → string[]`, `parseResult({stdout,stderr,exitCode}) → {sessionId, lastMessage, stopReason}` | `argv() → {bin, args}`, `parseResult(text) → {session_id, last_message, stop_reason, raw}` | one spawn shape for native exes and `node <entry>`; snake_case matches the ledger vocabulary |
|
|
35
|
+
| 2026-09-10 | src/adapters/gemini.mjs | argv from the spec: `-p <prompt> -o json --approval-mode <mode>` | adds `--skip-trust` | headless gemini in an untrusted folder forces approval mode `default` and exits 55 (fixtures/live/gemini/attempt-1-untrusted.err.log); workspace trust is not a permission bypass |
|
|
36
|
+
| 2026-09-10 | src/adapters/agy.mjs | argv from the spec: `-p <prompt> --output-format json --mode <mode> --print-timeout <kill>` | adds `--add-dir <worktree>` and a "Working directory: <worktree>" prompt preamble | agy ignores the process cwd and wrote into its own scratch workspace (fixtures/live/agy/attempt-1-scratch-workspace.out.log) |
|
|
37
|
+
| 2026-09-10 | src/adapters/index.mjs | plan: grok adapter only if the probe writes the file | grok.mjs written from `--help` and unit-tested, NOT registered | the build machine has no grok login: the probe printed a device-code prompt and exited "Cancelled" with no file |
|
|
38
|
+
| 2026-09-10 | src/adapters/gemini.mjs | plan: four live-verified adapters | gemini registered but not verified live | the machine's Gemini login is an ineligible tier ("migrate to the Antigravity suite"); agy is the verified successor; a gemini leg on this machine is an `auth_failed` leg |
|
|
39
|
+
| 2026-09-10 | src/adapters/claude.mjs | PROTOCOL rule: every `claude -p` passes an explicit `--model` | `--model` only when the chain entry names one | Leg has no house model; the contract documents that the account default answered the probe so cards can choose |
|
|
40
|
+
| 2026-09-10 | src/runner.mjs | supervisor spawns whatever argv the adapter returns | `adapter.argv()` throwing `forbidden flag` makes the supervisor record an `error` event, mark the run `failed` with the refusal text, and exit 13 without spawning | fail closed on YOLO flags from a chain entry |
|
|
41
|
+
| 2026-09-10 | src/git-snapshot.mjs | `git()` helper spawned with the inherited variables (phase-2 port was byte-identical) | the helper sets `MSYS_NO_PATHCONV=1` | LESSONS 07-09 became a named test (`msys-no-pathconv`) that requires it on every git spawn in src/ |
|
|
42
|
+
| 2026-09-10 | src/runner.mjs | phase-2: supervisor records `outcome: null`, exits 0 on child exit 0 | supervisor removes a stale `.leg/DONE` before spawning, records HEAD at start, and on exit calls `classify()`; writes outcome/signal/handoff/reason/done_marker/diff into run.json; exits 0 only for `completed` (12 killed/stalled, 13 otherwise); ledger event `limit_detected` / `error` (auth_failed, launch_failed) / `killed` / `leg_exited` | phase-4 spec wiring; the DONE-marker contract needs a fresh marker per leg |
|
|
43
|
+
| 2026-09-10 | src/limits.mjs | phase-4 spec: "non-zero exit → failed (handoff: true when the chain has a next leg)" | `classify` returns `handoff: true` for failed; the chain (phase 5) decides whether a next leg exists | the classifier does not know the chain |
|
|
44
|
+
| 2026-09-10 | src/limits.mjs | (spec left it implicit) | `auth_failed` and `killed` are the only non-completed outcomes with `handoff: false`; `launch_failed` hands off (the next CLI may launch fine) | spec's `auth-source-is-launch-failure` test requires handoff false for auth; a gemini "ineligible tier" leg therefore stops the chain until a human reassigns (board button, phase 6) |
|
|
45
|
+
| 2026-09-10 | src/adapters/index.mjs | one `fake` entry | `fake-claude`, `fake-codex` (stdin ignore), `fake-nostdin` aliases built by `makeFake(name, stdin)` | demo events read like a real chain; the stdin-ignore path gets a behavioural test |
|
|
46
|
+
| 2026-09-10 | fixtures/limits/claude/claude-model-limit.json | pattern `You.ve hit your <word> limit` | negative lookahead for session / weekly / usage | the model-limit pattern swallowed the session and weekly fixtures (caught by the per-fixture tests) |
|
|
47
|
+
| 2026-09-10 | context-handoff-bundle (upstream checkout) | PyPI 0.4.0 has no `--version` flag | `--version` (argparse `action="version"`) + `tests/test_version.py` added in the local checkout and installed with `pip install -e`; Leg's `chbVersion()` falls back to `importlib.metadata` so a plain PyPI install still works | the phase-5 mandatory command is `context-handoff-bundle --version`; not pushed or released (hard stop), sits uncommitted next to the checkout's own WIP |
|
|
48
|
+
| 2026-09-10 | src/ledger.mjs | `create --chain` only | `create` also takes `--pipeline --leases --trunk --land-mode --test-command --title`; `update --patch <json>` with an allowlist (`PATCHABLE`); new `log` subcommand for non-card events (`$LEG_HOME/events-<actor-key>.jsonl`) | the ledger stays the only writer while the orchestrator and scheduler need card fields and scheduler_started/stopped |
|
|
49
|
+
| 2026-09-10 | src/chain.mjs | spec: "leg_resumed" / status events on enqueue and start | no ledger event for enqueue, start, or bundle_written (the status change is the record; `handoff_written` and the next `leg_started` already name the adapters) | keeps the e2e event sequence exactly card_created, leg_started, limit_detected, handoff_written, leg_started, leg_exited, station_done, done |
|
|
50
|
+
| 2026-09-10 | src/chain.mjs | spec: "last station done → done" | `station_done` is emitted for the last station too, then `done` | the spec's own e2e sequence lists station_done before done for a one-station pipeline |
|
|
51
|
+
| 2026-09-10 | src/runner.mjs | supervise inherits the launcher's variables only | `launch --env-json '{…}'` forwards extra child variables (FAKE_MODE, FAKE_LIMIT_FIXTURE) through the detached supervisor; adapters still sanitize | two fake cards with different modes in one scheduler process |
|
|
52
|
+
| 2026-09-10 | src/orchestrator.mjs | spec: `runner.mjs launch` then wait; handoff after every `handoff:true` | also: a human `kill`/`pause` while a leg runs is honoured when the leg exits (the orchestrator re-reads card.json and stops instead of applying the leg result); `pause` writes a bundle | the board's Pause/Kill buttons (phase 6) need a seam that does not race the supervisor |
|
|
53
|
+
| 2026-09-10 | src/orchestrator.mjs | test command through a shell | `resolveCommand()` tokenizes the command and maps `npm`/`npx`/`node` to `node <npm-cli.js>`; no shell ever | LESSONS 07-11 (no .cmd shims, no shell) |
|
|
54
|
+
| 2026-09-10 | bin/leg.mjs | spec: `card add … card run` | adds `card queue <id>` / `--queue` (enqueue for the scheduler) and `--fake-mode adapter=mode`, `--fake-fixture adapter=id` for demos | the scheduler only picks queued cards; the demo needs per-adapter fake modes from the CLI |
|
|
55
|
+
| 2026-09-10 | src/handoff.mjs | spec: notes with four sections Task / Done so far / Diff / Open findings | the notes use the bundle CLI's own five headings (Scope, Projects mentioned, Findings, Opportunities, Open questions, Evidence anchors) and carry the four Leg parts as labelled lines under them | `parse_notes` drops bullets under unknown headings; one format, no fork |
|
|
56
|
+
| 2026-09-10 | src/server.mjs | spec: SSE `log` frames throttled 4/s | no `log` frames; the board fetches `/api/cards/:id/log` on demand and on each `event` frame | log files are large and change every second; polling the ledger every 500 ms already drives `card`/`event` frames |
|
|
57
|
+
| 2026-09-10 | src/server.mjs | spec: `POST /api/cards/:id/(run…)`, run starts the card | `run` (and `queue`) = enqueue; the in-process scheduler starts it under leases within one tick | starting a card outside the scheduler would bypass the lease check |
|
|
58
|
+
| 2026-09-10 | src/auth.mjs | spec: bearer header only | `/api/events` also accepts `?token=` | EventSource cannot set headers |
|
|
59
|
+
| 2026-09-10 | src/server.mjs | spec: static routes `/board.css`, `/board.js`, `/floor.js` | any file under src/board/ is served (path-traversal guarded); `/` and `/floor` map to the two pages | one guard, no route list to maintain |
|
|
60
|
+
| 2026-09-10 | src/cards.mjs | card creation logic lived in bin/leg.mjs | `createCard()` shared by the CLI and `POST /api/cards`; errors are `CardInputError` → exit 2 / HTTP 400 | one validation path for both surfaces |
|
|
61
|
+
| 2026-09-10 | src/mergequeue.mjs | spec: rebase the card branch (agents' work assumed committed) | step 1b commits the worktree first (`baton: <title>`, `--no-verify` unless `LEG_COMMIT_VERIFY=1`) | agents leave uncommitted work; a rebase refuses a dirty tree; a global wire-dark commit hook on the build machine rejected the toy commit, and the repo's test command is Leg's gate, not commit hooks |
|
|
62
|
+
| 2026-09-10 | src/commands.mjs | test commands ran with the inherited variables | `NODE_TEST*` variables are stripped from a repo command's child | a nested `node --test` inherits NODE_TEST_CONTEXT from Leg's own test runner and reports green to the parent instead of exiting red (caught by the land tests) |
|
|
63
|
+
| 2026-09-10 | src/chain.mjs | spec: test-station red bounces without a cap; land bounces capped at 3 | test and land bounces share `land_attempts` and the same cap (`LEG_MAX_LAND_ATTEMPTS`, default 3) → failed | a card that never goes green looped build → test → build forever in the first e2e run |
|
|
64
|
+
| 2026-09-10 | src/chain.mjs | bounce keeps `resume_from_bundle` false | every bounce (test or land) sets `resume_from_bundle` so the next build leg's prompt starts with the bounce bundle's resume | the failure has to reach the agent, not just the ledger |
|
|
65
|
+
| 2026-09-10 | src/*.mjs, bin/*.mjs | spawns without `windowsHide` | every spawn/spawnSync/execFileSync carries `windowsHide: true` | grandchildren of the detached supervisor opened a console window each on Windows (Wes saw terminals flashing during the test suite) |
|
|
66
|
+
| 2026-09-10 | src/orchestrator.mjs | `waitForRun` polled forever | a run whose supervisor pid is dead for 5 s is marked `orphaned` and treated as a failed leg | a crashed supervisor must not hang the orchestrator |
|
|
67
|
+
| 2026-09-10 | bin/fake-agent.mjs | fake modes were per adapter | `fakeMode: "a;b;c"` is consumed per bounce attempt (index = `land_attempts`); new modes break-test / fix-test / resolve-rebase; `fakeTarget` / `fakeContent` per entry | the 3-card landing demo needs a first run that fails and a second that fixes |
|
|
68
|
+
| 2026-09-10 | src/scheduler.mjs | spec: "a repo that is landing is busy for other land requests only" was implemented as busy for every queued card | only queued cards whose current station is `land` wait for a landing repo | spec wording; build stations keep running |
|
|
69
|
+
| 2026-09-10 | src/redact.mjs | source kept two secret-pattern lists on purpose (runner rewrites, ledger refuses) | one list in src/redact.mjs; runner re-exports `scrub`, ledger imports `SECRET_PATTERNS`; `redact()` also removes values the launcher holds for the well-known key variables | one place to extend; adds sk-ant-, ghs_, github_pat_ |
|
|
70
|
+
| 2026-09-10 | src/ledger.mjs, src/runner.mjs | `writeFileSync` of card.json / run.json | write-then-rename with retry on EPERM/EBUSY and a direct-write fallback (src/fsx.mjs) | the concurrency test read a half-written card.json under load once; then a plain rename failed on Windows while the orchestrator had run.json open, the supervisor lost its final `exited` record and the leg read as orphaned (land test, twice under full-suite load) |
|
|
71
|
+
| 2026-09-10 | src/launcher.mjs | spec: `.env` loaded by `node --env-file=.env` | `node --env-file-if-exists=.env` in the npm scripts (prints a one-line notice when absent); the launcher never reads the file itself | `--env-file` fails when the file is missing |
|
|
72
|
+
| 2026-09-10 | src/launcher.mjs | (none) | `LEG_SERVER_SCRIPT` test seam replaces the server child (used by the redaction test with a stub that prints a fake key) | the redaction of child output needs a child that leaks on purpose |
|
|
73
|
+
| 2026-09-10 | src/launcher.mjs | spec: `[sync:workboard]` / `[sync:dashclaw]` children | syncs run inside the ledger process on every write (src/sync/index.mjs `notify`), not as launcher children; the launcher only reports on/off | the ledger is the single writer and already sees every event; a separate process would have to tail files |
|
|
74
|
+
| 2026-09-10 | src/ledger.mjs | ported DashClaw sync posted to `/api/team-tasks` whenever DASHCLAW_URL + key were set | `POST /api/actions` with DashClaw's ACTION_RECORD_SCHEMA fields (app/lib/validate.js), only when `LEG_SYNC_DASHCLAW=1`; buffered records replay through `ledger sync` | the brief: off by default; the team-tasks route was the private protocol's page |
|
|
75
|
+
| 2026-09-10 | docs/DEMO.md | spec: demo card created from the board form with fakeMode per leg; toy-demo worktree has greeting.txt | the form gained a per-row "fake mode" field (fake adapters only) but no fake-target field; the fake agent writes `hello-fake.txt` regardless of the task, so the board demo's worktree holds hello-fake.txt + DONE and the headless replay passes `--fake-target` | keeping the form free of demo-only knobs; the fake never reads the task |
|
|
76
|
+
| 2026-09-10 | docs/DEMO.md | spec: five states in one continuous run | leg 2 carries `approve: true` so the card waits in needs_approval between the handoff and the codex leg | the fake legs finish in ~15 s each; without a pause the limit/handoff state is on screen for under two seconds |
|
|
77
|
+
| 2026-09-10 | fixtures/limits/claude/claude-max-turns.json | docs-only | observed-live from the real run: `subtype error_max_turns`, `terminal_reason max_turns`, exit 1 | the real claude leg hit the turn cap on purpose |
|
|
78
|
+
| 2026-09-10 | src/sync/workboard.mjs | spec: cache the verb list from `openclaw workboard --help` | the plugin is disabled on the build machine (verbatim message in fixtures/sync/workboard-help.txt); verbs are a small table tested against an argv stub; the unavailable answer is recorded once per LEG_HOME | OpenClaw config is Wes's; not touched (mtime unchanged before/after) |
|
|
79
|
+
| 2026-09-11 | src/runner.mjs | spawn error wrote run.json `status: failed` with no outcome | the supervisor classifies a spawn error (`launch_failed`, handoff) before exiting | phase 12 states pass: a missing CLI left the card in `running` forever because the orchestrator had no verdict to apply |
|
|
80
|
+
| 2026-09-11 | src/orchestrator.mjs, src/scheduler.mjs | a `running` card always launched a new leg; nobody re-read a finished run | run.json carries `driver_pid` and `settled_at`; runCard re-attaches to an unsettled run, the scheduler re-attaches only when the driver pid is dead, a second `card run` on a live driver refuses | `leg down` kills agents and the server together, so the supervisor's verdict was never applied; two live drivers must never apply one verdict |
|
|
81
|
+
| 2026-09-11 | src/board/board.js | "blocked by lease" chip for every `blocked_by` event | "blocked by lease" only when the summary starts with `blocked by`; "blocked: concurrency cap 2" otherwise; a separate "bounced: <reason> (attempt n)" chip while the card is queued, running or handing off; no station chip for `-` | phase 12 states pass on the board: the concurrency cap read as a lease, the bounce reason vanished once the retry leg started |
|
|
82
|
+
| 2026-09-11 | src/cards.mjs | repo check: `existsSync` only | must be a directory, a git repository root (rev-parse --show-toplevel), and neither LEG_HOME nor a parent of it | phase 12 security pass: an agent must never get a worktree that contains the ledger |
|
|
83
|
+
| 2026-09-11 | src/server.mjs | SSE fan-out polled `listCards()` every 500 ms and re-read every card | `fs.watch` on `cards/` re-reads only the changed card (30 ms debounce per id); log tail reads the last 64 KB with fstat + one read | phase 12 perf pass; the health heartbeat is its own 10 s timer |
|
|
84
|
+
| 2026-09-11 | src/board/index.html, board.css | custom-pipeline textarea had no accessible name; running-leg pulse always on | `aria-label` on the textarea; `prefers-reduced-motion` disables the pulse and transitions; test/board-a11y.test.mjs computes contrast (all chips ≥ 4.7:1) | phase 12 a11y pass |
|
|
85
|
+
| 2026-09-11 | src/stations/{agent,test,human}.mjs | plan (phase 5): one module per station kind | built inline in runCard until the final audit; audit fix 1 moved the three kind branches into src/stations/*.mjs (`run({ id, card, station, worktree, actor, ops })`), the orchestrator dispatches by kind and passes its helpers as `ops`; land stays inline because it drives the merge queue | the audit's deliverable check found the three files missing and no DEVIATIONS row explained it |
|
|
86
|
+
|
|
87
|
+
## 2026-09-11: the terminal-first entry (0.2.0)
|
|
88
|
+
|
|
89
|
+
0.2.0 made `leg claude|codex|agy` the way in. These are the places the built
|
|
90
|
+
shape differs from what the plan assumed. Same columns as the table above.
|
|
91
|
+
|
|
92
|
+
| date | file | planned shape | baton shape | why |
|
|
93
|
+
|------|------|---------------|-------------|-----|
|
|
94
|
+
| 2026-09-11 | bin/leg.mjs, src/attach.mjs | the board's New card form is the way in; an agent only ever runs headless in a worktree | `baton <agent> [args…]` runs the real interactive CLI in this terminal with stdio inherited, and the pipeline becomes an extra below the Terminals lane | a form is a detour when the agent is already the thing you type; the pipeline shape is unchanged, it is just no longer first |
|
|
95
|
+
| 2026-09-11 | src/taps/claude-usage.mjs | read the 5h/7d percentages from Claude Code's status-line JSON (`rate_limits.five_hour.used_percentage`) via a `statusLine` command in the per-session `--settings` file | poll `GET https://api.anthropic.com/api/oauth/usage` with the OAuth token Claude Code stored, header `anthropic-beta: oauth-2025-04-20`, every 60 s | Claude Code 2.1.268 does not run a custom `statusLine` from `--settings` or from a project `.claude/settings.local.json`: an `echo` command at both levels left the built-in status line in place, while hooks from the same `--settings` file fired. The `statusLine` entry is still written, so the endpoint poll becomes the fallback if a later build honours it |
|
|
96
|
+
| 2026-09-11 | src/taps/codex.mjs | inject a hook into codex the way Leg does for claude | no hook is injected; Leg finds the session's rollout file by `session_meta.cwd` and tails it for `token_count.rate_limits` and `task_complete.error` | codex shows a hooks-review prompt for a new hook, so a hook per session would mean a prompt per session; the rollout is flushed per event and already carries the rate limits, the prompts and the apply_patch file lists |
|
|
97
|
+
| 2026-09-11 | src/taps/agy.mjs | every agent exposes a usage percentage | agy exposes none; Leg passes `--log-file` per session and watches for `RESOURCE_EXHAUSTED` / "it resets in" / "out of quota", and the board shows "no % from agy" | agy 1.2.0 is a closed Go binary whose own status line fetches a quota summary from the backend and writes it nowhere on disk. The log strings are present in `agy.exe` but were never hit live, so they stay docs-only |
|
|
98
|
+
| 2026-09-11 | src/accounts.mjs | a config-dir override per agent, so every agent supports extra logins | claude (`CLAUDE_CONFIG_DIR`) and codex (`CODEX_HOME`) only; `accounts add agy` is refused | agy 1.2.0 has no config-directory override, so `LAYOUT.agy.env` is `null` |
|
|
99
|
+
| 2026-09-11 | src/adapters/, fixtures/limits/, test/ | four registered adapters: claude, codex, gemini, agy | gemini removed from the code, the fixtures and the tests; the real adapters are claude, codex and agy | Google retired Gemini CLI in favour of Antigravity's `agy`; the 2026-09-10 probe already returned `IneligibleTierError` pointing at Antigravity, so the adapter could never run |
|
|
100
|
+
|
|
101
|
+
## 2026-09-11: closing the gaps against the two tweets (0.3.0)
|
|
102
|
+
|
|
103
|
+
| date | file | planned shape | baton shape | why |
|
|
104
|
+
|------|------|---------------|-------------|-----|
|
|
105
|
+
| 2026-09-11 | bin/leg.mjs `simulate-limit` | "sends a real StopFailure payload with error rate_limit through src/hook.mjs" | the payload carries `baton_simulated: true`; the hook records the wall with a 2-minute reset instead of the default 5 h, never keeps it as live evidence, and the event says `(simulated)`; agy gets the RESOURCE_EXHAUSTED line appended to its Leg-owned session log; codex is refused (its wall lives in a rollout file Leg never writes) with `leg sessions handoff` as the alternative | a test must never wall the real claude login for five hours, and a simulated payload must never flip a docs row to observed-live |
|
|
106
|
+
| 2026-09-11 | src/live-capture.mjs | "save that payload under fixtures/live/" | `fixtures/live/<agent>/limit-<signal>.json` only in a dev clone (the checkout has `.git` and `fixtures/live/`), else `~/.leg/live/`; `LEG_LIVE_DIR` overrides; first arrival per (agent, signal) wins; docs rows flip through `scripts/live-limits.mjs` markers (`<!-- live:<agent>/<signal> -->`) in the same call | an installed package has no writable fixtures directory, and a doc row must flip from evidence on disk, not from memory |
|
|
107
|
+
| 2026-09-11 | src/attach.mjs `spawnSpec` | the next agent starts with no args | `BATON_<AGENT>_ARGS` (space-separated) is prepended to a leg Leg starts itself after a hand-off; the user's own `baton <agent> …` args still never carry over | the live test had to keep the handed-off leg on a cheap model, and a user has the same need for a chain |
|
|
108
|
+
| 2026-09-11 | src/taps/codex.mjs | the first `role: user` message is the task | a message starting `# AGENTS.md instructions` is skipped like `<environment_context>` | observed live 2026-09-11: codex prepends the AGENTS.md block as the first user message of every rollout, so the card's task read as the instructions file |
|
|
109
|
+
| 2026-09-11 | src/attach.mjs, src/wait.mjs | all out: print the reset times and exit 3 | status `waiting` with `session.waiting = { agent, account, resets_at, since }`; a one-line countdown on stderr (rewritten in place on a TTY, once a minute otherwise); at the reset the chooser runs again and the first option back starts from the bundle; Ctrl-C or a board End quits with exit 3 and an `ended` event "quit while waiting" | the terminal is where the work is; quitting hands the human a restart to type at the reset time |
|
|
110
|
+
| 2026-09-11 | src/attach.mjs `spawnSpec` | `BATON_<AGENT>_BIN` pointing at a `.mjs` was resolved `viaNode` with a null entry, so the spawn was `node` with no script | `viaNode` pushes `entry ?? bin` | the stub-agent e2e (test/attach-e2e.test.mjs) is the first thing to run an interactive leg through a `.mjs` override |
|
|
111
|
+
| 2026-09-11 | src/attach.mjs `ensureBoard` | the board is always started | `LEG_NO_BOARD=1` skips it | the e2e runs `leg claude` in the test suite without touching port 4747 |
|
|
112
|
+
| 2026-09-11 | src/attach.mjs `isolate` | "when a second live session starts in a repo that already has one, give it its own git worktree and branch" | the test is "another live session's working tree is this checkout" (`workRoot`: its worktree, else its repo), so a session started after the first one ended, or in a checkout nobody else is using, still works where it starts; the branch is cut from whatever the checkout has out; a detached HEAD gets a worktree but no Land | a collision is two agents in one working tree; two branches of one repo in two worktrees do not collide until they land |
|
|
113
|
+
| 2026-09-11 | src/land.mjs, src/server.mjs `POST /api/sessions/:id/land` | "a Land button on terminal cards that sends the session's branch through the existing merge queue" | Land exists only on a card with its own worktree (the first session's branch is the checkout's own, already on trunk); the queue runs in the board server with `allowDirtyRoot`: local changes in the checkout are allowed, and a fast-forward that would overwrite one bounces `dirty-trunk` naming the files; the queue's test run is async (`runCommandAsync`); `land.json` per session is written by the server only (the runner owns `session.json`); `landings.jsonl` keeps who landed what after a session is removed | cards keep the strict clean-root rule, but a live terminal's checkout is almost never clean, so the strict rule would bounce every Land; a `spawnSync` test run inside the board server would freeze every board for up to ten minutes |
|
|
114
|
+
| 2026-09-11 | src/server.mjs `DELETE /api/sessions/:id` | forget the session record | also removes the session's worktree and branch when the worktree is clean and the branch is already on its base; otherwise both stay and the response says why | Remove must never lose unlanded work, and must not leave a worktree behind for work that is already in |
|
|
115
|
+
| 2026-09-11 | src/worktree.mjs `ensureExcludeEntries`, src/attach.mjs `gitInfo` | exclude `.leg-worktrees/` and `.leg/` | also `.context-handoffs/` and `.dashclaw-local/`, applied again right before every Land; `gitInfo` leaves `.dashclaw-local/` out of `files_dirty`, and its porcelain output is `trimEnd`-ed, never `trim`-ed | observed live 2026-09-11: a DashClaw hook writes `.dashclaw-local/` into every directory an agent runs in, so three live sessions were all flagged as overlapping on it and a Land would have committed it; the leading space of ` M README.md` was trimmed and the card showed `EADME.md` |
|
|
116
|
+
|
|
117
|
+
## 2026-09-11: more than one human (0.3.0)
|
|
118
|
+
|
|
119
|
+
| date | file | planned shape | baton shape | why |
|
|
120
|
+
|------|------|---------------|-------------|-----|
|
|
121
|
+
| 2026-09-11 | src/share.mjs | "binds the board to the LAN or Tailscale address only when a token is set, and gives each human their own token and name" | `share.json` is the switch: on, a bind address, and at least one person, or the board stays on loopback; a token is stored as a sha256 hash and printed once (`leg share rotate <name>` issues a new one); a browser on the board's own machine is the owner with no token (`loopback_owner: false` asks for one even there); `LEG_TOKEN` is ignored while share is on | a link has to be revocable without ever being readable again, and the owner's own browser should not need a token to look at their own machine |
|
|
122
|
+
| 2026-09-11 | src/server.mjs `redactSession` | "no terminal output or bundle contents visible to non-owners" | a guest's card keeps agent, account, status, usage, owner, branch, worktree branch and land state, and drops task, cwd, repo path, file names, limit text, bundle, argv, chain, events and logs; the event stream is computed per viewer, and pipeline cards, floor, leases, presets and adapters are 403 for a guest | a guest needs to see that a terminal is alive and how close it is to its wall; nothing else on that machine is theirs |
|
|
123
|
+
| 2026-09-11 | src/ratelimit.mjs | "rate limits" | two fixed windows: 600 requests a minute per human (per address when nobody is named) and 20 wrong tokens a minute per address, after which that address waits the window out; a request with no token at all is never counted as a guess | the board polls itself, so the limit must not throttle its own owner, and a missing token is usually a browser that has not been given a link yet |
|
|
124
|
+
| 2026-09-11 | `POST /api/sessions/:id/request-handoff` | "a request handoff that the owner has to approve" | the request is a server-owned `requests.json` per session (the runner owns `session.json`), the owner approves on the card, and the runner is told `by: "<owner> for <guest>"` | the event trail has to name both humans, and a board write must never race a runner write |
|
|
125
|
+
| 2026-09-11 | src/attach.mjs `ensureBoard` | a health probe finds the board | 401 counts as "a board is there": with share on even `/api/health` needs a token | observed live 2026-09-11: the probe read its own guarded board as down, said so, and left the pidfile unwritten |
|
|
126
|
+
|
|
127
|
+
## 2026-09-11: security and correctness review pass (0.3.1)
|
|
128
|
+
|
|
129
|
+
One row per place a fix changed the built shape. All applied after an adversarial
|
|
130
|
+
review of the 0.3.0 body of work; every change is covered by the test suite.
|
|
131
|
+
|
|
132
|
+
| date | file | old shape | new shape | why |
|
|
133
|
+
|------|------|-----------|-----------|-----|
|
|
134
|
+
| 2026-09-11 | src/server.mjs | `share` snapshotted once at construction, closed over by authorize/viewFor | the real server (no explicit `share`) re-reads share.json per request, mtime-cached; an explicit `share` (tests) stays fixed | `share add/rotate/rm` did nothing until a board restart: a removed guest kept full access, a rotated link was inverted, a new link 401'd |
|
|
135
|
+
| 2026-09-11 | src/server.mjs createSse | one module-level `sig` Map, plain-object SSE broadcasts | per-client `sig`; every broadcast re-resolves the viewer from the live roster and drops a revoked one; card/removed broadcasts wrapped in the owner-only filter | a second SSE client starved the first of ledger events; three raw broadcasts pushed the owner's card (title, prompt, repo path) to a guest's stream |
|
|
136
|
+
| 2026-09-11 | src/server.mjs | guest deny-list omitted `trunk`; no Origin check | `trunk` added to the deny-list; a cross-origin state-changing request is 403 | a guest read the whole pipeline through /api/trunk; a page the owner visited could drive the loopback board |
|
|
137
|
+
| 2026-09-11 | src/server.mjs start | one listener on the bind address | when the bind is non-loopback (share on), also listen on 127.0.0.1 | the owner's own browser had no tokenless URL with a Tailscale bind; a real remote peer is never loopback and still needs a token |
|
|
138
|
+
| 2026-09-11 | src/share.mjs turnOn | first person became owner-of-record regardless of role | promotes an owner-role person (or creates one); rejects the reserved name `local` | `share add sam` before `share on` made a guest the owner of every terminal on the machine |
|
|
139
|
+
| 2026-09-11 | src/sessions.mjs updateSession | read-modify-write with no lock; `patch` object only | cross-process file lock (src/fsx.mjs withFileLock) + a reducer `patch(cur)` form; taps/claude.mjs uses it | concurrent hook/tap/poller processes lost files_touched, turns, and a limit to last-writer-wins races |
|
|
140
|
+
| 2026-09-11 | src/sessions.mjs | pidAlive treated every kill(pid,0) error as dead; requestControl replaced control.json | EPERM counts as alive; requestControl merges | an elevated runner was marked lost and its dir removed under it; an End then a handoff (or two board commands) dropped the first |
|
|
141
|
+
| 2026-09-11 | src/usage.mjs markLimited | soonest known window reset | the window that actually walled (highest used %); read-modify-write locked | a weekly wall was recorded as a 5-hour one, so the account was handed back and re-walled |
|
|
142
|
+
| 2026-09-11 | src/mergequeue.mjs | committed and rebased whatever state the worktree was in | refuses a worktree with a rebase/merge already in progress (`worktree-busy`); the bookkeeping promise can no longer reject | Land on a worktree left mid-rebase committed the conflict then aborted it, deleting the human's work; a git failure during Land crashed the board process |
|
|
143
|
+
| 2026-09-11 | src/worktree.mjs | `.git/info` assumed a directory; `branch -D`; a stale worktree entry trusted; excluded dirs only | git-common-dir for linked worktrees; `branch -d` then `-D` only with force; prune a stale entry; `.env`/`.env.*` excluded | a linked-worktree checkout orphaned the worktree; an unmerged branch was force-deleted; a deleted worktree dir launched an agent in a missing cwd; a Land could commit a .env |
|
|
144
|
+
| 2026-09-11 | src/land.mjs, src/server.mjs, bin/leg.mjs | Remove/`sessions rm` dropped the record even when the worktree could not be pruned; squash-merges never pruned | Remove is 409 unless ?force; the CLI prunes like the board; a squash/cherry-merged branch counts as landed | unlanded commits were orphaned with no surface left to clean them |
|
|
145
|
+
| 2026-09-11 | src/attach.mjs | chooseNext never told what is installed; all-out ignored the current agent; announce-then-exit at leg 6; session record after the worktree | installed map passed to chooseNext; the current agent's own reset is in the all-out wait; a 12-leg cap that stops explicitly; the record is written before the worktree | a handoff to an uninstalled agent killed the session exit 127; the wait counted to the wrong reset; the 6th leg announced a next agent then exited 0 without it; a crash mid-`worktree add` orphaned the worktree |
|
|
146
|
+
| 2026-09-11 | src/commands.mjs | runCommandAsync resolved on 'close' only; child.kill on timeout | resolves on 'exit' too; kills the whole tree on timeout | a test whose grandchild held the stdio pipes left the card in 'landing' forever and wedged the repo's merge queue |
|
|
147
|
+
| 2026-09-11 | src/live-capture.mjs | scrub() (secret shapes only) | redact() plus home/repo path folding to `[redacted-home]`/`[redacted-repo]` | a real captured limit wrote the operator's home path, repo path and any secret outside the ten patterns into a git-tracked file |
|
|
148
|
+
| 2026-09-11 | src/bundle.mjs | one RESUME.md per checkout | RESUME-<session-id>.md per session, RESUME.md kept as a copy | two sessions sharing a checkout overwrote each other's handoff |
|
|
149
|
+
| 2026-09-11 | src/env.mjs | stripped Anthropic/OpenAI keys only | also GEMINI_API_KEY, GOOGLE_API_KEY, GOOGLE_GEMINI_BASE_URL and the Google Vertex/genai vars, and ANTHROPIC_CUSTOM_HEADERS | the agy leg kept the Google credentials, the exact leak the strip exists to prevent |
|
|
150
|
+
|
|
151
|
+
## 2026-09-11, second adversarial review (whole tree)
|
|
152
|
+
|
|
153
|
+
49 confirmed findings fixed, each with a regression test seen red first; 340
|
|
154
|
+
tests. Rows for the shape changes a later reader would otherwise wonder about.
|
|
155
|
+
|
|
156
|
+
| date | file | old shape | new shape | why |
|
|
157
|
+
|------|------|-----------|-----------|-----|
|
|
158
|
+
| 2026-09-11 | src/orchestrator.mjs | a card was owned by whoever read `queued` first; `driver_pid` written after `runner launch` returned | `driver.lock` created atomically at the top of `runCard` and released on return; `runner launch --driver-pid` writes run.json before spawning the supervisor | between `queued` and run.json a `card run` beside the scheduler launched a second leg into the same worktree, or failed the card while the winner's leg ran unapplied |
|
|
159
|
+
| 2026-09-11 | src/orchestrator.mjs waitForRun, src/stations/agent.mjs | early exit on `killed`/`paused` only; the station applied `leg_result` to whatever status it found | every non-`running` status interrupts, after the supervisor has finished writing; the station acts on the fresh status (bundle + `bundle_written` for `handing_off`, continue for `queued`) | Hand off now and Reassign on a live leg threw IllegalTransition out of runCard: no bundle, card wedged in `handing_off`, one concurrency slot gone for good |
|
|
160
|
+
| 2026-09-11 | src/orchestrator.mjs killActiveRun | killed `agent_pid` only | a `launching` run (no agent yet) has its supervisor tree killed | Kill in the first few hundred ms killed nothing; the agent then ran the whole leg under a card the board showed as killed |
|
|
161
|
+
| 2026-09-11 | src/orchestrator.mjs, src/stations/test.mjs | `runCommand` (spawnSync) | `runCommandAsync` | the test station froze the whole board server (HTTP, SSE, scheduler, merge queue) for the length of the repo's suite |
|
|
162
|
+
| 2026-09-11 | src/runner.mjs, src/orchestrator.mjs | four run.json writers, three of them plain `writeFileSync`, no lock | `updateRun` = `withFileLock` + retry-on-torn-read + `writeJsonAtomic`, used by all | the supervisor's `running` write raced the orchestrator's `driver_pid` patch; last writer won and either the driver or the agent pid was lost |
|
|
163
|
+
| 2026-09-11 | src/ledger.mjs update | read-modify-write with no lock ("the ONLY writer" was a program, not a process) | `.card.lock` around the read-modify-write; arguments validated before the lock | a human Kill and a driver's status write from two `ledger update` children lost one of the two |
|
|
164
|
+
| 2026-09-11 | src/fsx.mjs withFileLock | any open error but EEXIST meant "run unlocked" | EPERM/EACCES/EBUSY retry like EEXIST | on Windows an `openSync('wx')` against a lock a sibling is unlinking returns EPERM; the 0.3.1 session and usage locks had the same hole (1 of 100 patches lost in a 4-process hammer) |
|
|
165
|
+
| 2026-09-11 | src/ledger.mjs sync | rewrote unsynced.jsonl wholesale after a multi-second flush; `cfg` null rewrote and exited 1 | renames the buffer to `.flushing` first, survivors go back by append; sync off exits 2 with a message | a record buffered during the flush was deleted; with sync off the buffer could never drain |
|
|
166
|
+
| 2026-09-11 | src/scheduler.mjs | re-attached only cards with an orphaned run.json; landing set by station NAME `land`; raw repo strings; pidfile overwritten | re-attaches any running card with no live `driver.lock` (test/land stations, a crash before launch); landing set by station KIND; canonical repo keys; pidfile `wx`, `scheduler start` refuses when one runs | a card left at a test station by `leg down` was never recovered; a land station named `ship` was not serialised; two schedulers drove the same cards |
|
|
167
|
+
| 2026-09-11 | src/chain.mjs | `land_result` dereferenced a null bounce target; `bounceTarget` fell back to the first agent station anywhere; `handoff_now` offered on every running card | null target fails the card; fallback limited to stations before the failing one; offered only with a next leg | a `[test, land]` pipeline threw and blocked the repo's queue forever; a red test bounced forward and the card finished `done` with red tests; the button always 409'd on a one-entry chain |
|
|
168
|
+
| 2026-09-11 | src/orchestrator.mjs runLeg, src/runner.mjs legOpts | `model`/`network` never left the chain entry | `--model`/`--network` through launch and supervise to `adapter.argv` | every card leg ran on the account default; the adapters' `if (opts.model)` branches were dead |
|
|
169
|
+
| 2026-09-11 | src/cards.mjs | trunk taken on faith; `ensure()` fell back to HEAD and recorded `trunk_fallback` nobody read | `card add` refuses a missing trunk naming the repo's default branch; `ensure()` refuses too | a `master` repo burned every land attempt |
|
|
170
|
+
| 2026-09-11 | src/mergequeue.mjs | per-process FIFO Map keyed on the raw repo string | file lock under `$LEG_HOME/locks/` keyed on the canonical root, held for the whole land | two spellings of one root, or the board plus a CLI land, landed concurrently into one checkout |
|
|
171
|
+
| 2026-09-11 | src/share.mjs, src/auth.mjs, bin/leg.mjs | `turnOn` promoted `people[0]`; `authorize` with share off and no token was owner for any address; `share off` wrote the file then stopped the board | only an owner-role person is promoted (else one is minted); the bind is re-checked per request; `share off` stops the listener first; `share on|off` use `stopBoard()`, not `down()` | a guest added before `share on` owned every terminal (the 0.3.1 changelog claimed this fixed; the code did not); a Tailscale board fell open during the restart; inviting someone killed every running agent |
|
|
172
|
+
| 2026-09-11 | src/launcher.mjs | pidfile trusted on PID liveness | a probe on the recorded port confirms the board; `status` clears a stale file | `status` reported a dead board running, `up` refused to start, `down` taskkilled whatever reused the PID |
|
|
173
|
+
| 2026-09-11 | src/taps/codex.mjs findRollout | day directories filtered against a UTC date | local calendar day, with a day of slack | codex names the directory from local time; a session started 20:00-24:00 ET never found its rollout, so its limit never fired |
|
|
174
|
+
| 2026-09-11 | src/attach.mjs agy reader | `statSync().size` (bytes) used as an offset into a decoded string | codex's byte-offset `createTail`, seeded at the log size per leg | every non-ASCII glyph moved the offset ahead of the text; RESOURCE_EXHAUSTED was skipped; a second leg re-fired the first leg's wall |
|
|
175
|
+
| 2026-09-14 | src/board/index.html card help | initial mock implied every background card waits for human review before Land | help follows the selected preset: Build only stops unmerged in its worktree; Build-land and Factory label their automatic test/merge stages | `src/presets.mjs` is the behavior source; reading its station arrays corrected the overbroad copy before release |
|
|
176
|
+
| 2026-09-14 | src/taps/codex.mjs, src/server.mjs session file display | a functions.exec-wrapped apply_patch could store its full source block as a touched-file path | the parser recognizes nested apply_patch calls; the board also hides only historical entries containing literal patch-envelope sentinels | the current older attach process cannot reload the parser, so display cleanup must preserve stored history and legitimate Windows paths such as `C:\new` |
|
|
177
|
+
| 2026-09-14 | src/usage.mjs `candidates()`, src/board/sessions.js preview | the saved order was applied as a rotation anchored on the agent running now, wrapping the agents above it to the back | the saved order is an absolute priority list: drop the agent already running, keep the rest in saved order | a rotation made `codex → claude → agy` hand a Claude terminal to agy first, so an agent parked last could not stay last; the whole point of the setting |
|
|
178
|
+
| 2026-09-14 | src/attach.mjs `restoreTerminal()` | leaving the alternate screen and showing the cursor was treated as restoring the terminal | every mode a killed agent can leave on is turned off explicitly, and the scrolling-region reset is wrapped in DECSC/DECRC | the agent is killed, so it never restores anything itself: the wheel printed mouse reports into the shell after End, and a leftover scrolling region made the next leg overwrite the lines on screen |
|
|
179
|
+
| 2026-09-14 | src/bundle.mjs `resumeFileExists()` | existence on disk answered "is there a resume file", and every reading of it treated that as "the handoff it describes is still true" | removed; `src/resume.mjs` `resumeVerdict()` recomputes freshness from git at read time and returns a state, not a boolean | the function was exported and unused, but the idea behind it was the bug: `.leg/RESUME.md` sat unchanged for hours after its session moved on, and nothing could tell |
|
|
180
|
+
| 2026-09-14 | src/resume.mjs `refreshPointers()` | the board was to skip any checkout with a live terminal, so a live hand-off is never clobbered | it skips only when the pointer's OWN stamped session is live | the case that started this had a hand-written three-day-old `RESUME.md` and a different terminal live in the same checkout; the first rule would have left it exactly as it was |
|
|
181
|
+
| 2026-09-14 | src/resume.mjs idle pointer | a checkpoint bundle was described as "the last hand-off", and the per-session file was named unconditionally | `lineage.to` decides hand-off vs checkpoint, and the file is named only when it exists on disk | seen in the live pointer for this repo: it claimed a hand-off that never happened and pointed at a `RESUME-<id>.md` that was never written, which is the same class of lie the module exists to stop |
|
|
182
|
+
| 2026-09-14 | src/board/sessions.js `renderDrawer()` | the whole panel is rebuilt every 3 s poll so relative timestamps stay honest | still rebuilt, but every scrollable box carries a stable `data-scroll-key` and its offset is carried across | the rebuild reset the task box, each message and the timeline to the top every three seconds, so a long message could not be read at all |
|