@vibelord/lanes 1.1.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +36 -5
  2. package/bin/lanes-install.mjs +166 -13
  3. package/lib/plan.mjs +113 -3
  4. package/package.json +1 -1
  5. package/payload/lanes/README.md +47 -23
  6. package/payload/lanes/VERSION +1 -1
  7. package/payload/lanes/bin/lanes +2753 -202
  8. package/payload/lanes/client/ghostty/lanes-palette-light.conf +19 -19
  9. package/payload/lanes/client/ghostty/lanes-palette.conf +19 -19
  10. package/payload/lanes/client/lanes-launch +289 -40
  11. package/payload/lanes/client/lanes-window +15 -3
  12. package/payload/lanes/container/hooks/claude-settings.json +1 -0
  13. package/payload/lanes/container/lane +147 -92
  14. package/payload/lanes/container/lane-edit +100 -0
  15. package/payload/lanes/container/lane-mcp +1150 -0
  16. package/payload/lanes/container/lane-signal +109 -29
  17. package/payload/lanes/container/lane_core.py +1318 -0
  18. package/payload/lanes/container/vimrc +31 -0
  19. package/payload/lanes/docs/ARCHITECTURE.md +793 -16
  20. package/payload/lanes/docs/DESIGN.md +127 -46
  21. package/payload/lanes/docs/INSTALL.md +175 -18
  22. package/payload/lanes/docs/RUNBOOK.md +968 -76
  23. package/payload/lanes/docs/SHARING.md +3 -2
  24. package/payload/lanes/docs/USERGUIDE.md +967 -150
  25. package/payload/lanes/holders.lock +5 -0
  26. package/payload/lanes/install/enable-docs.sh +78 -0
  27. package/payload/lanes/install/enable-web.sh +2 -0
  28. package/payload/lanes/install/host-install.sh +197 -7
  29. package/payload/lanes/install/services.py +148 -18
  30. package/payload/lanes/install.sh +68 -7
  31. package/payload/lanes/libexec/agent_state.py +74 -1
  32. package/payload/lanes/libexec/cursor_sync.py +6 -0
  33. package/payload/lanes/libexec/device_sync.py +47 -10
  34. package/payload/lanes/libexec/docs_store.py +995 -0
  35. package/payload/lanes/libexec/docsd.py +2010 -0
  36. package/payload/lanes/libexec/economy_poll.py +1 -0
  37. package/payload/lanes/libexec/enrold.py +41 -22
  38. package/payload/lanes/libexec/keys.py +54 -23
  39. package/payload/lanes/libexec/lanes_lib.py +620 -22
  40. package/payload/lanes/libexec/mdlite.py +575 -0
  41. package/payload/lanes/libexec/members.py +21 -2
  42. package/payload/lanes/libexec/notify.py +169 -29
  43. package/payload/lanes/libexec/registrations.py +216 -18
  44. package/payload/lanes/libexec/session_actions.py +182 -14
  45. package/payload/lanes/libexec/sidebar +11 -3
  46. package/payload/lanes/libexec/sidebar.py +2232 -365
  47. package/payload/lanes/libexec/statusd.py +231 -12
  48. package/payload/lanes/libexec/studio_host.py +8 -1
  49. package/payload/lanes/libexec/tokens.py +148 -35
  50. package/payload/lanes/libexec/tokens.sh +86 -2
  51. package/payload/lanes/libexec/update.py +1044 -0
  52. package/payload/lanes/libexec/uploads.py +538 -0
  53. package/payload/lanes/libexec/users.py +614 -0
  54. package/payload/lanes/libexec/view.sh +34 -6
  55. package/payload/lanes/systemd/lanes-docsd.service +29 -0
  56. package/payload/lanes/systemd/lanes-ensure.service +3 -0
  57. package/payload/lanes/systemd/lanes-gc.service +1 -1
  58. package/payload/lanes/systemd/lanes-update.service +17 -0
  59. package/payload/lanes/systemd/lanes-update.timer +17 -0
  60. package/payload/lanes/web/Caddyfile +24 -1
  61. package/payload/lanes/web/README.md +165 -28
  62. package/payload/lanes/web/browser.js +4 -8
  63. package/payload/lanes/web/docs.css +52 -0
  64. package/payload/lanes/web/docs.js +36 -0
  65. package/payload/lanes/web/mobile.css +17 -17
  66. package/payload/lanes/web/mobile.js +45 -32
  67. package/payload/lanes/web/upload.css +17 -0
  68. package/payload/lanes/web/upload.js +84 -0
  69. package/payload/lanes/workspaces.example/README.md +3 -1
  70. package/payload/lanes/zellij/config.kdl +106 -56
  71. package/payload/manifest.json +6 -3
@@ -4,7 +4,7 @@ Lanes keeps long-running agent terminals on one Linux workspace host and lets ev
4
4
  same terminals: a terminal window on a Mac or Linux desktop, Safari on a phone, a browser on a laptop or tablet, and
5
5
  the Cursor / VS Code terminals inside the workspace containers. This guide is the day-to-day reference for operators
6
6
  and teammates, and the install and update path for the administrator. It describes the tree it ships in; run
7
- `cat lanes/VERSION` (or `lanes hello` on the server) for the version.
7
+ `cat lanes/VERSION` (or `lanes version` on the server) for the version.
8
8
 
9
9
  Companion documents: [README](../README.md) (overview and quick start) · [INSTALL](INSTALL.md) (full setup) ·
10
10
  [RUNBOOK](RUNBOOK.md) (operations and incidents) · [ARCHITECTURE](ARCHITECTURE.md) (how the parts fit) ·
@@ -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
 
@@ -69,7 +69,7 @@ The server's catalog orders the tabs.
69
69
  | Kind | Comes from | In the sidebar |
70
70
  | --- | --- | --- |
71
71
  | Repository lane | A terminal declared in a repository's `.vscode/terminals.json` | The workspace's list; no close control |
72
- | Registered session | An observed session adopted with **Add to workspace** or `lanes register` (saved in `registered-sessions.json`) | The workspace's list; no close control |
72
+ | Registered session | An observed session adopted with **Add to workspace** or `lanes register` (saved in `registered-sessions.json`) | The workspace's list; no close control; `u` (or `lanes unregister`) moves it back to Unregistered |
73
73
  | Unregistered session | Any other tmux session observed in the workspace's container: a **New shell**, a session you started by hand, a Throttle Studio `st_…` session | **Unregistered (N)**; can be opened, renamed, added, promoted or closed |
74
74
 
75
75
  **The terminal status** (sidebar column 33) is one of six words `lanes-statusd` publishes, plus `helper` for a tool
@@ -97,11 +97,17 @@ attention sections:
97
97
  | Needs you | `✗` | `X` | errored | The turn failed |
98
98
  | Needs you | `◇` | `?` | needs_input | The turn finished; waiting for your prompt |
99
99
  | Working | `▸` | `>` | working | The agent is working |
100
- | Done | `✓` | `v` | done | The turn finished; stays until you open the lane from this device |
100
+ | Done | `✓` | `v` | done | The turn finished; stays until you open the lane — from any device of yours when the device has an owner, else from this one |
101
101
  | (rest) | `_` | `_` | shell | The harness ended into a shell |
102
102
  | (rest) | `·` | `.` | idle | The harness is up, no turn |
103
103
  | (rest) | blank | blank | — | A plain shell, no agent signal |
104
104
 
105
+ A lane moves up into a section (Needs you above Working above Done) at once, and down only after the agent has
106
+ been doing the less urgent thing for 8 s (leaving Needs you) or 5 s (leaving Working or Done), so a lane whose
107
+ agent flickers between two states stays where it is. A lane whose agent signal disappears keeps its glyph for 5 s.
108
+ Inside a section lanes keep the order in which they entered it, and the list does not scroll under your cursor
109
+ (section 3, "The sidebar").
110
+
105
111
  **What Throttle adds.** Lanes runs without Throttle. With the opt-in economy poller (section 8) the sidebar shows a
106
112
  fleet economy strip (overflow spend against the daily cap, new 429s, model tiers, spill, free accounts) and one
107
113
  economy cell per lane (`⚠` starving, `↓` downgraded, `$` mostly paid). Throttle's **Shared lanes** console and the
