@vibelord/lanes 1.0.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 (89) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +70 -0
  3. package/bin/lanes-install.mjs +158 -0
  4. package/lib/plan.mjs +65 -0
  5. package/package.json +37 -0
  6. package/payload/lanes/README.md +152 -0
  7. package/payload/lanes/VERSION +1 -0
  8. package/payload/lanes/bin/lanes +1035 -0
  9. package/payload/lanes/client/ghostty/common.conf +22 -0
  10. package/payload/lanes/client/ghostty/macos.conf +6 -0
  11. package/payload/lanes/client/ghostty/omarchy.conf +19 -0
  12. package/payload/lanes/client/install.sh +16 -0
  13. package/payload/lanes/client/lanes-launch +315 -0
  14. package/payload/lanes/client/lanes-upload +147 -0
  15. package/payload/lanes/client/lanes-window +36 -0
  16. package/payload/lanes/client/macos-helper.applescript +14 -0
  17. package/payload/lanes/client/omarchy/discover.sh +56 -0
  18. package/payload/lanes/client/omarchy/lanes.conf +5 -0
  19. package/payload/lanes/client/omarchy/lanes.desktop +9 -0
  20. package/payload/lanes/client/omarchy/lanes.lua +14 -0
  21. package/payload/lanes/client/ssh.stanza.in +54 -0
  22. package/payload/lanes/container/env.sh +16 -0
  23. package/payload/lanes/container/lane +268 -0
  24. package/payload/lanes/container/lane-run +34 -0
  25. package/payload/lanes/docs/ARCHITECTURE.md +93 -0
  26. package/payload/lanes/docs/HOST-GUARD.md +108 -0
  27. package/payload/lanes/docs/INSTALL.md +192 -0
  28. package/payload/lanes/docs/RUNBOOK.md +385 -0
  29. package/payload/lanes/docs/SHARING.md +124 -0
  30. package/payload/lanes/docs/ZMX-RING0.md +51 -0
  31. package/payload/lanes/docs/images/desktop.png +0 -0
  32. package/payload/lanes/docs/images/iphone.png +0 -0
  33. package/payload/lanes/holders/exec.sh +21 -0
  34. package/payload/lanes/holders/tmux.sh +111 -0
  35. package/payload/lanes/holders/zmx.sh +22 -0
  36. package/payload/lanes/holders.lock +5 -0
  37. package/payload/lanes/install/enable-phone.sh +43 -0
  38. package/payload/lanes/install/enable-web.sh +16 -0
  39. package/payload/lanes/install/host-install.sh +72 -0
  40. package/payload/lanes/install/services.py +61 -0
  41. package/payload/lanes/install.sh +603 -0
  42. package/payload/lanes/libexec/cursor_commit.py +159 -0
  43. package/payload/lanes/libexec/cursor_sync.py +166 -0
  44. package/payload/lanes/libexec/guard.py +231 -0
  45. package/payload/lanes/libexec/lanes_lib.py +610 -0
  46. package/payload/lanes/libexec/legacy_sessions.py +130 -0
  47. package/payload/lanes/libexec/session_actions.py +276 -0
  48. package/payload/lanes/libexec/sidebar +8 -0
  49. package/payload/lanes/libexec/sidebar.py +872 -0
  50. package/payload/lanes/libexec/statusd.py +764 -0
  51. package/payload/lanes/libexec/tmpscan.py +921 -0
  52. package/payload/lanes/libexec/view.sh +179 -0
  53. package/payload/lanes/scripts/adopt-sharing-candidate.py +109 -0
  54. package/payload/lanes/site/README.md +28 -0
  55. package/payload/lanes/site/beyondwork/known_hosts +1 -0
  56. package/payload/lanes/site/beyondwork/site.conf +11 -0
  57. package/payload/lanes/site/site.example.conf +10 -0
  58. package/payload/lanes/sudo/host-guard.sh +356 -0
  59. package/payload/lanes/sudo/one-time-root.sh +32 -0
  60. package/payload/lanes/systemd/lanes-ensure.service +7 -0
  61. package/payload/lanes/systemd/lanes-ensure.timer +10 -0
  62. package/payload/lanes/systemd/lanes-gc.service +7 -0
  63. package/payload/lanes/systemd/lanes-gc.timer +9 -0
  64. package/payload/lanes/systemd/lanes-guard.service +8 -0
  65. package/payload/lanes/systemd/lanes-guard.timer +10 -0
  66. package/payload/lanes/systemd/lanes-mobile.service +23 -0
  67. package/payload/lanes/systemd/lanes-statusd.service +15 -0
  68. package/payload/lanes/systemd/lanes-sync.service +10 -0
  69. package/payload/lanes/systemd/lanes-sync.timer +10 -0
  70. package/payload/lanes/systemd/lanes-tmpreap.service +11 -0
  71. package/payload/lanes/systemd/lanes-tmpreap.timer +13 -0
  72. package/payload/lanes/systemd/lanes-web.service +13 -0
  73. package/payload/lanes/systemd/lanes.slice +11 -0
  74. package/payload/lanes/web/Caddyfile +37 -0
  75. package/payload/lanes/web/README.md +35 -0
  76. package/payload/lanes/web/icon-192.png +0 -0
  77. package/payload/lanes/web/icon.svg +4 -0
  78. package/payload/lanes/web/index.html +30 -0
  79. package/payload/lanes/web/manifest.webmanifest +15 -0
  80. package/payload/lanes/web/mobile.css +301 -0
  81. package/payload/lanes/web/mobile.js +367 -0
  82. package/payload/lanes/workspaces.example/README.md +27 -0
  83. package/payload/lanes/workspaces.example/catalog.json +7 -0
  84. package/payload/lanes/workspaces.example/commit-targets.json +8 -0
  85. package/payload/lanes/workspaces.example/terminals.example.json +19 -0
  86. package/payload/lanes/workspaces.example/workspaces.product.json +15 -0
  87. package/payload/lanes/workspaces.example/workspaces.research.json +16 -0
  88. package/payload/lanes/zellij/config.kdl +360 -0
  89. package/payload/manifest.json +31 -0
