@vibelord/lanes 1.2.0 → 1.5.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/package.json +1 -1
- package/payload/lanes/README.md +7 -5
- package/payload/lanes/VERSION +1 -1
- package/payload/lanes/bin/lanes +1091 -76
- package/payload/lanes/container/hooks/grok-cli-hooks.json +8 -0
- package/payload/lanes/container/hooks/grok-hooks.json +12 -0
- package/payload/lanes/container/hooks/kimi-hooks.toml +49 -0
- package/payload/lanes/container/lane +55 -8
- package/payload/lanes/container/lane-mcp +120 -22
- package/payload/lanes/container/lane-signal +86 -23
- package/payload/lanes/container/lane_core.py +524 -36
- package/payload/lanes/container/throttle-pulse +456 -0
- package/payload/lanes/docs/ARCHITECTURE.md +357 -34
- package/payload/lanes/docs/DESIGN.md +13 -3
- package/payload/lanes/docs/INSTALL.md +47 -10
- package/payload/lanes/docs/RUNBOOK.md +761 -12
- package/payload/lanes/docs/USERGUIDE.md +421 -46
- package/payload/lanes/holders.lock +5 -0
- package/payload/lanes/install/enable-docs.sh +78 -0
- package/payload/lanes/install/host-install.sh +39 -5
- package/payload/lanes/libexec/agent_state.py +604 -12
- package/payload/lanes/libexec/docs_store.py +995 -0
- package/payload/lanes/libexec/docsd.py +2010 -0
- package/payload/lanes/libexec/economy_poll.py +6 -2
- package/payload/lanes/libexec/harness_grok.py +742 -0
- package/payload/lanes/libexec/lanes_lib.py +143 -20
- package/payload/lanes/libexec/mdlite.py +575 -0
- package/payload/lanes/libexec/members.py +5 -0
- package/payload/lanes/libexec/notify.py +65 -5
- package/payload/lanes/libexec/sidebar.py +626 -25
- package/payload/lanes/libexec/statusd.py +530 -32
- package/payload/lanes/libexec/tokens.py +42 -9
- package/payload/lanes/libexec/update.py +171 -26
- package/payload/lanes/libexec/uploads.py +538 -0
- package/payload/lanes/libexec/users.py +31 -3
- package/payload/lanes/site/site.example.conf +4 -0
- package/payload/lanes/systemd/lanes-docsd.service +29 -0
- package/payload/lanes/web/Caddyfile +24 -1
- package/payload/lanes/web/README.md +86 -8
- package/payload/lanes/web/browser.js +2 -4
- package/payload/lanes/web/docs.css +52 -0
- package/payload/lanes/web/docs.js +36 -0
- package/payload/lanes/web/mobile.js +3 -2
- package/payload/lanes/web/upload.css +17 -0
- package/payload/lanes/web/upload.js +84 -0
- package/payload/manifest.json +5 -3
|
@@ -41,7 +41,7 @@ Phone / tablet / laptop ── HTTPS (Tailscale :8443) ┤
|
|
|
41
41
|
▼
|
|
42
42
|
Linux workspace host (one unprivileged account)
|
|
43
43
|
one Zellij view session per device: workspace tabs, lane sidebar, 1–3 viewports
|
|
44
|
-
lanes-statusd · registry sync · upload intake · web services
|
|
44
|
+
lanes-statusd · registry sync · upload intake · docs service · web services
|
|
45
45
|
│ /proc/<container init pid>/root
|
|
46
46
|
▼
|
|
47
47
|
Workspace containers (home /home/coder, uid 1000)
|
|
@@ -52,7 +52,7 @@ Phone / tablet / laptop ── HTTPS (Tailscale :8443) ┤
|
|
|
52
52
|
|
|
53
53
|
| Layer | Owns | Lifetime |
|
|
54
54
|
| --- | --- | --- |
|
|
55
|
-
| Host | Views, the registry, status, uploads, web services | Services restart freely; nothing here holds an agent |
|
|
55
|
+
| Host | Views, the registry, status, uploads, the docs store, web services | Services restart freely; nothing here holds an agent |
|
|
56
56
|
| Container | Each lane's tmux session and the agent in it | Survives every view, reconnect and rebuild |
|
|
57
57
|
| View | One Zellij session per device (`mac`, `phone`, `browser`, …) | Disposable: `lanes rebuild <device>` recreates it, agents untouched |
|
|
58
58
|
|
|
@@ -442,7 +442,7 @@ redeemed from a second machine yet.
|
|
|
442
442
|
|
|
443
443
|
After a server update — with `--update-only` the installer runs these steps itself once the new release is switched
|
|
444
444
|
in, in this order: `systemctl --user daemon-reload`, `lanes sync`, the statusd restart, a `try-restart` of each
|
|
445
|
-
optional service that runs (economy, notify, enrolment, `lanes-mobile`, `lanes-web` and the Studio Host — restarted,
|
|
445
|
+
optional service that runs (economy, notify, enrolment, docs, `lanes-mobile`, `lanes-web` and the Studio Host — restarted,
|
|
446
446
|
not re-checked as `enable-studio-host.sh` does; one that is off stays off), `lanes install-container` for every
|
|
447
447
|
container `lanes sync` recorded, `lanes reload-views` and `lanes ensure --if-layout-changed`. A step that fails is
|
|
448
448
|
reported and the next one still runs. `lanes sync` exiting 1 — a board with an error, such as a container that no longer exists, whose
|
|
@@ -550,7 +550,7 @@ would put the bad release back, is refused (install it by hand with `npx @vibelo
|
|
|
550
550
|
tree's `VERSION` and the commit of its npm stamp (none for a checkout), the tree (`pkg/<version>` for an npm
|
|
551
551
|
install), whether `~/.local/share/lanes/current` names that tree (`current pkg/1.2.0 ✗` when it names another: this
|
|
552
552
|
`lanes` was started from an old tree), then the tree each long-running process recorded when it last started
|
|
553
|
-
(`~/.local/state/lanes/run/<name>.tree`: statusd, enrold, notify, economy, studio_host and the newest sidebar).
|
|
553
|
+
(`~/.local/state/lanes/run/<name>.tree`: statusd, enrold, docsd, notify, economy, studio_host and the newest sidebar).
|
|
554
554
|
Each `✗` there names a process that does not run the current code: its last start ran another tree
|
|
555
555
|
(`statusd runs 1.1.0 ✗`), or ran the current tree before its code last changed (`statusd started before its tree
|
|
556
556
|
changed ✗` — a checkout updated in place keeps its path, so `lanes version` compares the record's time with the
|
|
@@ -564,7 +564,8 @@ would put the bad release back, is refused (install it by hand with `npx @vibelo
|
|
|
564
564
|
- The client installer prints `lanes client <version>` and copies `VERSION` into `~/.local/share/lanes/client/`.
|
|
565
565
|
`lanes-launch --version` prints it (`lanes-launch 1.2.0`; a checkout's client reads `lanes/VERSION`; `dev` without
|
|
566
566
|
either), and the dim line a window shows while it connects names it: `lanes 1.2.0: connecting to lanes…`.
|
|
567
|
-
- On a phone or tablet, **More** shows the server's release as its first row, **Lanes v1.2.0**, above **
|
|
567
|
+
- On a phone or tablet, **More** shows the server's release as its first row, **Lanes v1.2.0**, above **Docs** and
|
|
568
|
+
**Text size**:
|
|
568
569
|
the shell asks the enrolment service's `/enrol/site` once when it opens (the release that service runs; the server
|
|
569
570
|
installer restarts it on every install). Nothing is shown when the service does not answer.
|
|
570
571
|
- `lanes-launch` passes that version to `lanes attach` (`--client-version`). A client older than the server gets one
|
|
@@ -603,6 +604,10 @@ would put the bad release back, is refused (install it by hand with `npx @vibelo
|
|
|
603
604
|
client folder afterwards. A question without an answer (no server, a busy one, or a server from before
|
|
604
605
|
1.2.0, which takes the flag for a device name) is only logged (`self-update:` lines in `launch-<target>.log`): the
|
|
605
606
|
connection that follows says what is wrong with the path.
|
|
607
|
+
- A client from before 1.2.0 does not update itself: its `lanes-launch` never asks `lanes hello --client-version`. Run
|
|
608
|
+
`npx @vibelord/lanes@1.5.0` once on that machine (your remembered options are given again); from then on its windows
|
|
609
|
+
follow the server by themselves. A 1.4.0 window asking a 1.5.0 server, for example, is answered ` update=1.5.0` and
|
|
610
|
+
installs exactly that release.
|
|
606
611
|
- A first install with an invite is the server's release too (W9 K3): the npm installer asks the enrolment service's
|
|
607
612
|
`/enrol/site` for the server's release before it installs anything and, when that is newer, runs `npx --yes
|
|
608
613
|
@vibelord/lanes@<release>` instead, which redeems the code ([Join with an invite](#join-with-an-invite)). The phone's
|
|
@@ -699,6 +704,7 @@ browser view). `lanes user show <login>` prints it.
|
|
|
699
704
|
| The lanes you opened, so a Done lane opened on the phone leaves Done on the Mac, and the last lane you opened | The device's own record of what it opened (`clients/<device>.seen.json`: an older release reads it) |
|
|
700
705
|
| Board order and hidden boards (`lanes workspaces order\|hide\|show`) | The layout (desktop or phone), viewports, the machine a window belongs to, its owner |
|
|
701
706
|
| Pinned and hidden lanes (`lanes lane pin\|unpin\|hide\|show`) | |
|
|
707
|
+
| The fleet block folded or not (the sidebar's `i`: `look.fleet_collapsed`) | A device nobody owns: that sidebar's own, until it restarts |
|
|
702
708
|
| Your own quiet window for pushes, `notify.quiet_s` (section 8; no verb sets it yet) | |
|
|
703
709
|
|
|
704
710
|
- **Per user or per device.** A look flag on a device with an owner goes to your profile, and every sidebar of every
|
|
@@ -822,10 +828,24 @@ Top to bottom, a desktop sidebar is:
|
|
|
822
828
|
would leave the pane. Then `zmx lanes (N)` when any exist, then **Unregistered (N)**
|
|
823
829
|
with its `+ New shell` row. A list longer than the pane ends in `⋮ N more`; moving onto that row or clicking it
|
|
824
830
|
scrolls instead of opening.
|
|
825
|
-
3. **
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
831
|
+
3. **Fleet block** (panes of 18 rows or more, once the server's statusd publishes the fleet facts), pinned above the
|
|
832
|
+
upload strip; the lane list scrolls in the rows left. Throttle in one row, a bar per provider, the host and its
|
|
833
|
+
biggest containers; `i` folds it to one line. What each row says, its colours and where the figures come from:
|
|
834
|
+
[Fleet](#fleet).
|
|
835
|
+
4. **Upload strip** (panes of 14 rows or more): ` ↑ uploads · click, then drop (U)` (section 5). Once this
|
|
836
|
+
workspace's lanes have published documents (the docs cockpit) it reads ` ↑ uploads · ▤ docs 3 (U)` — the count
|
|
837
|
+
is every document of its lanes, whoever published it — and the whole strip is a link to the docs page,
|
|
838
|
+
`https://<server>:8443/lanes-docs/` (⌘-click in Ghostty and iTerm2, Ctrl-click elsewhere); a plain click still
|
|
839
|
+
arms the upload. The cursor row's name is a link to that lane's own documents
|
|
840
|
+
(`/lanes-docs/?ws=<workspace>&lane=<lane id>`) while it has any, and `d` shows the link in the footer. The links
|
|
841
|
+
need the server's Tailscale name (statusd learns it as the enrolment service does): without it the strip still
|
|
842
|
+
counts and `d` shows the path alone. What the page holds and who sees what: section 4, [Docs](#docs).
|
|
843
|
+
5. **Strip row** (panes of 18 rows or more): the fleet economy line when the opt-in poller runs (section 8) and the
|
|
844
|
+
fleet block's Throttle row does not show the overflow money and every part after it (no block, the block folded,
|
|
845
|
+
an untokened ` up …` or a ` down` Throttle row, or a row that dropped `spill` or `429` to fit the pane: the economy
|
|
846
|
+
line keeps its place), otherwise the other workspaces that need you, e.g. ` ◆1 Worlds · ◇2 Code`;
|
|
847
|
+
blank when there is nothing to say.
|
|
848
|
+
6. A rule, then the **footer**: one line, by priority.
|
|
829
849
|
|
|
830
850
|
A workspace whose lanes live on another SSH host (`sshWorkspaces` with `layout: "lanes"`) is attach-only: it has no
|
|
831
851
|
Unregistered section, no upload strip and no `+`, and its footer has no `x close`.
|
|
@@ -845,8 +865,9 @@ Unregistered section, no upload strip and no `+`, and its footer has no `x close
|
|
|
845
865
|
| 35 | `×` close, on unregistered sessions only |
|
|
846
866
|
|
|
847
867
|
Marks and their ASCII twins: `▌` `|` cursor · `›` `=` viewing · `²` `2` / `³` `3` other viewport · `▲` `^` pinned · `·` `.` viewer ·
|
|
848
|
-
`…` `~` clipped · `×` `x` close · `+` `+` new / add · `⋮` `:` more rows · `↑` `^` uploads · `↓` `v` drop now ·
|
|
849
|
-
`⚠` `!` warning or starving · `↓` `v` downgraded · `$` `$` paid
|
|
868
|
+
`…` `~` clipped · `×` `x` close · `+` `+` new / add · `⋮` `:` more rows · `↑` `^` uploads · `↓` `v` drop now · `▤` `#` docs ·
|
|
869
|
+
`⚠` `!` warning or starving · `↓` `v` downgraded · `$` `$` paid · the fleet block's `⚙` `@` Throttle, `◆` `*` provider,
|
|
870
|
+
`▣` `[` host, and its bar `▰` `=` usable, `▱` `~` cooling, `▪` `:` benched, `·` `.` dead. The full tables, colours and mockups are in
|
|
850
871
|
[DESIGN.md](DESIGN.md).
|
|
851
872
|
|
|
852
873
|
**Footer**, highest priority first: the open prompt — the sidebar asks one thing at a time, in one of two shapes. A
|
|
@@ -863,7 +884,7 @@ draws the same footer (its picker says ` Choose a workspace · Tap Cancel`, and
|
|
|
863
884
|
idle). An empty workspace shows `no lanes declared · + New shell` on its New shell row.
|
|
864
885
|
|
|
865
886
|
**Sidebar keys** (`?` shows the short form of this table in place of the list until the next key; that key only closes
|
|
866
|
-
the sheet). Its first row says which lanes: `lanes <installed version> · view <the version this sidebar runs>`. The sheet puts companion keys on one line (`J K`, `Tab S-Tab`, `+ U`, `u z`, `p h`, `H o`, `r ?`), leaves `PageUp` / `PageDown`
|
|
887
|
+
the sheet). Its first row says which lanes: `lanes <installed version> · view <the version this sidebar runs>`. The sheet puts companion keys on one line (`J K`, `Tab S-Tab`, `+ U d`, `u z`, `p h`, `H o`, `r i ?`), leaves `PageUp` / `PageDown`
|
|
867
888
|
to this table and ends with `Ctrl-Alt-l` — not a sidebar key but the chord that brings the keyboard here from a lane
|
|
868
889
|
(section [Keybindings](#keybindings)). Every key below does something in the sidebar, and no other key does
|
|
869
890
|
(`tests/unit/test_sidebar.py` `DocumentedKeys` presses each one and every other key a keyboard sends):
|
|
@@ -890,7 +911,9 @@ to this table and ends with `Ctrl-Alt-l` — not a sidebar key but the chord tha
|
|
|
890
911
|
| `h` | Hide the cursor lane on every device you own (` Hid ‘name’ · o shows it`); on a hidden lane `o` shows, show it again |
|
|
891
912
|
| `H` | Hide this workspace on every device you own: `y` arms, `Enter` hides — its tab closes, its lanes keep running (the last workspace shown stays) |
|
|
892
913
|
| `o` | Show / hide again the lanes you hid (shown faint) |
|
|
914
|
+
| `i` | Fold the fleet block to one line, or unfold it, on every device you own (` fleet folded · i unfolds`); then the keyboard goes back to the lane |
|
|
893
915
|
| `U` | Arm the upload strip |
|
|
916
|
+
| `d` | Show the cursor lane's docs link in the footer for 3 s: `https://<server>:8443/lanes-docs/?ws=<workspace>&lane=<lane id>`, as much as the row holds, the whole line one link to all of it (⌘/Ctrl-click, or your terminal's Copy Link); then the keyboard goes back to the lane. ` no docs in ‘name’` on a lane without documents |
|
|
894
917
|
| `r` | Reload and repaint; while the footer says `update ready · reload (r)`, rebuild this view on the installed release (the window reconnects, agents are untouched); the keyboard goes back to the lane |
|
|
895
918
|
| `?` | The key sheet |
|
|
896
919
|
|
|
@@ -929,7 +952,7 @@ back clears it at once. On the phone the picker is a whole tab and has no such l
|
|
|
929
952
|
**A message stays 3 seconds, or until your next action.** What an action says in the footer (` Closed sh`,
|
|
930
953
|
` Renamed → …`, ` Cancelled`, ` view not running yet`, or why a close failed) stays after the keyboard has gone back
|
|
931
954
|
to the lane, for 3 seconds (within a second more: the sidebar looks once a second), then the legend returns. The next
|
|
932
|
-
action you start in the sidebar — opening a lane, `x`, `R`, `P`, `a`, `u`, `z`, `/`, `U`, `+`, `r`, `?`, `Tab`, a chord —
|
|
955
|
+
action you start in the sidebar — opening a lane, `x`, `R`, `P`, `a`, `u`, `z`, `/`, `U`, `+`, `r`, `?`, `d`, `Tab`, a chord —
|
|
933
956
|
replaces it at once. Moving the cursor, typing in a field, `Esc`, the wheel, a mouse release or the focus leaving and
|
|
934
957
|
coming back does not clear it.
|
|
935
958
|
|
|
@@ -1220,13 +1243,14 @@ The frontend chooses once per page load, by capability:
|
|
|
1220
1243
|
| --- | --- | --- |
|
|
1221
1244
|
| Phone | An iPhone / iPod / Android phone user agent, or a touch screen at most 600 px wide or 500 px tall | The phone shell: **Workspaces**, **Lanes**, **Panes**, **Type here** and **More** controls; the lane picker |
|
|
1222
1245
|
| Tablet | Any other touch screen | The same shell with the controls in a top bar and the workspace list as a left panel; the whole tab is shown, so a desktop view's sidebar sits beside its lanes (**More → Single pane** shows one pane for that load) |
|
|
1223
|
-
| Desktop | A fine pointer (mouse, trackpad) | The plain Zellij web client plus a **Lanes** chip (top right) that opens workspace and lane pickers in an overlay bar; the terminal keeps its size and every key |
|
|
1246
|
+
| Desktop | A fine pointer (mouse, trackpad) | The plain Zellij web client plus a **Lanes** chip (top right) that opens workspace and lane pickers and a **Docs** link (the docs page in a new tab) in an overlay bar; the terminal keeps its size and every key |
|
|
1224
1247
|
|
|
1225
1248
|
### Phone controls
|
|
1226
1249
|
|
|
1227
1250
|
- **Workspaces** (or the title in the header) opens the workspace sheet with a search field (each card shows its
|
|
1228
1251
|
counts, `◆ 1 · ◇ 2`); **Lanes** opens the current workspace's lane picker; **Panes** switches panes; **More** opens
|
|
1229
|
-
the client's own menu, where Lanes adds **Text size** (remembered on the phone)
|
|
1252
|
+
the client's own menu, where Lanes adds **Text size** (remembered on the phone), above it **Docs** — the docs page,
|
|
1253
|
+
`/lanes-docs/`, in a new tab, so the terminal stays where it is — and at the top the server's release
|
|
1230
1254
|
(**Lanes v1.2.0**, see [Versions](#versions)). The light/dark toggle repaints the whole terminal.
|
|
1231
1255
|
- The keyboard opens only when you tap **Type here** or another input. Swipes, live updates and reconnects leave it
|
|
1232
1256
|
closed. **Type here** is an ordinary text field: the normal keyboard with its suggestion bar, never the password
|
|
@@ -1259,12 +1283,110 @@ The frontend chooses once per page load, by capability:
|
|
|
1259
1283
|
on a row while the picker asks cancels the question or the name first, then opens that lane, so nothing is left
|
|
1260
1284
|
half-asked on the picker when you come back to it.
|
|
1261
1285
|
|
|
1286
|
+
### Docs
|
|
1287
|
+
|
|
1288
|
+
The Docs page is where an agent shows you its work and where you hand files to an agent — every item with a sha256
|
|
1289
|
+
receipt the server computed. There is one per server, `https://<server>:8443/lanes-docs/`: on the tailnet only, and
|
|
1290
|
+
open only to your own tailnet login. No token and no second password: you see your own uploads and what was
|
|
1291
|
+
published for you, and nobody else's documents.
|
|
1292
|
+
|
|
1293
|
+
**Open it.**
|
|
1294
|
+
|
|
1295
|
+
| From | How |
|
|
1296
|
+
| --- | --- |
|
|
1297
|
+
| Phone or tablet | **More → Docs**: the page opens in a new tab, so the terminal stays where it was |
|
|
1298
|
+
| Laptop browser | The **Lanes** chip (top right), then **Docs** in its bar (a new tab) |
|
|
1299
|
+
| Desktop sidebar | The upload strip says ` ↑ uploads · ▤ docs 3 (U)` once the workspace's lanes have documents: ⌘-click (Ghostty, iTerm2) or Ctrl-click it for the page; the cursor row's name is a link to that lane's documents, and `d` shows that link in the footer ([The sidebar](#the-sidebar)) |
|
|
1300
|
+
| A push notification | A `published` push opens the document when the server's `action_url` uses `{doc}` (section 8) |
|
|
1301
|
+
| An address you type, bookmark or open from a terminal | `/lanes-docs/` everything you may see · `/lanes-docs/?ws=<workspace>&lane=<lane id>` one lane (either key alone works) · `/lanes-docs/d/<id>` one document · `/lanes-docs/d/<id>/raw` its bytes · `/lanes-docs/receipt/<id>.json` its receipt |
|
|
1302
|
+
|
|
1303
|
+
Keep the slash: `/lanes-docs` without it is a view's address and opens the terminal page instead.
|
|
1304
|
+
|
|
1305
|
+
**What it shows.** The index lists your documents by workspace, then lane, newest first, with counts, and above them
|
|
1306
|
+
the upload form. A document opens by its kind:
|
|
1307
|
+
|
|
1308
|
+
| Kind | Opens as |
|
|
1309
|
+
| --- | --- |
|
|
1310
|
+
| `md` (`.md`, `.markdown`) | Rendered on the page, with ```` ```mermaid ```` fences drawn as diagrams (a fence Mermaid cannot read stays as its source); over 1 MiB it is not rendered: download it |
|
|
1311
|
+
| `html`, `svg` | A link, **Open the HTML artifact** (or SVG): the artifact on a page of its own, in a sandbox (below) |
|
|
1312
|
+
| `file` (anything else) | Its facts and a download |
|
|
1313
|
+
|
|
1314
|
+
Every document page also has **Download**, **Receipt** and the facts: kind, name, size, workspace, lane, time, who
|
|
1315
|
+
put it there, how it came (`publish`, `web`, `ssh`) and its sha256.
|
|
1316
|
+
|
|
1317
|
+
**Publish from a lane.** An agent publishes with the MCP tool `lanes_publish` (`path`, optional `title`, `for`,
|
|
1318
|
+
`kind`; section 7, [Agent tools (MCP)](#agent-tools-mcp)); in a lane's shell you do it with the `lane` command:
|
|
1319
|
+
|
|
1320
|
+
```sh
|
|
1321
|
+
~/.lz/bin/lane publish report.md # the kind follows the extension
|
|
1322
|
+
~/.lz/bin/lane publish out/queue.svg --title 'Queue design' --for anna@example.com
|
|
1323
|
+
```
|
|
1324
|
+
|
|
1325
|
+
`lane publish <file> [--title T] [--for <login>] [--kind md|html|svg|file]` takes a file of the container under
|
|
1326
|
+
`/home/coder`, at most 32 MiB, and copies nothing: it hashes the file once, leaves a small request in
|
|
1327
|
+
`~/.lz/state/publish/<lane id>/` and prints it as JSON — the request `id`, `path`, `kind`, `bytes`, `sha256`, the
|
|
1328
|
+
`receipt` path the answer will have, `picked_up_by: "lanes-docsd"` and `expected_within_s: 5`. Within about 5 s the
|
|
1329
|
+
server reads the file out of the container, checks that its sha256 is the one the request named (a file changed in
|
|
1330
|
+
between is refused, not stored), keeps its own copy and writes the receipt beside the request: `outcome: "ok"` with
|
|
1331
|
+
the document's `id`, `sha256`, `bytes` and `url` (`/lanes-docs/d/<id>`), or `outcome: "refused"` with a `reason`
|
|
1332
|
+
(RUNBOOK "Docs service", publish pick-up). The agent sees that answer as `published` in `lanes_status` of its lane.
|
|
1333
|
+
The document is listed under the lane it came from, and the sidebar's ` ▤ docs N` counts it within a few seconds.
|
|
1334
|
+
Change the file only after the receipt is there; publishing it again makes a new document.
|
|
1335
|
+
|
|
1336
|
+
**Who sees what.**
|
|
1337
|
+
|
|
1338
|
+
- An upload: your login alone.
|
|
1339
|
+
- A published document: the login `--for` names (it must be a member's); without `--for`, the person who last opened
|
|
1340
|
+
that lane, on any of their devices; when nobody has, everyone whose member record grants the lane's workspace. A
|
|
1341
|
+
record that lists no workspace grants every workspace the server serves.
|
|
1342
|
+
- Anyone else gets *not found*, exactly as for a document that does not exist. The one exception is the count:
|
|
1343
|
+
everyone who sees a lane's ` ▤ docs N` sees how many documents it holds (a number, never a title).
|
|
1344
|
+
|
|
1345
|
+
**Upload, with a receipt.** On the Docs page pick the lane, drop files on the dashed area — or tap it to choose them,
|
|
1346
|
+
several at once, 32 MiB in all — and **Upload**. Each file lands in that workspace's `uploads/` folder in its
|
|
1347
|
+
container, exactly as every other upload does ([Uploads](#5-uploads)), and the page lists its receipt: the name (a
|
|
1348
|
+
link to the document), size, sha256 and where it landed (`/home/coder/<repo>/uploads/<name>`) — give the agent that
|
|
1349
|
+
path. Every other way in (the sidebar's strip, the watch folder, `lanes-upload`, the macOS droplet) records the same
|
|
1350
|
+
receipt for you (section 5, "Receipts").
|
|
1351
|
+
|
|
1352
|
+
**The sandbox.** Nothing an agent publishes is trusted. Markdown is rendered on the server by Lanes's own renderer:
|
|
1353
|
+
raw HTML in it stays text, a link opens in a new tab and tells the target nothing of where it came from, and an image
|
|
1354
|
+
shows only from the Docs pages themselves or inline. An HTML or SVG artifact is never shown inside a Docs page:
|
|
1355
|
+
**Open the … artifact** loads it as a page of its own under a browser sandbox, where its scripts run on an opaque
|
|
1356
|
+
origin of their own — they cannot read the Docs pages, cannot reach your terminal session or its sign-in, and cannot
|
|
1357
|
+
type into a lane. No Docs page can be framed by another page. That is how the server serves it (unit-tested); a check
|
|
1358
|
+
in a real browser is the operator's (RUNBOOK checklist; DEF-1853).
|
|
1359
|
+
|
|
1360
|
+
**Encrypted at rest.** Off by default. With the operator's `age` switch on, the server keeps new store copies
|
|
1361
|
+
encrypted; your receipts stay the sha256 of what you sent and the files in `uploads/` stay plain for the agents
|
|
1362
|
+
(section 5, "Encrypted at rest").
|
|
1363
|
+
|
|
1364
|
+
**For the administrator.**
|
|
1365
|
+
|
|
1366
|
+
- The full server install, `npx @vibelord/lanes@<release> --server`, enables the docs service (`lanes-docsd`) and
|
|
1367
|
+
installs the page's pinned Mermaid (`install/enable-docs.sh`). An update with `--update-only` enables nothing, so a
|
|
1368
|
+
server set up before the docs service needs one full install, or `systemctl --user enable --now
|
|
1369
|
+
lanes-docsd.service`. `lanes install-container <name>` gives a container `lane publish` and the `lanes_publish`
|
|
1370
|
+
tool (new agent sessions load it).
|
|
1371
|
+
- **The pin gate.** The Docs page answers nobody until the Serve header check has run and its result is pinned with
|
|
1372
|
+
`lanes enrol mode request` (RUNBOOK "Enrolment service", Operator verification). Until then every request is
|
|
1373
|
+
refused `not-pinned`.
|
|
1374
|
+
- `lanes doctor` prints five docs lines (section 6): the service answering on its loopback port, the pin gate, the
|
|
1375
|
+
store's size, the Mermaid pin and the `age` switch.
|
|
1376
|
+
- `lanes docs push <id>` prints one document's receipt and exits 2, `not configured`: the hook for pushing documents
|
|
1377
|
+
to the data service's publication surface, which is not built yet.
|
|
1378
|
+
- The unit, `docs.log`, the routes, the limits and the operator's live checklist:
|
|
1379
|
+
[RUNBOOK](RUNBOOK.md#docs-service-lanes-docsd-w10-w3).
|
|
1380
|
+
|
|
1381
|
+
Not yet verified on a device: the Docs page on a real phone (the rendering, a Mermaid diagram, the drop area), the
|
|
1382
|
+
artifact sandbox in a real browser (DEF-1853), and the Mermaid route through a running Caddy (DEF-1871).
|
|
1383
|
+
|
|
1262
1384
|
### What works, what does not, what is unverified
|
|
1263
1385
|
|
|
1264
1386
|
- Works: everything the desktop view shows, through the same Zellij sessions; the phone has its own view selection of
|
|
1265
|
-
the same agents; the server keeps serving when your Mac sleeps
|
|
1266
|
-
|
|
1267
|
-
|
|
1387
|
+
the same agents; the server keeps serving when your Mac sleeps; uploads from the phone and what agents publish, on
|
|
1388
|
+
the Docs page (**More → Docs**, [Docs](#docs)).
|
|
1389
|
+
- Does not: a public URL (Tailscale only), approving an agent's permission prompt from a push notification.
|
|
1268
1390
|
- Not yet verified on a device: the quick keys, the lane filter, the text keyboard, paste, **Copy** and **Paste** on a
|
|
1269
1391
|
real iPhone (the [runbook](RUNBOOK.md) lists the check), the light/dark terminal repaint, the counts that the
|
|
1270
1392
|
picker's pane title carries to the Workspaces sheet and the **Lanes** button (a phone view created before that
|
|
@@ -1274,7 +1396,10 @@ The frontend chooses once per page load, by capability:
|
|
|
1274
1396
|
## 5. Uploads
|
|
1275
1397
|
|
|
1276
1398
|
Files land in the workspace's `uploads/` folder in its container, the same folder Cursor users drop files into. The
|
|
1277
|
-
in-container path ends up on your clipboard and in a notification, and can be typed into the lane for the agent.
|
|
1399
|
+
in-container path ends up on your clipboard and in a notification, and can be typed into the lane for the agent. Every
|
|
1400
|
+
upload, whichever way it came, also gets a **receipt**: its sha256 computed on the server as the bytes arrived, kept
|
|
1401
|
+
with a copy in the docs store, so it shows on the Docs page (`/lanes-docs/`, section 4, [Docs](#docs)) and in the
|
|
1402
|
+
sidebar's ` ▤ docs N` count.
|
|
1278
1403
|
|
|
1279
1404
|
| Way | How |
|
|
1280
1405
|
| --- | --- |
|
|
@@ -1283,12 +1408,41 @@ in-container path ends up on your clipboard and in a notification, and can be ty
|
|
|
1283
1408
|
| Command | `lanes-upload [-w <workspace>] [--paste] <files…>` · `lanes-upload --clipboard [-w <workspace>] [--paste]` (clipboard image or copied files) |
|
|
1284
1409
|
| macOS droplet | `~/Applications/Lanes Upload.app`: drop files on it; double-click uploads the clipboard screenshot |
|
|
1285
1410
|
| Omarchy | `Super+Shift+U` uploads the clipboard and types the path into the lane |
|
|
1411
|
+
| Docs page (phone, laptop) | **More → Docs** on the phone, **Docs** in the laptop bar, or `https://<server>:8443/lanes-docs/`: pick the lane, then drop files on the dashed area or tap it to choose them (several at once, 32 MiB in all), **Upload**. Each file's receipt is listed: its name (a link to the document), size, sha256 and where it landed in the container (`/home/coder/<repo>/uploads/<name>`) — tell the agent that path |
|
|
1286
1412
|
| Server | `lanes upload <workspace> \| --active <device> [--paste]` reads a tar stream on stdin |
|
|
1287
1413
|
|
|
1288
1414
|
**Where files land.** `<workspace folder>/uploads/<name>` in the container, where the workspace folder is its first
|
|
1289
1415
|
lane's repository folder (else the workspace's working directory). An existing name is never overwritten: the next
|
|
1290
|
-
one becomes `<name>-1.<ext>`. The folder is created on first use and, in a git repository
|
|
1291
|
-
yet, added
|
|
1416
|
+
one becomes `<name>-1.<ext>`. The folder is created on first use and, in a git repository whose `.git/info/exclude`
|
|
1417
|
+
does not name it yet, added there, so uploads are never committed. The Docs page lands a file exactly the same way
|
|
1418
|
+
(its name keeps only letters, digits, `.`, `_` and `-`). The server refuses to follow a symbolic link anywhere on the
|
|
1419
|
+
way, so an agent cannot redirect an upload outside its container: a folder reached through a link answers
|
|
1420
|
+
`uploads/ … not a directory (a symbolic link is refused)` (or, on the Docs page, *cannot be written now*).
|
|
1421
|
+
|
|
1422
|
+
**Receipts.** Each file is also a document in the docs store with a receipt — `sha256`, size, time and its page
|
|
1423
|
+
`/lanes-docs/d/<id>` — for you only (`for` your login): the Docs page lists it under its workspace and lane, and the
|
|
1424
|
+
sidebar counts it. `lanes upload` prints one `# docs: <name> <size> B sha256 <hash> <id>` line per file on stderr (the
|
|
1425
|
+
helper's log, or your terminal with `lanes-upload`). The kind follows the extension: `.md` opens rendered (with Mermaid
|
|
1426
|
+
diagrams), `.html`/`.svg` open as sandboxed artifacts, anything else as a download. The store keeps its own copy, so
|
|
1427
|
+
an agent editing its `uploads/` file never changes what the receipt names. What gets no receipt, though it still
|
|
1428
|
+
lands: a file over 32 MiB sent with `lanes upload` (the store keeps at most 32 MiB a file), a device no member record
|
|
1429
|
+
owns (the server logs its hash in `docs.log`), and files past the 64th of one `lanes upload`. A file name taken in only
|
|
1430
|
+
one of the two places (an agent's own file in `uploads/`, or an earlier upload you removed from the store) can get
|
|
1431
|
+
`-1` in one and not the other; the receipt names both.
|
|
1432
|
+
|
|
1433
|
+
**Encrypted at rest (optional).** The operator can have the server keep its docs-store copies encrypted with
|
|
1434
|
+
[`age`](https://age-encryption.org) (`LANES_DOCS_AGE_RECIPIENT`, [RUNBOOK](RUNBOOK.md#docs-service-lanes-docsd-w10-w3)).
|
|
1435
|
+
Nothing changes in `uploads/` — the agent reads plain files there — and receipts stay the hash of what you sent. When
|
|
1436
|
+
the docs service also holds the key, documents open as usual; when it does not, a download answers *encrypted at
|
|
1437
|
+
rest* and you fetch the file from the workspace's `uploads/` folder instead.
|
|
1438
|
+
|
|
1439
|
+
**Opening the Docs page from elsewhere.** Any link to a document opens it: the shell's **Docs** link, the sidebar's
|
|
1440
|
+
links, a notification, the address bar, and a link on another website — a web mail, a chat in the browser, ntfy's web
|
|
1441
|
+
app. What the docs service refuses from another website is everything that is not you following a link: a page's own
|
|
1442
|
+
background requests, images and scripts, and an upload — so an HTML artifact cannot read anything of yours, and only
|
|
1443
|
+
the Docs page itself uploads. A browser too old to say where a request came from sees *Refused: this request came from
|
|
1444
|
+
another site*. Viewing very quickly (over 300 pages a minute) answers *Too many requests* for a
|
|
1445
|
+
minute; an upload that trickles in slower than about 64 KiB/s is stopped and says so, with nothing stored.
|
|
1292
1446
|
|
|
1293
1447
|
**Why click first.** A terminal delivers a drop as a paste of the local path into the focused pane, so the strip must
|
|
1294
1448
|
hold the focus when you let go. The server receives only the path; the **upload helper** on your machine sends the
|
|
@@ -1502,7 +1656,10 @@ Names must be non-empty and free of control characters. The id, resolve file and
|
|
|
1502
1656
|
each container running, the statusd heartbeat, all registry lanes observed and present (stale lanes counted as
|
|
1503
1657
|
"shown with their last known state"), the process table, no host-side tmux servers, coexistence mode, Zellij
|
|
1504
1658
|
reachable, phone picker panes current, and the host not under critical memory pressure.
|
|
1505
|
-
- Information:
|
|
1659
|
+
- Information: the harness mix (1.5.0, [Harnesses](#harnesses)): `lanes: claude 50 · codex 20 · kimi 3 · grok 0 ·
|
|
1660
|
+
grok-cli 0 · shell 0`, from each lane's `agent.harness` (`unknown` only when a lane has it; a lane with no agent is
|
|
1661
|
+
not counted; `shell` stays 0 in practice: a lane whose harness ended into its shell is published under that
|
|
1662
|
+
harness's name with the status `shell`); lanes per create-recipe class (`idempotent`, `attach-only`, `null`, `external`, `retired-path`), how many
|
|
1506
1663
|
create with their identity, where the `lane` contract is installed, zmx-held lanes, the preview renderer, the live
|
|
1507
1664
|
view sessions, and the pressure summary.
|
|
1508
1665
|
- Warnings (`⚠`, never a `✗`): lanes that `lane open` cannot recreate (fix: `lanes cursor-sync --canonicalize`),
|
|
@@ -1516,10 +1673,23 @@ Names must be non-empty and free of control characters. The id, resolve file and
|
|
|
1516
1673
|
present (`✓`), missing or pointing elsewhere (`✗` once a web unit is enabled, `⚠` before), or not checked (`⚠`: no
|
|
1517
1674
|
tailscale, no answer, or no Tailscale name on the host — then `lanes web` configures no Serve and prints a loopback
|
|
1518
1675
|
URL, so doctor does not ask for the mapping either).
|
|
1676
|
+
- The docs service (W10): `lanes-docsd` enabled, active and answering on its loopback port (`✓`; `⚠` not enabled; `✗`
|
|
1677
|
+
enabled but not running, or not answering), the pin gate (`✓ docs: pinned`, else `⚠ docs: refuses until the Serve
|
|
1678
|
+
header check is pinned`), the store (documents, bytes, `docs.log`'s size), the pinned Mermaid (`✓`, `⚠` not installed
|
|
1679
|
+
— `install/enable-docs.sh`, which every server install and update runs —, `✗` a file that is not the pinned build) and
|
|
1680
|
+
the `age` switch (`✗` when it is set without an `age` binary); RUNBOOK "Docs service" has every line.
|
|
1681
|
+
- The fleet (W11, [Fleet](#fleet)): `throttle pulse: 4 s old (container bw-tools), daemon up, tokened: no — lanes
|
|
1682
|
+
throttle adopt-token bw-tools` (`✓`; `⚠` when the daemon does not answer, when the pulse is more than 90 s old — it
|
|
1683
|
+
stopped — or when status.json has none yet, with `lanes install-container <container>`, which starts it), `throttle
|
|
1684
|
+
pools: claude 12/19 · codex 3/5 · kimi 0/0 usable` (1.5.0: the three pools the pulse reports; `?` for one it did not,
|
|
1685
|
+
`⚠` when a configured pool has no usable account), `fleet: cpu
|
|
1686
|
+
42% · mem 61% · psi 3% · containers 7` (psi is the highest PSI avg10; `⚠` when stale or missing — a statusd from
|
|
1687
|
+
before 1.4.0), and `fleet dashboard: LANES_FLEET_DASHBOARD_URL unset`, or the URL, or `⚠ … ignored — not an https
|
|
1688
|
+
URL`. None of them is ever a `✗`: Lanes runs without Throttle.
|
|
1519
1689
|
- The release that runs, against `~/.local/share/lanes/current` (the tree an install makes current; without the link,
|
|
1520
1690
|
a `⚠` and this `lanes`'s own tree): the link names a tree that exists, whose `VERSION` agrees with its
|
|
1521
1691
|
`pkg/<version>` name and its npm stamp, and that this `lanes` runs (`✗` with the fix: run the current one, or re-run
|
|
1522
|
-
the server installer). Every lanes process running now (statusd, enrold, notify, economy, a sidebar — found in the
|
|
1692
|
+
the server installer). Every lanes process running now (statusd, enrold, docsd, notify, economy, a sidebar — found in the
|
|
1523
1693
|
same process-table sweep by the command Lanes starts it with, `python3 -S -E <tree>/libexec/<script>`, so a file
|
|
1524
1694
|
open in an editor or a one-shot such as `notify.py --test` is not one) has a [run record](#versions) of the current
|
|
1525
1695
|
code (`✗ … statusd runs 1.1.0`, or `statusd runs without a run record (started by an older release)`, then
|
|
@@ -1553,11 +1723,15 @@ anything, picks the first signal that applies:
|
|
|
1553
1723
|
turn keep it, unless the report was `working`.
|
|
1554
1724
|
2. A **Claude Code session file** `~/.claude/sessions/<pid>.json` naming the pane, for a live pid (recycled pids are
|
|
1555
1725
|
rejected).
|
|
1556
|
-
3.
|
|
1557
|
-
|
|
1558
|
-
4. The **
|
|
1726
|
+
3. **Kimi Code's session files**, then **Grok Build's** (which session the pane's `grok` runs, its event log, else its
|
|
1727
|
+
pane title), for a pane whose process is that harness ([Harnesses](#harnesses)).
|
|
1728
|
+
4. The **Codex pane title**, only where Codex can run: `[ ! ] Action Required` is needs approval, a braille spinner is
|
|
1729
|
+
working, a task title is idle.
|
|
1730
|
+
5. The **harness process itself**: nothing above claimed the pane, so it is the harness the pane runs, idle.
|
|
1731
|
+
6. The **window bell**, throughout: an idle lane or a finished turn plus a bell is done.
|
|
1559
1732
|
|
|
1560
|
-
A Codex title left on a bare shell with no child process
|
|
1733
|
+
A Codex title left on a bare shell with no child process, or a Grok Build title over a pane that runs only shells, is
|
|
1734
|
+
`shell` (the harness ended). Since 1.5.0 the harness itself is observed, not guessed: see [Harnesses](#harnesses).
|
|
1561
1735
|
|
|
1562
1736
|
What the sidebar shows is not that word itself but a debounced one. The sections rank Needs you > Working > Done > the
|
|
1563
1737
|
workspace's other lanes: a status that moves the lane up is shown at once, one that moves it down only after it has
|
|
@@ -1570,6 +1744,107 @@ starts signals Working (`PreToolUse`); that hook fires before the permission dia
|
|
|
1570
1744
|
A Codex approval read from its pane title (a lane without hooks) keeps the same 8 s dwell once approved. The full rules:
|
|
1571
1745
|
[Attention queue](ARCHITECTURE.md#attention-queue).
|
|
1572
1746
|
|
|
1747
|
+
### Harnesses
|
|
1748
|
+
|
|
1749
|
+
Lanes 1.5.0 (W12, [spec](../../docs/superpowers/specs/2026-10-15-lanes-harness-observers-design.md)) reads five agent
|
|
1750
|
+
harnesses: Claude Code, Codex, Kimi Code, Grok Build (xAI's official `grok`) and grok-cli (the community `grok`).
|
|
1751
|
+
Which one runs in a lane is an **observed fact**: statusd walks the pane's process tree (plain `/proc` reads, no new
|
|
1752
|
+
process) and publishes `agent.harness` as `claude`, `codex`, `kimi` (Kimi Code), `grok` (Grok Build), `grok-cli` or
|
|
1753
|
+
`unknown`; a pane that runs only shells is `shell` to the walk and shows no agent of its own (or the harness that ended
|
|
1754
|
+
there, with the status shell). A pane title that looks like Codex's is read as Codex only where Codex runs: before 1.5.0 any
|
|
1755
|
+
title of two or more words read as an idle Codex, so a Kimi lane showed `codex / idle`. `lanes doctor` prints the mix:
|
|
1756
|
+
`lanes: claude 50 · codex 20 · kimi 3 · grok 0 · grok-cli 0 · shell 0`. Each harness gets the lane-signal hooks, the
|
|
1757
|
+
Lanes MCP server and the Throttle trace its own files allow:
|
|
1758
|
+
|
|
1759
|
+
| Harness | What Lanes reads with no hooks (observed) | Hooks (`lanes hooks install --agent …`, a dry run until `--write`) | MCP server |
|
|
1760
|
+
| --- | --- | --- | --- |
|
|
1761
|
+
| Claude Code | `~/.claude/sessions/<pid>.json`, the window bell | `--agent claude`: `~/.claude/settings.json` ("Hooks" below) | `~/.claude.json` ("Agent tools (MCP)" below) |
|
|
1762
|
+
| Codex | the pane title, the window bell | `--agent codex`: `~/.codex/hooks.json` and `notify` | `~/.codex/config.toml` |
|
|
1763
|
+
| Kimi Code 2.1.1 | its session files: `~/.kimi-code/sessions/wd_*/session_<uuid>/state.json` and the last 64 KiB of `agents/main/wire.jsonl` | `--agent kimi`: a `# >>> lanes hooks >>>` block of `[[hooks]]` in `~/.kimi-code/config.toml` | `~/.kimi-code/mcp.json` |
|
|
1764
|
+
| Grok Build 1.0.52 | `$GROK_HOME/active_sessions.json` (which pid runs which session), the session's `events.jsonl` and `summary.json` (its title), the pane title | `--agent grok`: `~/.grok-build/hooks/lanes.json` plus blocks in `~/.grok-build/config.toml` | a `# >>> lanes mcp >>>` block in `~/.grok-build/config.toml` |
|
|
1765
|
+
| grok-cli 1.1.7 | nothing: it sets no title and rings no bell | `--agent grok-cli`: `hooks` in `~/.grok/user-settings.json` | the `mcp.servers` item `lanes` in `~/.grok/user-settings.json` |
|
|
1766
|
+
|
|
1767
|
+
Per container, on the server (each a dry run that prints its diff until `--write`):
|
|
1768
|
+
|
|
1769
|
+
```sh
|
|
1770
|
+
lanes hooks install --agent kimi --container <name> --write # Kimi Code: ~/.kimi-code/config.toml
|
|
1771
|
+
lanes hooks install --agent grok --container <name> --write # Grok Build: ~/.grok-build/hooks/lanes.json, config.toml
|
|
1772
|
+
lanes hooks install --agent grok-cli --container <name> --write # grok-cli: ~/.grok/user-settings.json
|
|
1773
|
+
```
|
|
1774
|
+
|
|
1775
|
+
What the hooks of the three newer harnesses give the boards (Claude Code's and Codex's: "Hooks" below):
|
|
1776
|
+
|
|
1777
|
+
| Harness | Event | Status |
|
|
1778
|
+
| --- | --- | --- |
|
|
1779
|
+
| Kimi Code | `UserPromptSubmit`, `TurnStarted`, `PreToolUse`, `PostToolUse`, `PermissionResult` | working |
|
|
1780
|
+
| Kimi Code | `PermissionRequest` | needs_approval |
|
|
1781
|
+
| Kimi Code | `Stop`, `Interrupt` | needs_input |
|
|
1782
|
+
| Kimi Code | `StopFailure` | errored |
|
|
1783
|
+
| Kimi Code | `SessionStart` / `SessionEnd` | idle / shell |
|
|
1784
|
+
| Grok Build | `UserPromptSubmit`, `PreToolUse`, `PostToolUse` | working |
|
|
1785
|
+
| Grok Build | `Notification` (permission prompt) | needs_approval |
|
|
1786
|
+
| Grok Build | `Stop`, `StopCancelled`, `Notification` (idle prompt) | needs_input |
|
|
1787
|
+
| Grok Build | `StopFailure` | errored |
|
|
1788
|
+
| Grok Build | `SessionStart` / `SessionEnd` | idle / shell |
|
|
1789
|
+
| grok-cli | `UserPromptSubmit`, `PreToolUse`, `PostToolUse` | working |
|
|
1790
|
+
| grok-cli | `Stop` / `StopFailure` / `SessionEnd` | needs_input / errored / shell |
|
|
1791
|
+
|
|
1792
|
+
**Kimi Code** comes from its own installer (`~/.kimi-code`). Without hooks, a Kimi session is joined to its lane by
|
|
1793
|
+
working folder: the lane's Kimi process whose folder is the session's `cwd`, newest `updatedAt` first. Two Kimi lanes
|
|
1794
|
+
in one folder are ambiguous, and then the lane falls back to hooks and the title. From `wire.jsonl`: a turn that
|
|
1795
|
+
starts, a prompt or a tool call is working; an approval request not yet resolved is needs approval; a finished turn is
|
|
1796
|
+
needs input (done when the window rang); a failed turn is errored. A session file more than an hour old says nothing.
|
|
1797
|
+
The hooks make it event-driven. Lanes writes only its own marked block into `~/.kimi-code/config.toml` and never
|
|
1798
|
+
rewrites the rest of the file; it never touches Kimi's device identity files. Kimi rings the terminal bell only when
|
|
1799
|
+
told to: `lanes onboard`'s optional `--step kimi-notify` sets `notification_condition = "always"` in
|
|
1800
|
+
`~/.kimi-code/tui.toml`, so Kimi Code rings the bell focused or not (the operator decides, per container). Kimi's own
|
|
1801
|
+
default file already has the `[notifications]` table: that one line is edited and its old value kept in a comment.
|
|
1802
|
+
|
|
1803
|
+
**Grok Build lives in `GROK_HOME=~/.grok-build`, never `~/.grok`.** Its own installer would replace grok-cli's
|
|
1804
|
+
`~/.grok/bin/grok` with a link to itself, so every lane recipe sets `GROK_HOME=/home/coder/.grok-build`, and
|
|
1805
|
+
`lanes onboard <login> --container <name> --step grok-build --write` installs it there from the stable channel
|
|
1806
|
+
(`https://x.ai/cli/stable`, the binary and its `bin/grok` link, `[cli] installer = "internal"`). That channel publishes
|
|
1807
|
+
no checksum, so the receipt records the download's size and sha256 as unverified for you to check. grok-cli keeps
|
|
1808
|
+
`~/.grok`.
|
|
1809
|
+
|
|
1810
|
+
**Grok Build without hooks.** From `events.jsonl`: a permission prompt is needs approval; waiting for the model,
|
|
1811
|
+
streaming or running a tool is working; a completed turn is needs input (done when the window rang), an error
|
|
1812
|
+
errored, a cancelled or interrupted turn needs input. The title says the same: `⚠ Action Required …` is needs approval
|
|
1813
|
+
(it blinks while the pane is not focused, so a busy title right after it still reads needs approval), a braille spinner
|
|
1814
|
+
is working, `grok` or `<name> - grok` is idle; a Grok title left over a pane that runs only shells is shell. Grok
|
|
1815
|
+
Build's leader mode (`use_leader = true`) is not read: such a lane shows no agent state until its hooks speak.
|
|
1816
|
+
|
|
1817
|
+
**Grok Build's hooks install** writes the hooks file (the events above; it runs Claude Code's hooks by default, which
|
|
1818
|
+
would report `claude`) and two options in `~/.grok-build/config.toml`: `[compat.claude] hooks = false, mcps = false`,
|
|
1819
|
+
so Claude Code's hooks and its `lane-mcp` entry do not also run inside Grok, and `[ui.notifications] condition =
|
|
1820
|
+
"always", method = "bel"`, so every turn end, approval and error rings the pane's bell. If the file already has a
|
|
1821
|
+
`[compat.claude]` or `[ui.notifications]` table of yours, the install changes nothing and asks you to set those keys
|
|
1822
|
+
there.
|
|
1823
|
+
|
|
1824
|
+
**grok-cli** comes from its own installer (`~/.grok`) and has hooks only, with exact-string matchers. It has no
|
|
1825
|
+
approval signal and no title or bell: its lane shows working, needs input, errored or shell, never needs approval.
|
|
1826
|
+
|
|
1827
|
+
**Agent tools (MCP).** `~/.lz/bin/lane mcp install` (which `lanes install-container` runs, or `lane mcp install
|
|
1828
|
+
--agent kimi|grok|grok-cli`) writes the Lanes server into each harness's file in the table above: Kimi Code's
|
|
1829
|
+
`~/.kimi-code/mcp.json` (the Claude `mcpServers` shape), Grok Build's `[mcp_servers.lanes]` as a marked block (its
|
|
1830
|
+
`[compat.claude] mcps = false` keeps `lane-mcp` from starting twice; Grok Build passes its session id to the server),
|
|
1831
|
+
and grok-cli's `mcp.servers`. grok-cli starts MCP servers with a stripped environment, so `lanes_whoami` reads the
|
|
1832
|
+
lane's identity from the environment of the grok-cli process that started it.
|
|
1833
|
+
|
|
1834
|
+
**The Throttle trace.** Kimi Code blanks `ANTHROPIC_CUSTOM_HEADERS`, so every recipe that sets it also sets
|
|
1835
|
+
`KIMI_CODE_CUSTOM_HEADERS='x-throttle-trace: lanes/<ws>/<id>'`. Grok Build's global extra headers would trace every
|
|
1836
|
+
lane alike, so recipes export `LANES_TRACE` and its hooks install writes `env_http_headers = {"x-throttle-trace" =
|
|
1837
|
+
"LANES_TRACE"}` for its built-in models `grok-4.6` and `grok-4.5`. grok-cli sends no custom headers: its requests carry
|
|
1838
|
+
no trace. A trace only matters for a harness attached to a Throttle pool: Kimi Code when `throttle setup
|
|
1839
|
+
--install-clients` attached it, Grok not at all (Throttle has no xAI pool).
|
|
1840
|
+
|
|
1841
|
+
**Files Lanes changes.** Each is kept as `<file>.bak-<UTC>` first. In a TOML file Lanes changes only its marked block,
|
|
1842
|
+
so to undo, delete the block. The diff a dry run prints shows a credential's value (grok-cli's `apiKey`) as `…`.
|
|
1843
|
+
`lane-signal` without `--harness` takes the harness from `LANE_HARNESS`, else from the agent process above it, else
|
|
1844
|
+
records `unknown`.
|
|
1845
|
+
|
|
1846
|
+
Operator steps per container: [RUNBOOK "Harnesses"](RUNBOOK.md#harnesses).
|
|
1847
|
+
|
|
1573
1848
|
### A user's agents in one step: `lanes onboard`
|
|
1574
1849
|
|
|
1575
1850
|
On the server, one command gives a new user's workspace container what its agents need. Without `--write` it prints
|
|
@@ -1585,13 +1860,14 @@ lanes onboard anna --container ws-anna --workspace anna-tools --repo /home/coder
|
|
|
1585
1860
|
| Board | A workspace tab for the repository (`lanes workspaces add`, section 6) on every device's view |
|
|
1586
1861
|
| Contract | `lane`, `lane-run`, `lane-signal`, `lane-edit`, the quick-edit `vimrc`, `lane-mcp` and the hook definitions in the container's `~/.lz` (`lanes install-container`) |
|
|
1587
1862
|
| Agent tools | The Lanes MCP server in Claude Code's and Codex's configuration, so an agent can see the lanes around it ("Agent tools (MCP)" below) |
|
|
1588
|
-
| Hooks | The lane-signal hooks for Claude Code and Codex ("Hooks" below), so the sidebar's agent glyphs are event-driven |
|
|
1863
|
+
| Hooks | The lane-signal hooks for Claude Code and Codex ("Hooks" below), and for Kimi Code, Grok Build and grok-cli where they are installed ([Harnesses](#harnesses)), so the sidebar's agent glyphs are event-driven |
|
|
1589
1864
|
| Throttle | `throttle setup --install-clients` inside the container when Throttle is installed and configured there, so the agents' requests go through the pool; otherwise the one-time setup to run on a terminal is printed (a first setup signs in, which needs a person; Lanes runs without Throttle) |
|
|
1590
1865
|
| Recipes | `lanes cursor-sync <id> --canonicalize --write` when the repository has a `.vscode/terminals.json`: each lane's create recipe gives it its identity and trace ("Lane identity" below) |
|
|
1591
1866
|
| Sync and doctor | The registry knows the new lanes; `lanes doctor`'s result is reported, never fatal |
|
|
1592
1867
|
|
|
1593
1868
|
The receipt is `~/.local/state/lanes/onboard/<login>-<UTC>.json`, and running the command again changes nothing that
|
|
1594
|
-
is already done. `--no-mcp` leaves out `lane-mcp` and its entries in the agents' configuration.
|
|
1869
|
+
is already done. `--no-mcp` leaves out `lane-mcp` and its entries in the agents' configuration. Two steps run only
|
|
1870
|
+
when named: `--step kimi-notify` and `--step grok-build` ([Harnesses](#harnesses)). Every step
|
|
1595
1871
|
and status: [install guide](INSTALL.md#onboard-a-users-container).
|
|
1596
1872
|
|
|
1597
1873
|
**Laptop agents on the pool (operator-gated).** Agents a user runs on their own laptop, outside any lane, can use the
|
|
@@ -1612,10 +1888,11 @@ lanes hooks install --agent claude --container <name> --write # applies it (b
|
|
|
1612
1888
|
lanes hooks install --agent codex --container <name> --write # ~/.codex/hooks.json (+ notify in config.toml)
|
|
1613
1889
|
```
|
|
1614
1890
|
|
|
1615
|
-
`lanes hooks install --agent claude|codex (--container <name> | --home <dir>) [--dry-run | --write]
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1891
|
+
`lanes hooks install --agent claude|codex|kimi|grok|grok-cli (--container <name> | --home <dir>) [--dry-run | --write]`
|
|
1892
|
+
is a dry run unless `--write` (Kimi Code, Grok Build and grok-cli: [Harnesses](#harnesses)); your own hooks and
|
|
1893
|
+
keys stay, and an earlier lane-signal install is replaced, not duplicated. An install that is already current is
|
|
1894
|
+
left where it is, even when another tool (Throttle's `setup --install-clients`) has put its own group before or
|
|
1895
|
+
after it or re-sorted the file's keys, so running it again writes nothing. For
|
|
1619
1896
|
Codex it also adds a top-level `notify` to `~/.codex/config.toml` only when none exists. Codex asks you to review and
|
|
1620
1897
|
trust new hooks; Lanes never writes that trust. Expect hooks to reach agent sessions started after the install
|
|
1621
1898
|
(inferred from how both harnesses read their configuration, not measured). Undo:
|
|
@@ -1654,7 +1931,8 @@ verbs, so the holder stays swappable.
|
|
|
1654
1931
|
| `lane kill <id> --yes [--force]` | End the lane (the only verb that ends an agent); refuses while the hook sidecar says `working` unless `--force` |
|
|
1655
1932
|
| `lane new <id> [--ws W] [--title T] [--cwd DIR] [--icon I] [--color C] -- <command…>` | Create a zmx-held lane |
|
|
1656
1933
|
| `lane gc --sockets` | Unlink this container's tmux socket files that tmux is refused on (no server listens); a live, empty or silent server keeps its file |
|
|
1657
|
-
| `lane mcp install [--agent claude\|codex\|all] [--write]` | Add the Lanes MCP server to `~/.claude.json` and `~/.codex/config.toml
|
|
1934
|
+
| `lane mcp install [--agent claude\|codex\|kimi\|grok\|grok-cli\|all] [--write]` | Add the Lanes MCP server to `~/.claude.json` and `~/.codex/config.toml`, and to Kimi Code's, Grok Build's and grok-cli's files where they are installed ("Agent tools (MCP)" below); a dry run until `--write` |
|
|
1935
|
+
| `lane publish <file> [--title T] [--for <login>] [--kind md\|html\|svg\|file]` | Put a file of this container on the docs page from the lane it runs in ("Publish to the docs page" below); prints the request as JSON |
|
|
1658
1936
|
|
|
1659
1937
|
Every tmux client these verbs start runs with `-N`, so none of them can start a tmux server: an attach to a lane
|
|
1660
1938
|
whose server has just ended fails instead of leaving an empty server and its socket file behind.
|
|
@@ -1675,6 +1953,18 @@ file is written in place (it keeps its mode and owner), swap and backup files go
|
|
|
1675
1953
|
beside the file, modelines are off, a file without a final newline keeps none, and no `~/.viminfo` is written.
|
|
1676
1954
|
Everything it creates is private (`umask 077`). Quitting the editor closes the pane.
|
|
1677
1955
|
|
|
1956
|
+
### Publish to the docs page
|
|
1957
|
+
|
|
1958
|
+
An agent (or you, in a lane's shell) puts a file on the [Docs page](#docs) with `~/.lz/bin/lane publish <file>` or
|
|
1959
|
+
the MCP tool `lanes_publish`: Markdown with Mermaid diagrams is rendered there, an HTML or SVG artifact opens
|
|
1960
|
+
sandboxed on its own page, and any other file is a download, each with the sha256 the server computed. The verb writes
|
|
1961
|
+
a small request in `~/.lz/state/publish/<lane>/` and copies nothing; within about 5 s the docs service on the server
|
|
1962
|
+
reads the file from the container, checks its hash against the request's (a file changed in between is refused, not
|
|
1963
|
+
stored) and writes `<id>.receipt.json` beside the request — `lanes_status` shows it as `published`, with the
|
|
1964
|
+
document's link. The file must be under `/home/coder` and at most 32 MiB. Who sees it (`--for`, else the person who
|
|
1965
|
+
last opened the lane, else everyone whose member record grants its workspace), the receipt's fields and how each kind
|
|
1966
|
+
opens: section 4, [Docs](#docs).
|
|
1967
|
+
|
|
1678
1968
|
### Agent tools (MCP)
|
|
1679
1969
|
|
|
1680
1970
|
`lane-mcp` is an MCP server in each container (`~/.lz/bin/lane-mcp`). Claude Code and Codex start it from their own
|
|
@@ -1684,14 +1974,14 @@ only read:
|
|
|
1684
1974
|
| Tool | Answers |
|
|
1685
1975
|
| --- | --- |
|
|
1686
1976
|
| `lanes_whoami` | Which lane the agent runs in: id, board, name, kind (declared, registered or unregistered), tmux socket and session. It comes from `LANE_ID`, else from the agent's own tmux pane. Outside a lane the answer is `lane: null` with the reason |
|
|
1687
|
-
| `lanes_list` | Every lane in this container with its state and its agent (harness, status, task). Filters: `workspace`, `needs_you: true` (needs approval, errored or needs input, the sidebar's "Needs you" section) and `agent` (`any`, `none`, `claude`, `codex`). `status.fresh` is false once the host's observation is more than 60 s old |
|
|
1977
|
+
| `lanes_list` | Every lane in this container with its state and its agent (harness, status, task). Filters: `workspace`, `needs_you: true` (needs approval, errored or needs input, the sidebar's "Needs you" section) and `agent` (`any`, `none`, `claude`, `codex`, `kimi`, `grok` for Grok Build, `grok-cli`). `status.fresh` is false once the host's observation is more than 60 s old |
|
|
1688
1978
|
| `lanes_status` | One lane in full: what the registry says, the observed state and agent, whether its tmux session is alive now (`null` when tmux does not answer in 5 s) and its hook sidecar |
|
|
1689
1979
|
| `lanes_capture` | The last `lines` (1 to 500, default 100) non-blank lines of a lane's screen and scrollback, as plain text. Any lane of the container can be read |
|
|
1690
1980
|
|
|
1691
1981
|
Where the answers come from: `~/.lz/registry.json`, which `lanes sync` writes, and `~/.lz/status.json`, which the
|
|
1692
1982
|
status daemon writes. A container that has neither yet answers with empty lists and says why.
|
|
1693
1983
|
|
|
1694
|
-
|
|
1984
|
+
Seven tools write. Each reaches the lanes of its own container only, and answers what it changed, what picks the
|
|
1695
1985
|
change up (`picked_up_by`) and how soon (`expected_within_s`):
|
|
1696
1986
|
|
|
1697
1987
|
| Tool | Changes | Seen on the boards |
|
|
@@ -1701,6 +1991,7 @@ change up (`picked_up_by`) and how soon (`expected_within_s`):
|
|
|
1701
1991
|
| `lanes_rename` | Sets the lane's `displayName` in its terminals.json; the id never changes. The lane's own title removes it. A name the host sets for the lane in its workspace manifest still wins, and the answer says so (`host_override`) | same |
|
|
1702
1992
|
| `lanes_new` | `name`, optional `cwd`, `command`, `workspace`, `repo`. Declares a lane on the board's shell socket `lanes-shells-<workspace>` and starts its session (detached, 100x30) with the lane's identity; the workspace defaults to the agent's own board, the folder to the repository's, the command to `bash -l`. When that starts the board's shell server, the server gets the account and locale variables (and `TMUX_TMPDIR`), never the agent's own environment, which every later shell on the board would inherit | the session at once; the lane after the next `lanes sync` |
|
|
1703
1993
|
| `lanes_send` | `lane`, `text`, optional `enter` (default true), `force`. Types printable text into the lane, then Enter half a second later as a separate keystroke, so an agent's prompt submits it instead of taking it as a paste | at once |
|
|
1994
|
+
| `lanes_publish` | `path` (an absolute path under `/home/coder`), optional `title`, `for` (a login), `kind` (`md`, `html`, `svg`, `file`; default from the extension). Writes a publish request for the file from the agent's own lane ("Publish to the docs page" below); the docs service reads the file itself and answers with a receipt that `lanes_status` shows as `published` | on the docs page within about 5 s |
|
|
1704
1995
|
| `lanes_signal` | `status` (the agent words: `working`, `needs_input`, `needs_approval`, `done`, `errored`, `shell`, `idle`), optional `note`. Reports the agent's own status through `lane-signal`; the note (one line, at most 120 characters) is shown as the lane's task line. The report stands until the agent does something new: its next tool call, a new prompt, a permission prompt, a failed turn or the session's end. The end of its turn keeps it (a `working` report ends there), so an agent reports `needs_input`, `done` or `errored` as the last thing before it stops. A `working` note stays while the agent keeps working | on the next status cycle, within about 5 s |
|
|
1705
1996
|
|
|
1706
1997
|
What they refuse. A refusal is `isError` with `{"refused": <code>, "why", "human"}`: `human` says what a person can
|
|
@@ -1715,7 +2006,10 @@ refuses an unregistered session (register it first) and a name with a control ch
|
|
|
1715
2006
|
folder outside `/home/coder` or one that does not exist (`invalid_cwd`), a name or session in use and a command with
|
|
1716
2007
|
a control character. `lanes_send` types nothing while the lane's agent is working or waiting for an approval
|
|
1717
2008
|
(`agent_working`, `agent_needs_approval`) unless `force` is true, and never a control character: no newline, tab,
|
|
1718
|
-
escape or Ctrl-C (`invalid_text`).
|
|
2009
|
+
escape or Ctrl-C (`invalid_text`). `lanes_publish` refuses a file outside `/home/coder` once every link is resolved
|
|
2010
|
+
(`outside_home`), anything but a regular file (`not_a_file`), a file over 32 MiB (`too_large`), a title or login that
|
|
2011
|
+
is not one printable line (`invalid_title`, `invalid_for`), and an agent outside a lane (`not_in_lane`). Every tool
|
|
2012
|
+
refuses a lane of another container (`not_in_container`). The edits go
|
|
1719
2013
|
through the same minimal editor as everything else here: one span of strict JSON, the file re-read just before the
|
|
1720
2014
|
atomic write, never the root `throttle` object, never a rename or reorder of terminals. The file is the
|
|
1721
2015
|
repository's, so its history is git's: no backup file is written.
|
|
@@ -1762,13 +2056,17 @@ are in the [runbook](RUNBOOK.md#install-check-uninstall). What to tell your agen
|
|
|
1762
2056
|
### Lane identity and the canonical recipe
|
|
1763
2057
|
|
|
1764
2058
|
A lane's identity is its environment: `LANE_ID` (`<ws>--<slug>`), `LANE_WS` (`<ws>`), and the Throttle trace
|
|
1765
|
-
`lanes/<ws>/<id>` as `THROTTLE_TRACE
|
|
2059
|
+
`lanes/<ws>/<id>` as `THROTTLE_TRACE`, as `LANES_TRACE` (Grok Build's `env_http_headers` read it), and as
|
|
2060
|
+
`ANTHROPIC_CUSTOM_HEADERS` and `KIMI_CODE_CUSTOM_HEADERS` (`'x-throttle-trace: lanes/<ws>/<id>'`; Kimi Code ignores the
|
|
2061
|
+
Anthropic one). The recipe also sets `GROK_HOME=/home/coder/.grok-build` ("Harnesses" above). A tmux
|
|
1766
2062
|
lane gets it only from the `-e` flags of its create recipe; `lane open` deliberately exports none (the client that
|
|
1767
2063
|
starts a shared tmux server would hand its environment to every later session there). The canonical recipe is:
|
|
1768
2064
|
|
|
1769
2065
|
```sh
|
|
1770
2066
|
tmux -L <socket> new -ADs <session> -c <folder> -e LANE_ID=<id> -e LANE_WS=<ws> \
|
|
1771
|
-
-e ANTHROPIC_CUSTOM_HEADERS='x-throttle-trace: lanes/<ws>/<id>' -e THROTTLE_TRACE=lanes/<ws>/<id>
|
|
2067
|
+
-e ANTHROPIC_CUSTOM_HEADERS='x-throttle-trace: lanes/<ws>/<id>' -e THROTTLE_TRACE=lanes/<ws>/<id> \
|
|
2068
|
+
-e KIMI_CODE_CUSTOM_HEADERS='x-throttle-trace: lanes/<ws>/<id>' -e LANES_TRACE=lanes/<ws>/<id> \
|
|
2069
|
+
-e GROK_HOME=/home/coder/.grok-build bash -l
|
|
1772
2070
|
```
|
|
1773
2071
|
|
|
1774
2072
|
`-A` attaches when the session exists, so the identity lands only on a real create. A running lane gains it when it is
|
|
@@ -1782,6 +2080,69 @@ zmx lane's agent state yet (DEF-1573). Gate results: [ZMX-RING0.md](ZMX-RING0.md
|
|
|
1782
2080
|
|
|
1783
2081
|
## 8. Economy and notifications
|
|
1784
2082
|
|
|
2083
|
+
### Fleet
|
|
2084
|
+
|
|
2085
|
+
The sidebar's free rows hold a pinned health block (Lanes 1.4.0, W11): on panes of 18 rows or more, once the server's
|
|
2086
|
+
statusd publishes the fleet facts, it sits above the upload strip and the lane list scrolls in the rows left. It is
|
|
2087
|
+
live (statusd rewrites it at most every 10 s), local, and needs no collector. At 25 rows or more it is a rule and six
|
|
2088
|
+
rows (1.5.0 added the Kimi pool's row; 1.4.0 drew five from 24 rows):
|
|
2089
|
+
|
|
2090
|
+
```
|
|
2091
|
+
── fleet ──────────────────────────
|
|
2092
|
+
⚙ throttle $8.5k/16k 53% · 429 +4 · spill 76%
|
|
2093
|
+
◆ anthropic ▰▰▰▰▰▰▱▱▱▱ 12/19 · wk 61% · reset 2h
|
|
2094
|
+
◆ openai ▰▰▰▱▱▱▱▱▱▱ 3/5 · 5h 28%
|
|
2095
|
+
◆ moonshot ▰▰▰▰▰▰▱▪▪· 5/8 · wk 44% · 5h 12%
|
|
2096
|
+
▣ productbox cpu 42% · mem 61% · load 37 · psi 3%
|
|
2097
|
+
▣ bw-tools 15G · worlds 9G · sakio 4G
|
|
2098
|
+
```
|
|
2099
|
+
|
|
2100
|
+
| Row | What it shows |
|
|
2101
|
+
| --- | --- |
|
|
2102
|
+
| `⚙ throttle` | One line for the Throttle pool: with the opt-in token, overflow spend today against the daily cap, new 429s and the spill fraction — one pool's figure, never a sum: the Anthropic and OpenAI overflow budgets are separate money, so the row shows the pool nearest its cap, one at its alert level first (Kimi has no paid overflow, so its pool never gives the money); without the token ` ⚙ throttle up · spill 76% · 429 +4`, and ` down` in red when the daemon does not answer |
|
|
2103
|
+
| `◆ anthropic`, `◆ openai`, `◆ moonshot` | One availability bar per provider pool — Claude, Codex and (1.5.0) Kimi Code's, whose vendor is Moonshot AI: ten cells of usable `▰`, cooling `▱`, benched `▪` and dead `·` accounts (ASCII `=` `~` `:` `.`), then the usable count of the configured ones (` ◆ anthropic ▰▰▰▰▰▰▱▪·· 12/19`), then — with the token — the worst weekly or 5-hour burn and the soonest reset. A daemon with no Kimi account shows ` ◆ moonshot ·········· 0/0`, which is no alarm; `no data` means the pulse did not report the pool (a pulse from before 1.5.0 still running: [RUNBOOK](RUNBOOK.md#harnesses)). There is no Grok row: Throttle has no xAI pool |
|
|
2104
|
+
| `▣ productbox` | The host: CPU and memory utilization, the 1-minute load, the highest pressure (PSI) figure |
|
|
2105
|
+
| `▣ <container> …` | The three workspace containers using the most memory |
|
|
2106
|
+
|
|
2107
|
+
At 18–24 rows the block is three rows — Throttle, the provider with the smallest usable share (a pool with nothing
|
|
2108
|
+
configured is never that one), the host — and smaller panes show none. Parts that do not fit are dropped from the end. A row turns amber when overflow spend reaches its alert
|
|
2109
|
+
level, a provider has no usable account, memory is fully stalled, memory use reaches 90 % or the CPU 95 % (its lead
|
|
2110
|
+
mark turns amber even when that part was dropped); red when the daemon does not answer. The whole block is dim, with a
|
|
2111
|
+
`?` after `throttle`, when its figures are frozen because statusd or the Throttle pulse stopped: the host's more than a
|
|
2112
|
+
minute old, the pulse's more than 90 s (it rewrites every 60 s and is read up to about 84 s later).
|
|
2113
|
+
|
|
2114
|
+
**`i`** folds the block to one line, ` fleet ✓ · throttle 53% · cpu 42% · mem 61%` (`⚠` something amber, `✗` Throttle
|
|
2115
|
+
down), and unfolds it again; the choice is kept in your profile (`look.fleet_collapsed`), so every device you own
|
|
2116
|
+
follows it. The `?` key sheet takes the block's rows while it shows. On the phone the same block is drawn above the
|
|
2117
|
+
footer.
|
|
2118
|
+
|
|
2119
|
+
The strip row keeps its economy line and gives way only when the block already shows the same facts: while the
|
|
2120
|
+
unfolded block's Throttle row shows the overflow money and every part after it, the strip row leaves the economy line
|
|
2121
|
+
to it. At the default 36 columns that row drops `spill` to fit, so the strip keeps the line; a wider pane shows the
|
|
2122
|
+
whole row.
|
|
2123
|
+
|
|
2124
|
+
Where the figures come from: the host's own `/proc` (statusd, skipped while the host is under critical pressure) and a
|
|
2125
|
+
small pulse that runs inside the container where the Throttle daemon runs — the daemon listens only on that container's
|
|
2126
|
+
loopback, so the host cannot ask it itself. `lanes install-container` starts that pulse in the daemon's container (the
|
|
2127
|
+
one `LANES_THROTTLE_CONTAINER` in the server's `~/.config/lanes/site.conf` names, else the container of the Throttle
|
|
2128
|
+
tab) and every server update ships it again. The pulse only reads (GET), never changes the daemon, and writes one small
|
|
2129
|
+
file.
|
|
2130
|
+
|
|
2131
|
+
**Money and burn are opt-in.** Without a token the pulse reads only the daemon's open routes (readiness and metrics):
|
|
2132
|
+
no spend, no burn figures. The operator turns them on once, per daemon container: `lanes throttle adopt-token
|
|
2133
|
+
<container>` copies the container's own local Throttle client token (the local proxy bearer in its
|
|
2134
|
+
`~/.claude/settings.json`, never an Anthropic credential) into the pulse's token file (`~/.lz/throttle-token`, mode
|
|
2135
|
+
0600). Nothing does it automatically.
|
|
2136
|
+
|
|
2137
|
+
**The dashboard link is off until it is safe.** History (days, weeks) belongs to the fleet-management Grafana, not to
|
|
2138
|
+
the cockpit. When the server's `~/.config/lanes/site.conf` names `LANES_FLEET_DASHBOARD_URL=https://…`, the
|
|
2139
|
+
`▣ productbox` row becomes a link to it and the phone's menu gains a **Fleet** row. It stays unset until an
|
|
2140
|
+
authenticated Grafana deployment exists; only an `https://host[:port][/path]` URL is accepted, and no Lanes command
|
|
2141
|
+
ever writes it. `lanes doctor` says which it is.
|
|
2142
|
+
|
|
2143
|
+
The host the block names is the Lanes server itself: the Hetzner machine `productbox` (tailnet address 100.91.241.52,
|
|
2144
|
+
the address the bundled `beyondwork` site profile connects to), not `devbox3`.
|
|
2145
|
+
|
|
1785
2146
|
### Economy projection (opt-in, operator gate)
|
|
1786
2147
|
|
|
1787
2148
|
`lanes-economy.service` polls the Throttle daemon's control port (`GET /status` and `/sessions`, default
|
|
@@ -1797,7 +2158,7 @@ systemctl --user enable --now lanes-economy.service
|
|
|
1797
2158
|
|
|
1798
2159
|
`lanes economy --once [--dry-run] [--status-file F --sessions-file F]` runs one poll; stderr names each source it
|
|
1799
2160
|
could not read. Settings: `LANES_THROTTLE_CONTROL` (base URL, or `none` for no request), `LANES_THROTTLE_TOKEN_FILE`
|
|
1800
|
-
(a bearer in a file), `LANES_THROTTLE_CONTAINER` (read the log through that container's home volume),
|
|
2161
|
+
(a bearer in a file — `/status` and `/sessions` need it on loopback too, else they answer `HTTP 401`), `LANES_THROTTLE_CONTAINER` (read the log through that container's home volume),
|
|
1801
2162
|
`LANES_THROTTLE_LOG_DIR` (where `proxy.log` lives). A daemon bound to loopback inside its container is not reachable
|
|
1802
2163
|
from the host: use `LANES_THROTTLE_CONTROL=none` there (the log fold still works). Details:
|
|
1803
2164
|
[RUNBOOK](RUNBOOK.md#economy-projection-opt-in).
|
|
@@ -1845,7 +2206,7 @@ you configure, so enabling it is your decision. Not yet run against a real ntfy
|
|
|
1845
2206
|
| `topic` | required | `[A-Za-z0-9_-]{1,64}`; on a public server the topic name is the only secret |
|
|
1846
2207
|
| `token_file` | unset | A file holding an ntfy access token (an unreadable file disables sending; written, rotated or made readable later, it is picked up within 3 s) |
|
|
1847
2208
|
| `quiet_when_device_active_s` | `120` | Presence window; `0` never suppresses — except for the one person present when their profile sets a `notify.quiet_s` of its own |
|
|
1848
|
-
| `action_url` | unset | A URL template over `{lane}`; becomes the tap target and an **Open lane** button |
|
|
2209
|
+
| `action_url` | unset | A URL template over `{lane}`; becomes the tap target and an **Open lane** button. `{doc}` is the docs page of the document a `published` push names (`/lanes-docs/d/<id>`, so `https://<server>:8443{doc}` opens it) and empty in a needs-you push. The docs service sends a `published` push (lane name, kind and size, never the title) when it stores a document an agent published, at most one per lane per minute, muted like the others while you are at a desk |
|
|
1849
2210
|
|
|
1850
2211
|
Nothing is sent when the status did not change, when the service just started (no burst on restart), or when a desk
|
|
1851
2212
|
device was present within the presence window: a `lanes attach` from any device, or a lane opened from a desk
|
|
@@ -1975,6 +2336,7 @@ All units run in `lanes.slice` and are rendered by the server installer from `la
|
|
|
1975
2336
|
| `lanes-gc.timer` / `.service` | Daily | Server install | `lanes gc`: truncate zmx logs over 5 MB, drop stale dynamic resolve files, prune old npm-staged trees — each folder keeps its 3 newest and every older tree `current`, `current.previous`, a view layout, a running daemon's run record or a process of this account uses; none when `/proc` cannot be read ([INSTALL](INSTALL.md#6-updates-optional-operations-and-removal)) |
|
|
1976
2337
|
| `lanes-update.timer` / `.service` | 15 min after boot, then every 30 min (+0–5 min) | Server install (the full one: `--update-only` enables nothing — `systemctl --user enable --now lanes-update.timer` on a server set up before 1.2.0) | `lanes update --timer`: install the newest npm release when it is newer ([Update](#update)); a checkout server is left alone; stopped after 1 h |
|
|
1977
2338
|
| `lanes-enrold.service` | Always on | Server install (enabled and restarted by every one) | Enrolment service on `127.0.0.1:8184`, reached as `https://<host>:8443/enrol/*`: invite redemption, enrolment requests, `/enrol/pending` approvals ([RUNBOOK](RUNBOOK.md#enrolment-service-lanes-enrold-w8-a3)) |
|
|
2339
|
+
| `lanes-docsd.service` | Always on | Server install (enabled and restarted by every one) | Docs service on `127.0.0.1:8185`, reached as `https://<host>:8443/lanes-docs/`: login-bound documents, sandboxed artifacts and uploads with receipts; refuses every request until `lanes enrol mode request` is pinned ([RUNBOOK](RUNBOOK.md#docs-service-lanes-docsd-w10-w3)) |
|
|
1978
2340
|
| `lanes-web.service` | Always on | `enable-web.sh` (via `lanes web` / `enable-phone.sh`) | Zellij web server on `127.0.0.1:8082` |
|
|
1979
2341
|
| `lanes-mobile.service` | Always on | `enable-web.sh` | Caddy frontend on `127.0.0.1:8083` |
|
|
1980
2342
|
| `lanes-studio-host.service` | Always on | `enable-studio-host.sh` (opt-in) | The co-located Throttle Studio Host |
|
|
@@ -2025,8 +2387,9 @@ Clients also get `lanes-helper.service` and `lanes-upload.path`/`.service` (Linu
|
|
|
2025
2387
|
| Editor | `lanes cursor-commit [ws…] [--push]` | Section 6 |
|
|
2026
2388
|
| Containers | `lanes install-container <name> [--no-mcp]` | Install the `lane` contract (section 2) and the agents' MCP entries for `lane-mcp` (section 7, "Agent tools (MCP)"); `--no-mcp` leaves both out |
|
|
2027
2389
|
| Containers | `lanes onboard <login> --container <name> [--workspace <id> --repo <path> [--title T]] [--write] [--no-mcp]` | One user's container in one command: board, contract, Claude and Codex hooks, Throttle attachment, canonical recipes, sync, doctor, and a receipt (a dry run until `--write`; section 2) |
|
|
2028
|
-
| Containers | `lanes hooks install --agent claude\|codex (--container <name> \| --home <dir>) [--dry-run \| --write]` | Section 7 |
|
|
2390
|
+
| Containers | `lanes hooks install --agent claude\|codex\|kimi\|grok\|grok-cli (--container <name> \| --home <dir>) [--dry-run \| --write]` | Section 7 ([Harnesses](#harnesses)) |
|
|
2029
2391
|
| Uploads | `lanes upload <ws> \| --active <device> [--paste]` | Read a tar stream into that workspace's `uploads/` |
|
|
2392
|
+
| Uploads | `lanes docs push <id>` | Print one docs-store document's receipt (any login's: you are the operator) and exit 2 `not configured`: the hook for the data service's publication surface, not built yet ([Docs](#docs)) |
|
|
2030
2393
|
| Client | `lanes hello [device] [--client-version V]` | Prove path, key and server version; ` update=<version>` when V is older |
|
|
2031
2394
|
| Client | `lanes client-config` | Site facts for clients (`LANES_*` lines from `~/.config/lanes/site.conf`) |
|
|
2032
2395
|
| Access | `lanes keys list \| add <device> <pubkey-file\|-> \| revoke <device> \| adopt [--dry-run] [--login LOGIN]` | Who may log in: the lanes-managed block of `~/.ssh/authorized_keys`, projected from `members/`; `--login` names the owner of every key adopt moves in and folds their views into that profile |
|
|
@@ -2073,7 +2436,7 @@ Server (`~/.local/state/lanes/`, `$LANES_STATE`):
|
|
|
2073
2436
|
| --- | --- |
|
|
2074
2437
|
| `registry.json`, `registry.synced`, `resolve/`, `resolve.dynamic/` | The published registry, the last sync time, per-lane targets |
|
|
2075
2438
|
| `status.json`, `health` | statusd's snapshot and the one-line health record |
|
|
2076
|
-
| `run/<name>.tree` | The tree each long-running process started from — `statusd`, `enrold`, `economy`, `notify`, `studio_host` (the enable step its enrolment timer runs) and `sidebar` (the most recently started one): one resolved path, rewritten at every start, so a record that differs from where `~/.local/share/lanes/current` points is a process still running an older version |
|
|
2439
|
+
| `run/<name>.tree` | The tree each long-running process started from — `statusd`, `enrold`, `docsd`, `economy`, `notify`, `studio_host` (the enable step its enrolment timer runs) and `sidebar` (the most recently started one): one resolved path, rewritten at every start, so a record that differs from where `~/.local/share/lanes/current` points is a process still running an older version |
|
|
2077
2440
|
| `containers.json`, `containers/` | Container init pids and home volumes |
|
|
2078
2441
|
| `devices/<device>.json` | Each known device: layout, viewports, base device, owner, look (colour, theme, glyphs, dim; `device_only` lists the keys `--device-only` kept for this device alone), desktop notifications on/off and the quick-edit editor |
|
|
2079
2442
|
| `layouts/<device>.kdl` | The layout a view session was built from |
|
|
@@ -2095,7 +2458,9 @@ Server (`~/.local/state/lanes/`, `$LANES_STATE`):
|
|
|
2095
2458
|
| `invites/<id>.json` | Invites minted by `lanes invite`: the code's hash only (never the code), who minted it, expiry, invitee, and the redemption or revocation |
|
|
2096
2459
|
| `enrol-requests/<id>.json` | Enrolment requests waiting for, or past, a member's approval (24 h) |
|
|
2097
2460
|
| `enrol.log` | The enrolment audit log, one line per attempt, invite, revocation, offboard and mode change (0600, append-only; never a code) |
|
|
2098
|
-
| `enrol-mode` | Request mode as `lanes enrol mode request\|invite` pinned it (0600; absent = auto); the enrolment service
|
|
2461
|
+
| `enrol-mode` | Request mode as `lanes enrol mode request\|invite` pinned it (0600; absent = auto); the enrolment service and the docs service read it on every request (docsd serves only while it says `request`) |
|
|
2462
|
+
| `docs/<login>/<ws>/<lane>/<id>/`, `docs/counts.json` | The docs store: each document with its `meta.json` and sha256 `receipt.json` (0700/0600; `<name>.age` when encrypted at rest); counts per lane. Written by every upload (the Docs page and `lanes upload`) and by the docs service's publish pick-up (`lane publish`, `lanes_publish`), read by the docs service ([RUNBOOK](RUNBOOK.md#docs-service-lanes-docsd-w10-w3)) |
|
|
2463
|
+
| `docs.log` | The docs audit log: one line per intake, removal, read, upload refusal, publish request and docs-service denial (0600, append-only; never content, a title or a name) |
|
|
2099
2464
|
| `web-tokens.json`, `web-tokens.lock` | Which view — and which machine — each web login token `lanes web --token` minted was for (names and creation times only, never a token; `lanes offboard` signs a machine out by it) and the lock every mint and revocation holds (section 4, "Tokens") |
|
|
2100
2465
|
| `onboard/<login>-<UTC>.json` | One receipt per `lanes onboard` run: each step with its status (section 7) |
|
|
2101
2466
|
| `update/last.json`, `update/last.log`, `update/.lock` | The last `install.sh --server --update-only` run: `{checked_at, latest, installed, previous, result, steps: [{name, rc}], log}` with `result` `ok`, `partial`, `refused` or `failed` (a step whose exit status is a warning — `lanes sync` reporting a board with an error — carries `"warning": true`; keys it does not own are kept), and that run's full output; the run lock every server install and update holds while it runs (`{pid, mode, started}` of the last one to take it). `lanes update` adds `check` — its last check, `{at, by: timer\|hand, channel, tag, latest, installed, outcome, said[, exit]}`, `outcome` one of `current`, `older`, `bad`, `unanswered`, `refused`, `installing`, `installed`, `failed` — `bad` (the release a rollback left, written with the record of its flip, before its post-commit) and `rerun` (the release whose post-commit it ran again; a rollback drops it); a rollback's run is recorded with the step `rollback` first, `partial` until its post-commit has run |
|
|
@@ -2105,7 +2470,8 @@ Server (`~/.local/state/lanes/`, `$LANES_STATE`):
|
|
|
2105
2470
|
Other server paths: `~/.config/lanes/` (`workspaces/`, `site.conf`, `notify.conf`, `update.conf` — `channel=stable`
|
|
2106
2471
|
makes `lanes update` follow the `lanes-stable` dist-tag — and the optional invite key files
|
|
2107
2472
|
`resend.key` and `tailscale-api.key`), `~/.local/share/lanes/` (pinned
|
|
2108
|
-
binaries in `bin/`, the
|
|
2473
|
+
binaries in `bin/`, the docs page's pinned Mermaid in `vendor/` — `install/enable-docs.sh`, which every server install and update runs —, the `current`
|
|
2474
|
+
link to the installed tree — the path units and layouts name —,
|
|
2109
2475
|
`current.previous`, the tree it named before the last install that moved it, and `.install.lock`, the lock installs
|
|
2110
2476
|
and unit re-renders take turns on; npm-staged `pkg/<version>/`), and
|
|
2111
2477
|
`~/.lz/<container>` (links to each
|
|
@@ -2193,12 +2559,18 @@ Server and views:
|
|
|
2193
2559
|
| `LANES_ENROL_PORT` | `libexec/enrold.py` | `8184` | The enrolment service's loopback port (the Caddyfile's `/enrol/*` route must follow) |
|
|
2194
2560
|
| `LANES_ENROL_DOMAINS` | `libexec/enrold.py` | `beyondwork.ai` | Login domains that may file an access request or approve one on `/enrol/pending` (comma or space separated; `*` = any); invites are not gated |
|
|
2195
2561
|
| `LANES_ENROL_REQUEST_MODE` | `libexec/enrold.py` | unset | `1` forces request mode on, `0` forces invite-only, over any `lanes enrol mode` pin; unset, the pin decides, and without one the first request through Tailscale Serve after a start (RUNBOOK "Enrolment service") |
|
|
2562
|
+
| `LANES_DOCS_PORT` | `libexec/docsd.py` | `8185` | The docs service's loopback port (the Caddyfile's `/lanes-docs/*` route must follow) |
|
|
2563
|
+
| `LANES_DOCS_DOMAINS` | `libexec/docsd.py` | `LANES_ENROL_DOMAINS`, else `beyondwork.ai` | Login domains the docs service answers (comma or space separated; `*` = any) |
|
|
2564
|
+
| `LANES_DOCS_AGE_RECIPIENT` | `libexec/docs_store.py` (the docs service and `lanes upload`) | unset (off) | Keep new docs-store copies as `age -r <recipient>` output; the server install refuses it without an `age` binary ([RUNBOOK](RUNBOOK.md#docs-service-lanes-docsd-w10-w3)) |
|
|
2565
|
+
| `LANES_DOCS_AGE_IDENTITY` | `libexec/docsd.py` | unset | The `age` identity file the docs service decrypts with; without it an encrypted document's download answers 409 |
|
|
2196
2566
|
| `LANES_DEVICE` | `libexec/enrol.py` (`lanes invite`) | unset | Recorded as `minted_by.device` of an invite when it is a device name (a claim, not an identity) |
|
|
2197
2567
|
| `SSH_CLIENT` | `libexec/enrol.py` (`lanes enrol approve\|deny`) | set by sshd | Its address is recorded in the approver `cli[:<device>]@<address>`; `local` without one |
|
|
2198
2568
|
| `LANES_TESTS_FORCE` | `tests/guards.py` | unset | `1` runs live suites on a busy host (on purpose only) |
|
|
2199
2569
|
|
|
2200
2570
|
`LANES_THROTTLE_*` and `LANES_NOTIFY_CONF` set in the installer's environment are rendered into their units only. The
|
|
2201
|
-
`LANES_ENROL_*` settings go in a drop-in, `systemctl --user edit lanes-enrold.service`, which a re-install keeps
|
|
2571
|
+
`LANES_ENROL_*` settings go in a drop-in, `systemctl --user edit lanes-enrold.service`, which a re-install keeps; the
|
|
2572
|
+
`LANES_DOCS_*` settings in lanes-docsd's, `systemctl --user edit lanes-docsd.service` (`lanes upload` reads the age
|
|
2573
|
+
switch from the same files).
|
|
2202
2574
|
`LANES_REPO`, `LANES_ZELLIJ` and `LANES_WS` are set by Lanes itself for its units and panes.
|
|
2203
2575
|
|
|
2204
2576
|
Client:
|
|
@@ -2248,6 +2620,7 @@ Container:
|
|
|
2248
2620
|
| `lanes preview` / `next` / `prev` / `edit` without `--focused` | `2` |
|
|
2249
2621
|
| `lanes edit --focused` | the editor's own status once a file is picked (`docker exec` passes on `lane-edit`'s) · `0` closed with `q`/`Esc`, nothing to edit, or another Edit pane in this tab now has the focus · `1` another Edit pane is open in another tab (or zellij would not focus it), zellij did not answer, a lane that is not a tmux lane, a container that is not running or has no `lane-edit`, no git checkout, docker could not be started, or another error |
|
|
2250
2622
|
| `lanes version` | `0` (a stale process is shown, not an exit status) · `2` an argument other than `--json` |
|
|
2623
|
+
| `lanes docs push <id>` | `2` not configured (the receipt printed, nothing sent) · `1` no document with that id, an id that is not one, or another argument (the usage) |
|
|
2251
2624
|
| `lanes update` | `0` nothing to do (up to date, not due, an older or bad release) or installed `ok` · `4` partial (the install's, or a post-commit run again) · `3` refused, nothing done: another update or install is running, a checkout server, no installed release, a bad `update.conf`, no release to roll back to — under `--timer` these are `0` · `1` failed: the registry gave no release, no usable recorded npx, the install failed or recorded nothing · `2` usage; `--status`: `0`, or `1` when nothing is recorded yet |
|
|
2252
2625
|
| `lanes studio-host status` | `0` only when running enrolled; `1` otherwise; `2` usage |
|
|
2253
2626
|
| `lanes invite <name>` | `0` minted (the kit printed; a failed email or Tailscale mint is a note, not an error) · `2` not on a terminal, nothing minted · `1` error |
|
|
@@ -2256,9 +2629,11 @@ Container:
|
|
|
2256
2629
|
| `enable-studio-host.sh` | `0` running or waiting for enrolment · `2` usage · `3` typed refusal · `1` failure |
|
|
2257
2630
|
| `enable-phone.sh` | `0` · `2` usage · `64` not in your own terminal, Tailscale Serve refused, or the running `lanes-web` reads another token database (from `lanes web`) · `1` a `lanes web` error |
|
|
2258
2631
|
| `enable-web.sh` | `0` · `2` usage · `1` Caddy missing |
|
|
2632
|
+
| `enable-docs.sh` | `0` the pinned Mermaid installed or already there · `1` a prerequisite or the download failed · `2` usage · `3` refused (no valid pin, a digest mismatch, the member missing): nothing installed |
|
|
2259
2633
|
| `lanes-launch` | `64` invalid name or option (it never exits on its own otherwise); server exits 64–78 are refusals it retries after 60 s |
|
|
2260
2634
|
| `lanes-window` | `64` invalid suffix, or `--device-only` without `--ascii`/`--light`/`--dark` · `69` no Ghostty (macOS) or neither Ghostty nor Foot (Linux) |
|
|
2261
2635
|
| `lanes-upload` | `64` usage · `66` no such file or empty clipboard · otherwise the transfer's status |
|
|
2262
2636
|
| `lane status` | `1` when the lane is not running |
|
|
2637
|
+
| `lane publish <file>` | `0` the request is written (the receipt follows within about 5 s) · `1` refused or a usage error (one line on stderr) |
|
|
2263
2638
|
| `lane-edit <file>` (container) | the editor's own status · `2` usage · `127` no editor on PATH; on a terminal both refusals wait for `Enter` first |
|
|
2264
2639
|
| Live test suites (`tests/*.py`) | `75` host busy, not started |
|