@@ -148,6 +154,7 @@ A member of the site sends you an invite: five lines from `lanes invite` ("Invit
148
154
  | Step | The installer prints |
149
155
  | --- | --- |
150
156
  | Site | `site beyondwork, inferred from the invite code's prefix LN` (npm only; an explicit `--site` wins) |
157
+ | Release | Only when the server runs a newer release than the package you ran (npm only; see below): `server runs 1.2.0: fetching that installer first (this one is 1.1.0; nothing is installed yet, and the invite is kept for it)`, then `lanes-install: the server runs 1.2.0: npx --yes @vibelord/lanes@1.2.0, with the same options` — the server's release then prints every row below and redeems the code |
151
158
  | Tailscale | `tailscale: connected as <login> (<address>)`, after opening the login when needed ("Tailscale" below) |
152
159
  | SSH alias | `ssh: host alias 'lanes' -> dev@<MagicDNS name> (fallback lanes-ip -> <tailnet address>), key …` |
153
160
  | Invite | `invite: redeemed — this machine's key is now authorized for <login> (device <device>)` |
@@ -158,6 +165,19 @@ request body (never on a command line) and never prints or stores it, and a code
158
165
  with echo off. A code whose prefix belongs to another site is refused before anything is installed. Phone and
159
166
  browser views: section 4. Uploads: section 5.
160
167
 
168
+ The code is redeemed by the server's own release (W9 K3). With an invite, the npm installer first asks the site's
169
+ enrolment service which release the server runs (`GET /enrol/site`, one call of at most 15 s; on a tailnet site only
170
+ when this machine is already connected, so join the tailnet first, step 1 — it never opens the Tailscale login for
171
+ this). When the server runs a newer release than the package, that run ends before anything is installed, sent or
172
+ redeemed, and the wrapper runs `npx --yes @vibelord/lanes@<release>` with your options instead — the code passes
173
+ through the environment, not a command line — once: that installer never asks again. When it cannot, the wrapper says
174
+ why in one line and the package you ran installs and redeems the code as before: no npx with its npm on the PATH, npm
175
+ not answering, or a release npm does not have yet (`lanes-install: the server runs 1.2.0, which npm does not have
176
+ (yet): installing 1.1.0 from this package instead` — a server run from a checkout names its `VERSION` before the
177
+ release is published). A window then updates that client by itself once npm has the release ([Versions](#versions)).
178
+ `LANES_NO_SELF_UPDATE=1` (or `LANES_NO_AUTOUPDATE=1`) installs exactly the package you ran. A checkout's `./install.sh`
179
+ only says `server runs 1.2.0; this installer is 1.1.0 — going on with 1.1.0`.
180
+
161
181
  When the server already has a device of this machine's name — two laptops with one hostname, or a replaced laptop
162
182
  whose old enrolment is still there — it answers `409 (device-taken)` and uses nothing up: the installer says so and
163
183
  names the re-run, `npx @vibelord/lanes --invite <code> --device <another name>` (it remembers the name). If the old
@@ -215,12 +235,15 @@ lanes-window # open the desktop view
215
235
  | `--site NAME` | A bundled profile (`beyondwork`); a value containing `/` or ending in `.conf` is your own file |
216
236
  | `--stage-only` | Copy the bundled tree to `~/.local/share/lanes/pkg/<version>/` and print the path; run nothing |
217
237
  | `--version` | Print the package version |
218
- | `--invite CODE` | Hand the code to the installer; without `--site`, the code's prefix picks the bundled profile ("Join with an invite" above) |
238
+ | `--invite CODE` | Hand the code to the installer; without `--site`, the code's prefix picks the bundled profile ("Join with an invite" above). When the server runs a newer release, its installer runs instead and redeems the code (`LANES_NO_SELF_UPDATE=1`: this package) |
219
239
  | `--wrapper-help` | Wrapper help only |
220
240
 
221
241
  Every other option goes to `install.sh`. The wrapper stages the tree under `~/.local/share/lanes/pkg/<version>/`
222
- (`$LANES_PKG_ROOT` overrides the parent), reuses an identical tree, and keeps the newest three versions plus any
223
- version a server unit still runs from. See [`npm/lanes/README.md`](../../npm/lanes/README.md).
242
+ (`$LANES_PKG_ROOT` overrides the parent, under any name: the wrapper's `.lanes-package.json` stamp in the tree is what
243
+ marks it as staged, so its views follow `~/.local/share/lanes/current` like any other), reuses an identical tree, and
244
+ on a client (no `~/.local/share/lanes/current`) keeps the newest three versions. A server's trees are pruned by `lanes
245
+ gc` instead, which also keeps every older tree still in use ([systemd user units](#systemd-user-units)). See
246
+ [`npm/lanes/README.md`](../../npm/lanes/README.md).
224
247
 
225
248
  ### Client from a checkout
226
249
 
@@ -305,7 +328,9 @@ the server (a profile without such a name skips this):
305
328
 
306
329
  The same for `./install.sh` and `npx @vibelord/lanes`: `0` ready · `3` installed, but this machine's key is not yet
307
330
  authorized on the server · `4` installed, but this machine is not in the tailnet, or the server is unreachable or
308
- did not answer in time · `1` error.
331
+ did not answer in time · `1` error. `install.sh` also ends with `5` — nothing installed, the server runs a newer
332
+ release — but only when the npm wrapper asks for that (`LANES_WRAPPER_REPLY`, [Join with an
333
+ invite](#join-with-an-invite)); the wrapper then runs that release, and `npx @vibelord/lanes` ends with its status.
309
334
 
310
335
  ### Server
311
336
 
@@ -323,14 +348,21 @@ sudo loginctl enable-linger <account> # administrator, once: services stay up
323
348
  | --- | --- |
324
349
  | `--check` | Check prerequisites, the user manager and Docker; print `Server prerequisites: ready`; change nothing |
325
350
  | `--no-start` | Install files and units without contacting Docker or starting services |
351
+ | `--update-only` | An update (W9 U2) of a server a full install set up (refused when none is installed): install this release, then restart what runs it — every step after the switch runs even when one before it failed — and record the run in `~/.local/state/lanes/update/last.json` ([Update](#update)) |
326
352
  | `-h`, `--help` | Usage |
327
353
 
328
354
  A normal server install verifies and installs the pinned binaries from `holders.lock` (Zellij 0.45.1, zmx 0.8.1, and
329
355
  glow 3.0.0 unless a glow is already on the view server's PATH) into `~/.local/share/lanes/bin`, adds the
330
356
  `xterm-lanesview` terminfo alias, renders every unit in `lanes/systemd/` into the user manager's unit directory,
331
- links `~/.local/share/lanes/current` and `~/.local/bin/lanes` to this checkout, creates an empty catalog only when no
357
+ links `~/.local/share/lanes/current` to this checkout (moving it last, in one step: the units, `~/.local/bin/lanes` and
358
+ the view layouts name that link, never the checkout path, and `current.previous` keeps the tree it named before; only
359
+ this installer moves the link — `lanes web` and `install/enable-studio-host.sh` re-render the units of the release it
360
+ names),
361
+ creates an empty catalog only when no
332
362
  configuration exists, runs `lanes sync`, and enables `lanes-statusd.service`, `lanes-sync.timer`,
333
- `lanes-ensure.timer`, `lanes-gc.timer` and the enrolment service `lanes-enrold.service` (restarted by every install, so
363
+ `lanes-ensure.timer`, `lanes-gc.timer`, the update timer `lanes-update.timer` (an npm-installed server then installs
364
+ each newer release itself, [Update](#update); a checkout is left alone) and the enrolment service
365
+ `lanes-enrold.service` (restarted by every install, so
334
366
  it runs the installed code; it restarts a running `lanes-mobile` too, so Caddy reads the new `/enrol/*` route). Web
335
367
  access, the Studio Host, the economy poller, push notifications,
336
368
  the `/tmp` reaper and the OOM guards stay off (section 11 lists each unit).
@@ -343,12 +375,14 @@ lanes sync
343
375
  lanes doctor
344
376
  ```
345
377
 
346
- `lanes install-container <name>` is additive. In that container's home it copies `lane`, `lane-run` and
347
- `lane-signal` into `~/.lz/bin/`, the agent hook definitions into `~/.lz/hooks/`, and `env.sh` into `~/.lz/env.sh`;
378
+ `lanes install-container <name>` is additive. In that container's home it copies `lane`, `lane-run`, `lane-signal`
379
+ and `lane-edit` into `~/.lz/bin/`, the quick-edit `vimrc` into `~/.lz/vimrc`, the agent hook definitions into
380
+ `~/.lz/hooks/`, and `env.sh` into `~/.lz/env.sh`;
348
381
  it creates `~/.tmp` and appends one marker-guarded line (`# lanes-env`) to the login files (`.bash_profile` or
349
382
  `.profile`, `.bashrc`, and `.zshrc` when it exists) so new shells keep temp files off the RAM-backed `/tmp`; it copies
350
383
  the pinned zmx binaries without overwriting a running one; and it links `~/.lz/<container>` on the host. It installs
351
- no agent hook (section 7).
384
+ no agent hook (section 7) and no editor: `Ctrl-Alt-e` uses one the container already has, and vi is the floor
385
+ ([section 7](#the-in-container-lane-command)).
352
386
 
353
387
  For a new user's container, `lanes onboard <login> --container <name> --workspace <id> --repo /home/coder/<repo>`
354
388
  prints every step and changes nothing. It checks the container first, so an unknown container stops it before the
@@ -401,38 +435,179 @@ redeemed from a second machine yet.
401
435
 
402
436
  | Where | Update | Then |
403
437
  | --- | --- | --- |
404
- | Client (npm) | `lanes-launch update`, which runs `npx --yes @vibelord/lanes@latest` with every choice in `~/.config/lanes/install-options` given again as its flag (the command is printed first; flags you add follow them and win, e.g. `lanes-launch update --no-palette`); or `npx @vibelord/lanes@latest` yourself (your remembered options are repeated; add a flag only to change one) | `npx @vibelord/lanes --check` |
438
+ | Client (npm) | Nothing, usually: a window updates its client itself when the server is newer and npm has the server's release, at most every 6 h ([Versions](#versions); `LANES_NO_AUTOUPDATE=1` turns that off). By hand: `lanes-launch update`, which runs `npx --yes @vibelord/lanes@latest` with every choice in `~/.config/lanes/install-options` given again as its flag (the command is printed first; flags you add follow them and win, e.g. `lanes-launch update --no-palette`); or `npx @vibelord/lanes@latest` yourself (your remembered options are repeated; add a flag only to change one) | `npx @vibelord/lanes --check` |
405
439
  | Client (checkout) | `git pull`, then `./install.sh` (`lanes-launch update` refuses a client that runs from a checkout: npm would replace it with a copy) | `./install.sh --check` |
406
- | Server (npm) | `npx @vibelord/lanes@latest --server` (runs from `~/.local/share/lanes/pkg/<version>/`; older trees a service still uses are kept) | the restarts below |
407
- | Server (checkout) | `git pull` in the installed checkout, then `./install.sh --server` (configured workspaces and agent sessions are preserved) | the restarts below |
408
-
409
- After a server update:
440
+ | Server (npm) | Nothing, usually: `lanes-update.timer` installs each newer release within about 30 min of its publish (below) — once it is enabled: the full `--server` install enables it, an update with `--update-only` never does, so a server set up before 1.2.0 needs `npx @vibelord/lanes@latest --server` once, or `systemctl --user enable --now lanes-update.timer`. By hand: `lanes update --now`, or `npx @vibelord/lanes@latest --server --update-only` (runs from `~/.local/share/lanes/pkg/<version>/`; older trees a service still uses are kept), which makes the restarts below itself; without `--update-only` the full install runs | `lanes version`; `lanes update --status`; `lanes doctor` |
441
+ | Server (checkout) | `git pull` in the installed checkout, then `./install.sh --server --update-only` (or the full `./install.sh --server`; configured workspaces and agent sessions are preserved) | the restarts below |
442
+
443
+ After a server update — with `--update-only` the installer runs these steps itself once the new release is switched
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, docs, `lanes-mobile`, `lanes-web` and the Studio Host — restarted,
446
+ not re-checked as `enable-studio-host.sh` does; one that is off stays off), `lanes install-container` for every
447
+ container `lanes sync` recorded, `lanes reload-views` and `lanes ensure --if-layout-changed`. A step that fails is
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
449
+ previous lanes sync keeps — is reported as `WARN` and leaves the result `ok`: fix the board, not the update. The run
450
+ ends `ok`, `partial` (installed; a step after the switch failed — run it by hand), `refused` (nothing changed: a
451
+ prerequisite said no, no release is installed yet — run the full `--server` install first, it enables what an update
452
+ restarts — or another install or update is running, in which case nothing is recorded either) or `failed` (nothing was
453
+ installed); `~/.local/state/lanes/update/last.json` keeps the result and each step's exit status, `update/last.log`
454
+ every step's output. One install or update runs at a time. After a full install, or for a step `--update-only`
455
+ reports as `FAIL`:
410
456
 
411
457
  - `systemctl --user restart lanes-statusd.service` (the status observer runs the old code until restarted).
458
+ `lanes version` then lists statusd among the names before its `✓`; each name it still marks `✗` started before
459
+ the update: `runs 1.1.0 ✗` after an npm update (it runs the previous `pkg/<version>` tree), `started before its tree
460
+ changed ✗` after a checkout update (the checkout keeps its path; `git pull` gave the code a newer time than that
461
+ start).
412
462
  - When web unit definitions or the pinned Zellij changed: `systemctl --user restart lanes-web.service
413
463
  lanes-mobile.service`; browsers reconnect.
414
464
  - When the Studio Host is enabled: re-run `sh install/enable-studio-host.sh` (it checks, then restarts the Host).
415
465
  - Re-run `lanes install-container <container>` to update the in-container `lane` contract; an older `lane` refuses a
416
466
  newer registry with exactly that hint.
417
467
  - Existing view sessions keep their layout. `lanes upgrade-session` (also run by every `lanes sync`) adds new tabs and
418
- renames titles without a rebuild; a changed pane layout needs `lanes rebuild <device>` (agents are never touched).
468
+ renames titles without a rebuild, and closes the tab of a workspace your profile hides; a changed pane layout needs
469
+ a rebuild (agents are never touched). The update's last step, `lanes ensure --if-layout-changed`, makes it where it
470
+ can: a view whose tabs the new release lays out differently is rebuilt when no client is attached to it; one you are
471
+ looking at keeps running and its sidebar footer says `update ready · reload (r)` — press `r` in the sidebar (or run
472
+ `lanes rebuild <device>`) when it suits you: the window reconnects. Close the window instead and
473
+ `lanes-ensure.timer` rebuilds it within a minute. A full `--server` install does not run that step: run `lanes
474
+ ensure --if-layout-changed` yourself afterwards.
475
+ A layout built by 1.2.0 or later names its sidebar and viewports through `~/.local/share/lanes/current`, so running
476
+ views move onto the new release by themselves: each sidebar notices the new version within 30 s and, as soon as
477
+ nothing is open on it (no prompt, filter being typed, armed upload strip, picker, key sheet, action, undo offer,
478
+ upload or `r` rebuild on its way), saves its cursor, scroll, opened-lane marks and filter and restarts from the new
479
+ tree (a ~100 ms repaint). The update's `lanes reload-views` tells each viewport (`view.sh`) that can take it to
480
+ re-execute itself from `current` the next time its lane's viewer ends by itself — a detach, a switch to another
481
+ lane, the end of a status screen — never by cutting an attached lane (by hand: `kill -USR2 <pid>`, its pid in
482
+ `~/.local/state/lanes/views/<device>/<workspace>.<slot>.pid`; only a viewport of 1.2.0 or later — an older one ends
483
+ on that signal, so `lanes reload-views` names it and leaves it alone). A layout built before names its
484
+ `pkg/<version>` tree and stays on it until `lanes rebuild <device>` (`lanes doctor` warns; its footer says ` server …
485
+ · view … — ?`). Either kind keeps its tabs: `lanes sync` recognises them by the file they run.
486
+ - `lanes doctor` then names whatever still runs the previous code, each with its fix: a process that runs with an
487
+ older run record, or with none at all (it was started by a release before the records, such as a statusd or a
488
+ sidebar still running from 1.1.0), a device layout that runs the old tree (`lanes rebuild <device>`),
489
+ a unit whose `ExecStart` does (re-run the server installer). See [`lanes doctor`](#lanes-doctor).
419
490
 
420
491
  Back up the catalog (`~/.config/lanes/workspaces/`, including `registered-sessions.json`) and the running checkout
421
- before a server update. To roll back, restore those files; do not rebuild device sessions as a rollback shortcut.
492
+ before a server update. An npm-installed server rolls back with `lanes update --rollback` (below); a checkout, by
493
+ checking out the previous commit and running `./install.sh --server --update-only`. Do not rebuild device sessions as a
494
+ rollback shortcut.
495
+
496
+ **Automatic server updates (W9 U3).** A full `--server` install enables `lanes-update.timer`: 15 minutes after a boot,
497
+ then every 30 minutes (each time up to 5 minutes later, at random), it runs `lanes update --timer`. On a server the npm
498
+ installer set up (`~/.local/share/lanes/current` names a staged `pkg/<version>` tree), that:
499
+
500
+ 1. runs the post-commit of the installed release again when the last update ended `partial` (from the timer once per
501
+ release, since a cause that stays would restart every service every 30 minutes; by hand every time);
502
+ 2. asks the npm registry which release the dist-tag `latest` names — or `lanes-stable`, with the line `channel=stable`
503
+ in `~/.config/lanes/update.conf` (`#` comment lines; any other channel is refused and nothing is asked) — unless a
504
+ check answered less than 20 minutes ago (`lanes update --now` asks anyway);
505
+ 3. installs it when it is newer than the installed release (major.minor.patch as numbers: never a downgrade) and not
506
+ the release a rollback left (`bad`), with the npx the npm installer recorded when it installed the server
507
+ (`~/.local/state/lanes/update/npx`; a unit's PATH has no Node.js): `npx --yes @vibelord/lanes@<release> --server
508
+ --update-only`, the update described above. The economy poller's, the notifier's and `LANES_WORKSPACES` settings
509
+ the installed units carry are given to it again, so the units it renders keep them.
510
+
511
+ Only the full install enables the timer: an update (`--update-only`) enables nothing, the timer included. A server set
512
+ up before 1.2.0, the first release with `lanes update`, and updated since with `npx @vibelord/lanes@latest --server
513
+ --update-only` therefore has the timer off and installs nothing by itself — run the full `npx @vibelord/lanes@latest
514
+ --server` once, or `systemctl --user enable --now lanes-update.timer`. Until then `lanes doctor` says `⚠ updates: …
515
+ lanes-update.timer is not enabled`.
516
+
517
+ `lanes update` by hand does the same; one runs at a time. Each check is recorded in `update/last.json` as `check`
518
+ (when, which channel and tag, what the registry named, what came of it), beside the install's own record; `lanes
519
+ update --status` prints that file, and `lanes doctor` says how long ago the last check was and how the last update
520
+ ended. A registry that does not answer, a recorded npx that is gone (Node.js moved or upgraded: install once by hand
521
+ with `npx @vibelord/lanes --server --update-only` from a shell whose npx works, which records it again) or an install
522
+ that fails changes nothing and exits 1 (the timer's unit then shows as failed); a release older than the installed
523
+ one, or the one marked bad, is not installed (exit 0). A server run from a checkout is left alone (`git pull`).
524
+
525
+ `lanes update --rollback` installs the release `~/.local/share/lanes/current.previous` names again, in place: this
526
+ release's `install/services.py` renders that tree's units and moves `current` back to it (no network, no npx), the
527
+ release left is marked `bad` in the record — no update installs it again; the next newer release is installed as
528
+ usual — and its post-commit runs. The mark is written the moment `current` moves, with the result `partial`: a rollback
529
+ stopped during its post-commit (Ctrl-C, a dropped SSH session) keeps the mark, and the next timer tick runs the
530
+ post-commit again (Ctrl-C and hangups wait for that moment and its record, then end the run). A second rollback, which
531
+ would put the bad release back, is refused (install it by hand with `npx @vibelord/lanes@<release> --server
532
+ --update-only` if you mean to).
422
533
 
423
534
  ### Versions
424
535
 
425
536
  - `lanes/VERSION` is the release version; the npm package version equals it (`npx @vibelord/lanes --version`).
537
+ - The sidebar: `?` shows `lanes <installed> · view <running>` on the key sheet's first row. A server update installs a
538
+ new tree and points `~/.local/share/lanes/current` at it, while each running sidebar keeps the tree it started from;
539
+ until it runs the new one its footer says ` server 1.2.0 · view 1.1.0 — press ?` (below the health banner; at 36
540
+ columns ` — ?`). The installed version is read every 30 s, so the note appears within half a minute. A checkout
541
+ install is its own tree and never shows it.
426
542
  - `lanes hello [device]` on the server prints `ok device=<device> version=<version> boards=<n>`. The client
427
543
  installer calls it to prove the SSH path, the key and the server version; an answer without that greeting is
428
- reported as "an older server?".
544
+ reported as "an older server?". A client that adds its own release, `lanes hello --client-version 1.1.0`, gets
545
+ ` update=<version>` at the end of the line when it is older than the server (`ok device=- version=1.2.0 boards=0
546
+ update=1.2.0`): the exact release to update to, compared as `lanes attach` compares (major.minor.patch, as numbers:
547
+ `1.9.3` is older than `1.10.0`). An equal or newer client, `dev` on either side, or no flag gets the plain line.
548
+ - `lanes version` on the server says which release runs there and what still runs an older one, in one line:
549
+ `lanes 1.2.0 (39998ed) · tree pkg/1.2.0 · current ✓ · statusd runs 1.1.0 ✗ · enrold notify ✓`. In order: this
550
+ tree's `VERSION` and the commit of its npm stamp (none for a checkout), the tree (`pkg/<version>` for an npm
551
+ install), whether `~/.local/share/lanes/current` names that tree (`current pkg/1.2.0 ✗` when it names another: this
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, docsd, notify, economy, studio_host and the newest sidebar).
554
+ Each `✗` there names a process that does not run the current code: its last start ran another tree
555
+ (`statusd runs 1.1.0 ✗`), or ran the current tree before its code last changed (`statusd started before its tree
556
+ changed ✗` — a checkout updated in place keeps its path, so `lanes version` compares the record's time with the
557
+ newest file of the tree's `VERSION`, `bin/` and `libexec/`; docs and web files do not count). Restart it (a stopped
558
+ service keeps its last record, so its `✗` stays until it starts again). `lanes version --json` prints the same
559
+ facts as one `lanes.version/1` document: `{schema, version, commit, tree, current, running: {<name>: {tree,
560
+ version, started, current, changed}}, client_min}` — `started` is the record's time (UTC), `changed` the time of the
561
+ tree's newest code when it is later than `started` (else null; `current` is false then), `client_min` the release
562
+ from which `lanes attach` stops telling a `lanes-launch` to update (the server's major.minor.patch; none on a `dev`
563
+ tree).
429
564
  - The client installer prints `lanes client <version>` and copies `VERSION` into `~/.local/share/lanes/client/`.
565
+ `lanes-launch --version` prints it (`lanes-launch 1.2.0`; a checkout's client reads `lanes/VERSION`; `dev` without
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 **Docs** and
568
+ **Text size**:
569
+ the shell asks the enrolment service's `/enrol/site` once when it opens (the release that service runs; the server
570
+ installer restarts it on every install). Nothing is shown when the service does not answer.
430
571
  - `lanes-launch` passes that version to `lanes attach` (`--client-version`). A client older than the server gets one
431
572
  dim line before the view opens, and again after it detaches: `lanes client 1.0.4 is older than the server (1.1.0) —
432
573
  run lanes-launch update`. A `lanes-launch` from before the flag (1.0.4 or earlier; it has no `update` verb) is told
433
574
  `update it: npx @vibelord/lanes@latest` only by a server newer than 1.0.4 — a 1.0.4 server says nothing to it.
434
575
  Only major.minor.patch is compared (`1.1.0-rc.1` counts as `1.1.0`); a `dev` version on either side, and your own
435
576
  `lanes attach` on the server, get no line.
577
+ - A window updates its own client (W9 K2). Before `lanes-launch` connects a window (a device, a workspace or
578
+ `lanes-launch lane <id>`), and at most once every 6 h per machine, it asks the server `lanes hello --client-version
579
+ <its release>` over the same SSH path (`BatchMode`, 5 s to connect, 10 s in all; the dim line `lanes 1.2.0: asking
580
+ lanes for a newer client…` shows meanwhile). When the answer ends ` update=<version>`, it first asks npm whether it
581
+ has exactly that release (`npm view @vibelord/lanes@<version> version --fetch-retries=0`, at most 15 s; the dim line
582
+ `lanes 1.1.0: the server runs 1.2.0; asking npm for it…` shows meanwhile), then runs `npx --yes
583
+ @vibelord/lanes@<version>` — exactly the server's release, never `@latest` — with the choices in
584
+ `~/.config/lanes/install-options` given again as flags (as `lanes-launch update` does, without your window's
585
+ options), then starts again as the new client with the same command line, which connects as usual. That restart
586
+ carries `LANES_AUTOUPDATED=1` and never updates again, so a window updates at most once. Each question touches
587
+ `~/.local/state/lanes-client/checked`, answered or not; windows that open at the same moment ask once between
588
+ them. It never runs for `--helper`, `ls`, `web`, `update` or `--version`, for a client run from a checkout (`git
589
+ pull` updates that one), or for one without a release. `LANES_NO_AUTOUPDATE=1` in the window's environment turns
590
+ it off.
591
+ A release npm does not have yet installs nothing and shows nothing: a server run from a checkout names its
592
+ `VERSION` as soon as it is pulled, which can be before the release is published, and no client can change that.
593
+ The window logs it (`self-update: the server runs 1.2.0, which npm does not have (yet): not installed`) and the
594
+ next question, 6 h later, asks npm again, so the clients follow the publish by themselves. Until then `lanes
595
+ attach` still says the client is older than the server.
596
+ When it cannot update, it says so in one dim line and the window goes on with the client it has:
597
+ `lanes: the update to 1.2.0 did not finish (exit 1) — this window goes on with 1.1.0; windows try again in 6 h, or
598
+ now: npx @vibelord/lanes@1.2.0`; also when npm cannot be asked (`asking npm for it failed (exit 1, ENOTFOUND)`),
599
+ when no npx with its npm is found (on the window's PATH, nor through your login shell's profile — a window a
600
+ desktop app opened has the system's minimal PATH), and when `install-options` was written by a newer installer
601
+ (`lanes-launch update` then decides). The install gets 10 minutes. When that runs out, or you close the window or
602
+ press Ctrl-C while it runs, the window ends every process of it — npx, npm, the npm wrapper, `install.sh` and what
603
+ that started — before it goes on or closes, so nothing of the install keeps writing into the window or swaps the
604
+ client folder afterwards. A question without an answer (no server, a busy one, or a server from before
605
+ 1.2.0, which takes the flag for a device name) is only logged (`self-update:` lines in `launch-<target>.log`): the
606
+ connection that follows says what is wrong with the path.
607
+ - A first install with an invite is the server's release too (W9 K3): the npm installer asks the enrolment service's
608
+ `/enrol/site` for the server's release before it installs anything and, when that is newer, runs `npx --yes
609
+ @vibelord/lanes@<release>` instead, which redeems the code ([Join with an invite](#join-with-an-invite)). The phone's
610
+ **Lanes v1.2.0** row reads the same `version`.
436
611
  - `lanes-launch web` asks the server's `lanes hello` first, on the same connection. A 1.0.x server does not know the
437
612
  options a 1.1.0 client adds (`--default-device`, `--no-qr`), so the client leaves them out there and says so in one
438
613
  line: `the server runs lanes 1.0.4, older than this client: no QR code, and the upload box of this view does not
@@ -443,8 +618,9 @@ before a server update. To roll back, restore those files; do not rebuild device
443
618
 
444
619
  - Client: `./install.sh --uninstall` or `npx @vibelord/lanes --uninstall`. Your view session on the server stays until
445
620
  an administrator removes it; it holds no agents.
446
- - Server: disable `lanes-statusd.service`, `lanes-sync.timer`, `lanes-ensure.timer`, `lanes-gc.timer` and any
447
- opt-in units you enabled; for the web endpoint also run `tailscale serve --https=8443 off`. Keep the state and the
621
+ - Server: disable `lanes-statusd.service`, `lanes-sync.timer`, `lanes-ensure.timer`, `lanes-gc.timer`,
622
+ `lanes-update.timer` and any opt-in units you enabled; for the web endpoint also run `tailscale serve --https=8443
623
+ off`. Keep the state and the
448
624
  container lane data until you have decided which sessions to retain. See
449
625
  [INSTALL §6](INSTALL.md#6-updates-optional-operations-and-removal).
450
626
 
@@ -457,19 +633,20 @@ before a server update. To roll back, restore those files; do not rebuild device
457
633
  | `lanes-window` | Your device's view in its own Ghostty instance (Linux: Ghostty, else Foot with the same window class) |
458
634
  | `lanes-window 2` | An independent second view, device `<device>-2` (another monitor) |
459
635
  | `lanes-window w2` / `lanes-window w3` | A wide view, device `<device>-w2` / `-w3`, with two or three viewports per workspace |
460
- | `lanes-window --ascii` / `--light` / `--dark` [suffix] | The same, with that look recorded for the device (section 9) |
636
+ | `lanes-window --ascii` / `--light` / `--dark` [`--device-only`] [suffix] | The same, with that look recorded as `lanes attach` records it: in the owner's profile on a device with an owner, in the device file with `--device-only` or on a device without one (section 9) |
461
637
  | macOS **Lanes** app | `~/Applications/Lanes.app` (Spotlight: Lanes) runs `lanes-window` |
462
638
  | Omarchy | `Super+Shift+L` opens or focuses the window; `Super+Shift+Alt+L` opens the `w2` wide view |
463
639
  | `lanes-launch` | Your device's view inside the current terminal |
464
640
  | `lanes-launch <workspace>` | Your device's view with that workspace's tab in front |
465
641
  | `lanes-launch <device> --viewports N` | A view with N (2–3) viewports per workspace |
466
- | `lanes-launch <device> [--ascii] [--theme dark\|light\|auto] [--glyphs M] [--color C]` | The view with a recorded look |
642
+ | `lanes-launch <device> [--ascii] [--theme dark\|light\|auto] [--glyphs M] [--color C] [--notify 1\|0] [--dim 90\|2] [--editor E]` | The view with a recorded look (and desktop notifications on or off); `--editor` is the editor of [quick edit](#quick-edit) |
467
643
  | `lanes-launch lane <lane-id>` | One lane straight in this terminal, no multiplexer (native terminal scrollback) |
468
644
  | `lanes-launch ls` | List the lanes with live status (`lanes ls` on the server) |
645
+ | `lanes-launch here [device]` | Show here the lane you opened last on another of your devices (`lanes here` on the server; default: this machine's device). An open window shows it at once with its tab in front; a closed one shows it when it next attaches. Asked for, never automatic |
469
646
  | `lanes-launch web phone` / `laptop` / `<device>` | Not a terminal view: a browser view's private URL and a login token (section 4) |
470
647
 
471
- `web`, `update`, `lane` and `ls` are verbs: a device or workspace with one of those names is not reached through
472
- `lanes-launch <name>`.
648
+ `web`, `update`, `lane`, `ls` and `here` are verbs: a device or workspace with one of those names is not reached
649
+ through `lanes-launch <name>`.
473
650
 
474
651
  Closing the window detaches; nothing on the server stops. `lanes-launch` never ends by itself: when the connection
475
652
  drops, the server drops a slow client, or the server refuses, it says so, waits with a countdown (backing off up to
@@ -477,7 +654,7 @@ drops, the server drops a slow client, or the server refuses, it says so, waits
477
654
  configuration defines, first `lanes-ip` (the server's tailnet address, when `lanes` names it by its MagicDNS name), then
478
655
  `lanes-public` (when the site defines one), and after the last it goes back to `lanes`. Exits 64–78 are server refusals and retry after 60 s. Its log is
479
656
  `~/.local/state/lanes-client/launch-<target>.log`. When the server rebuilt the view, the client reconnects straight
480
- away.
657
+ away. Before it connects, a window may first update its own client from npm ([Versions](#versions)).
481
658
 
482
659
  ### Devices and views
483
660
 
@@ -486,12 +663,69 @@ away.
486
663
  - Each view has one tab per workspace in catalog order, then SSH split workspaces and tool tabs (the **Throttle**
487
664
  monitor when a manifest configures `monitor`), then **Host** (a login shell on the server). An `sshWorkspaces`
488
665
  entry with `layout: "lanes"` sits just before Host unless its `position` is `workspace`.
666
+ - On a device with an owner the workspace tabs are that person's: the profile's `workspaces.hidden` boards have no tab
667
+ on any of their devices, and `workspaces.order` puts the boards it names first, in its order (the others follow in
668
+ catalog order; an id no board has is ignored). Every `lanes sync` and `lanes ensure` brings running views in line
669
+ — a hidden board's tab is closed (only its sidebar and view panes: the lanes keep running in their containers), a
670
+ board shown again comes back as a new tab. A profile that hides every board hides none. A device nobody owns keeps
671
+ the catalog order.
489
672
  - Phones and browsers are separate devices (`phone`, `browser`, or a name you choose); a device keeps the layout
490
673
  (desktop or phone) it was created with.
491
674
  - On the server: `lanes device list` shows each device (phone or desktop with its viewports, the owner's login once
492
675
  the machine was enrolled through an invite or a request — the current member's, so a handed-over machine shows its
493
676
  new owner — running or not);
494
677
  `lanes device forget <name>` stops `lanes ensure` recreating it; `lanes rebuild <device>` recreates a stuck view.
678
+ - A device from before enrolment has no owner (`lanes doctor` names it), so it keeps its look and opened-lane marks to
679
+ itself. `lanes device claim <device> --login <login> [--name N]` names one: it records the owner on the device and its
680
+ windows and folds their look (a `--device-only` key stays the device's) and marks into that person's profile; run
681
+ again (with or without `--name`) it keeps the
682
+ name it recorded unless `--name` gives a new one. Its running sidebars follow the profile from their next tick: the
683
+ person's look, and a Done lane opened on any of their devices leaves Done on all of them. On a device enrolled
684
+ before profiles existed the profile already wins wherever it sets a key; a claim naming the login its member record
685
+ names folds the look the device recorded then into the profile. A member record decides
686
+ whenever one exists — a claim that disagrees with it is refused and names `lanes keys owner`. `lanes user list` shows
687
+ the people lanes knows with their devices, `lanes user show <login>` one profile (what it holds: [Your
688
+ profile](#your-profile)).
689
+
690
+ ### Your profile
691
+
692
+ Once a device has an owner — its member record from an invite or a request, or one `lanes keys owner` or `lanes
693
+ device claim` named for a device from before enrolment — Lanes keeps what you choose per person, not per device: one
694
+ file on the server, `users/<login>.json`, shared by every device you own (the Mac, its `mac-2` window, `mac-phone`, a
695
+ browser view). `lanes user show <login>` prints it.
696
+
697
+ | Kept in your profile (every device you own) | Kept per device (`devices/<device>.json`, `clients/`) |
698
+ | --- | --- |
699
+ | The look: theme, colour mode, glyphs (`lanes attach --theme\|--color\|--glyphs\|--ascii`, `lanes-window --light\|--dark\|--ascii`) | A look key set with `--device-only`, and `--notify` and `--dim` (always per device) |
700
+ | 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) |
701
+ | Board order and hidden boards (`lanes workspaces order\|hide\|show`) | The layout (desktop or phone), viewports, the machine a window belongs to, its owner |
702
+ | Pinned and hidden lanes (`lanes lane pin\|unpin\|hide\|show`) | |
703
+ | Your own quiet window for pushes, `notify.quiet_s` (section 8; no verb sets it yet) | |
704
+
705
+ - **Per user or per device.** A look flag on a device with an owner goes to your profile, and every sidebar of every
706
+ device you own repaints within a tick. Add `--device-only` to keep it on that device alone (`lanes-window --light
707
+ --device-only`); the same flag without `--device-only` hands the key back to the profile. A device nobody owns keeps
708
+ everything in its own file, exactly as before profiles (section 9 has the order a sidebar paints in).
709
+ - **Claim and adopt.** A device from before enrolment follows no profile: `lanes doctor` names it, and `lanes device
710
+ claim <device> --login <login>` gives it an owner and folds its look (never a `--device-only` key) and opened-lane
711
+ marks into that person's profile, with its windows. `lanes keys adopt --login <login>` and `lanes keys owner <device>
712
+ <login>` fold the devices whose keys they move in or name the same way. A device whose member record already names
713
+ you folds a look it recorded before profiles existed with the same claim.
714
+ - **Arrange boards and lanes**, on the server, for the owner of `--device <device>` (else of `$LANES_DEVICE`) or for
715
+ `--login <login>`: `lanes workspaces hide <id…>` / `show <id…>` takes boards off your tabs and puts them back (hiding
716
+ the last visible board is refused); `lanes workspaces order <id…>` puts those boards first, in that order, the rest
717
+ in catalog order. `lanes lane pin <lane-id>` lists a lane first in its section; `lanes lane hide <lane-id>` leaves a
718
+ registered lane out of your lists (a running unregistered session is always listed); `unpin` and `show` undo them.
719
+ The sidebar keys do the same where you are looking: `p` pins or unpins the cursor lane, `h` hides it, `H` hides
720
+ this board (after `y` and Enter) and `o` shows what you hid, faint, until you press it again. Every sidebar of yours
721
+ follows within a tick (pinned lanes first, hidden ones out); the tabs take the board order and close a hidden
722
+ board's tab at the next `lanes upgrade-session` — `H` starts one at once, and every `lanes sync` (2-minute timer) and
723
+ `lanes ensure` (1-minute timer) runs one.
724
+ - **Pick up elsewhere.** `lanes-launch here` (on the server `lanes here <device>`) shows in this machine's view the lane
725
+ you opened last on any of your devices and brings its tab to the front; it is asked for, never automatic. It says
726
+ why when it cannot: nothing opened yet, the lane is gone, or its board is one you hide.
727
+ - **Safe to delete.** Without the file the look falls back to the device files and the environment, Done lanes show
728
+ again until opened, and the arrangement resets.
495
729
 
496
730
  ### Panes and viewports
497
731
 
@@ -514,20 +748,28 @@ Keys in the `shared` block work in every mode, locked included:
514
748
  | `Ctrl-Alt-↓` / `Ctrl-Alt-↑` | Point the focused viewport (the first one when the sidebar has the focus) at the next / previous lane of its workspace in registry order, wrapping. Runs `lanes next\|prev --focused` in a floating pane that closes at once; nothing is typed and the focus stays. Does nothing on Host or tool tabs |
515
749
  | `Ctrl-Alt-/` | Find: moves the focus to the sidebar and starts its filter |
516
750
  | `Ctrl-Alt-u` | Arm the sidebar's upload strip, then drop files anywhere on the window |
751
+ | `Ctrl-Alt-l` | Move the focus to the sidebar, nothing else: browse it with its keys, `Esc` gives the keyboard back to the lane |
517
752
  | `Ctrl-Alt-c` | Copy the selection |
518
753
  | `Ctrl-Alt-PageUp` / `Ctrl-Alt-PageDown` | Page up / down and enter scroll mode |
519
754
  | `Ctrl-Alt-End` | Scroll to the bottom and return to normal mode |
520
755
  | `Ctrl-Alt-s` | Zellij's session manager (floating) |
521
756
 
522
- `Ctrl-Alt-/` and `Ctrl-Alt-u` walk the focus left to the sidebar first; on a tab without a sidebar (Host, Throttle,
523
- the phone's view tab) the character goes to that tab's pane.
757
+ `Ctrl-Alt-/`, `Ctrl-Alt-u` and `Ctrl-Alt-l` walk the focus left to the sidebar (three steps, enough from the third
758
+ viewport of a wide view). `Ctrl-Alt-/` and `Ctrl-Alt-u` then type nothing: they run `lanes sidebar-key filter|upload
759
+ --focused` in a floating pane that closes at once, which tells that sidebar to start its filter or arm its strip
760
+ (cockpit v3 S4; until then they typed `/` or `U`, which on a tab without a sidebar went into the lane). On a tab
761
+ without a sidebar (Host, Throttle, the phone's view tab) they do nothing. The first keys after the chord already
762
+ reach the filter; keys typed while the floating pane is still open (a fraction of a second) go to it, not to the
763
+ sidebar. Zellij 0.45.1 matches `Ctrl-Alt-/` in the kitty keyboard protocol and xterm `modifyOtherKeys` encodings
764
+ (measured); the legacy `Esc` + `Ctrl-_` byte pair is not read as `Ctrl-Alt-/` and reaches the focused pane as is.
524
765
 
525
766
  Normal mode:
526
767
 
527
768
  | Key | Action |
528
769
  | --- | --- |
529
- | `Ctrl-Alt-g` | Lock the view; press again to unlock. The mode ribbon shows LOCKED in the urgent accent. While locked, the mode keys below and `Ctrl-Alt-m` reach the lane; the `shared` keys above stay active |
770
+ | `Ctrl-Alt-g` | Lock the view; press again to unlock. The mode ribbon shows LOCKED in the urgent accent. While locked, the mode keys below, `Ctrl-Alt-m` and `Ctrl-Alt-e` reach the lane; the `shared` keys above stay active |
530
771
  | `Ctrl-Alt-m` | Markdown preview of the focused lane (every mode except locked; see below) |
772
+ | `Ctrl-Alt-e` | [Quick edit](#quick-edit) of the focused lane's AGENTS.md, CLAUDE.md or .env.local: a one-key picker (`1`–`5`, `Enter` when one file is there, `q`/`Esc` closes), then an editor inside its container ([which one](#the-in-container-lane-command); Lanes installs none, vi is the floor), in a floating **Edit** pane that closes when the editor exits (every mode except locked). One Edit pane per view: pressing it again while one is open goes back to that editor (in another tab it names the tab instead) and opens no second one |
531
773
  | `Ctrl-Alt-p` | Pane mode |
532
774
  | `Ctrl-Alt-t` | Tab mode |
533
775
  | `Ctrl-Alt-n` | Resize mode |
@@ -566,12 +808,29 @@ Top to bottom, a desktop sidebar is:
566
808
  1. **Header**: the workspace title, then the attention counts in the column-2 glyphs, e.g. ` Sakio ◆1 ◇2 ▸6 ✓1`
567
809
  (needs approval, needs input, working, done; zeros left out; a `done` you have opened is not counted). Without
568
810
  agents it says `N lanes`. The counts are dimmed while the footer warns that status is old.
569
- 2. **Body**: **Needs you** (`◆` `✗` `◇`, most urgent then oldest first), **Working** (oldest first), **Done** (newest
570
- first), then the workspace's other lanes under its own title (`── Sakio (25) ──`). The sections appear only when at
571
- least one lane needs you, is working or is done. Then `zmx lanes (N)` when any exist, then **Unregistered (N)**
811
+ 2. **Body**: **Needs you** (`◆` `✗` `◇`, most urgent first, then in the order the lanes started needing you),
812
+ **Working** (in the order they started working), **Done** (the newest first), then the workspace's other lanes
813
+ under its own title (`── Sakio (25) ──`). Once any lane runs an agent, the **Needs you** and **Working** headers
814
+ stay on screen even when empty (`── ▸ Working (0)`, glyph dimmed; a header with lanes says its name in its section's
815
+ colour in truecolor and 256 colours), so a lane moving between them never pushes the
816
+ rows below it around; **Done** appears only when a lane is in it. A pending approval or an error sorts to the top
817
+ of Needs you while it lasts; after that (a question, an approval prompt, the question again) the lane is back in
818
+ the place it entered at. A lane moves up a section at once (a question or an approval is never delayed) and down
819
+ only after the new state has held: 8 s to leave Needs you, 5 s to leave Working or Done (the status daemon looks
820
+ every 3 s, so in practice 9 s and 6 s). An agent that asks, works for a few seconds and asks again stays in Needs
821
+ you, a working lane whose signal blinks for a cycle stays in Working, and the rows below them stay put. When a lane changes section above
822
+ your cursor, the list stays where it is and the cursor moves with its lane; the list scrolls only when that lane
823
+ would leave the pane. Then `zmx lanes (N)` when any exist, then **Unregistered (N)**
572
824
  with its `+ New shell` row. A list longer than the pane ends in `⋮ N more`; moving onto that row or clicking it
573
825
  scrolls instead of opening.
574
- 3. **Upload strip** (panes of 14 rows or more): ` ↑ uploads · click, then drop (U)` (section 5).
826
+ 3. **Upload strip** (panes of 14 rows or more): ` ↑ uploads · click, then drop (U)` (section 5). Once this
827
+ workspace's lanes have published documents (the docs cockpit) it reads ` ↑ uploads · ▤ docs 3 (U)` — the count
828
+ is every document of its lanes, whoever published it — and the whole strip is a link to the docs page,
829
+ `https://<server>:8443/lanes-docs/` (⌘-click in Ghostty and iTerm2, Ctrl-click elsewhere); a plain click still
830
+ arms the upload. The cursor row's name is a link to that lane's own documents
831
+ (`/lanes-docs/?ws=<workspace>&lane=<lane id>`) while it has any, and `d` shows the link in the footer. The links
832
+ need the server's Tailscale name (statusd learns it as the enrolment service does): without it the strip still
833
+ counts and `d` shows the path alone. What the page holds and who sees what: section 4, [Docs](#docs).
575
834
  4. **Strip row** (panes of 18 rows or more): the fleet economy line when the opt-in poller runs (section 8), otherwise
576
835
  the other workspaces that need you, e.g. ` ◆1 Worlds · ◇2 Code`; blank when there is nothing to say.
577
836
  5. A rule, then the **footer**: one line, by priority.
@@ -583,29 +842,39 @@ Unregistered section, no upload strip and no `+`, and its footer has no `x close
583
842
 
584
843
  | Column | Shows |
585
844
  | --- | --- |
586
- | 1 | `▌` the cursor |
845
+ | 1 | `▌` the cursor; in truecolor and 256 colours the whole cursor row (to column 35) also sits on a soft background |
587
846
  | 2 | The agent glyph (table in section 1); dimmed while the lane is stale |
588
- | 3 | `›` the lane this pane shows (not on the cursor row, where the bold name says it) · `²` / `³` shown in viewport 2 / 3 |
589
- | 4–27 | The name in its identity colour, bold while shown; up to three `·` when other sessions view the lane; a clipped name ends in `…` |
847
+ | 3 | `›` the lane this pane shows (not on the cursor row, where the bold name says it) · `²` / `³` shown in viewport 2 / 3 · `…` (dim; ASCII `~`) an action on its way: a session `x` is closing, a session `P` is registering (until its lane is listed), a shell being created (on the New shell row) · `▲` (ASCII `^`) a lane you pinned (`p`), the lowest of these marks |
848
+ | 4–27 | The name in its identity colour (else the theme's ink in truecolor and 256 colours), bold while shown, faint for a lane you hid while `o` shows it; up to three `·` when other sessions view the lane; a clipped name ends in `…` |
590
849
  | 28 | `+` on an unregistered tmux session that can be added to a workspace |
591
- | 29–31 | Age: the agent's `since`, else the last output: `45s`, ` 3m`, ` 2h`, ` 4d` (at most `99d`); blank when unknown; amber inside **Needs you** |
592
- | 33 | The terminal status glyph (section 1); dimmed when an agent glyph is in column 2 (`▸` with `○` is a long tool call, not a stuck lane) |
850
+ | 29–31 | Age: the agent's `since`, else the last output: `45s`, ` 3m`, ` 2h`, ` 4d` (at most `99d`); blank when unknown; amber inside **Needs you**, and in truecolor and 256 colours blue in **Working** and green in **Done** |
851
+ | 33 | The terminal status glyph (section 1); dimmed when the lane is stale, and in 16 colours also when an agent glyph is in column 2 (`▸` with `○` is a long tool call, not a stuck lane) |
593
852
  | 34 | One tail cell by priority: `?` not observed right now > `⚠` starving > `↓` downgraded > `$` mostly paid (section 8) |
594
853
  | 35 | `×` close, on unregistered sessions only |
595
854
 
596
- Marks and their ASCII twins: `▌` `|` cursor · `›` `=` viewing · `²` `2` / `³` `3` other viewport · `·` `.` viewer ·
597
- `…` `~` clipped · `×` `x` close · `+` `+` new / add · `⋮` `:` more rows · `↑` `^` uploads · `↓` `v` drop now ·
855
+ Marks and their ASCII twins: `▌` `|` cursor · `›` `=` viewing · `²` `2` / `³` `3` other viewport · `▲` `^` pinned · `·` `.` viewer ·
856
+ `…` `~` clipped · `×` `x` close · `+` `+` new / add · `⋮` `:` more rows · `↑` `^` uploads · `↓` `v` drop now · `▤` `#` docs ·
598
857
  `⚠` `!` warning or starving · `↓` `v` downgraded · `$` `$` paid. The full tables, colours and mockups are in
599
858
  [DESIGN.md](DESIGN.md).
600
859
 
601
- **Footer**, highest priority first: the close prompt ` › close ‘name’? y/n` · the rename prompt ` › Rename: …█` ·
602
- the promote prompt ` › Register ‘name’? y/n` · the Add to workspace prompt ` Choose a workspace · Esc cancels` · the
603
- filter ` /abc · 3 matches` · the health banner (` ⚠ host under memory pressure · status 3 m old`, or
604
- ` ⚠ lane sockets deleted · lanes run; on the server: lanes sockets repair`) · ` ⚠ registry: …` · the last message ·
605
- the legend ` ⏎ open · / find · x close · ? keys`. An empty workspace shows `no lanes declared · + New shell` on its
606
- New shell row.
607
-
608
- **Sidebar keys** (`?` shows this sheet in place of the list until the next key; that key only closes the sheet):
860
+ **Footer**, highest priority first: the open prompt — the sidebar asks one thing at a time, in one of two shapes. A
861
+ name field: ` › Rename: …█`, or ` › Register as: …█` for promote (a name longer than the field shows its end, `…` first,
862
+ so what you type stays in view). A confirmation: ` › close ‘name’ — y⏎ confirm · Esc cancel` or
863
+ ` › register ‘name’ — y⏎ confirm · Esc cancel` (promote), `⏎ confirm` once `y` armed it; at 36 columns ` cancel`
864
+ gives way first, then ` · Esc`, then the name is clipped, so the keys always show. Then the Add to workspace prompt
865
+ ` Choose a workspace · Esc cancels` · the filter ` /abc · 3 matches` · the health banner
866
+ (` ⚠ host under memory pressure · status 3 m old`, or ` ⚠ lane sockets deleted · lanes run; on the server: lanes
867
+ sockets repair`) · the version note ` server 1.2.0 · view 1.1.0 — press ?` while this sidebar runs another tree than
868
+ the installed one (at 36 columns ` — ?`; section [Versions](#versions)) · ` ⚠ registry: …` · the last message · the undo offer ` Unregistered ‘name’ · z undo` (10 s after
869
+ `u`) · the legend ` ⏎ open · / find · x close · ? keys`. The phone
870
+ draws the same footer (its picker says ` Choose a workspace · Tap Cancel`, and ` Swipe to scroll · Tap to open` when
871
+ idle). An empty workspace shows `no lanes declared · + New shell` on its New shell row.
872
+
873
+ **Sidebar keys** (`?` shows the short form of this table in place of the list until the next key; that key only closes
874
+ 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 ?`), leaves `PageUp` / `PageDown`
875
+ to this table and ends with `Ctrl-Alt-l` — not a sidebar key but the chord that brings the keyboard here from a lane
876
+ (section [Keybindings](#keybindings)). Every key below does something in the sidebar, and no other key does
877
+ (`tests/unit/test_sidebar.py` `DocumentedKeys` presses each one and every other key a keyboard sends):
609
878
 
610
879
  | Key | Action |
611
880
  | --- | --- |
@@ -616,22 +885,64 @@ New shell row.
616
885
  | `Tab` / `Shift-Tab` | Open the next / previous lane that needs you, cycling from the lane shown (` nothing needs you` when the section is empty) |
617
886
  | `2` / `3` | Show the cursor lane in viewport 2 / 3 (wide views) |
618
887
  | `/` | Find a lane |
619
- | `Esc` | Clear the filter; cancel a prompt, a rename, Add to workspace or an armed upload strip |
888
+ | `Esc` | Clear the filter; cancel a prompt, a rename, Add to workspace or an armed upload strip; the keyboard goes back to the lane |
620
889
  | `g` / `G` | First / last row |
621
890
  | `+` | New persistent shell in this workspace |
622
891
  | `a` | Add the cursor session to a workspace |
623
- | `x` | Close the cursor session (asks `y/n`) |
892
+ | `x` | Close the cursor session: `y` arms, `Enter` closes, any other key cancels (on a registered lane: ` registered: u to unregister first`) |
893
+ | `u` | Unregister the cursor session added with `a`: it moves back to **Unregistered**, still running, and the cursor goes with it; on a lane a repository's `terminals.json` declares, take its terminal out of that file |
894
+ | `z` | Undo that unregister (within 10 s) |
624
895
  | `R` | Rename the cursor lane |
625
- | `P` | Promote (register) the cursor session in the repository's `terminals.json` |
896
+ | `P` | Promote (register) the cursor session in the repository's `terminals.json`: the name first (`Enter` accepts), then `y` and `Enter` |
897
+ | `p` | Pin / unpin the cursor lane: on every device you own it leads its section, marked `▲` (`^` in ASCII), and never falls below `⋮ N more` |
898
+ | `h` | Hide the cursor lane on every device you own (` Hid ‘name’ · o shows it`); on a hidden lane `o` shows, show it again |
899
+ | `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) |
900
+ | `o` | Show / hide again the lanes you hid (shown faint) |
626
901
  | `U` | Arm the upload strip |
627
- | `r` | Reload and repaint |
902
+ | `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 |
903
+ | `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 |
628
904
  | `?` | The key sheet |
629
905
 
630
- Mouse: a click opens a lane; the wheel moves the cursor three rows; a click on `×` asks to close, on `+` starts Add to
631
- workspace, on the upload strip arms it. The cursor follows the lane, not the row: when a lane changes section the
632
- cursor and scroll position go with it.
906
+ **Pins and hidden lanes** (`p`, `h`, `H`, `o`) are yours, not the device's: they live in your profile
907
+ (`users/<login>.json`, section [Devices and views](#devices-and-views)) and every device you own follows them on its next
908
+ tick. A hidden lane leaves the list, the header counts, the fleet strip and the desktop notifications; a session under
909
+ **Unregistered** is never hidden (nothing alive is), and `/` finds only the lanes shown — `o` first to search hidden
910
+ ones. The filter keeps its match order; pins lead their section everywhere else. A hidden workspace's tab closes on
911
+ every view of yours (`lanes upgrade-session`, which `H` starts); `lanes workspaces show <id>` (cockpit v3 R6) brings it
912
+ back. On a device nobody owns there is no profile: `p`, `h` and `H` say ` no owner: lanes device claim`.
633
913
 
634
- ### Find, rename, add, promote, new, close
914
+ Mouse: a click opens a lane; the wheel moves the cursor three rows; a click on `×` asks to close, on `+` starts Add to
915
+ workspace, on the upload strip arms it. Under an open prompt the wheel does nothing and a click ends the prompt first
916
+ (` Cancelled`), then does what it does (a click on a row opens that lane); a click on the upload strip beside a Rename
917
+ or Register-as field arms the strip and leaves the field. The cursor follows the lane, not the row: when a lane changes
918
+ section the cursor and scroll position go with it.
919
+
920
+ **The keyboard goes back to the lane.** The sidebar and the lane share one keyboard: whichever pane has the focus gets
921
+ what you type. So every sidebar action that ends hands the focus back to the pane's viewport — opening a lane, a
922
+ close, a rename (`Enter`), a register (`a` → workspace, or `P` → `y` → `Enter`), an unregister (`u`) and its undo
923
+ (`z`), a cancel (`n`, `Esc` or any other key at a prompt), `r`, the key that closes the `?` sheet, a drop on the upload strip, and a click that opens nothing
924
+ (a section header, `⋮ N more`, an empty row). Anything else typed in the same burst was meant for the lane and is
925
+ dropped, never read as sidebar keys. A prompt, the filter and the armed upload strip keep the keyboard until you
926
+ answer them, and so does a refused key (`x` on a lane that cannot be closed, `u` on a declared lane), so the `y` you
927
+ type next lands in the
928
+ sidebar, not in an agent's approval prompt. The wheel and a mouse release never move the focus. On the phone the
929
+ picker tab keeps it; only opening a lane leaves it.
930
+
931
+ **The footer says when the keyboard is on the sidebar.** While it is, the legend line reads
932
+ ` ▌ keys go to sidebar · Esc → lane` in the accent colour (` | keys go to sidebar - Esc -> lane` in ASCII) instead of
933
+ ` ⏎ open · / find · x close · ? keys` — no extra row; a prompt, the filter, a banner or a message still take the line
934
+ first. The sidebar asks Zellij for focus reports and follows them (Zellij 0.45.1 sends them); a Zellij that never
935
+ sends one leaves the legend up for 3 s after each key you type in the sidebar. Every action that hands the keyboard
936
+ back clears it at once. On the phone the picker is a whole tab and has no such legend.
937
+
938
+ **A message stays 3 seconds, or until your next action.** What an action says in the footer (` Closed sh`,
939
+ ` Renamed → …`, ` Cancelled`, ` view not running yet`, or why a close failed) stays after the keyboard has gone back
940
+ to the lane, for 3 seconds (within a second more: the sidebar looks once a second), then the legend returns. The next
941
+ action you start in the sidebar — opening a lane, `x`, `R`, `P`, `a`, `u`, `z`, `/`, `U`, `+`, `r`, `?`, `d`, `Tab`, a chord —
942
+ replaces it at once. Moving the cursor, typing in a field, `Esc`, the wheel, a mouse release or the focus leaving and
943
+ coming back does not clear it.
944
+
945
+ ### Find, rename, add, unregister, promote, new, close
635
946
 
636
947
  - **Find** (`/` or `Ctrl-Alt-/`): type a few letters. Matching is fuzzy (the letters in order; whole-substring and
637
948
  prefix matches rank first, word starts score higher). The list flattens (sections drop, column 2 stays) and the
@@ -639,19 +950,74 @@ cursor and scroll position go with it.
639
950
  `Esc` clears. No hits:
640
951
  ` no lane matches ‘abc’ · Esc`.
641
952
  - **Rename** (`R`, or `lanes rename <lane-id> <new-name>` on the server): edit in the footer, `Enter` saves, `Esc`
642
- cancels. The lane id never changes, so view targets and Cursor's `lane open <id>` keep working. Where the name is
643
- stored depends on the lane kind (section 6).
953
+ cancels. While you type, the keyboard is the field's: `Space`, `j`, `k`, `J`, `K` are letters, and the arrows,
954
+ `Tab`, `Shift-Tab`, `PageUp` and `PageDown` type nothing — none of them opens a lane, moves the cursor or throws the
955
+ name away. A click on the list cancels the field before it opens anything, so a name never stays open in the footer
956
+ after the keyboard has gone to a lane. The lane id never changes, so view targets and Cursor's `lane open <id>`
957
+ keep working. Where the name is stored depends on the lane kind (section 6). `Enter` on an emptied name, or on the
958
+ name the lane already has, writes nothing and says ` name unchanged`.
644
959
  - **Add to workspace** (click `+` on the row, or `a`): choose a workspace that uses the session's container (the
645
960
  current one first), or **Cancel** / `Esc`. The session keeps its id, name, programs, scrollback and views, moves to
646
- that workspace's list, loses its `×`, and survives sync and reconnects.
647
- - **Promote** (`P`): asks ` › Register ‘name’? y/n`. `y` declares the unregistered tmux session in the workspace's
648
- repository `terminals.json` (only when the workspace has exactly one repository registry) and syncs; the lane then
649
- gets a repository id `<workspace>--<slug>`. Refused on a lane that is already registered.
961
+ that workspace's list, loses its `×`, and survives sync and reconnects. The cursor goes with it into the list;
962
+ **Cancel** / `Esc` puts it back on the session (added to another workspace, the session leaves this list and the
963
+ cursor keeps its row).
964
+ - **Unregister** (`u` on a session added that way, or `lanes unregister <lane-id>` on the server): takes it out of the
965
+ workspace again at once. Nothing stops: a running session moves back to **Unregistered** with the same id and name,
966
+ in the same workspace even if its directory now belongs to another one, its views stay attached, and the cursor goes
967
+ with it; a session that no longer runs leaves the list. The footer
968
+ offers ` Unregistered ‘name’ · z undo`, and `z` within 10 seconds adds it back exactly as it was — unless the session
969
+ was closed and started again under the same name meanwhile, which the undo refuses (`a` adds the new one). `u` hands
970
+ the keyboard back to the lane like every action, so press `z` on the sidebar (`Ctrl-Alt-l` first; the phone's picker
971
+ keeps the keyboard). On a busy host where tmux cannot be asked about the session, `u` changes nothing and says
972
+ ` Session could not be checked; nothing was unregistered`; press it again in a moment. If the record was saved but the
973
+ workspace list could not be published (a full disk), it says ` Unregistered; run lanes sync to finish publishing: …`
974
+ and `z` still undoes it. On a lane a repository's `terminals.json` declares, `u` takes its terminal out of that file
975
+ (the same editor the agents' `lanes_unregister` tool uses: the terminal and its `lanes.terminals` entry go, nothing
976
+ else in the file moves, a `.bak-<UTC>` copy is kept the first time), starts `lanes sync` and says ` Unregistered
977
+ name`; the session keeps running and is listed under **Unregistered** after the sync. There is no `z` for it. The
978
+ editor refuses, in its own words, the file's last terminal, a symbolic link in the container's home and a file that
979
+ is gone. A lane of an SSH workspace says ` declared in catalog.json — edit the file`; an unregistered session says
980
+ ` not registered — a adds it to a workspace`. `x` on any registered lane says ` registered: u to unregister first`.
981
+ - **Promote** (`P`): asks the name first, ` › Register as: name█` (prefilled with the session's; edit it, `Enter`
982
+ accepts, an emptied name cancels; the field takes keys and clicks as Rename's does), then
983
+ ` › register ‘name’ — y⏎ confirm`. `y` arms it (` › register ‘name’ — ⏎
984
+ confirm`) and `Enter` declares the unregistered tmux session under that name in the workspace's repository
985
+ `terminals.json` (only when the workspace has exactly one repository registry) and syncs; the lane then gets a
986
+ repository id `<workspace>--<slug>` (the slug of the repository's prefix + the name). Any other key cancels.
987
+ Refused on a lane that is already registered, and on a session that `terminals.json` already declares under any
988
+ name — a `P` on another device, or one whose row has let go before the sync listed it: ` Already declared as
989
+ 'name'; lanes sync lists it`, nothing written. Until the sync lists the lane (a few seconds) the row stays where it
990
+ is, under the new name, with a dim `…` in column 3 (ASCII `~`) and no `+` or `×`; the footer says
991
+ ` Registering ‘name’` — from the `Enter` on, before the write itself has answered; if the write refuses, the row
992
+ goes back to its own name and the footer says why. Then the row moves into the workspace's list and the cursor, if
993
+ it was on the row, moves with it. Meanwhile `x`, `a`, `R`, `P` and `u` on that row say ` registering ‘name’ — a moment`; opening it works.
994
+ If no sync has listed the lane 80 s later — the longest one can still take when docker answers slowly — and the
995
+ sync the sidebar started has ended, the mark goes and the footer says ` ‘name’ not listed yet — lanes
996
+ sync` (run `lanes sync` on the server; a broken `terminals.json` shows as ` ⚠ registry: …`).
650
997
  - **New shell** (`+`, or the `+ New shell` row): starts a persistent shell in the workspace container on the socket
651
- `lanes-shells-<workspace>` and shows it. Reopening it attaches to the same shell.
652
- - **Close** (`x`, or a click on `×`): unregistered sessions only. ` › close ‘name’? y/n`: `y` ends the session and its
653
- jobs and clears views showing it; any other key cancels. A session others are grouped onto (a Studio browser
998
+ `lanes-shells-<workspace>` and shows it. Reopening it attaches to the same shell. While it is being created (up to
999
+ 15 s on a busy host) the sidebar keeps working: the New shell row has a dim `…` (ASCII `~`) after its `+`, the footer
1000
+ says ` Creating a shell`, and a second `+` says ` creating a shell — a moment` instead of making another. The keyboard
1001
+ stays on the sidebar meanwhile; the shell opens in the viewport and takes the keyboard when it is ready — unless you
1002
+ have moved on (the keyboard went elsewhere, you opened another lane there, or a prompt is open), in which case it is
1003
+ only listed under **Unregistered** and the cursor, if it was still on the New shell row, moves onto it (`Enter` opens
1004
+ it). Either way it is listed once, even when the status update has already found it.
1005
+ - **Close** (`x`, or a click on `×`): unregistered sessions only. ` › close ‘name’ — y⏎ confirm · Esc cancel`: `y` arms
1006
+ it (` › close ‘name’ — ⏎ confirm`; a long name is clipped so the keys fit) and `Enter` ends the session and its jobs and
1007
+ clears views showing it; any other key cancels, at either step, so typed text closes nothing unless it is `x`,
1008
+ `y`, `Enter` in a row. The keyboard goes back to the lane at the `Enter`; the close itself runs in the background
1009
+ (a busy registry lock or a slow tmux can take seconds): the row stays with a dim `…` in column 3 (ASCII `~`), no `+`
1010
+ or `×`, and the footer says ` Closing ‘name’`, then ` Closed name` — or why nothing was closed, with the row back as
1011
+ it was. A viewport of this window that shows the session is cleared at the `Enter`, before the keyboard moves, so
1012
+ what you type next lands on its **No lane selected** screen (which ignores keys), never in the shell being ended;
1013
+ if nothing was closed, a viewport still cleared gets the session back (one you pointed elsewhere meanwhile keeps
1014
+ your choice). A view that cannot be cleared (a full disk) closes nothing: the footer says why and the keyboard
1015
+ stays. Meanwhile every key on that row — `x`, `a`, `R`, `P`, `u`, `Enter` — says ` closing ‘name’ — a moment`; the
1016
+ rest of the sidebar answers as usual. A session others are grouped onto (a Studio browser
654
1017
  `view-…` viewer) has those viewers ended first; any other grouped session makes the close refuse and say why.
1018
+ - **Paste into a prompt**: a paste into ` › Rename: `, ` › Register as: ` or the filter is typed there, after what is
1019
+ already in the field. Line breaks and tabs become one space, so a copied line never acts as `Enter`; the prompt waits
1020
+ for yours. Only the armed upload strip, or a sidebar with no prompt open, takes a paste as a file drop.
655
1021
 
656
1022
  ### Markdown preview
657
1023
 
@@ -663,6 +1029,41 @@ adds a pinned glow unless the view server's PATH already has one; `lanes doctor`
663
1029
  (`preview renderer:`). Preview reads tmux lanes only; on a Host or tool tab, or a lane that named no existing `.md`
664
1030
  file, it says so and closes on `Enter`.
665
1031
 
1032
+ ### Quick edit
1033
+
1034
+ `Ctrl-Alt-e` opens a floating **Edit** pane over the focused lane with the files that steer its agent:
1035
+
1036
+ | Key | File | Offered when |
1037
+ | --- | --- | --- |
1038
+ | `1` / `2` | `AGENTS.md` / `CLAUDE.md` of the lane's checkout | always |
1039
+ | `3` / `4` | `AGENTS.md` / `CLAUDE.md` of the main checkout | the lane works in a linked git worktree |
1040
+ | `3` (`5` in a worktree) | `.env.local` of the main checkout | always |
1041
+
1042
+ The checkout is the git checkout the lane's working directory is in (else its repository folder), found inside the
1043
+ container: a worktree's `.git` file leads to its main checkout. A file is offered only as a regular file inside its
1044
+ checkout; a symbolic link is never offered, and a missing file is shown dim, `not present` (quick edit creates no
1045
+ file). One key picks, without `Enter`, even one typed while the pane was still opening; `Enter` alone takes the file
1046
+ when only one is there; `q` or `Esc` closes. The editor then runs inside the lane's container as uid 1000, in that
1047
+ checkout: `docker exec -it -u coder … lane-edit <file>` with no shell between, so a save is the container's own write
1048
+ (owner and mode kept). Quitting the editor closes the pane.
1049
+
1050
+ Which editor: the one `lanes attach <device> --editor micro|hx|nvim|vim` recorded for the device (`--editor auto`
1051
+ takes the choice back; from your machine, `lanes-launch <device> --editor E` passes the same flag), else the first of
1052
+ micro, hx, nvim, vim and vi in the container ([lane-edit](#the-in-container-lane-command)). `LANES_EDITOR` counts
1053
+ only for a `lanes edit --focused` you run by hand: the `Ctrl-Alt-e` pane has the view server's clean environment,
1054
+ which does not carry it.
1055
+
1056
+ Quick edit reads tmux lanes only. It is refused with the reason, and closes on `Enter`, when the container is not
1057
+ running, when it has no `lane-edit` (`lanes install-container <name>`) or when no git checkout holds the lane's
1058
+ directory. `.env.local` is named by its label only, never by its path, in anything quick edit prints.
1059
+
1060
+ **What quick edit can reach, and what leaves the container.** The server side (`lanes edit`) only looks: through
1061
+ `/proc/<container init>/root` it resolves the checkout and checks that each file is a regular file inside it, and it
1062
+ never opens one. The file is opened, edited and saved by the editor inside the container, as uid 1000 (`coder`, the
1063
+ agents' own user), so it can reach nothing the lane's agent could not, and the save is that user's write. Nothing is
1064
+ copied out of the container: no temporary copy on the server, no upload, no log of the contents. What you see on
1065
+ screen travels the way the lane's own output does — the terminal stream of your view — and nothing else.
1066
+
666
1067
  ### Scroll, mouse and copy
667
1068
 
668
1069
  - A tmux lane keeps its own history and draws on the alternate screen, so scroll inside the lane (mouse wheel or the
@@ -675,10 +1076,13 @@ file, it says so and closes on `Enter`.
675
1076
  ### Pane title and desktop notifications
676
1077
 
677
1078
  The sidebar pane's title is `WORKSPACE · ◆1 ◇2` (the header counts, rewritten only when they change). A lane that
678
- starts needing you (`◆` `✗` `◇`) raises one desktop notification `name: task` through OSC 9: once per transition,
1079
+ starts needing you (`◆` `✗` `◇`) raises one desktop notification `name: task` through OSC 9: once each time it
1080
+ enters Needs you (an approval prompt after a question, while it is still waiting on you, is not a second one),
679
1081
  never for the lane this pane shows, never on a phone layout. A sidebar that starts while lanes already need you
680
1082
  announces each once. Ghostty shows OSC 9 only while the lanes window is not focused (`desktop-notifications = true`
681
- in the window's config). Not yet verified on a Mac.
1083
+ in the window's config). Not yet verified on a Mac. To silence them for one device, live:
1084
+ `lanes-launch <device> --notify 0` from the client (or `lanes attach <device> --notify 0` on the server); `--notify 1`
1085
+ turns them back on, also over a `LANES_NOTIFY=0` in the view's environment (section 9).
682
1086
 
683
1087
  ## 4. Phone, tablet and laptop browser
684
1088
 
@@ -825,21 +1229,24 @@ The frontend chooses once per page load, by capability:
825
1229
  | --- | --- | --- |
826
1230
  | 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 |
827
1231
  | 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) |
828
- | 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 |
1232
+ | 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 |
829
1233
 
830
1234
  ### Phone controls
831
1235
 
832
1236
  - **Workspaces** (or the title in the header) opens the workspace sheet with a search field (each card shows its
833
1237
  counts, `◆ 1 · ◇ 2`); **Lanes** opens the current workspace's lane picker; **Panes** switches panes; **More** opens
834
- the client's own menu, where Lanes adds **Text size** (remembered on the phone). The light/dark toggle repaints the
835
- whole terminal.
1238
+ the client's own menu, where Lanes adds **Text size** (remembered on the phone), above it **Docs** — the docs page,
1239
+ `/lanes-docs/`, in a new tab, so the terminal stays where it is — and at the top the server's release
1240
+ (**Lanes v1.2.0**, see [Versions](#versions)). The light/dark toggle repaints the whole terminal.
836
1241
  - The keyboard opens only when you tap **Type here** or another input. Swipes, live updates and reconnects leave it
837
1242
  closed. **Type here** is an ordinary text field: the normal keyboard with its suggestion bar, never the password
838
1243
  keyboard. A suggestion you tap arrives as the whole word; long-press the field and choose **Paste** to paste.
839
1244
  - The lane picker shows three rows per lane: the viewing mark, agent glyph and name; then what the agent needs, since
840
1245
  when and the harness (`Needs you · 25m · claude`; a shell says `Quiet · 2h`); then the agent's task. Its first line
841
1246
  counts the lanes that need you (`◆1 need approval · ◇2 need you · ▸6 working`). The footer says
842
- `Swipe to scroll · Tap to open`. The picker has no upload strip.
1247
+ `Swipe to scroll · Tap to open`, and otherwise exactly what the desktop footer says: every prompt (Rename,
1248
+ Register as, the close and register confirmations), the filter and its match count, a banner, the last message.
1249
+ The picker has no upload strip.
843
1250
  - On a lane's own tab a row of quick keys sits above the dock: `⏎ y n Esc ^C ↑ ↓`. A tap sends the key to the lane
844
1251
  without opening the keyboard; `n` and `^C` need a 400 ms hold (a short tap says "Hold to send" in the header for
845
1252
  about a second and a half).
@@ -851,16 +1258,121 @@ The frontend chooses once per page load, by capability:
851
1258
  - **More** opens the client's menu; tap it again to close the menu.
852
1259
  - The filter bar on the picker filters the lane list as you type, non-ASCII letters included: it sends each change
853
1260
  (a typed letter, a backspace) once, and clearing the field or pressing Esc closes the filter. On a picker tab the dock
854
- shows the filter in place of **Panes**. Leaving the picker forgets the filter.
855
- - `+` and `×` on picker rows add and close unregistered sessions, as on the desktop; tapping `+` then a workspace
856
- needs no typing.
1261
+ shows the filter in place of **Panes**. Leaving the picker with a filter typed (a tap on a match opens that lane)
1262
+ sends nothing to the lane and forgets the filter; that picker's list is shown whole again the next time you are on
1263
+ it. Moving to another workspace's picker from the Workspaces sheet leaves the filter behind too: that picker's bar
1264
+ starts empty.
1265
+ - `+` and `×` on picker rows add and close unregistered sessions, at the same columns as on the desktop; tapping `+`
1266
+ then a workspace needs no typing. While the picker asks (` › close ‘sh’ — y⏎ confirm`, ` › register ‘sh’ — y⏎
1267
+ confirm`, or Add to workspace), a short row `⏎ y Esc` sits above the dock: `y` then `⏎` closes, `Esc` cancels — no keyboard needed.
1268
+ While you type a name (Rename, Register as) the row stays away, so a `y` meant as yes is never typed into it. A tap
1269
+ on a row while the picker asks cancels the question or the name first, then opens that lane, so nothing is left
1270
+ half-asked on the picker when you come back to it.
1271
+
1272
+ ### Docs
1273
+
1274
+ The Docs page is where an agent shows you its work and where you hand files to an agent — every item with a sha256
1275
+ receipt the server computed. There is one per server, `https://<server>:8443/lanes-docs/`: on the tailnet only, and
1276
+ open only to your own tailnet login. No token and no second password: you see your own uploads and what was
1277
+ published for you, and nobody else's documents.
1278
+
1279
+ **Open it.**
1280
+
1281
+ | From | How |
1282
+ | --- | --- |
1283
+ | Phone or tablet | **More → Docs**: the page opens in a new tab, so the terminal stays where it was |
1284
+ | Laptop browser | The **Lanes** chip (top right), then **Docs** in its bar (a new tab) |
1285
+ | 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)) |
1286
+ | A push notification | A `published` push opens the document when the server's `action_url` uses `{doc}` (section 8) |
1287
+ | 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 |
1288
+
1289
+ Keep the slash: `/lanes-docs` without it is a view's address and opens the terminal page instead.
1290
+
1291
+ **What it shows.** The index lists your documents by workspace, then lane, newest first, with counts, and above them
1292
+ the upload form. A document opens by its kind:
1293
+
1294
+ | Kind | Opens as |
1295
+ | --- | --- |
1296
+ | `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 |
1297
+ | `html`, `svg` | A link, **Open the HTML artifact** (or SVG): the artifact on a page of its own, in a sandbox (below) |
1298
+ | `file` (anything else) | Its facts and a download |
1299
+
1300
+ Every document page also has **Download**, **Receipt** and the facts: kind, name, size, workspace, lane, time, who
1301
+ put it there, how it came (`publish`, `web`, `ssh`) and its sha256.
1302
+
1303
+ **Publish from a lane.** An agent publishes with the MCP tool `lanes_publish` (`path`, optional `title`, `for`,
1304
+ `kind`; section 7, [Agent tools (MCP)](#agent-tools-mcp)); in a lane's shell you do it with the `lane` command:
1305
+
1306
+ ```sh
1307
+ ~/.lz/bin/lane publish report.md # the kind follows the extension
1308
+ ~/.lz/bin/lane publish out/queue.svg --title 'Queue design' --for anna@example.com
1309
+ ```
1310
+
1311
+ `lane publish <file> [--title T] [--for <login>] [--kind md|html|svg|file]` takes a file of the container under
1312
+ `/home/coder`, at most 32 MiB, and copies nothing: it hashes the file once, leaves a small request in
1313
+ `~/.lz/state/publish/<lane id>/` and prints it as JSON — the request `id`, `path`, `kind`, `bytes`, `sha256`, the
1314
+ `receipt` path the answer will have, `picked_up_by: "lanes-docsd"` and `expected_within_s: 5`. Within about 5 s the
1315
+ server reads the file out of the container, checks that its sha256 is the one the request named (a file changed in
1316
+ between is refused, not stored), keeps its own copy and writes the receipt beside the request: `outcome: "ok"` with
1317
+ the document's `id`, `sha256`, `bytes` and `url` (`/lanes-docs/d/<id>`), or `outcome: "refused"` with a `reason`
1318
+ (RUNBOOK "Docs service", publish pick-up). The agent sees that answer as `published` in `lanes_status` of its lane.
1319
+ The document is listed under the lane it came from, and the sidebar's ` ▤ docs N` counts it within a few seconds.
1320
+ Change the file only after the receipt is there; publishing it again makes a new document.
1321
+
1322
+ **Who sees what.**
1323
+
1324
+ - An upload: your login alone.
1325
+ - A published document: the login `--for` names (it must be a member's); without `--for`, the person who last opened
1326
+ that lane, on any of their devices; when nobody has, everyone whose member record grants the lane's workspace. A
1327
+ record that lists no workspace grants every workspace the server serves.
1328
+ - Anyone else gets *not found*, exactly as for a document that does not exist. The one exception is the count:
1329
+ everyone who sees a lane's ` ▤ docs N` sees how many documents it holds (a number, never a title).
1330
+
1331
+ **Upload, with a receipt.** On the Docs page pick the lane, drop files on the dashed area — or tap it to choose them,
1332
+ several at once, 32 MiB in all — and **Upload**. Each file lands in that workspace's `uploads/` folder in its
1333
+ container, exactly as every other upload does ([Uploads](#5-uploads)), and the page lists its receipt: the name (a
1334
+ link to the document), size, sha256 and where it landed (`/home/coder/<repo>/uploads/<name>`) — give the agent that
1335
+ path. Every other way in (the sidebar's strip, the watch folder, `lanes-upload`, the macOS droplet) records the same
1336
+ receipt for you (section 5, "Receipts").
1337
+
1338
+ **The sandbox.** Nothing an agent publishes is trusted. Markdown is rendered on the server by Lanes's own renderer:
1339
+ 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
1340
+ shows only from the Docs pages themselves or inline. An HTML or SVG artifact is never shown inside a Docs page:
1341
+ **Open the … artifact** loads it as a page of its own under a browser sandbox, where its scripts run on an opaque
1342
+ origin of their own — they cannot read the Docs pages, cannot reach your terminal session or its sign-in, and cannot
1343
+ type into a lane. No Docs page can be framed by another page. That is how the server serves it (unit-tested); a check
1344
+ in a real browser is the operator's (RUNBOOK checklist; DEF-1853).
1345
+
1346
+ **Encrypted at rest.** Off by default. With the operator's `age` switch on, the server keeps new store copies
1347
+ encrypted; your receipts stay the sha256 of what you sent and the files in `uploads/` stay plain for the agents
1348
+ (section 5, "Encrypted at rest").
1349
+
1350
+ **For the administrator.**
1351
+
1352
+ - The full server install, `npx @vibelord/lanes@<release> --server`, enables the docs service (`lanes-docsd`) and
1353
+ installs the page's pinned Mermaid (`install/enable-docs.sh`). An update with `--update-only` enables nothing, so a
1354
+ server set up before the docs service needs one full install, or `systemctl --user enable --now
1355
+ lanes-docsd.service`. `lanes install-container <name>` gives a container `lane publish` and the `lanes_publish`
1356
+ tool (new agent sessions load it).
1357
+ - **The pin gate.** The Docs page answers nobody until the Serve header check has run and its result is pinned with
1358
+ `lanes enrol mode request` (RUNBOOK "Enrolment service", Operator verification). Until then every request is
1359
+ refused `not-pinned`.
1360
+ - `lanes doctor` prints five docs lines (section 6): the service answering on its loopback port, the pin gate, the
1361
+ store's size, the Mermaid pin and the `age` switch.
1362
+ - `lanes docs push <id>` prints one document's receipt and exits 2, `not configured`: the hook for pushing documents
1363
+ to the data service's publication surface, which is not built yet.
1364
+ - The unit, `docs.log`, the routes, the limits and the operator's live checklist:
1365
+ [RUNBOOK](RUNBOOK.md#docs-service-lanes-docsd-w10-w3).
1366
+
1367
+ Not yet verified on a device: the Docs page on a real phone (the rendering, a Mermaid diagram, the drop area), the
1368
+ artifact sandbox in a real browser (DEF-1853), and the Mermaid route through a running Caddy (DEF-1871).
857
1369
 
858
1370
  ### What works, what does not, what is unverified
859
1371
 
860
1372
  - Works: everything the desktop view shows, through the same Zellij sessions; the phone has its own view selection of
861
- the same agents; the server keeps serving when your Mac sleeps.
862
- - Does not: a public URL (Tailscale only), uploads from a phone browser (the drop helper is a desktop client feature),
863
- approving an agent's permission prompt from a push notification.
1373
+ the same agents; the server keeps serving when your Mac sleeps; uploads from the phone and what agents publish, on
1374
+ the Docs page (**More → Docs**, [Docs](#docs)).
1375
+ - Does not: a public URL (Tailscale only), approving an agent's permission prompt from a push notification.
864
1376
  - Not yet verified on a device: the quick keys, the lane filter, the text keyboard, paste, **Copy** and **Paste** on a
865
1377
  real iPhone (the [runbook](RUNBOOK.md) lists the check), the light/dark terminal repaint, the counts that the
866
1378
  picker's pane title carries to the Workspaces sheet and the **Lanes** button (a phone view created before that
@@ -870,7 +1382,10 @@ The frontend chooses once per page load, by capability:
870
1382
  ## 5. Uploads
871
1383
 
872
1384
  Files land in the workspace's `uploads/` folder in its container, the same folder Cursor users drop files into. The
873
- in-container path ends up on your clipboard and in a notification, and can be typed into the lane for the agent.
1385
+ in-container path ends up on your clipboard and in a notification, and can be typed into the lane for the agent. Every
1386
+ upload, whichever way it came, also gets a **receipt**: its sha256 computed on the server as the bytes arrived, kept
1387
+ with a copy in the docs store, so it shows on the Docs page (`/lanes-docs/`, section 4, [Docs](#docs)) and in the
1388
+ sidebar's ` ▤ docs N` count.
874
1389
 
875
1390
  | Way | How |
876
1391
  | --- | --- |
@@ -879,16 +1394,46 @@ in-container path ends up on your clipboard and in a notification, and can be ty
879
1394
  | Command | `lanes-upload [-w <workspace>] [--paste] <files…>` · `lanes-upload --clipboard [-w <workspace>] [--paste]` (clipboard image or copied files) |
880
1395
  | macOS droplet | `~/Applications/Lanes Upload.app`: drop files on it; double-click uploads the clipboard screenshot |
881
1396
  | Omarchy | `Super+Shift+U` uploads the clipboard and types the path into the lane |
1397
+ | 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 |
882
1398
  | Server | `lanes upload <workspace> \| --active <device> [--paste]` reads a tar stream on stdin |
883
1399
 
884
1400
  **Where files land.** `<workspace folder>/uploads/<name>` in the container, where the workspace folder is its first
885
1401
  lane's repository folder (else the workspace's working directory). An existing name is never overwritten: the next
886
- one becomes `<name>-1.<ext>`. The folder is created on first use and, in a git repository that does not ignore it
887
- yet, added to the repository-local `.git/info/exclude`, so uploads are never committed.
1402
+ one becomes `<name>-1.<ext>`. The folder is created on first use and, in a git repository whose `.git/info/exclude`
1403
+ does not name it yet, added there, so uploads are never committed. The Docs page lands a file exactly the same way
1404
+ (its name keeps only letters, digits, `.`, `_` and `-`). The server refuses to follow a symbolic link anywhere on the
1405
+ way, so an agent cannot redirect an upload outside its container: a folder reached through a link answers
1406
+ `uploads/ … not a directory (a symbolic link is refused)` (or, on the Docs page, *cannot be written now*).
1407
+
1408
+ **Receipts.** Each file is also a document in the docs store with a receipt — `sha256`, size, time and its page
1409
+ `/lanes-docs/d/<id>` — for you only (`for` your login): the Docs page lists it under its workspace and lane, and the
1410
+ sidebar counts it. `lanes upload` prints one `# docs: <name> <size> B sha256 <hash> <id>` line per file on stderr (the
1411
+ helper's log, or your terminal with `lanes-upload`). The kind follows the extension: `.md` opens rendered (with Mermaid
1412
+ diagrams), `.html`/`.svg` open as sandboxed artifacts, anything else as a download. The store keeps its own copy, so
1413
+ an agent editing its `uploads/` file never changes what the receipt names. What gets no receipt, though it still
1414
+ lands: a file over 32 MiB sent with `lanes upload` (the store keeps at most 32 MiB a file), a device no member record
1415
+ owns (the server logs its hash in `docs.log`), and files past the 64th of one `lanes upload`. A file name taken in only
1416
+ one of the two places (an agent's own file in `uploads/`, or an earlier upload you removed from the store) can get
1417
+ `-1` in one and not the other; the receipt names both.
1418
+
1419
+ **Encrypted at rest (optional).** The operator can have the server keep its docs-store copies encrypted with
1420
+ [`age`](https://age-encryption.org) (`LANES_DOCS_AGE_RECIPIENT`, [RUNBOOK](RUNBOOK.md#docs-service-lanes-docsd-w10-w3)).
1421
+ Nothing changes in `uploads/` — the agent reads plain files there — and receipts stay the hash of what you sent. When
1422
+ the docs service also holds the key, documents open as usual; when it does not, a download answers *encrypted at
1423
+ rest* and you fetch the file from the workspace's `uploads/` folder instead.
1424
+
1425
+ **Opening the Docs page from elsewhere.** Any link to a document opens it: the shell's **Docs** link, the sidebar's
1426
+ links, a notification, the address bar, and a link on another website — a web mail, a chat in the browser, ntfy's web
1427
+ app. What the docs service refuses from another website is everything that is not you following a link: a page's own
1428
+ background requests, images and scripts, and an upload — so an HTML artifact cannot read anything of yours, and only
1429
+ the Docs page itself uploads. A browser too old to say where a request came from sees *Refused: this request came from
1430
+ another site*. Viewing very quickly (over 300 pages a minute) answers *Too many requests* for a
1431
+ minute; an upload that trickles in slower than about 64 KiB/s is stopped and says so, with nothing stored.
888
1432
 
889
1433
  **Why click first.** A terminal delivers a drop as a paste of the local path into the focused pane, so the strip must
890
1434
  hold the focus when you let go. The server receives only the path; the **upload helper** on your machine sends the
891
- file.
1435
+ file. Once the drop lands the sidebar gives the keyboard back to the lane straight away; the end of the upload never
1436
+ moves the focus (you may be typing somewhere else by then).
892
1437
 
893
1438
  **The upload helper and its allowlist.** The helper is the security boundary for drops. It reads only from folders
894
1439
  listed in `~/.config/lanes/upload-allow` (created with Desktop, Downloads, Pictures, Documents, Lanes Uploads and the
@@ -930,7 +1475,7 @@ The agent must hold the key (`AddKeysToAgent yes` in the `lanes` alias adds it o
930
1475
  | `✗ not allowed on this machine: … - run: lanes-upload --allow FOLDER` | The file's folder is not on the allowlist (or you chose Deny) | `lanes-upload --allow <folder>`, then drop again |
931
1476
  | `✗ macOS denied folder access: allow Lanes Helper in System Settings > …` | macOS refused the helper | Allow Lanes Helper in Files and Folders |
932
1477
  | `✗ no upload helper on this device (reopen lanes-launch)` | No helper heartbeat for this device | `./install.sh --check`; reopen the window |
933
- | `✗ that was not a file drop` | The paste was not a list of absolute paths | Drop files, not text |
1478
+ | `✗ that was not a file drop` | The paste was not a list of absolute paths (a paste into an open Rename, Register-as or filter prompt is typed there instead) | Drop files, not text |
934
1479
  | `✗ upload timed out` | No progress for 120 s | Check the helper log |
935
1480
 
936
1481
  ## 6. Workspaces and registry
@@ -986,14 +1531,30 @@ run.
986
1531
  | Key | Meaning |
987
1532
  | --- | --- |
988
1533
  | `terminals[]` | The terminals the editor extension opens and Lanes turns into lanes: `name` (the lane title; the id is its slug), `command`, optional `focus`, `color` (`terminal.ansi<Colour>` / `terminal.ansiBright<Colour>`), `icon` (a codicon name) |
989
- | `lanes.terminals[<name>]` | The exact binding Lanes uses: `holder`, `tmux: {socketName, sessionName}`, `id` (added by `cursor-sync`), and `create` (the recipe `lane open` runs when the lane does not exist; `createPrevious` keeps an earlier one after `cursor-sync --canonicalize`/`--identity`). Written by `lanes cursor-sync`; ignored by the editor extension |
1534
+ | `lanes.terminals[<name>]` | The exact binding Lanes uses: `holder`, `tmux: {socketName, sessionName}`, `id` (added by `cursor-sync`), and `create` (the recipe `lane open` runs when the lane does not exist; `createPrevious` keeps an earlier one after `cursor-sync --canonicalize`/`--identity`). Optional `displayName`: the name Lanes shows for the lane (see below). Written by `lanes cursor-sync`, which carries `displayName` as is; ignored by the editor extension |
990
1535
  | `throttle.terminals[<name>].tmux` | A binding Lanes also accepts (Throttle Studio's), used when `lanes.terminals` has none |
991
1536
 
992
- Every terminal needs an explicit tmux binding; there is no guessed command parsing. Display names are not set in
993
- `terminals.json`: the manifest's `displayNames` holds them.
1537
+ Every terminal needs an explicit tmux binding; there is no guessed command parsing.
1538
+
1539
+ A repository lane's shown name has two layers, both keyed by the terminal's `name`, neither of which changes the id:
1540
+
1541
+ 1. `repositories[i].displayNames` in the manifest (host) — the operator's override, written by `R` and
1542
+ `lanes rename`. It wins.
1543
+ 2. `lanes.terminals[<name>].displayName` in `terminals.json` (container) — the repository's own name for the lane.
1544
+ A value that is not a usable name (not a string, blank or invisible characters only, not valid Unicode text,
1545
+ or with a control character) is ignored. So is a name another lane on the same board already answers to — its
1546
+ title, its `displayNames` override, or its own `displayName` (then neither lane keeps it): the repository names
1547
+ only its own lane and never makes two rows read the same. Names are compared as a row reads them, so extra
1548
+ spaces, a zero-width character or a full-width form do not make a second `main`. No board error is raised; the
1549
+ lane shows its override, else its title.
1550
+
1551
+ Without either, the lane shows its prefix plus the terminal's `name`.
994
1552
 
995
1553
  **`registered-sessions.json`** (site directory, version 1): `sessions[]` of `{id, workspace, name, container,
996
- tmux: {socket, session}, folder}`, written by Add to workspace and `lanes register`. Back it up with the catalog.
1554
+ tmux: {socket, session}, folder}`, written by Add to workspace and `lanes register`; `u` in the sidebar or
1555
+ `lanes unregister <lane-id>` removes a record (`z` puts it back within 10 s). `lanes unregister <id>` also removes a
1556
+ record no workspace lists any more (its workspace left the catalog, uses another container, or a repository declares
1557
+ the session). Back it up with the catalog.
997
1558
 
998
1559
  ### Lane ids
999
1560
 
@@ -1015,7 +1576,10 @@ target as JSON.
1015
1576
  - **Hot-sync**: `lanes-statusd` starts one `lanes sync` when a referenced `terminals.json` changes, at most one per
1016
1577
  10 s.
1017
1578
  - `lanes-ensure.timer` runs `lanes ensure` every minute: it recreates any known device session that is missing, and
1018
- never when Zellij did not answer.
1579
+ never when Zellij did not answer. A view an update left running because a client was attached
1580
+ (`clients/<device>.update-ready`) is rebuilt by the first tick that finds no client attached. While another
1581
+ `lanes ensure`, `lanes rebuild` or the create of a view runs, a tick waits 10 s for it and then skips (`ensure:
1582
+ skipped: …`); the next tick follows a minute later.
1019
1583
  - A repository whose registry is temporarily invalid keeps its previous lanes; the sidebar footer and `lanes doctor`
1020
1584
  say why. `lanes sync` exits 1 then (the timer treats that as informational).
1021
1585
  - A device with no attached client gets new tabs on its next connection and the next ensure tick.
@@ -1045,7 +1609,7 @@ running lane gains its identity when it is next created.
1045
1609
 
1046
1610
  | Lane kind | `R` / `lanes rename <lane-id> <name>` writes |
1047
1611
  | --- | --- |
1048
- | Repository lane | `repositories[i].displayNames` in `workspaces.<workspace>.json` (a `.bak-<UTC>` copy on the first write), then syncs. Renaming back to the terminal's own title removes the override |
1612
+ | Repository lane | `repositories[i].displayNames` in `workspaces.<workspace>.json` (a `.bak-<UTC>` copy on the first write), then syncs. Never `terminals.json`. Renaming back to the lane's default (its `terminals.json` `displayName` when it has one, else the terminal's own title) removes the override |
1049
1613
  | Registered session | Its record's `name` in `registered-sessions.json` |
1050
1614
  | Unregistered session | `~/.local/state/lanes/user-sessions/<id>.json` |
1051
1615
 
@@ -1056,12 +1620,18 @@ Names must be non-empty and free of control characters. The id, resolve file and
1056
1620
  - **Register** (Add to workspace, or `lanes register <lane-id> [workspace]` with the id from
1057
1621
  `lanes ls --unregistered`): saves the session's existing id, name and binding in `registered-sessions.json`. It
1058
1622
  has no create recipe and does not edit any editor terminal list; an ended registered session stays absent until
1059
- someone starts it. Only workspaces using the session's container are offered.
1623
+ someone starts it. Only workspaces using the session's container are offered. If that container refuses its own
1624
+ copy of the registry (`~/.lz` is no real directory), the registration is still saved and listed, and Lanes says so
1625
+ at once: `Registration saved; run lanes sync to finish publishing: container <name>: …` (a rename of a registered
1626
+ lane says `Rename saved; …` the same way); `lane open` in that container sees the change only after the fix
1627
+ ([RUNBOOK](RUNBOOK.md)).
1060
1628
  - **Promote** (`P`): declares the session in the workspace's single repository `terminals.json` (a terminal entry
1061
1629
  and its `lanes.terminals` binding), so it gets a repository id, editor integration and a saved recipe when one was
1062
1630
  known.
1063
- - **Unregister** a registered session: remove its record from `sessions` and run `lanes sync`; the terminal keeps
1064
- running and returns to Unregistered. Remove a repository lane by deleting its terminal from `terminals.json`.
1631
+ - **Unregister** a registered session: `u` in the sidebar, or `lanes unregister <lane-id>` (which also clears a record
1632
+ `lanes sync` ignores); the terminal keeps running and returns to Unregistered with the same id and name. Removing the
1633
+ record from `sessions` by hand and running `lanes sync` works too, without the name hand-back. Remove a repository
1634
+ lane by deleting its terminal from `terminals.json`.
1065
1635
  - If a repository later declares the same binding as a registered session, the repository declaration wins.
1066
1636
 
1067
1637
  ### `lanes doctor`
@@ -1086,6 +1656,32 @@ Names must be non-empty and free of control characters. The id, resolve file and
1086
1656
  present (`✓`), missing or pointing elsewhere (`✗` once a web unit is enabled, `⚠` before), or not checked (`⚠`: no
1087
1657
  tailscale, no answer, or no Tailscale name on the host — then `lanes web` configures no Serve and prints a loopback
1088
1658
  URL, so doctor does not ask for the mapping either).
1659
+ - The docs service (W10): `lanes-docsd` enabled, active and answering on its loopback port (`✓`; `⚠` not enabled; `✗`
1660
+ enabled but not running, or not answering), the pin gate (`✓ docs: pinned`, else `⚠ docs: refuses until the Serve
1661
+ header check is pinned`), the store (documents, bytes, `docs.log`'s size), the pinned Mermaid (`✓`, `⚠` not installed
1662
+ — `install/enable-docs.sh`, which every server install and update runs —, `✗` a file that is not the pinned build) and
1663
+ the `age` switch (`✗` when it is set without an `age` binary); RUNBOOK "Docs service" has every line.
1664
+ - The release that runs, against `~/.local/share/lanes/current` (the tree an install makes current; without the link,
1665
+ a `⚠` and this `lanes`'s own tree): the link names a tree that exists, whose `VERSION` agrees with its
1666
+ `pkg/<version>` name and its npm stamp, and that this `lanes` runs (`✗` with the fix: run the current one, or re-run
1667
+ the server installer). Every lanes process running now (statusd, enrold, docsd, notify, economy, a sidebar — found in the
1668
+ same process-table sweep by the command Lanes starts it with, `python3 -S -E <tree>/libexec/<script>`, so a file
1669
+ open in an editor or a one-shot such as `notify.py --test` is not one) has a [run record](#versions) of the current
1670
+ code (`✗ … statusd runs 1.1.0`, or `statusd runs without a run record (started by an older release)`, then
1671
+ `systemctl --user restart …` or `lanes rebuild <device>`); a record whose process is not running is a line of
1672
+ information, never a `✗`. Every device's layout runs the current tree (`✗ … mac runs 1.1.0 — lanes rebuild mac`;
1673
+ `⚠` when it names that tree by another path than `lanes rebuild` writes now: the next update would leave that view
1674
+ behind). Every installed lanes unit's `ExecStart` runs it (`✗ … 12 units run 1.1.0 (…) — re-run the server installer`; `⚠` when no unit is found under
1675
+ `~/.config/systemd/user` or `$XDG_CONFIG_HOME/systemd/user`).
1676
+ - The updates ([Update](#update)), from `update/last.json`: `updates: checked 12 min ago: latest names 1.2.0: installed ·
1677
+ last update ok: 1.1.0 → 1.2.0`. `✗` when the last update ended `partial` (with the steps that failed, the log, and
1678
+ that `lanes update` runs its post-commit again) or `failed`, or when the last check found a release it could not
1679
+ install (no recorded npx); `⚠` when the registry gave no release, the last update was refused, no update is recorded
1680
+ yet, `lanes-update.timer` is not enabled (no `timers.target.wants/lanes-update.timer` link in
1681
+ `~/.config/systemd/user` or `$XDG_CONFIG_HOME/systemd/user`: said at once, with `systemctl --user enable --now
1682
+ lanes-update.timer` — an update with `--update-only` never enables it), or the last check is more than 2 hours old
1683
+ (is `lanes-update.timer` on?); a release a rollback marked bad is named. A checkout server gets one line of
1684
+ information; without an installed release there is no line.
1089
1685
 
1090
1686
  ## 7. Agents in lanes
1091
1687
 
@@ -1094,8 +1690,12 @@ Names must be non-empty and free of control characters. The id, resolve file and
1094
1690
  `lanes-statusd` reads each container through `/proc/<init pid>/root` every cycle (3 s) and, without starting
1095
1691
  anything, picks the first signal that applies:
1096
1692
 
1097
- 1. A **hook sidecar** `~/.lz/state/hooks/<lane id>.json` written by `lane-signal` (when hooks are installed), while
1098
- its agent process is alive and its last event is under an hour old.
1693
+ 1. A **hook sidecar** `~/.lz/state/hooks/<lane id>.json` written by `lane-signal` (when hooks are installed, or when
1694
+ the agent reports itself with `lanes_signal`), while its agent process is alive and its last event is under an
1695
+ hour old. Its `name`, when there is one, is the agent's own note: the lane's task line. A `lanes_signal` report
1696
+ stands against the agent's own hooks until the agent does something new (its next tool call, a new prompt, a
1697
+ permission prompt, a failed turn, the session's end); the hook of the report's own tool call and the end of the
1698
+ turn keep it, unless the report was `working`.
1099
1699
  2. A **Claude Code session file** `~/.claude/sessions/<pid>.json` naming the pane, for a live pid (recycled pids are
1100
1700
  rejected).
1101
1701
  3. The **Codex pane title**: `[ ! ] Action Required` is needs approval, a braille spinner is working, a task title is
@@ -1104,6 +1704,17 @@ anything, picks the first signal that applies:
1104
1704
 
1105
1705
  A Codex title left on a bare shell with no child process is `shell` (the harness ended).
1106
1706
 
1707
+ What the sidebar shows is not that word itself but a debounced one. The sections rank Needs you > Working > Done > the
1708
+ workspace's other lanes: a status that moves the lane up is shown at once, one that moves it down only after it has
1709
+ held for 8 s (leaving Needs you) or 5 s (leaving Working or Done), and a lane whose agent signal disappears keeps its
1710
+ glyph 5 s. `status.json` carries both:
1711
+ `agent.status` (debounced) and `agent.raw_status` (this cycle's word), with `agent.stable_since`, when the lane
1712
+ entered its section — the sidebar orders each section by it. With the Claude hooks installed, every tool call that
1713
+ starts signals Working (`PreToolUse`); that hook fires before the permission dialog, so a call you approve still shows
1714
+ `◆` while it runs and for the 8 s dwell after it ends (`PostToolUse`; DEF-1571, open — [RUNBOOK](RUNBOOK.md#installing-agent-hooks-opt-in-per-container-dry-run-first)).
1715
+ A Codex approval read from its pane title (a lane without hooks) keeps the same 8 s dwell once approved. The full rules:
1716
+ [Attention queue](ARCHITECTURE.md#attention-queue).
1717
+
1107
1718
  ### A user's agents in one step: `lanes onboard`
1108
1719
 
1109
1720
  On the server, one command gives a new user's workspace container what its agents need. Without `--write` it prints
@@ -1117,14 +1728,15 @@ lanes onboard anna --container ws-anna --workspace anna-tools --repo /home/coder
1117
1728
  | Step | What the agents get |
1118
1729
  | --- | --- |
1119
1730
  | Board | A workspace tab for the repository (`lanes workspaces add`, section 6) on every device's view |
1120
- | Contract | `lane`, `lane-run`, `lane-signal` and the hook definitions in the container's `~/.lz` (`lanes install-container`) |
1731
+ | 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`) |
1732
+ | 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) |
1121
1733
  | Hooks | The lane-signal hooks for Claude Code and Codex ("Hooks" below), so the sidebar's agent glyphs are event-driven |
1122
1734
  | 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) |
1123
1735
  | 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) |
1124
1736
  | Sync and doctor | The registry knows the new lanes; `lanes doctor`'s result is reported, never fatal |
1125
1737
 
1126
1738
  The receipt is `~/.local/state/lanes/onboard/<login>-<UTC>.json`, and running the command again changes nothing that
1127
- is already done. `--no-mcp` leaves out `lane-mcp`, the in-container MCP server, which has not shipped yet. Every step
1739
+ is already done. `--no-mcp` leaves out `lane-mcp` and its entries in the agents' configuration. Every step
1128
1740
  and status: [install guide](INSTALL.md#onboard-a-users-container).
1129
1741
 
1130
1742
  **Laptop agents on the pool (operator-gated).** Agents a user runs on their own laptop, outside any lane, can use the
@@ -1156,7 +1768,7 @@ restore the `.lanes-bak-*` file, or delete the groups whose command contains `la
1156
1768
 
1157
1769
  | Harness | Event | Status |
1158
1770
  | --- | --- | --- |
1159
- | Claude Code | `UserPromptSubmit`, `PostToolUse` | working |
1771
+ | Claude Code | `UserPromptSubmit`, `PreToolUse`, `PostToolUse` | working |
1160
1772
  | Claude Code | `Stop`, `Notification` (idle prompt) | needs_input |
1161
1773
  | Claude Code | `PermissionRequest`, `Notification` (permission prompt) | needs_approval |
1162
1774
  | Claude Code | `StopFailure` | errored |
@@ -1168,7 +1780,8 @@ restore the `.lanes-bak-*` file, or delete the groups whose command contains `la
1168
1780
  | Codex | `SessionEnd` | shell |
1169
1781
 
1170
1782
  Known limit: after you approve a permission prompt the lane reads needs approval until the approved tool call
1171
- returns, because neither harness signals "approved".
1783
+ returns, because neither harness signals "approved". With Claude Code, after a denied prompt or a failed approved
1784
+ call the lane reads working again as soon as the agent starts its next tool call (`PreToolUse`).
1172
1785
 
1173
1786
  ### The in-container `lane` command
1174
1787
 
@@ -1179,19 +1792,135 @@ verbs, so the holder stays swappable.
1179
1792
  | --- | --- |
1180
1793
  | `lane open <lane-id>` | The editor entry point: attach to the lane; if it does not exist, run its recorded create recipe. Refuses to nest inside tmux; attaches with `ignore-size` while a lanes view shows the lane |
1181
1794
  | `lane attach <id>` | Attach this terminal; never creates; refuses to nest |
1182
- | `lane send <id> [--no-enter] <text…>` | Type into the lane: tmux literally, then Enter unless `--no-enter`; zmx sends the bytes as given |
1795
+ | `lane send <id> [--no-enter] <text…>` | Type into the lane: tmux literally, then Enter half a second later unless `--no-enter` (an agent's prompt takes text and Enter that arrive together as a paste and never submits it); zmx sends the bytes as given |
1183
1796
  | `lane capture <id> [--lines N] [--vt]` | Print the scrollback (plain, or with escapes under `--vt`) |
1184
1797
  | `lane ls [--json] [--all]` | zmx lanes; `--all` adds the registry's tmux lanes with `alive` |
1185
1798
  | `lane status <id>` | JSON: alive, the hook agent, metadata; exit 1 when not running |
1186
1799
  | `lane kill <id> --yes [--force]` | End the lane (the only verb that ends an agent); refuses while the hook sidecar says `working` unless `--force` |
1187
1800
  | `lane new <id> [--ws W] [--title T] [--cwd DIR] [--icon I] [--color C] -- <command…>` | Create a zmx-held lane |
1188
1801
  | `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 |
1802
+ | `lane mcp install [--agent claude\|codex\|all] [--write]` | Add the Lanes MCP server to `~/.claude.json` and `~/.codex/config.toml` ("Agent tools (MCP)" below); a dry run until `--write` |
1803
+ | `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 |
1804
+
1805
+ Every tmux client these verbs start runs with `-N`, so none of them can start a tmux server: an attach to a lane
1806
+ whose server has just ended fails instead of leaving an empty server and its socket file behind.
1189
1807
 
1190
1808
  `lane-run` is the root process of a zmx lane: it runs the agent command, then a login shell in the same directory,
1191
1809
  forever ("lanes never die on agent exit"). `~/.lz/env.sh`, sourced by new shells, points `TMPDIR` and
1192
1810
  `CLAUDE_CODE_TMPDIR` at `/home/coder/.tmp` (`LANES_TMP_DIR` names another; `LANES_TMP_OPTOUT` opts out); tmux sockets
1193
1811
  stay in `/tmp/tmux-<uid>`.
1194
1812
 
1813
+ `lane-edit <file>` is the editor `Ctrl-Alt-e` opens in the container, as uid 1000 in the checkout's root. It prints
1814
+ one line, `editing <file> · <checkout> · :wq to finish` (micro: `Ctrl-s, Ctrl-q to finish`), then runs
1815
+ `$LANES_EDITOR` when that names a program on the container's PATH (one word, a name or an absolute path, no
1816
+ arguments; `lanes edit` sets it from the device's `lanes attach --editor`), else the first of `micro`, `hx`, `nvim`,
1817
+ `vim` and `vi` it finds. **Lanes installs no editor**: a
1818
+ container with none of them gets `lane-edit: no editor on PATH` (exit 127) and nothing opens, so vi (or vim) is the
1819
+ floor. vim and nvim, and a `vi` that is vim behind a link, start with `-u ~/.lz/vimrc` instead of your own vimrc: the
1820
+ file is written in place (it keeps its mode and owner), swap and backup files go to `~/.lz/tmp/` (0700) rather than
1821
+ beside the file, modelines are off, a file without a final newline keeps none, and no `~/.viminfo` is written.
1822
+ Everything it creates is private (`umask 077`). Quitting the editor closes the pane.
1823
+
1824
+ ### Publish to the docs page
1825
+
1826
+ An agent (or you, in a lane's shell) puts a file on the [Docs page](#docs) with `~/.lz/bin/lane publish <file>` or
1827
+ the MCP tool `lanes_publish`: Markdown with Mermaid diagrams is rendered there, an HTML or SVG artifact opens
1828
+ sandboxed on its own page, and any other file is a download, each with the sha256 the server computed. The verb writes
1829
+ a small request in `~/.lz/state/publish/<lane>/` and copies nothing; within about 5 s the docs service on the server
1830
+ reads the file from the container, checks its hash against the request's (a file changed in between is refused, not
1831
+ stored) and writes `<id>.receipt.json` beside the request — `lanes_status` shows it as `published`, with the
1832
+ document's link. The file must be under `/home/coder` and at most 32 MiB. Who sees it (`--for`, else the person who
1833
+ last opened the lane, else everyone whose member record grants its workspace), the receipt's fields and how each kind
1834
+ opens: section 4, [Docs](#docs).
1835
+
1836
+ ### Agent tools (MCP)
1837
+
1838
+ `lane-mcp` is an MCP server in each container (`~/.lz/bin/lane-mcp`). Claude Code and Codex start it from their own
1839
+ configuration, and it lets the agent in a lane see the lanes around it and, within limits, change them. Four tools
1840
+ only read:
1841
+
1842
+ | Tool | Answers |
1843
+ | --- | --- |
1844
+ | `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 |
1845
+ | `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 |
1846
+ | `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 |
1847
+ | `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 |
1848
+
1849
+ Where the answers come from: `~/.lz/registry.json`, which `lanes sync` writes, and `~/.lz/status.json`, which the
1850
+ status daemon writes. A container that has neither yet answers with empty lists and says why.
1851
+
1852
+ Seven tools write. Each reaches the lanes of its own container only, and answers what it changed, what picks the
1853
+ change up (`picked_up_by`) and how soon (`expected_within_s`):
1854
+
1855
+ | Tool | Changes | Seen on the boards |
1856
+ | --- | --- | --- |
1857
+ | `lanes_register` | `name`, optional `session` and `repo`. Declares an unregistered tmux session — the one the agent runs in, or the one `session` names — as a lane: one terminal appended to its board's `.vscode/terminals.json` (command `lane open <id>`, the exact tmux binding, the canonical create recipe). Nothing is started, stopped or renamed. Answers the id the lane will have, `<workspace>--<slug of prefix + name>` | after the next `lanes sync`, within about 15 s |
1858
+ | `lanes_unregister` | Removes a declared lane's terminal (its `terminals[]` item and its `lanes.terminals` entry). The session keeps running and shows as an unregistered session again | same |
1859
+ | `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 |
1860
+ | `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` |
1861
+ | `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 |
1862
+ | `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 |
1863
+ | `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 |
1864
+
1865
+ What they refuse. A refusal is `isError` with `{"refused": <code>, "why", "human"}`: `human` says what a person can
1866
+ do instead, and nothing was changed. `lanes_register` refuses a session that is a lane already (`already_declared`,
1867
+ or `adopted_lane` for a session added with `lanes register` on the host), a name or display name another lane of the
1868
+ board shows (`duplicate_name`), a name whose id another lane holds or is made from (`slug_collision`), a name without
1869
+ a letter or digit A–Z/0–9 (`invalid_name`), a binding the file declares already (`duplicate_binding`), a session that
1870
+ is not running (`session_gone`) and a board with more than one repository in the container unless `repo` names one
1871
+ (`ambiguous_source`). `lanes_unregister` never removes the last terminal of a file (`last_terminal`), an adopted
1872
+ session or a lane whose declaration is not in a file the container holds (`not_declared_here`). `lanes_rename`
1873
+ refuses an unregistered session (register it first) and a name with a control character. `lanes_new` refuses a
1874
+ folder outside `/home/coder` or one that does not exist (`invalid_cwd`), a name or session in use and a command with
1875
+ a control character. `lanes_send` types nothing while the lane's agent is working or waiting for an approval
1876
+ (`agent_working`, `agent_needs_approval`) unless `force` is true, and never a control character: no newline, tab,
1877
+ escape or Ctrl-C (`invalid_text`). `lanes_publish` refuses a file outside `/home/coder` once every link is resolved
1878
+ (`outside_home`), anything but a regular file (`not_a_file`), a file over 32 MiB (`too_large`), a title or login that
1879
+ is not one printable line (`invalid_title`, `invalid_for`), and an agent outside a lane (`not_in_lane`). Every tool
1880
+ refuses a lane of another container (`not_in_container`). The edits go
1881
+ through the same minimal editor as everything else here: one span of strict JSON, the file re-read just before the
1882
+ atomic write, never the root `throttle` object, never a rename or reorder of terminals. The file is the
1883
+ repository's, so its history is git's: no backup file is written.
1884
+
1885
+ Left to people, on purpose (no tool exists for them): ending or closing a lane, interrupting one, opening or
1886
+ attaching one, uploads, the host's `lanes sync`/`gc`/`ensure`, renaming a tmux session, and any change to a session
1887
+ added with `lanes register` (its name and membership live on the host, in `registered-sessions.json`).
1888
+
1889
+ `lanes install-container` (and so `lanes onboard`) adds the server to both agents by default:
1890
+ `"mcpServers": {"lanes": {"command": "/home/coder/.lz/bin/lane-mcp"}}` in `~/.claude.json` and
1891
+
1892
+ ```toml
1893
+ [mcp_servers.lanes]
1894
+ command = "/home/coder/.lz/bin/lane-mcp"
1895
+ env_vars = ["LANE_ID", "LANE_WS", "TMUX", "TMUX_PANE"]
1896
+ ```
1897
+
1898
+ in `~/.codex/config.toml`. Codex starts an MCP server with a cleared environment (only `HOME`, `PATH`, `USER` and a
1899
+ few more of its own), so `env_vars` passes the server what `lanes_whoami` reads; without it a Codex agent could see
1900
+ every lane but its own. Claude Code's servers inherit the agent's environment. An entry is written only where it is
1901
+ missing, and nothing else in either file changes. A Codex table that starts `lane-mcp` but does not pass all four
1902
+ names (the one `codex mcp add` writes, for instance) gains the `env_vars` line, or the names it lacks. The old file
1903
+ is kept as `<file>.bak-<UTC time>` first. `--no-mcp` leaves out `lane-mcp` and both entries, and removes nothing an
1904
+ earlier install wrote. Inside the container the same change is one command:
1905
+
1906
+ ```sh
1907
+ ~/.lz/bin/lane mcp install # prints the diff to both files
1908
+ ~/.lz/bin/lane mcp install --agent claude --write # applies it for Claude Code only
1909
+ ```
1910
+
1911
+ An entry named `lanes` that starts something other than `lane-mcp` is never replaced. A file that is not strict
1912
+ JSON or valid TOML is left alone too, and so is a Codex `env_vars` written other than as one line of plain names.
1913
+ All of these are reported and exit 1. New agent sessions load the server; a
1914
+ running session keeps the tools it started with. Undo: restore the `.bak-*` file, or delete the `lanes` entry. The
1915
+ server logs one line per request (the tool, the lane, the outcome and a refusal's code; never a capture's text, a
1916
+ sent text or a note) to `~/.lz/s/logs/lane-mcp.log`. It speaks MCP protocol
1917
+ versions 2025-06-18, 2025-03-26 and 2024-11-05 over stdio.
1918
+
1919
+ More on it: the wire contract, the latency of each write and what stays with people, in
1920
+ [ARCHITECTURE "Agent API"](ARCHITECTURE.md#agent-api). Uninstall, troubleshooting and a two-frame protocol smoke
1921
+ are in the [runbook](RUNBOOK.md#install-check-uninstall). What to tell your agents is in the
1922
+ [`lanes-agent-tools` skill](../../skills/lanes-agent-tools/SKILL.md).
1923
+
1195
1924
  ### Lane identity and the canonical recipe
1196
1925
 
1197
1926
  A lane's identity is its environment: `LANE_ID` (`<ws>--<slug>`), `LANE_WS` (`<ws>`), and the Throttle trace
@@ -1262,7 +1991,8 @@ Covered in section 3: one notification per lane entering Needs you, from the sid
1262
1991
  ### Push notifications (opt-in, operator gate)
1263
1992
 
1264
1993
  `lanes-notify.service` sends one push to an [ntfy](https://ntfy.sh) topic when a lane's agent enters needs you
1265
- (`Needs you`, `Needs approval`, `Error`). It reads only `status.json`, the registry and `enrol-requests/`, every 3 s.
1994
+ (`Needs you`, `Needs approval`, `Error`). It reads only `status.json`, the registry and `enrol-requests/`, every 3 s,
1995
+ and on such a transition the files that say who was at a desk (below).
1266
1996
  A push says
1267
1997
  `<lane name>: <agent task> — <status>` and `lanes-launch lane <id>`. It also pushes once per new enrolment request
1268
1998
  (`Enrol request`: `<name or login> · <device>` and `lanes enrol approve <id>`), never muted by presence, once per
@@ -1276,12 +2006,17 @@ you configure, so enabling it is your decision. Not yet run against a real ntfy
1276
2006
  | `url` | required | The ntfy server, `https://…` |
1277
2007
  | `topic` | required | `[A-Za-z0-9_-]{1,64}`; on a public server the topic name is the only secret |
1278
2008
  | `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) |
1279
- | `quiet_when_device_active_s` | `120` | Presence window; `0` never suppresses |
1280
- | `action_url` | unset | A URL template over `{lane}`; becomes the tap target and an **Open lane** button |
2009
+ | `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 |
2010
+ | `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 |
1281
2011
 
1282
2012
  Nothing is sent when the status did not change, when the service just started (no burst on restart), or when a desk
1283
2013
  device was present within the presence window: a `lanes attach` from any device, or a lane opened from a desk
1284
- device's sidebar. Opening a lane from the phone never mutes pushes. A failed send is one journal line, not retried.
2014
+ device's sidebar (its seen file, or your profile when that desk device wrote it). Opening a lane from the phone
2015
+ never mutes pushes, and neither does an admin verb run on the command line for you (`lanes workspaces …`, `lanes
2016
+ lane …`, `lanes device claim`, `lanes keys adopt --login`): it writes your profile as `cli:<command>`, not as your
2017
+ device. The window is yours when only you were present and your profile sets `notify.quiet_s` (`0`:
2018
+ never mute your pushes; no verb sets it yet, see the [runbook](RUNBOOK.md#push-notifications-opt-in)); when several
2019
+ people were, the host's `quiet_when_device_active_s` decides. A failed send is one journal line, not retried.
1285
2020
 
1286
2021
  ```sh
1287
2022
  $EDITOR ~/.config/lanes/notify.conf
@@ -1299,20 +2034,48 @@ files. `python3 libexec/tokens.py --check` fails on drift; `--write` regenerates
1299
2034
 
1300
2035
  | Switch | Values | Default |
1301
2036
  | --- | --- | --- |
1302
- | Colour (`--color`, `LANES_COLOR`) | `truecolor` · `16` · `none` | `truecolor` when `COLORTERM` is `truecolor`/`24bit` (the view's config sets it for every pane), else `16`; `NO_COLOR` set means `none` |
2037
+ | Colour (`--color`, `LANES_COLOR`) | `truecolor` · `256` · `16` · `none` | `truecolor` when `COLORTERM` is `truecolor`/`24bit` (the view's config sets it for every pane, so inside a view this is always `truecolor`; see below), else `256` when `TERM` contains `256color`, else `16`; `NO_COLOR` set means `none`. `truecolor` and `256` colour the section words, names and ages and lay the cursor row on a soft background; `16` (your terminal's palette) and `none` keep the quieter look. A viewport's status screens ("THIS VIEWER DISCONNECTED", "retrying in …") follow the same choice and theme: an amber lead and a readable dim line in `truecolor` and `256` |
1303
2038
  | Theme (`--theme`, `LANES_THEME`) | `dark` · `light` · `auto` | `auto`: the sidebar asks the terminal's background once (OSC 11) and paints dark without an answer in 0.3 s |
1304
2039
  | Glyphs (`--glyphs`, `--ascii`, `LANES_GLYPHS`) | `unicode` · `ascii` · `emoji` | `unicode`; `ascii` when no locale variable names UTF-8. `emoji` adds an icon column from the lane's `icon` |
1305
- | Dim (`LANES_DIM`) | `90` · `2` | `90`; 16-colour mode only: `2` for palettes whose colour 8 is near the background |
1306
-
1307
- **Set the look per device**, live: `lanes attach <device> --ascii` (or `--glyphs M`, `--theme T`, `--color C`) records
1308
- it in `~/.local/state/lanes/devices/<device>.json` for a device that already has that file; every running sidebar of
1309
- the device picks it up within a tick, and the choice survives `lanes ensure` and `lanes rebuild`. From a client,
1310
- `lanes-window --ascii|--light|--dark [suffix]` and `lanes-launch <device> --ascii|--theme T|--glyphs M|--color C`
1311
- pass the same flags. The device file wins over the environment.
1312
-
1313
- The environment variables reach a sidebar only through the view server's environment when its session is created,
1314
- and `bin/lanes` starts view sessions from a clean environment that does not forward them. On a standard server use
1315
- the per-device flags; the variables matter when you run the sidebar by hand.
2040
+ | Dim (`--dim`, `LANES_DIM`) | `90` · `2` | `90`; 16-colour mode only: `2` for palettes whose colour 8 is near the background |
2041
+ | Notifications (`--notify`, `LANES_NOTIFY`) | `1` · `0` | `1` on desktop layouts; `0` silences the OSC 9 desktop notifications (section 3). A phone never gets them |
2042
+
2043
+ **Set the look**, live: `lanes attach <device> --ascii` (or `--glyphs M`, `--theme T`, `--color C`, `--notify N`,
2044
+ `--dim D`, and `--editor E`, the editor of [quick edit](#quick-edit)) records it for a device that already has a view (`~/.local/state/lanes/devices/<device>.json`). On a device
2045
+ with an owner — its member record's, its machine's for a window such as `mac-2`, or one `lanes device claim` named —
2046
+ `--theme`, `--color`, `--glyphs` and `--ascii` go to that person's profile, `users/<login>.json`, which every device they
2047
+ own shares, and leave the device file; add `--device-only` to keep them in the device file for that device alone,
2048
+ marked as its own (the file's `device_only` list), where they win over the profile; the same flag without
2049
+ `--device-only` hands the key back to the profile. `--notify`, `--dim` and `--editor` always stay per device, and a device without
2050
+ an owner records every flag in its device file. Every running sidebar of the device picks a device-file change up
2051
+ within a tick, and every sidebar of every device the person owns picks a profile change up within a tick too (`lanes
2052
+ attach phone --theme light` repaints the Mac); the choice survives `lanes ensure` and `lanes rebuild`. A sidebar
2053
+ paints, per key, the device file's `--device-only` value, else the profile's, else the environment's, else the default.
2054
+ A look a device recorded before it had a profile — a device enrolled before profiles existed that ran `lanes-window
2055
+ --dark` then — is not the device's own: it paints only where the profile sets no such key, so recording that key from
2056
+ any device of the person repaints this one too. `lanes user show <login>` names each look a device of that person
2057
+ keeps in its own file, and why; `lanes device claim <device> --login <login>` folds such a look into the profile (the
2058
+ profile's own keys win) and out of the device file — also on a device whose member record already names that login.
2059
+ Lanes never folds it by itself: a device handed to another person still holds its previous owner's look. From a
2060
+ client,
2061
+ `lanes-window --ascii|--light|--dark [--device-only]
2062
+ [suffix]` and `lanes-launch <device> --ascii|--theme T|--glyphs M|--color C|--notify N|--dim D|--editor E|--device-only`
2063
+ pass the same flags (`lanes-window --light --device-only`: light on this device alone). The device file and the profile win
2064
+ over the environment. `lanes user show <login>` prints a profile and the devices' own looks.
2065
+
2066
+ The environment variables `LANES_COLOR`, `LANES_THEME`, `LANES_GLYPHS`, `LANES_DIM` and `LANES_NOTIFY` reach a
2067
+ sidebar through the view server's environment: `bin/lanes` forwards them, when set, from the shell that creates a
2068
+ view session (`lanes attach` of a device whose session is not running, `lanes ensure`, `lanes rebuild`) into that
2069
+ session, so every sidebar it starts inherits them. A running session keeps the environment it was created with, and
2070
+ SSH does not pass them from your client's shell; for a device, the flags above are the live path.
2071
+
2072
+ **256 colours inside a view** are something you choose; Lanes never detects them there. The view's
2073
+ `zellij/config.kdl` gives every pane `COLORTERM=truecolor`, which the sidebar reads before `TERM`, so the
2074
+ `256color` rule (and `NO_COLOR`, which is not forwarded) applies only outside Zellij, to a sidebar started by hand. If
2075
+ your terminal cannot show 24-bit colour, run `lanes attach <device> --color 256` (live, kept by `lanes ensure` and
2076
+ `lanes rebuild`) or create the session with `LANES_COLOR=256 lanes rebuild <device>`. The 256 set uses the fixed xterm
2077
+ colours 16–255 (the `256 dark` / `256 light` columns of [`docs/DESIGN.md`](DESIGN.md)), so your own palette never
2078
+ changes it; `--color none` is the glyphs-only look.
1316
2079
 
1317
2080
  **Ghostty palette** (opt-in): by default the lanes window inherits your everyday Ghostty look. `./install.sh --palette`
1318
2081
  installs `~/.config/lanes/ghostty-palette.conf` generated from the tokens (a light/dark theme pair on Ghostty 1.1 or
@@ -1370,9 +2133,11 @@ All units run in `lanes.slice` and are rendered by the server installer from `la
1370
2133
  | `lanes.slice` | — | (used by every unit) | High CPU weight and `MemoryLow` for the harness, so it stays scheduled on a thrashing host |
1371
2134
  | `lanes-statusd.service` | Always on | Server install | Observes every lane (never touches one); writes `status.json` and `health`; hot-sync |
1372
2135
  | `lanes-sync.timer` / `.service` | Every 2 min | Server install | `lanes sync` |
1373
- | `lanes-ensure.timer` / `.service` | Every 1 min | Server install | `lanes ensure`: recreate missing device view sessions |
1374
- | `lanes-gc.timer` / `.service` | Daily | Server install | `lanes gc`: truncate zmx logs over 5 MB, drop stale dynamic resolve files |
2136
+ | `lanes-ensure.timer` / `.service` | Every 1 min | Server install | `lanes ensure`: recreate missing device view sessions; rebuild a view an update left once no client is attached |
2137
+ | `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)) |
2138
+ | `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 |
1375
2139
  | `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)) |
2140
+ | `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)) |
1376
2141
  | `lanes-web.service` | Always on | `enable-web.sh` (via `lanes web` / `enable-phone.sh`) | Zellij web server on `127.0.0.1:8082` |
1377
2142
  | `lanes-mobile.service` | Always on | `enable-web.sh` | Caddy frontend on `127.0.0.1:8083` |
1378
2143
  | `lanes-studio-host.service` | Always on | `enable-studio-host.sh` (opt-in) | The co-located Throttle Studio Host |
@@ -1394,38 +2159,50 @@ Clients also get `lanes-helper.service` and `lanes-upload.path`/`.service` (Linu
1394
2159
  | Registry | `lanes resolve <lane-id>` | One lane's exact target as JSON |
1395
2160
  | Registry | `lanes status` | One line per workspace: state counts and the `status.json` age |
1396
2161
  | Registry | `lanes register <lane-id> [workspace]` | Add an unregistered session to a workspace without restarting it |
2162
+ | Registry | `lanes unregister <lane-id>` | Take a session added that way out of its workspace again (the sidebar's `u`): nothing stops, a running one is listed under Unregistered with the same id and name; refused while tmux cannot check it, and for a lane a repository's `terminals.json` declares (the file to edit is named) |
1397
2163
  | Registry | `lanes rename <lane-id> <new-name>` | Change a lane's display name (not in the usage text) |
1398
2164
  | Registry | `lanes workspaces` | Print the workspace ids (used by the client installer) |
1399
2165
  | Registry | `lanes workspaces add <id> --container <name> --repo <path> [--title T] [--session S] [--write]` | Add a one-repository board to the site catalog (a dry run until `--write`; section 6) |
1400
- | Views | `lanes attach <device> [--client ID] [--mobile] [--viewports N] [--ascii] [--glyphs …] [--theme …] [--color …]` | Attach this terminal to the device's view (create it if needed); record the look |
2166
+ | Views | `lanes attach <device> [--client ID] [--mobile] [--viewports N] [--ascii] [--glyphs …] [--theme …] [--color …] [--editor …] [--device-only]` | Attach this terminal to the device's view (create it if needed); record the look — in the owner's profile on a device with an owner, on the device with `--device-only` or without one (section 9) — and the quick-edit editor (always per device) |
1401
2167
  | Views | `lanes prepare <device> [--mobile\|--desktop] [--viewports N]` | Create a view without attaching; a different explicit layout is refused |
1402
2168
  | Views | `lanes web <device> [--desktop\|--mobile] [--viewports N] [--token \| --print-only]` | Browser view in one step, Tailscale Serve included (section 4) |
1403
2169
  | Views | `lanes token list` / `revoke <device>` / `revoke --name NAME` / `revoke-all [--yes]` | Web login tokens and their devices (section 4) |
1404
2170
  | Views | `lanes open <lane-id>` | Attach this terminal straight to one lane |
1405
2171
  | Views | `lanes layout <device> [--mobile]` | Print the generated Zellij layout |
1406
- | Views | `lanes rebuild <device>` | Recreate a device's view (agents never touched) |
1407
- | Views | `lanes upgrade-session [device…]` | Sync catalog tabs and titles into live views without a rebuild |
1408
- | Views | `lanes ensure` | Recreate known device sessions that are missing |
2172
+ | Views | `lanes rebuild <device>` | Recreate a device's view (agents never touched); waits up to 120 s while another `ensure`, `rebuild` or view create runs (the view-session lock), then names it and exits 1 |
2173
+ | Views | `lanes upgrade-session [device…]` | Sync catalog tabs and titles into live views without a rebuild; close the tabs of workspaces the owner's profile hides |
2174
+ | Views | `lanes ensure [--if-layout-changed]` | Recreate known device sessions that are missing; with the flag (the update's last step) also rebuild each view the installed release lays out differently that has no client attached, and mark the others `update ready · reload (r)`. One run at a time with every rebuild and view create: with the flag it waits up to 120 s for another, without it 10 s and then skips the run |
1409
2175
  | Views | `lanes reattach` | Make every view re-attach its lane |
2176
+ | Views | `lanes reload-views` | Ask every running viewport to start again from the installed release at its next lane switch (a viewport of an older release, which the signal would end, is left alone and named: `lanes rebuild <device>`); the server update runs it |
1410
2177
  | Views | `lanes device list` / `lanes device forget <name>` | The devices `ensure` keeps / stop keeping one |
2178
+ | Views | `lanes device claim <device> --login L [--name N]` | Name the owner of a device no member record speaks for (one from before enrolment) and fold it and its windows into that person's profile (all but `--device-only` keys); with the login its member record names it only folds (a look from before the profile); refused when a member record names someone else or nobody |
2179
+ | Views | `lanes user list` / `lanes user show <login> [--json]` | The people lanes knows with their devices / one person's profile and every look one of their devices keeps in its own file (`--device-only`, or from before the profile) (read-only) |
2180
+ | Views | `lanes workspaces hide\|show\|order <id…> [--device D \| --login L]` | Arrange one person's boards on every device they own, in their profile: hide or show boards (a hide that would leave no board is refused), or name the order of the first boards (the rest follow in catalog order). The person is the owner of `--device D`, else of `$LANES_DEVICE`, or `--login L`. Stored in the profile, which the tabs (cockpit v3 R4) and the sidebars (R5, and their keys `p h H o`) of every device of that person follow |
2181
+ | Views | `lanes lane pin\|unpin\|hide\|show <lane-id> [--device D \| --login L]` | The same person's pinned lanes (listed first) and hidden lanes (left out of their lists); hide takes a registered lane only |
2182
+ | Views | `lanes here <device>` | Show on that device the lane its owner opened last on any of their devices; says why when it cannot (`lanes-launch here` runs it for your own device) |
1411
2183
  | Views | `lanes next\|prev --focused [--device D]` | What `Ctrl-Alt-↓` / `Ctrl-Alt-↑` run |
1412
2184
  | Views | `lanes preview --focused [--device D]` | What `Ctrl-Alt-m` runs |
2185
+ | Views | `lanes sidebar-key filter\|upload --focused [--device D]` | What `Ctrl-Alt-/` / `Ctrl-Alt-u` run after moving the focus to the sidebar |
2186
+ | Views | `lanes edit --focused [--device D]` | What `Ctrl-Alt-e` runs: the focused lane's AGENTS.md, CLAUDE.md or .env.local in a one-key picker, then the editor in its container ([quick edit](#quick-edit)); with another Edit pane open it focuses or names that one and opens nothing |
1413
2187
  | Editor | `lanes cursor-sync [ws…] [--write] [--diff] [--canonicalize] [--identity]` | Section 6 |
1414
2188
  | Editor | `lanes cursor-commit [ws…] [--push]` | Section 6 |
1415
- | Containers | `lanes install-container <name> [--no-mcp]` | Install the `lane` contract (section 2); `--no-mcp` is for the future `lane-mcp` and changes nothing today |
2189
+ | 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 |
1416
2190
  | 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) |
1417
2191
  | Containers | `lanes hooks install --agent claude\|codex (--container <name> \| --home <dir>) [--dry-run \| --write]` | Section 7 |
1418
2192
  | Uploads | `lanes upload <ws> \| --active <device> [--paste]` | Read a tar stream into that workspace's `uploads/` |
1419
- | Client | `lanes hello [device]` | Prove path, key and server version |
2193
+ | 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)) |
2194
+ | Client | `lanes hello [device] [--client-version V]` | Prove path, key and server version; ` update=<version>` when V is older |
1420
2195
  | Client | `lanes client-config` | Site facts for clients (`LANES_*` lines from `~/.config/lanes/site.conf`) |
1421
- | Access | `lanes keys list \| add <device> <pubkey-file\|-> \| revoke <device> \| adopt [--dry-run]` | Who may log in: the lanes-managed block of `~/.ssh/authorized_keys`, projected from `members/` |
1422
- | Access | `lanes keys owner <device> <login> [--name NAME] [--email ADDR]` | Name the person behind a member; a key added by hand has none, and `/enrol/pending` lets in only a login a member names ([RUNBOOK](RUNBOOK.md#enrolment-service-lanes-enrold-w8-a3)) |
2196
+ | 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 |
2197
+ | Access | `lanes keys owner <device> <login> [--name NAME] [--email ADDR]` | Name the person behind a member; a key added by hand has none, and `/enrol/pending` lets in only a login a member names ([RUNBOOK](RUNBOOK.md#enrolment-service-lanes-enrold-w8-a3)); the device's view and its windows fold into that person's profile |
1423
2198
  | Access | `lanes invite <name> [--email ADDR] [--expires 7d] [--workspaces a,b] [--tailscale-link URL] [--md]` | Mint a one-time code and print the welcome kit, on a terminal only (section 2, "Invite a user") |
1424
2199
  | Access | `lanes invite list \| revoke <id>` | The invites and their state; refuse a code from now on |
1425
2200
  | Access | `lanes enrol list [--all] \| approve <id> [--as <device>] \| deny <id> [--as <device>]` | The enrolment requests; decide one as `/enrol/pending` does |
1426
2201
  | Access | `lanes enrol mode [request\|invite\|auto]` | Pin request mode across restarts (or say what is pinned) |
1427
2202
  | Access | `lanes offboard <device> --yes` | Revoke the key, forget the device, sign out the web login tokens of the machine and its views, end its view (own terminal only) |
1428
2203
  | Health | `lanes doctor` | Full health check (section 6) |
2204
+ | Health | `lanes version [--json]` | The release and tree this server runs, whether `current` names it, and each process still on older code (`✗`: another tree, or started before a checkout's `git pull`) |
2205
+ | Health | `lanes update [--now \| --timer \| --status \| --rollback]` | Install the newest npm release when it is newer (what `lanes-update.timer` runs as `--timer`; `--now` asks the registry even within 20 min of the last check), print the record (`--status`), or install the previous release again and mark the one left bad (`--rollback`) — [Update](#update) |
1429
2206
  | Health | `lanes pressure` | Memory / io / cpu pressure, swap, per-container memory and `/tmp`; plain files only; exit 1 unless ok |
1430
2207
  | Health | `lanes invariant save\|check` | Snapshot / verify every live pane's pid and window size |
1431
2208
  | Health | `lanes sockets [check\|repair]` | tmux servers whose socket file was deleted; `repair` sends tmux's SIGUSR1, nothing restarts |
@@ -1441,10 +2218,11 @@ The client helpers call three more verbs (`rebuilt`, `upload-requests`, `upload-
1441
2218
 
1442
2219
  | Command | Does |
1443
2220
  | --- | --- |
1444
- | `lanes-window [--ascii] [--light\|--dark] [suffix]` | Open the lanes window (section 3) |
1445
- | `lanes-launch [device \| workspace \| lane <id> \| ls]` | The reconnecting client (section 3) |
2221
+ | `lanes-window [--ascii] [--light\|--dark] [--device-only] [suffix]` | Open the lanes window (section 3; the look: section 9) |
2222
+ | `lanes-launch [device \| workspace \| lane <id> \| ls \| here [device]]` | The reconnecting client (section 3); `here` shows the lane you opened last on another of your devices |
1446
2223
  | `lanes-launch web phone\|laptop\|<device> [--desktop\|--mobile] [--no-qr]` | A browser view plus a login token, in this terminal (section 4) |
1447
- | `lanes-launch update [install.sh options]` | The newest client from npm with your remembered install options (section 2) |
2224
+ | `lanes-launch update [install.sh options]` | The newest client from npm with your remembered install options (section 2); a window also updates itself to the server's release, at most every 6 h ([Versions](#versions)) |
2225
+ | `lanes-launch --version` | This client's release (`lanes-launch 1.2.0`; [Versions](#versions)) |
1448
2226
  | `lanes-launch --helper <device> [--service \| --while-pid PID]` | Run only the upload helper (the service form is what the installer sets up) |
1449
2227
  | `lanes-upload …` | Uploads (section 5) |
1450
2228
  | `sh install/enable-phone.sh …` | Phone or browser view plus token; on the server a wrapper over `lanes web … --token` (section 4) |
@@ -1459,13 +2237,15 @@ Server (`~/.local/state/lanes/`, `$LANES_STATE`):
1459
2237
  | --- | --- |
1460
2238
  | `registry.json`, `registry.synced`, `resolve/`, `resolve.dynamic/` | The published registry, the last sync time, per-lane targets |
1461
2239
  | `status.json`, `health` | statusd's snapshot and the one-line health record |
2240
+ | `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 |
1462
2241
  | `containers.json`, `containers/` | Container init pids and home volumes |
1463
- | `devices/<device>.json` | Each known device: layout, viewports, base device, look |
2242
+ | `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 |
1464
2243
  | `layouts/<device>.kdl` | The layout a view session was built from |
1465
- | `views/<device>/` | Per-viewport target, pid and error files |
1466
- | `clients/` | Client pids, `.ready`, `.rebuilt`, `<device>.seen.json` (opened `done` lanes), `.theme`, `.glyphs` |
2244
+ | `views/<device>/` | Per-viewport target, pid and error files; `<workspace>.sidebar.cmd`, the key `Ctrl-Alt-/` or `Ctrl-Alt-u` handed to that workspace's sidebar (`{"cmd": "filter"\|"upload", "at": …}`, ignored once 10 s old); `<workspace>.<slot>.sidebar.state`, what a sidebar that swapped itself onto a new tree hands its successor (cursor lane, scroll, opened-lane marks, filter; taken and deleted at once, ignored once 60 s old) |
2245
+ | `clients/` | Client pids, `.ready`, `.rebuilt`, `<device>.seen.json` (the `done` lanes opened on that device; a device with an owner also marks them in the profile), `.theme`, `.glyphs`, `.color`, `<device>.update-ready` (an update changed that view's layout while it was attached: its sidebar offers the reload) |
1467
2246
  | `defaults/<workspace>` | The lane a workspace's first viewport shows by default (its `focus` lane, else the first) |
1468
2247
  | `user-sessions/<id>.json` | Names of unregistered sessions |
2248
+ | `handback/<digest>.json` | The workspace and id a session unregistered while it ran keeps (`u`, `lanes unregister`): statusd lists it there under that id; it applies to that one tmux session only |
1469
2249
  | `upload-requests/<device>/` | Upload hand-off between sidebar and helper |
1470
2250
  | `economy.json`, `economy.cursor.json` | The opt-in economy projection and its log cursor |
1471
2251
  | `notify.enrol-seen.json` | The enrolment requests the opt-in push notifier already announced (0600, pruned to the requests on disk); deleting it announces the ones still pending once more |
@@ -1475,25 +2255,42 @@ Server (`~/.local/state/lanes/`, `$LANES_STATE`):
1475
2255
  | `tmpscan/` | `lanes tmp` plans and `deleted.log` |
1476
2256
  | `studio-host/` | Optional Studio Host: public key, receipt, enrolment record |
1477
2257
  | `members/<device>.json`, `members.lock` | Who each enrolled machine key belongs to (`lanes keys`; the managed block of `~/.ssh/authorized_keys` is its projection) and the lock every writer of it holds |
2258
+ | `users/<login>.json`, `users/<login>.lock` | One profile per person, named by the percent-encoded login (`anna%40example.com.json`) and shared by every device that person owns: look, opened-lane marks, workspace order and hidden boards, pinned and hidden lanes, the last lane opened; and the lock each writer holds for at most a second (0600). Written by `lanes attach`'s look flags on a device with an owner, `lanes device claim`, `lanes keys adopt --login`, `lanes keys owner`, by `lanes workspaces hide\|show\|order`, `lanes lane pin\|unpin\|hide\|show` and the sidebar keys `p`, `h`, `H` (the arrangement), and by the sidebars, `lanes next\|prev` and `lanes here` when a lane is opened (its mark and the last lane opened); read by `lanes user list\|show`, by `lanes here` (the last lane opened), by the push notifier (a desk device's write is presence — never a command-line verb's, stamped `cli:<command>`; `notify.quiet_s` is your own quiet window) by the view's tabs (`create_session`, `lanes upgrade-session`: board order and hidden boards) and by every sidebar of a device with an owner, which paints its look beneath the device file's own keys, lists its pins first and leaves its hidden lanes out, and re-reads it within a tick of a change |
1478
2259
  | `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 |
1479
2260
  | `enrol-requests/<id>.json` | Enrolment requests waiting for, or past, a member's approval (24 h) |
1480
2261
  | `enrol.log` | The enrolment audit log, one line per attempt, invite, revocation, offboard and mode change (0600, append-only; never a code) |
1481
- | `enrol-mode` | Request mode as `lanes enrol mode request\|invite` pinned it (0600; absent = auto); the enrolment service reads it on every request |
2262
+ | `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`) |
2263
+ | `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)) |
2264
+ | `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) |
1482
2265
  | `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") |
1483
2266
  | `onboard/<login>-<UTC>.json` | One receipt per `lanes onboard` run: each step with its status (section 7) |
2267
+ | `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 |
2268
+ | `locks/sessions.lock` | The view-session lock: `lanes ensure`, `lanes rebuild` (and the sidebar's `r`), the create of `attach`/`prepare`/`web`, `lanes offboard`'s end of a view and `lanes device forget` hold it from their session list to their last Zellij call, so two of them never create or end one view at once; `{pid, doing, since}` of the holder while held, empty after (DEF-1792) |
2269
+ | `update/npx`, `locks/update.lock` | The npx the npm installer ran a `--server` install with (one absolute path; `lanes update` installs with it); the lock one `lanes update` at a time holds (`{pid, started}`) |
1484
2270
 
1485
- Other server paths: `~/.config/lanes/` (`workspaces/`, `site.conf`, `notify.conf`, and the optional invite key files
2271
+ Other server paths: `~/.config/lanes/` (`workspaces/`, `site.conf`, `notify.conf`, `update.conf` — `channel=stable`
2272
+ makes `lanes update` follow the `lanes-stable` dist-tag — and the optional invite key files
1486
2273
  `resend.key` and `tailscale-api.key`), `~/.local/share/lanes/` (pinned
1487
- binaries in `bin/`, the `current` checkout link, npm-staged `pkg/<version>/`), and `~/.lz/<container>` (links to each
2274
+ binaries in `bin/`, the docs page's pinned Mermaid in `vendor/` — `install/enable-docs.sh`, which every server install and update runs —, the `current`
2275
+ link to the installed tree — the path units and layouts name —,
2276
+ `current.previous`, the tree it named before the last install that moved it, and `.install.lock`, the lock installs
2277
+ and unit re-renders take turns on; npm-staged `pkg/<version>/`), and
2278
+ `~/.lz/<container>` (links to each
1488
2279
  container's `.lz`).
1489
2280
 
1490
- In each container (`/home/coder/.lz/`): `bin/` (`lane`, `lane-run`, `lane-signal`, `zmx-*`), `hooks/` (hook
1491
- definitions), `env.sh`, `registry.json` (the projection `lanes sync` writes), `state/hooks/` (hook sidecars),
1492
- `state/<zmx name>.json` (`lane-run` phase records), `meta/` and `s/` (zmx metadata and sockets).
2281
+ In each container (`/home/coder/.lz/`): `bin/` (`lane`, `lane_core.py`, `lane-run`, `lane-signal`, `lane-edit`,
2282
+ `lane-mcp`, `zmx-*`), `VERSION` (the Lanes version that installed them), `s/logs/lane-mcp.log`, `hooks/` (hook
2283
+ definitions), `env.sh`, `vimrc` (what `lane-edit` gives vim and nvim), `tmp/` (their swap files while an edit is open;
2284
+ created by `lane-edit`, 0700), `registry.json` (the projection `lanes sync` writes), `status.json` (this container's
2285
+ lane states, which the status daemon writes), `state/hooks/` (hook sidecars), `state/<zmx name>.json` (`lane-run` phase
2286
+ records), `meta/` and `s/` (zmx metadata and sockets). In the container home itself, the agents' entries for
2287
+ `lane-mcp`: `mcpServers.lanes` in `~/.claude.json` and `[mcp_servers.lanes]` in `~/.codex/config.toml`, with the
2288
+ `.bak-<UTC time>` copies their first write kept.
1493
2289
 
1494
2290
  Client: `~/.config/lanes/` (`device`, `install-options`, `ghostty.conf`, `client-site.conf`, `client-known-hosts`,
1495
2291
  `site.cache`, `upload-allow`, `uploads-dir`, and `enrol-request` while an access request is open),
1496
- `~/.local/state/lanes-client/` (`launch-<target>.log`, `helper.alive`, `helper-service.log`), `~/.local/share/lanes/client/`.
2292
+ `~/.local/state/lanes-client/` (`launch-<target>.log`, `helper.alive`, `helper-service.log`, `checked` — the time of a
2293
+ window's last update question — and `checking/` while one asks), `~/.local/share/lanes/client/`.
1497
2294
 
1498
2295
  ### NEVER
1499
2296
 
@@ -1549,9 +2346,9 @@ Server and views:
1549
2346
  | `LANES_WORKSPACES` | `libexec/lanes_lib.py`, `host-install.sh`, `services.py` | unset | Site directory; unset means `~/.config/lanes/workspaces` (if it has a catalog), else the checkout's `workspaces/` |
1550
2347
  | `LANES_STATUS_INTERVAL` | `libexec/statusd.py` | `3` | Seconds between statusd cycles |
1551
2348
  | `LANES_CONTAINER_UID` | `holders/tmux.sh`, `libexec/view.sh` | `1000` | The containers' tmux uid in socket paths |
1552
- | `LANES_COLOR`, `LANES_THEME`, `LANES_GLYPHS` | `libexec/sidebar.py` | see section 9 | Sidebar look; the device file wins; not forwarded into view sessions by `bin/lanes` |
1553
- | `LANES_DIM` | `libexec/tokens.py` (in the sidebar) | `90` | `2` uses SGR 2 for dim text in 16-colour mode; not forwarded into view sessions |
1554
- | `LANES_NOTIFY` | `libexec/sidebar.py` | unset (on) | `0` silences OSC 9; not forwarded into view sessions |
2349
+ | `LANES_COLOR`, `LANES_THEME`, `LANES_GLYPHS` | `libexec/sidebar.py` | see section 9 | Sidebar look; the device file wins; forwarded by `bin/lanes` into the view sessions it creates |
2350
+ | `LANES_DIM` | `libexec/sidebar.py` (`libexec/tokens.py` outside it) | `90` | `2` uses SGR 2 for dim text in 16-colour mode; the device file's `dim` wins; forwarded into the view sessions `bin/lanes` creates |
2351
+ | `LANES_NOTIFY` | `libexec/sidebar.py` | unset (on) | `0` silences OSC 9; the device file's `notify` wins; forwarded into the view sessions `bin/lanes` creates |
1555
2352
  | `NO_COLOR`, `COLORTERM`, `LC_ALL`, `LC_CTYPE`, `LANG` | `libexec/sidebar.py` | — | Colour and glyph defaults |
1556
2353
  | `LANES_THROTTLE_CONTROL` | `libexec/economy_poll.py` | `http://127.0.0.1:19181` | Throttle control URL; `none` sends no request |
1557
2354
  | `LANES_THROTTLE_TOKEN_FILE` | `libexec/economy_poll.py` | unset | File holding a bearer token |
@@ -1563,12 +2360,18 @@ Server and views:
1563
2360
  | `LANES_ENROL_PORT` | `libexec/enrold.py` | `8184` | The enrolment service's loopback port (the Caddyfile's `/enrol/*` route must follow) |
1564
2361
  | `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 |
1565
2362
  | `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") |
2363
+ | `LANES_DOCS_PORT` | `libexec/docsd.py` | `8185` | The docs service's loopback port (the Caddyfile's `/lanes-docs/*` route must follow) |
2364
+ | `LANES_DOCS_DOMAINS` | `libexec/docsd.py` | `LANES_ENROL_DOMAINS`, else `beyondwork.ai` | Login domains the docs service answers (comma or space separated; `*` = any) |
2365
+ | `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)) |
2366
+ | `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 |
1566
2367
  | `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) |
1567
2368
  | `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 |
1568
2369
  | `LANES_TESTS_FORCE` | `tests/guards.py` | unset | `1` runs live suites on a busy host (on purpose only) |
1569
2370
 
1570
2371
  `LANES_THROTTLE_*` and `LANES_NOTIFY_CONF` set in the installer's environment are rendered into their units only. The
1571
- `LANES_ENROL_*` settings go in a drop-in, `systemctl --user edit lanes-enrold.service`, which a re-install keeps.
2372
+ `LANES_ENROL_*` settings go in a drop-in, `systemctl --user edit lanes-enrold.service`, which a re-install keeps; the
2373
+ `LANES_DOCS_*` settings in lanes-docsd's, `systemctl --user edit lanes-docsd.service` (`lanes upload` reads the age
2374
+ switch from the same files).
1572
2375
  `LANES_REPO`, `LANES_ZELLIJ` and `LANES_WS` are set by Lanes itself for its units and panes.
1573
2376
 
1574
2377
  Client:
@@ -1582,7 +2385,11 @@ Client:
1582
2385
  | `LANES_INVITE` | `install.sh`, `npm/lanes` wrapper | unset | An invite code, as `--invite` (the wrapper picks the site from its prefix) |
1583
2386
  | `LANES_ENROL_WAIT`, `LANES_ENROL_POLL` | `install.sh` | `600`, `5` | Seconds an install waits for an access request's approval, and between two looks |
1584
2387
  | `LANES_ENROL_URL` | `install.sh` | `https://<LANES_SERVER_NAME>:8443` | The enrolment service's base URL (development and tests) |
1585
- | `LANES_PKG_ROOT` | `npm/lanes` wrapper | `~/.local/share/lanes/pkg` | Where npm-staged trees go |
2388
+ | `LANES_NO_AUTOUPDATE` | `lanes-launch`, `npm/lanes` wrapper | unset | Any value: a window never updates its client by itself ([Versions](#versions)), and the wrapper never hands an invite to the server's newer release |
2389
+ | `LANES_AUTOUPDATED` | `lanes-launch` | unset | Set by a window that has just updated itself, for the client it starts again: that one never updates again (one update per window) |
2390
+ | `LANES_NO_SELF_UPDATE` | `npm/lanes` wrapper, `install.sh` | unset | Any value: an invite is redeemed by the package you ran, without asking `/enrol/site` for the server's release |
2391
+ | `LANES_WRAPPER_REPLY` | `install.sh` | unset | Set by the npm wrapper only: the file into which `install.sh` writes `update-to=<release>` before it exits 5 (Appendix B) |
2392
+ | `LANES_PKG_ROOT` | `npm/lanes` wrapper | `~/.local/share/lanes/pkg` | Where npm-staged trees go (any directory name: a staged tree is known by the wrapper's stamp, `lanes_lib.staged_tree`) |
1586
2393
  | `LANES_NPM_ENV_FILE`, `LANES_NPM_TOKEN_KEY` | `npm/lanes/scripts/publish.sh` | `.env.local`, `NPMJS2` | Maintainers only: the publish token's file and key |
1587
2394
 
1588
2395
  Container:
@@ -1593,22 +2400,29 @@ Container:
1593
2400
  | `LANE_ZMX_VERSION` | `lane` | `0.8.1` | zmx binary version |
1594
2401
  | `LANES_TMP_DIR` | `env.sh` | `/home/coder/.tmp` | Temp directory for new shells |
1595
2402
  | `LANES_TMP_OPTOUT` | `env.sh` | unset | Set (any value) to keep `/tmp` |
1596
- | `LANE_ID`, `LANE_WS`, `THROTTLE_TRACE`, `ANTHROPIC_CUSTOM_HEADERS` | Set by the canonical recipe; `lane-signal` reads `LANE_ID`; `lane` strips them | — | Lane identity (section 7) |
2403
+ | `LANE_ID`, `LANE_WS`, `THROTTLE_TRACE`, `ANTHROPIC_CUSTOM_HEADERS` | Set by the canonical recipe; `lane-signal` reads `LANE_ID`; `lane-mcp` reads `LANE_ID` (Codex passes it `LANE_ID`, `LANE_WS`, `TMUX` and `TMUX_PANE` through its entry's `env_vars`); `lane` strips them | — | Lane identity (section 7) |
1597
2404
  | `LANE_HARNESS`, `CLAUDE_SESSION_ID`, `CODEX_THREAD_ID` | `lane-signal` | — | Fallbacks for the harness name and session id |
1598
2405
  | `TMUX_TMPDIR` | `lane gc --sockets` | `/tmp` | Where tmux socket files are looked for |
2406
+ | `LANES_EDITOR` | `lane-edit`; `lanes edit` | unset | The editor quick edit runs: one program on the container's PATH, by name or absolute path, without arguments. Unset, empty or not found: the first of `micro`, `hx`, `nvim`, `vim`, `vi` (one that is not found is named on stderr). `lanes edit` sets it for `lane-edit` from the device's `lanes attach --editor`, else passes on its own `LANES_EDITOR` (a value with a control character is dropped; the `Ctrl-Alt-e` pane's clean environment has none, so there the device's choice is the one that counts) |
2407
+ | `LANES_EDIT_LABEL` | `lane-edit` | the file's name | What the `editing …` line names |
1599
2408
 
1600
2409
  ## Appendix B: exit codes
1601
2410
 
1602
2411
  | Command | Codes |
1603
2412
  | --- | --- |
1604
- | `install.sh`, `npx @vibelord/lanes` | `0` ready · `3` key not yet authorized (no enrolment service, a refused invite, or an unanswered request) · `4` not in the tailnet, server unreachable or too slow · `1` error (also: a malformed invite code, or one for another site) |
1605
- | `install.sh --server` (`host-install.sh`) | `0` · `1` missing prerequisite, not Linux x86_64, or run as root · `2` unknown option |
2413
+ | `install.sh`, `npx @vibelord/lanes` | `0` ready · `3` key not yet authorized (no enrolment service, a refused invite, or an unanswered request) · `4` not in the tailnet, server unreachable or too slow · `5` (`install.sh` only, and only for the npm wrapper's `LANES_WRAPPER_REPLY`) nothing installed: the server runs a newer release, which the wrapper then runs, ending with its status · `1` error (also: a malformed invite code, or one for another site) |
2414
+ | `install.sh --server` (`host-install.sh`) | `0` · `1` missing prerequisite, not Linux x86_64, or run as root · `2` unknown option · `3` the install was refused before anything changed (another install or update is running — it holds `~/.local/state/lanes/update/.lock` — or another install holds `~/.local/share/lanes/.install.lock`, or `current`, `current.previous` or `~/.local/bin/lanes` is no symlink) |
2415
+ | `install.sh --server --update-only` | `0` ok (a `lanes sync` warning included) · `4` partial: installed, a step after the switch failed · `3` refused: a prerequisite, no installed release or another install said no, nothing changed · `1` failed: nothing was installed · `2` usage (also with `--check` or `--no-start`) — the same result is in `update/last.json`, except for a run refused because another install or update was running, which records nothing |
1606
2416
  | `lanes <verb>` | `0` · `1` error (message `lanes: …`) or unknown verb (usage printed) |
1607
2417
  | `lanes sync` | `1` when any workspace's registry had errors (previous lanes kept) |
1608
2418
  | `lanes doctor`, `lanes pressure`, `lanes invariant check`, `lanes sockets` | `1` when something needs attention |
1609
2419
  | `lanes attach <workspace-id>` without a default device, `lanes web --token` without a terminal or while the running `lanes-web.service` reads another token database, `lanes web` when Tailscale Serve cannot be set (operator step printed) | `64` |
1610
2420
  | `lanes token revoke-all` without `--yes` | `2` (nothing changed) |
1611
- | `lanes preview` / `next` / `prev` without `--focused` | `2` |
2421
+ | `lanes preview` / `next` / `prev` / `edit` without `--focused` | `2` |
2422
+ | `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 |
2423
+ | `lanes version` | `0` (a stale process is shown, not an exit status) · `2` an argument other than `--json` |
2424
+ | `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) |
2425
+ | `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 |
1612
2426
  | `lanes studio-host status` | `0` only when running enrolled; `1` otherwise; `2` usage |
1613
2427
  | `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 |
1614
2428
  | `lanes invite revoke <id>` | `0` revoked or already revoked · `1` already redeemed (offboard the device) or no such invite |
@@ -1616,8 +2430,11 @@ Container:
1616
2430
  | `enable-studio-host.sh` | `0` running or waiting for enrolment · `2` usage · `3` typed refusal · `1` failure |
1617
2431
  | `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 |
1618
2432
  | `enable-web.sh` | `0` · `2` usage · `1` Caddy missing |
2433
+ | `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 |
1619
2434
  | `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 |
1620
- | `lanes-window` | `64` invalid suffix · `69` no Ghostty (macOS) or neither Ghostty nor Foot (Linux) |
2435
+ | `lanes-window` | `64` invalid suffix, or `--device-only` without `--ascii`/`--light`/`--dark` · `69` no Ghostty (macOS) or neither Ghostty nor Foot (Linux) |
1621
2436
  | `lanes-upload` | `64` usage · `66` no such file or empty clipboard · otherwise the transfer's status |
1622
2437
  | `lane status` | `1` when the lane is not running |
2438
+ | `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) |
2439
+ | `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 |
1623
2440
  | Live test suites (`tests/*.py`) | `75` host busy, not started |