@@ -0,0 +1,108 @@
1
+ # lanes — host guard (`sudo/host-guard.sh`)
2
+
3
+ One small root script, run by hand. It exists because of 2026-09-19: ceobox thrashed (swap full, PSI memory ≈ 68 %),
4
+ the ~1 GiB lanes harness and the ssh login path were swapped out with everything else, and the only thing standing
5
+ between a container at its memory limit and a dead lane was the OOM killer's default choice.
6
+
7
+ ```
8
+ sudo sh ./sudo/host-guard.sh # read-only status + the plan of what --apply would do
9
+ sudo sh ./sudo/host-guard.sh --apply
10
+ sudo sh ./sudo/host-guard.sh --undo
11
+ ```
12
+
13
+ It refuses to run when it is not root, when cgroup v2 is not mounted, or when `user-<uid>.slice` of the calling user
14
+ (`SUDO_USER`, or explicit `--user ACCOUNT`) does not exist. It never restarts, stops or signals anything, never calls docker, and
15
+ never runs or opens anything inside a container. Status: **not yet run on ceobox** — tested only against a fake
16
+ `/proc` + cgroup tree on a Mac (see the end).
17
+
18
+ The no-argument status shows: `MemoryLow` of the three units (systemd's view and the cgroup file), whether the cgroup2
19
+ mount has `memory_recursiveprot`, PSI, per container `memory.current` / `max` / `high` / `swap.max` / `oom.group` /
20
+ anon / shmem / `oom_kill` and the size and use of its `/tmp` (one `statfs` through `/proc/<init-pid>/root`, no
21
+ nsenter), `oom_score_adj` per protected class, systemd-oomd's state, whether the guard timer is installed — and then
22
+ the plan of what `--apply` would do.
23
+
24
+ ## What `--apply` does, and why each piece needs root
25
+
26
+ | Piece | What | Why root |
27
+ |---|---|---|
28
+ | Memory envelope | `systemctl set-property` `user.slice MemoryLow=4G`, `user-<uid>.slice MemoryLow=4G`, `user@<uid>.service MemoryLow=3G`. Live and persistent (drop-ins under `/etc/systemd/system.control/`), no daemon-reload, nothing restarts. 4G covers the harness + the ssh sessions (`session-N.scope` sits beside `user@`, not under it); 3G of it is for the user manager, where statusd, the zellij servers and the viewers live. | These are system units. A user manager cannot protect itself: `memory.low` is capped by every ancestor, so the value has to be set from `user.slice` down, which only PID 1 does for root. |
29
+ | `lanes-oom-guard` | `/usr/local/sbin/lanes-oom-guard` + `lanes-oom-guard.service/.timer`, every 60 s. Lowers `oom_score_adj` of tmux servers and container inits (−901), agents and zellij (−501). | *Lowering* an `oom_score_adj` needs `CAP_SYS_RESOURCE`; raising one does not (that is why Step 0.4 of the plan, +800 on builds, needs no root). |
30
+ | Record | Previous `MemoryLow` values under `/var/lib/lanes-host-guard/prev/`, written once — a second `--apply` keeps the first record. | root-owned state for `--undo`. |
31
+
32
+ `MemoryLow` protects from *future* reclaim; it does not swap anything back in. It only reaches the leaf cgroups when
33
+ the cgroup2 mount has `memory_recursiveprot` (systemd's default; the status says yes/no and warns on no). And the
34
+ envelope belongs to everything in the user's slice, not to lanes: protection is shared out by usage, so a large
35
+ host-side process of the same user (the restic run) takes its share of it. It is a floor for the login path, not a
36
+ reservation for the harness.
37
+
38
+ ### The OOM guard
39
+
40
+ - **Where it looks:** `cgroup.procs` of every docker container cgroup (`system.slice/docker-*.scope`, or `docker/*` with
41
+ the cgroupfs driver; sub-cgroups included) and of the whole `user-<uid>.slice` (host zellij servers, any host tmux).
42
+ - **What it matches:** the exact `comm` — `tmux: server` (tmux names itself so; clients are `tmux: client`), `zellij`,
43
+ and the agent list in `/etc/lanes-host-guard.conf` (`agents='claude codex'`; exact `/proc/<pid>/comm` strings as
44
+ verified in Step 0 — edit it if an agent runs as `node` or a version-named binary, but then every `node` is an
45
+ agent). Container inits come from `~/.local/state/lanes/containers/*.pid`, believed only when the pid is pid 1 of
46
+ its own pid namespace **and** in a docker cgroup (the files are written by the unprivileged server and can be stale).
47
+ Nothing else is ever given a protective value.
48
+ - **Why −901 / −501 and not the plan's −900 / −500:** `oom_score_adj` is inherited over fork. Every pane shell a
49
+ marked tmux server starts, and every build a marked agent starts, is born with the parent's value; left alone,
50
+ within days everything descended from a lane is as protected as the lane, and the killer takes the Coder agent or
51
+ sshd instead. So each pass also sets an *inherited* mark back to 0 — and the odd values are how the guard knows a
52
+ mark is its own (docker's own value is −500; nobody uses −501). A foreign value is never raised (a tmux server at
53
+ −1000 stays there) and never reset. A fresh child carries the mark for at most 60 s.
54
+ - **Never −1000:** a cgroup in which nothing can be killed turns an OOM into a hang.
55
+ - Cost: `sh`, one `find`+`cat` per cgroup tree, then builtin reads of two tiny `/proc` files per pid. No fork per pid.
56
+ It logs one line to the journal only when it changed something.
57
+
58
+ ## Verify
59
+
60
+ ```
61
+ sudo sh ./sudo/host-guard.sh # memory.low 4294967296 / 4294967296 / 3221225472; marks by class
62
+ systemctl list-timers lanes-oom-guard.timer # next run within 60 s
63
+ journalctl -u lanes-oom-guard -n 5 # "N protected, M inherited marks cleared"
64
+ lanes invariant check # as the workspace user: no lane changed
65
+ ```
66
+
67
+ ## Undo
68
+
69
+ `sudo sh ./sudo/host-guard.sh --undo` sets the recorded `MemoryLow` values back (`set-property`, so a drop-in
70
+ saying `MemoryLow=0` stays behind — it is inert), sets every −901 / −501 mark back to 0, disables and removes the
71
+ timer, service and script, and reloads systemd. `/etc/lanes-host-guard.conf` is kept (your agent list).
72
+ "Back to 0" is exact inside a container (docker's default); on the host, a process the user manager had started at
73
+ 100 / 200 ends at 0 — marginally *more* protected than before, and only until it restarts.
74
+
75
+ ## Deferred — by hand, and not code
76
+
77
+ The plan defers two tighter limits until pressure recurs *after* `/tmp` has been emptied (Steps 1–2). They are
78
+ deliberately **not** sub-commands of the root script: neither is needed now, and the first cannot be done from outside
79
+ without running a container's binary as host root. What would otherwise be forgotten is here instead. Both need the
80
+ status output first (it prints every number below, the init pid and the `docker-<id>.scope` name).
81
+
82
+ | Measure | Only when | By hand | Back |
83
+ |---|---|---|---|
84
+ | Smaller tmpfs `/tmp` (96 → 64 → 32 G, one notch at a time) | `/tmp` is its own tmpfs and holds ≤ 60 % of the **new** size. The kernel itself refuses a size below current use (`EINVAL`), so a wrong number fails instead of destroying — but a cap close to use is `ENOSPC` for every writer in the container minutes later. It frees nothing; it only stops growth. | `nsenter -t <init-pid> -m mount -o remount,<the mount's current flags>,size=64G /tmp` — flags (`nosuid,nodev,noexec…`) from field 6 of the `/tmp` line in `/proc/<init-pid>/mountinfo`: not every `mount` keeps them on a remount. **Hazard:** `nsenter -m` also moves the root directory, so the `mount` that runs is the *container's* file, as host root and outside the container's confinement. Only for a container whose contents you trust as root. | the same command with the old size |
85
+ | Container `memory.high` (e.g. 185 G under a 208 G `memory.max`) | `memory.current` ≤ 90 % **and** anon + shmem ≤ 80 % of the value. Above `memory.high` every allocation in the container is throttled into reclaim; with swap full that is not a kill but a freeze of all lanes. | `systemctl set-property --runtime docker-<id>.scope MemoryHigh=185G` — through systemd, which rewrites `memory.*` of its scopes from its own view at every daemon-reload (a raw `echo` into `memory.high` can silently disappear). | `MemoryHigh=infinity` |
86
+
87
+ ## NEVER
88
+
89
+ - systemd-oomd on docker scopes or `system.slice` (it kills whole cgroups: every lane of a container at once).
90
+ - `memory.oom.group=1` on a container (same effect, from the kernel).
91
+ - `memory.max` below current usage — that is a live OOM sweep. `memory.high` only, and only through the check above.
92
+ - `swapoff` under pressure.
93
+ - Touching `/tmp/tmux-1000` (the lanes' sockets).
94
+ - Restarting a container, docker, or `user@<uid>.service`.
95
+ - `oom_score_adj` −1000 for everything.
96
+ - Tightening anything before usage has dropped.
97
+
98
+ ## How it was tested
99
+
100
+ Against a synthetic `/proc` + cgroup tree with `id` / `systemctl` / `stat` / `df` shims on `PATH`, under both `sh` and
101
+ `dash` on macOS: refusals (not root, no cgroup v2, no user slice, unknown or surplus arguments); status writes nothing
102
+ and calls nothing but `systemctl show` / `is-active`; `/tmp` via GNU `stat -f` and the `df` fallback; `--apply` order
103
+ (parent first), record-once, atomic install, conf kept on re-apply; the guard's classification incl. inherited marks,
104
+ foreign values (−1000, docker's −500, a build at +800), sub-cgroups, vanished pids and a stale pid file; `--undo`
105
+ incl. clearing the marks, and that it is idempotent. The overrides that make this possible (`LANES_PROC_ROOT`,
106
+ `LANES_CGROUP_ROOT`, `LANES_HG_DESTDIR`) do not survive sudo's `env_reset`.
107
+ Not covered by any of that: real systemd, a real kernel, a real container — the first real run is the no-argument
108
+ status, which changes nothing.
@@ -0,0 +1,192 @@
1
+ # Install the complete Lanes setup
2
+
3
+ The same `lanes/` directory installs either a desktop client or the workspace server. A deployment needs
4
+ one server and one client installation per desktop. Safari uses the optional web services on the server.
5
+ Configuration identifies your server and workspaces; credentials and generated runtime state stay outside Git.
6
+
7
+ ## 1. Prepare a workspace host
8
+
9
+ Supported server: Linux x86_64, Python 3.9+, systemd user services, OpenSSH server, Docker, tmux, curl, tar,
10
+ `sha256sum`, `infocmp` and `tic`. The pinned binaries in `holders.lock` are Linux x86_64 builds.
11
+ Install missing packages using your distribution's package manager. The installer checks them before changing files.
12
+
13
+ Run the server as an unprivileged workspace account with Docker access. Container homes must be mounted at
14
+ `/home/coder` with UID 1000; the host account must be able to read the mounted files and reach those users'
15
+ Unix sockets through `/proc`. This is the current container contract, including on a host provisioned without Coder.
16
+ Containers need Bash, Python 3 and tmux. Use compatible tmux versions on the host and containers (tested with
17
+ tmux 3.4): the host client attaches directly to the container server. Zmx is installed as an optional holder,
18
+ not a migration of existing lanes.
19
+
20
+ For services to stay available after logout, an administrator enables the account's user manager:
21
+
22
+ ```sh
23
+ sudo loginctl enable-linger workspace # replace workspace with the actual account
24
+ ```
25
+
26
+ As that account, clone the repository and install:
27
+
28
+ ```sh
29
+ git clone https://github.com/bwllaming/bw-tools.git
30
+ cd bw-tools/lanes
31
+ ./install.sh --server --check
32
+ ./install.sh --server
33
+ ```
34
+
35
+ `--server --no-start` installs files and units without contacting Docker or starting services. The normal install
36
+ verifies archive checksums, installs the pinned binaries, registers this checkout's absolute paths, and enables
37
+ `lanes-statusd`, `lanes-sync.timer`, `lanes-ensure.timer` and `lanes-gc.timer`. It creates an empty workspace catalog
38
+ only if no configuration exists. Host protection, temporary-file deletion and web access remain opt-in.
39
+
40
+ ## 2. Configure workspaces and editor sync
41
+
42
+ Start from the included configuration, then edit container names and repository paths:
43
+
44
+ ```sh
45
+ mkdir -p ~/.config/lanes/workspaces
46
+ cp -R workspaces.example/. ~/.config/lanes/workspaces/
47
+ $EDITOR ~/.config/lanes/workspaces/catalog.json
48
+ $EDITOR ~/.config/lanes/workspaces/workspaces.product.json
49
+ $EDITOR ~/.config/lanes/workspaces/workspaces.research.json
50
+ lanes install-container my-workspace-container
51
+ lanes sync
52
+ lanes doctor
53
+ ```
54
+
55
+ Remove unused example boards from the catalog. A complete terminal registry is provided in
56
+ [`workspaces.example/terminals.example.json`](../workspaces.example/terminals.example.json): adapt it to the
57
+ repository's `.vscode/terminals.json` inside the container. Every terminal needs an explicit tmux socket/session
58
+ binding; there is no guessed command parsing. See [the configuration reference](../workspaces.example/README.md).
59
+
60
+ ```sh
61
+ lanes cursor-sync --diff # preview editor commands and retained create recipes
62
+ lanes cursor-sync --write # apply with backups, after reviewing the preview
63
+ lanes sync
64
+ lanes attach laptop
65
+ ```
66
+
67
+ A lane that has not started is shown as absent. Open its terminal in Cursor / VS Code, or run
68
+ `/home/coder/.lz/bin/lane open product--shell` inside its container to create it from its recorded recipe.
69
+ The viewer itself only attaches. `lanes cursor-commit --push` is a separate, explicit publishing action;
70
+ it is not run by installation or automatic sync.
71
+
72
+ ## 3. Give clients a site profile
73
+
74
+ Copy [`site/site.example.conf`](../site/site.example.conf) and fill in the server's reachable hostname,
75
+ SSH account, and host-key alias. Put the server's public Ed25519 **host** key in a file named `known_hosts`
76
+ next to that profile. Obtain it through a trusted administrator/session:
77
+
78
+ ```text
79
+ workspace-server ssh-ed25519 <base64 public key from /etc/ssh/ssh_host_ed25519_key.pub>
80
+ ```
81
+
82
+ The actual known_hosts line has three fields: alias, `ssh-ed25519`, and the base64 key. Never distribute
83
+ `ssh_host_ed25519_key` (the private key). [Site instructions](../site/README.md) cover profiles and updates.
84
+ The supplied `site/beyondwork/` profile is an explicit choice for the existing team server, not a default.
85
+
86
+ On each Mac or Linux desktop, install from npm (members of the `@vibelord` npm organization; Node.js 18+):
87
+
88
+ ```sh
89
+ npm login # once per machine, or a read-only token in ~/.npmrc
90
+ npx @vibelord/lanes --site beyondwork # bundled team profile; or --site /path/to/site.conf
91
+ ```
92
+
93
+ The package runs this same installer with the same options and exit codes; see
94
+ [`npm/lanes/README.md`](../../npm/lanes/README.md). From a checkout instead:
95
+
96
+ ```sh
97
+ git clone https://github.com/bwllaming/bw-tools.git
98
+ cd bw-tools/lanes
99
+ ./install.sh --site /path/to/site.conf
100
+ ```
101
+
102
+ OpenSSH is required. Install Ghostty for a dedicated window, or run `lanes-launch` in your existing terminal.
103
+ Linux also supports Foot, plus optional Omarchy/Hyprland shortcuts. Tailscale is required when your profile uses
104
+ a tailnet address. The installer installs its own configuration; your normal terminal remains usable.
105
+
106
+ Authorize the printed public key in the server account's `~/.ssh/authorized_keys`. The client never edits that
107
+ file remotely. Then `./install.sh --check` and `lanes-window`. Useful options:
108
+
109
+ - `--device NAME`: independent view name; a stable per-machine name is the default.
110
+ - `--key PATH`: choose an existing SSH private key; the key stays on the client.
111
+ - `--no-desktop` / `--no-uploads`: omit those integrations.
112
+ - `--link`: develop against the checkout instead of installing a client copy.
113
+ - `--yes`: noninteractive installation; a newly created dedicated key has no passphrase.
114
+
115
+ The site selection is saved in `~/.config/lanes/client-site.conf`. Subsequent installs and checks can omit `--site`.
116
+ On macOS, the signed Lanes Helper app requests access to protected folders. Allow Downloads/Desktop/Documents
117
+ as needed. The allowlist is separate from macOS permission; inspect it with `lanes-upload --allowed`.
118
+
119
+ ## 4. Optional Safari / Home Screen access
120
+
121
+ Install Caddy and Tailscale on the server and enable Tailscale HTTPS for its tailnet. The web services use
122
+ loopback ports **8082** (native Zellij) and **8083** (Lanes frontend); private HTTPS uses **8443**.
123
+ Reserve those ports for Lanes on this host. Caddy runs as a separate user service with its admin API disabled.
124
+
125
+ Once, a server administrator allows the workspace account to manage Tailscale Serve:
126
+
127
+ ```sh
128
+ sudo sh sudo/one-time-root.sh --user workspace
129
+ ```
130
+
131
+ This does not change SSH daemon settings unless `--ssh-keepalive` is explicitly supplied. Then, in your own
132
+ interactive terminal, either run on the server:
133
+
134
+ ```sh
135
+ sh install/enable-phone.sh --local
136
+ ```
137
+
138
+ or run from an installed Mac/Linux client:
139
+
140
+ ```sh
141
+ sh install/enable-phone.sh --host lanes
142
+ ```
143
+
144
+ The remote path uses the installed checkout shortcut and the SSH key selected by the client installer;
145
+ there is no password fallback or agent forwarding. It enables the services, prepares a separate `phone` view,
146
+ starts private Tailscale Serve and prints a fresh token. Existing tokens remain valid.
147
+
148
+ Connect Tailscale on the phone, open the printed URL in Safari, paste only the token value, then
149
+ **Share → Add to Home Screen**. A token grants terminal access as the server account. Revoke a named token with:
150
+
151
+ ```sh
152
+ ~/.local/share/lanes/bin/zellij-0.45.1 web --revoke-token NAME
153
+ ```
154
+
155
+ `sh install/enable-web.sh` starts only the loopback services and prints no token. The frontend and services
156
+ run on the workspace host; a Mac does not need to stay awake for the phone to work.
157
+
158
+ ## 5. Updates, optional operations and removal
159
+
160
+ A server installed with `npx @vibelord/lanes --server` runs from the staged tree `~/.local/share/lanes/pkg/<version>/`; updating is `npx @vibelord/lanes@latest --server`, and the previous versions a service may still run from are kept.
161
+
162
+ Keep the server checkout at a stable location. To move it, run `./install.sh --server` from the new location:
163
+ service definitions and `~/.local/share/lanes/current` are regenerated. Existing workspace configuration is preserved.
164
+ Services get the explicit `LANES_SHARE` and `LANES_STATE` paths supplied at installation, or the defaults below;
165
+ use the same overrides when reinstalling. `LANES_WORKSPACES` is also carried into the generated user services
166
+ when supplied; the standard configuration directory is the simplest default.
167
+
168
+ After updating server code, restart the status observer with `systemctl --user restart lanes-statusd.service`.
169
+ If web service definitions or the pinned Zellij binary changed, restart `lanes-web` and `lanes-mobile` too;
170
+ browser clients reconnect. Existing Zellij view sessions use their original layout until explicitly rebuilt.
171
+ `lanes rebuild DEVICE` replaces only that device's view, never the underlying agent sessions.
172
+
173
+ Normal paths:
174
+
175
+ | Path | Contents |
176
+ | --- | --- |
177
+ | `~/.config/lanes/` | Site profile, client settings and server workspace configuration |
178
+ | `~/.local/share/lanes/` | Client copy, pinned binaries, download cache, installed checkout link and npm-staged trees (`pkg/<version>/`) |
179
+ | `~/.local/state/lanes/` | Host registry, device selections, status, upload handoff and housekeeping projections |
180
+ | `~/.local/state/lanes-client/` | Client helper heartbeat and logs |
181
+ | `/home/coder/.lz/` in each container | Lane helper, bindings and optional zmx metadata |
182
+
183
+ `lanes pressure`, `lanes sockets check` and `lanes tmp plan` are inspection tools. Review
184
+ [the runbook](RUNBOOK.md) before applying cleanup/repair; read [host protection](HOST-GUARD.md) before enabling
185
+ its root guard. These operations are included in the package but are not an install-time requirement.
186
+
187
+ Clients uninstall with `./install.sh --uninstall`; SSH keys, `~/Lanes Uploads`, and any server workspace
188
+ configuration and state on the same account are kept.
189
+ To stop the host services, disable `lanes-statusd.service`, `lanes-sync.timer`, `lanes-ensure.timer`,
190
+ `lanes-gc.timer`, and any optional units you enabled. For the web endpoint, also run
191
+ `tailscale serve --https=8443 off`. Keep the state and container lane data until you have decided which
192
+ sessions to retain; no broad deletion command is part of uninstall.