@vibelord/lanes 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/package.json +1 -1
- package/payload/lanes/README.md +58 -21
- package/payload/lanes/VERSION +1 -1
- package/payload/lanes/bin/lanes +21 -8
- package/payload/lanes/container/env.sh +1 -1
- package/payload/lanes/container/lane +5 -5
- package/payload/lanes/container/lane-signal +4 -4
- package/payload/lanes/docs/ARCHITECTURE.md +16 -8
- package/payload/lanes/docs/DESIGN.md +3 -3
- package/payload/lanes/docs/INSTALL.md +38 -2
- package/payload/lanes/docs/RUNBOOK.md +57 -27
- package/payload/lanes/docs/SHARING.md +239 -4
- package/payload/lanes/docs/USERGUIDE.md +1239 -0
- package/payload/lanes/holders.lock +10 -0
- package/payload/lanes/install/enable-studio-host.sh +14 -0
- package/payload/lanes/install/host-install.sh +5 -2
- package/payload/lanes/install/services.py +101 -4
- package/payload/lanes/install.sh +1 -1
- package/payload/lanes/libexec/agent_state.py +1 -1
- package/payload/lanes/libexec/cursor_sync.py +2 -2
- package/payload/lanes/libexec/economy_poll.py +3 -3
- package/payload/lanes/libexec/lanes_lib.py +1 -1
- package/payload/lanes/libexec/notify.py +3 -3
- package/payload/lanes/libexec/registrations.py +1 -1
- package/payload/lanes/libexec/session_actions.py +1 -1
- package/payload/lanes/libexec/sidebar.py +7 -7
- package/payload/lanes/libexec/statusd.py +25 -10
- package/payload/lanes/libexec/studio_host.py +1608 -0
- package/payload/lanes/systemd/lanes-economy.service +1 -1
- package/payload/lanes/systemd/lanes-studio-host-enrol.service +11 -0
- package/payload/lanes/systemd/lanes-studio-host-enrol.timer +10 -0
- package/payload/lanes/systemd/lanes-studio-host.service +37 -0
- package/payload/lanes/web/README.md +2 -2
- package/payload/lanes/zellij/config.kdl +1 -1
- package/payload/manifest.json +4 -3
package/README.md
CHANGED
|
@@ -6,6 +6,9 @@ registry sync, uploads and a phone interface. The package carries the committed
|
|
|
6
6
|
|
|
7
7
|
The package is **restricted** to the `@vibelord` npm organization.
|
|
8
8
|
|
|
9
|
+
How to use Lanes once installed (desktop, phone and browser views, keys, uploads, operations): the
|
|
10
|
+
[Lanes user guide](../../lanes/docs/USERGUIDE.md).
|
|
11
|
+
|
|
9
12
|
## Install on a new Mac or Linux machine
|
|
10
13
|
|
|
11
14
|
Requirements: Node.js 18+ (for npm only), OpenSSH, and a route to the server (Tailscale for the Beyond Work profile).
|
package/package.json
CHANGED
package/payload/lanes/README.md
CHANGED
|
@@ -1,17 +1,47 @@
|
|
|
1
1
|
# Lanes
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
Lanes is the shared terminal setup for long-running coding agents. The agents run in tmux sessions inside workspace
|
|
4
|
+
containers on one Linux host; every device you use gets its own live view of the same terminals: Zellij workspace
|
|
5
|
+
tabs with a lane sidebar that sorts the agents that need you to the top, registry sync from the repositories'
|
|
6
|
+
terminal lists, Cursor / VS Code integration, file uploads, desktop launchers, and a phone, tablet and laptop browser
|
|
7
|
+
interface. The complete implementation and its tests live in this folder.
|
|
8
|
+
|
|
9
|
+
**Start here: the [Lanes user guide](docs/USERGUIDE.md)** covers daily use on every surface, every key, sidebar
|
|
10
|
+
glyph and command, installation and updates, and the operations reference.
|
|
11
|
+
|
|
12
|
+
Closing a view or losing a connection leaves the terminals running in their workspace containers: a lane ends only
|
|
13
|
+
when someone closes it explicitly.
|
|
14
|
+
|
|
15
|
+
**Who uses it.** Operators and teammates who run several Claude Code or Codex agents at once use Lanes daily to see
|
|
16
|
+
which lane needs them, switch to it, answer, and move on, from a desk or from a phone. An administrator installs the
|
|
17
|
+
server once on the workspace host and authorizes each person's SSH key; each person installs the client on their own
|
|
18
|
+
Mac or Linux machine.
|
|
19
|
+
|
|
20
|
+
**Three surfaces, one set of lanes:**
|
|
21
|
+
|
|
22
|
+
| Surface | How you reach it | Typical use |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| Desktop terminal | `lanes-window` (own Ghostty window) or `lanes-launch` in any terminal, over SSH | Daily work: workspace tabs, the sidebar, one to three lanes side by side |
|
|
25
|
+
| Browser | `/phone` in Safari (Add to Home Screen), or a laptop or tablet browser view from `lanes web`, private over Tailscale | Checking and answering agents away from the desk |
|
|
26
|
+
| Editor | Cursor / VS Code terminals that run `lane open <lane-id>` inside the container | Opening the same lane next to the code |
|
|
27
|
+
|
|
28
|
+
**How it relates to the rest of the repository.**
|
|
29
|
+
|
|
30
|
+
- **Throttle**: Lanes runs without it. With the opt-in economy poller, the sidebar shows what the fleet spends and
|
|
31
|
+
which lanes are starving, downgraded or mostly paid, read from the Throttle daemon. A lane created with the
|
|
32
|
+
canonical recipe sends a `lanes/<workspace>/<lane>` trace to the Throttle proxy (enabling that on a live workspace is
|
|
33
|
+
an operator decision).
|
|
34
|
+
- **Throttle Studio**: a second client of the same lanes. Its **Shared lanes** console gives teammates a read-only,
|
|
35
|
+
SSO-protected view of selected workspaces, and one co-located Studio Host can serve every workspace of a Lanes server
|
|
36
|
+
([sharing](docs/SHARING.md)).
|
|
37
|
+
- **Cursor / VS Code**: each repository's `.vscode/terminals.json` is the source of truth for its lanes;
|
|
38
|
+
`lanes cursor-sync` points the editor's terminals at the same tmux sessions the views show.
|
|
9
39
|
|
|
10
40
|
```text
|
|
11
|
-
Mac / Linux terminal
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
41
|
+
Mac / Linux terminal ──── SSH ───────┐
|
|
42
|
+
├─ Linux workspace host ── container tmux sessions ── Cursor / VS Code
|
|
43
|
+
Phone / tablet / laptop ── HTTPS ────┘ Zellij views agent lanes `lane open <id>`
|
|
44
|
+
browser (Tailscale) registry + status
|
|
15
45
|
```
|
|
16
46
|
|
|
17
47
|
## Quick start
|
|
@@ -78,25 +108,28 @@ The installer starts an empty catalog safely; it does not assume anyone's contai
|
|
|
78
108
|
| Switch workspace | Ctrl–Alt–Left / Right |
|
|
79
109
|
| Next or previous lane | Ctrl–Alt–Down / Up (the focused pane moves to the next / previous lane of its workspace; nothing is typed, the focus stays), or tap the sidebar |
|
|
80
110
|
| Plain-ASCII glyphs, light or dark tokens | `lanes-window --ascii`, `lanes-window --light` / `--dark` (remembered per device in `devices/<device>.json`; a running sidebar picks it up within one tick) |
|
|
81
|
-
| Lock the view (
|
|
82
|
-
|
|
|
111
|
+
| Lock the view (plain keys and Zellij's mode keys reach the lane) | Ctrl–Alt–G, again to unlock; the bar shows LOCKED in red. The Ctrl–Alt keys of `zellij/config.kdl`'s `shared` block (←/→/↓/↑, `/`, U, C, S, PageUp/PageDown/End) stay active while locked. Ctrl–G itself always reaches the lane (Claude Code's editor key) |
|
|
112
|
+
| Zellij modes | Ctrl–Alt–P pane · Ctrl–Alt–T tab · Ctrl–Alt–N resize · Ctrl–Alt–H move · Ctrl–Alt–F scroll · Ctrl–Alt–O session (`d` detaches) · Ctrl–Alt–B tmux-style; Esc returns to normal. Every key: [user guide](docs/USERGUIDE.md#keybindings) |
|
|
113
|
+
| Search lanes | Ctrl–Alt–/, or `/` in the sidebar: fuzzy (the letters in order, substring and prefix matches first); Enter opens the match under the cursor, Esc clears |
|
|
83
114
|
| Select a lane for pane 2 or 3 | `2` / `3` in the sidebar of a wide view |
|
|
84
115
|
| Open one lane directly | `lanes-launch lane <lane-id>`; find IDs with `lanes-launch ls` |
|
|
85
116
|
| Read a markdown file a lane mentioned | Ctrl–Alt–M opens a floating **Preview** pane over the focused lane: the `.md` paths it printed in its last 300 lines that exist in its container (relative ones resolved against its working directory). One is shown at once; several give a numbered menu, newest first (Enter = the newest, `q` closes). Rendered by `glow` in the view's dark/light theme (the server installer adds a pinned glow unless the view server's PATH — `~/.local/share/lanes/bin`, the checkout's `bin`, `/usr/local/bin`, `/usr/bin`, `/bin` — already has one; a glow only in `~/.local/bin` or `~/go/bin` does not count), else `less -R`; `lanes doctor` names which (`preview renderer:`); quitting the pager closes the pane |
|
|
86
|
-
| Create a persistent workspace shell | **+ New shell** under **Unregistered** (
|
|
87
|
-
| Register an existing session | **+** beside an unregistered session, or select it and press `a`; choose a workspace |
|
|
117
|
+
| Create a persistent workspace shell | The **+ New shell** row under **Unregistered** (desktop sidebar and phone picker), or `+` in the sidebar |
|
|
118
|
+
| Register an existing session | **+** beside an unregistered session, or select it and press `a`; choose a workspace (`lanes register <lane-id> [workspace]` on the server) |
|
|
119
|
+
| Declare a session in the repository | `P` in the sidebar, then `y`: writes the session's binding into the workspace's one repository `terminals.json`, so it becomes a repository lane `<workspace>--<slug>` |
|
|
120
|
+
| Rename a lane | `R` in the sidebar (Enter saves, Esc cancels), or `lanes rename <lane-id> <name>` on the server; the lane id never changes |
|
|
88
121
|
| See which agents need you | The sidebar sorts **Needs you** (◆ approval, ✗ error, ◇ waiting for a prompt), **Working** (▸) and **Done** (✓, until you open the lane) above the rest; the header counts them in the same glyphs (`◆1 ◇2 ▸6 ✓1`, zeros left out; `N lanes` when no lane runs an agent) |
|
|
89
122
|
| Read a sidebar row | `▌` cursor · agent glyph · `›` the lane this pane shows (`²`/`³`: shown in pane 2/3) · the name (`·` per other viewer, `…` when clipped) · an **age column** on every row (agent turn, else last output: `45s`, ` 3m`, ` 2h`, ` 4d`; amber in **Needs you**) · stream glyph · one tail cell by priority (`?` not observed right now > `⚠` starving: producing output but no request for over 10 min, or a 429 > `↓` downgraded > `$` mostly paid — the economy cells need the opt-in economy poller; a dim `⚠`/`↓` is a frozen last-known figure, not a live alarm) · `×` close. `⋮ N more` ends a list that continues below |
|
|
90
123
|
| End an unregistered session and its jobs | **×** beside the session, or select it and press `x`; confirm with `y` (any other key cancels) |
|
|
91
124
|
| Every sidebar key | `?` replaces the list with the key sheet until the next key (that key only closes it). The footer legend says ` ⏎ open · / find · x close · ? keys`; prompts share one shape (` › close ‘name’? y/n`, ` › Rename: …█`, ` › Register ‘name’? y/n`) and a filter counts its matches (` /abc · 3 matches`) |
|
|
92
|
-
| Jump to the next lane that needs you | `Tab` / `Shift-Tab` in the sidebar (cycles **Needs you** from the lane the pane shows; the pane title carries the counts and a lane that starts needing you raises one OSC 9 desktop notification — `LANES_NOTIFY=0` silences it) |
|
|
125
|
+
| Jump to the next lane that needs you | `Tab` / `Shift-Tab` in the sidebar (cycles **Needs you** from the lane the pane shows; the pane title carries the counts and a lane that starts needing you raises one OSC 9 desktop notification on desktop layouts — `LANES_NOTIFY=0` in the sidebar's environment silences it, see the [user guide](docs/USERGUIDE.md#9-appearance) for how environment variables reach a sidebar) |
|
|
93
126
|
| Other workspaces that need you | The row above the footer rule: ` ◆1 Worlds · ◇2 Code` (blank and absent when none; panes of 18 rows or more) |
|
|
94
127
|
| What the fleet spends | With the opt-in economy poller ([RUNBOOK](docs/RUNBOOK.md#economy-projection-opt-in)) the same row is the fleet economy strip instead: ` $8.5k/16k 53% · F2 S1 · spill 76% · free 3/19` — overflow spend against the daily cap (amber at the alert level), new upstream 429s, this workspace's lanes by model tier (`F` fable, `O` opus, `S` sonnet, `H` haiku, `G` gpt), the pool's spill fraction, free accounts and the next 5 h reset, whole parts dropped from the end to fit; a leading `?` means the figures are frozen — the poller could not reach the daemon, or it stopped (no rewrite for over 3 min) |
|
|
95
128
|
| Upload | Click the one-row upload strip above the footer (` ↑ uploads · click, then drop (U)`) or press `U`, then drop anywhere on the window; or use `~/Lanes Uploads/<workspace>/`, or `lanes-upload <files>` |
|
|
96
|
-
| Phone setup from a client | `sh install/enable-phone.sh --host lanes` |
|
|
97
|
-
| Laptop or tablet browser view | `lanes web <device> --desktop [--token]` on the server, or `sh install/enable-phone.sh --host lanes --desktop <device>` ([guide](docs/INSTALL.md#laptop-and-tablet-browser)) |
|
|
129
|
+
| Phone setup from a client | `sh install/enable-phone.sh --host lanes` (prepares the `phone` view; open `https://<server>:8443/phone` in Safari) |
|
|
130
|
+
| Laptop or tablet browser view | `lanes web <device> [--desktop\|--mobile] [--viewports N] [--token \| --print-only]` on the server (prepares the view, enables the loopback web services, prints the private URL), or `sh install/enable-phone.sh --host lanes --desktop <device>` ([guide](docs/INSTALL.md#laptop-and-tablet-browser)) |
|
|
98
131
|
| Switch workspace or lane in a laptop browser | The **Lanes** chip (top right) opens the workspace and lane pickers |
|
|
99
|
-
| Detach | Close the window
|
|
132
|
+
| Detach | Close the window, or Ctrl–Alt–O then `d`; the agents keep running |
|
|
100
133
|
|
|
101
134
|
Unregistered sessions also appear when they use shared or previously unlisted tmux servers. Reopening a
|
|
102
135
|
session reuses it; closing a view leaves it running. Registered lanes have no **×** control. See the
|
|
@@ -180,13 +213,17 @@ See [workspace configuration](workspaces.example/README.md) and the [architectur
|
|
|
180
213
|
| `site/`, `workspaces.example/` | Deployment profiles and editable configuration examples |
|
|
181
214
|
| `sudo/` | Optional administrator setup and host protection, applied explicitly |
|
|
182
215
|
| `tests/` | Unit tests plus isolated host/container integration suites |
|
|
183
|
-
| `docs/` | [
|
|
216
|
+
| `docs/` | [User guide](docs/USERGUIDE.md), [install](docs/INSTALL.md), [architecture](docs/ARCHITECTURE.md), [operations](docs/RUNBOOK.md), [design](docs/DESIGN.md), [sharing](docs/SHARING.md), [host guard](docs/HOST-GUARD.md) |
|
|
184
217
|
|
|
185
218
|
## Update and verify
|
|
186
219
|
|
|
187
220
|
After `git pull`, clients re-run `./install.sh`; servers re-run `./install.sh --server` from their installed
|
|
188
|
-
checkout.
|
|
189
|
-
|
|
221
|
+
checkout. npm installations update with `npx @vibelord/lanes@latest` (add `--server` on the host). After a server
|
|
222
|
+
update, restart `lanes-statusd.service` (and `lanes-web`/`lanes-mobile` when their units or the pinned Zellij
|
|
223
|
+
changed); existing views keep their layout until `lanes rebuild <device>`. `lanes hello` prints the server's
|
|
224
|
+
version ([user guide](docs/USERGUIDE.md#update)). The server installer resolves the checkout's actual path,
|
|
225
|
+
including `bw-tools/lanes`; no `~/lanes` clone is required. It preserves configured workspaces and existing agent
|
|
226
|
+
sessions.
|
|
190
227
|
|
|
191
228
|
```sh
|
|
192
229
|
./install.sh --check # client connectivity
|
package/payload/lanes/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.4
|
package/payload/lanes/bin/lanes
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
attach-only, missing script) with the canonical one whose `tmux new -e` flags give the
|
|
37
37
|
lane LANE_ID/LANE_WS and the lanes/<ws>/<id> trace; --identity adds those flags to a
|
|
38
38
|
working plain `tmux -L S new -ADs N [-c D] CMD` recipe, keeping CMD. The old recipe is
|
|
39
|
-
kept as createPrevious. Only these -e flags give a lane its identity (DEF-
|
|
39
|
+
kept as createPrevious. Only these -e flags give a lane its identity (DEF-1559)
|
|
40
40
|
lanes cursor-commit [ws…] [--push]
|
|
41
41
|
commit the normalized terminals.json on top of each repo's REMOTE branch tip and push it,
|
|
42
42
|
with git plumbing only (no working tree or index is touched). Dry-run without --push.
|
|
@@ -79,6 +79,15 @@
|
|
|
79
79
|
its last 300 lines (guarded tmux capture, resolved against its cwd, existing ones only),
|
|
80
80
|
newest first; one is shown at once, several give a numbered menu. glow (pinned in
|
|
81
81
|
holders.lock) in the device's dark/light theme, else less -R
|
|
82
|
+
lanes studio-host status [--json] [--check]
|
|
83
|
+
the co-located Throttle Studio Host (opt-in: install/enable-studio-host.sh): binary
|
|
84
|
+
digest, config mode, enrolment, units. Exit 0 only when it runs enrolled
|
|
85
|
+
lanes studio-host install [--from PATH [--sha256 HEX]]
|
|
86
|
+
select the Host binary: the holders.lock opt-in pin, or a locally built file (both
|
|
87
|
+
sha256-verified); restarts nothing
|
|
88
|
+
lanes studio-host re-key [--yes] [--dry-run]
|
|
89
|
+
after the owner revoked this machine: stop the Host, remove the machine key and its
|
|
90
|
+
dead enrolment, publish a NEW public key for the next deploy. Nothing without --yes
|
|
82
91
|
lanes doctor full health check (every external call is bounded; includes the pressure summary), with
|
|
83
92
|
the lanes per create-recipe class (idempotent / attach-only / null / external /
|
|
84
93
|
retired-path), lane identity, numeric session names, crowded `default` sockets and
|
|
@@ -257,7 +266,7 @@ def mobile_workspace_tab(device, workspace, picker):
|
|
|
257
266
|
command = str(L.REPO / 'libexec' / ('sidebar' if picker else 'view.sh'))
|
|
258
267
|
args = f'{q(device)} {q(workspace["id"])}' + ('' if picker else ' "main" "--mobile"')
|
|
259
268
|
# The picker's sidebar pane has NO layout name: Zellij reports a non-empty pane name instead of the pane's own
|
|
260
|
-
# OSC 2 title, and that title (the attention counts) is what the phone reads (DEF-
|
|
269
|
+
# OSC 2 title, and that title (the attention counts) is what the phone reads (DEF-1531).
|
|
261
270
|
name = '' if picker else f' name={q(workspace["title"])}'
|
|
262
271
|
return [f' tab name={q(title)} {{',
|
|
263
272
|
f' pane borderless=true{name} command={q(command)} {{',
|
|
@@ -268,7 +277,7 @@ def stale_picker_layouts():
|
|
|
268
277
|
"""Phone devices whose stored layout still names the picker's sidebar pane (the pre-Task 26 `name="Lanes"`).
|
|
269
278
|
|
|
270
279
|
`upgrade-session` / `ensure` only add missing tabs and rename titles: a picker tab created from an older layout
|
|
271
|
-
keeps its pane name, which Zellij reports instead of the OSC 2 counts, until `lanes rebuild <device>` (DEF-
|
|
280
|
+
keeps its pane name, which Zellij reports instead of the OSC 2 counts, until `lanes rebuild <device>` (DEF-1531).
|
|
272
281
|
`create_session` is the only writer of layouts/<device>.kdl, so the file is the layout the session was built from.
|
|
273
282
|
"""
|
|
274
283
|
sidebar = str(L.REPO / 'libexec' / 'sidebar')
|
|
@@ -1214,7 +1223,7 @@ def cmd_gc(_args):
|
|
|
1214
1223
|
# shells) and `zmx` into `lanes` (statusd.observe/build_payload; pinned by test_statusd
|
|
1215
1224
|
# Cycle.test_every_published_row_id_is_also_a_lanes_key), so `lanes` alone covers them today. The union is
|
|
1216
1225
|
# defensive: the sweep does not rest on that invariant if the payload is ever decoupled. The mtime is no
|
|
1217
|
-
# liveness signal — make_entry rewrites an unregistered session's file only when it changes (DEF-
|
|
1226
|
+
# liveness signal — make_entry rewrites an unregistered session's file only when it changes (DEF-1537).
|
|
1218
1227
|
known = set(status.get('lanes') or {})
|
|
1219
1228
|
for key in ('unregistered', 'zmx'):
|
|
1220
1229
|
for entries in (status.get(key) or {}).values():
|
|
@@ -1234,6 +1243,10 @@ def cmd_guard(args):
|
|
|
1234
1243
|
return libexec_module('guard').run(args)
|
|
1235
1244
|
|
|
1236
1245
|
|
|
1246
|
+
def cmd_studio_host(args):
|
|
1247
|
+
return libexec_module('studio_host').main(args) # verbs, refusals and exit codes are studio_host's
|
|
1248
|
+
|
|
1249
|
+
|
|
1237
1250
|
def libexec_module(name):
|
|
1238
1251
|
try:
|
|
1239
1252
|
return __import__(name)
|
|
@@ -1295,7 +1308,7 @@ DEFAULT_SOCKET_LANES = 4
|
|
|
1295
1308
|
|
|
1296
1309
|
def recipe_report(registry):
|
|
1297
1310
|
"""(info lines, warnings) of the registry's tmux lanes for `lanes doctor` (plan Task 18): lanes per create-recipe
|
|
1298
|
-
class (lanes_lib.classify_recipe), how many create with their identity (-e LANE_ID, DEF-
|
|
1311
|
+
class (lanes_lib.classify_recipe), how many create with their identity (-e LANE_ID, DEF-1559), lanes bound to a
|
|
1299
1312
|
numeric session name, and crowded `default` sockets. Reads registry.json only: no tmux, no container."""
|
|
1300
1313
|
counts, identity, total = dict.fromkeys(L.RECIPE_CLASSES, 0), 0, 0
|
|
1301
1314
|
broken, numeric, on_default = [], [], {}
|
|
@@ -1499,7 +1512,7 @@ def cmd_doctor(_args):
|
|
|
1499
1512
|
except (OSError, L.LanesError) as error:
|
|
1500
1513
|
check(False, 'zellij reachable', str(error))
|
|
1501
1514
|
stale_pickers = stale_picker_layouts()
|
|
1502
|
-
check(not stale_pickers, 'phone picker panes report their own title (DEF-
|
|
1515
|
+
check(not stale_pickers, 'phone picker panes report their own title (DEF-1531)',
|
|
1503
1516
|
'built from a layout that names the picker pane, so the phone shows no counts; upgrade-session does not '
|
|
1504
1517
|
're-lay-out a tab — run ' + '; '.join(f'lanes rebuild {d}' for d in stale_pickers))
|
|
1505
1518
|
print('PRESSURE')
|
|
@@ -1545,7 +1558,7 @@ def cmd_rename(args):
|
|
|
1545
1558
|
if not lane:
|
|
1546
1559
|
raise L.LanesError(f'Unknown lane: {lane_id}')
|
|
1547
1560
|
# An adopted session (Add to workspace / `lanes register`) takes its name from registered-sessions.json,
|
|
1548
|
-
# which is the one file renamed here; its id and binding never change (DEF-
|
|
1561
|
+
# which is the one file renamed here; its id and binding never change (DEF-1556).
|
|
1549
1562
|
if lane.get('registered_session'):
|
|
1550
1563
|
import registrations
|
|
1551
1564
|
registrations.rename(lane_id, new_name)
|
|
@@ -1800,7 +1813,7 @@ COMMANDS = {'rebuilt': cmd_rebuilt, 'hello': cmd_hello, 'client-config': cmd_cli
|
|
|
1800
1813
|
'prepare': cmd_prepare, 'web': cmd_web, 'attach': cmd_attach, 'rebuild': cmd_rebuild, 'ensure': cmd_ensure, 'invariant': cmd_invariant,
|
|
1801
1814
|
'doctor': cmd_doctor, 'pressure': cmd_pressure, 'sockets': cmd_sockets, 'tmp': cmd_tmp,
|
|
1802
1815
|
'guard': cmd_guard, 'rename': cmd_rename, 'register': cmd_register, 'hooks': cmd_hooks, 'economy': cmd_economy,
|
|
1803
|
-
'preview': cmd_preview, 'device': cmd_device, 'next': cmd_next, 'prev': cmd_prev}
|
|
1816
|
+
'preview': cmd_preview, 'device': cmd_device, 'next': cmd_next, 'prev': cmd_prev, 'studio-host': cmd_studio_host}
|
|
1804
1817
|
|
|
1805
1818
|
if __name__ == '__main__':
|
|
1806
1819
|
if len(sys.argv) < 2 or sys.argv[1] not in COMMANDS:
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
# (<ws>--<slug>), LANE_WS (<ws>), THROTTLE_TRACE and ANTHROPIC_CUSTOM_HEADERS ('x-throttle-trace: lanes/<ws>/<id>';
|
|
15
15
|
# trace charset [A-Za-z0-9._-/], at most 256). Never export them from a profile or a client: the client that starts
|
|
16
16
|
# a shared board socket's tmux server copies its env into the server's GLOBAL env, which every later session there
|
|
17
|
-
# without its own -e inherits (DEF-
|
|
17
|
+
# without its own -e inherits (DEF-1559) — so `lane open` strips them. A running lane gains its identity only when it
|
|
18
18
|
# is next created by a recipe carrying -e; a legacy recipe without -e gives none (`lanes cursor-sync --identity`).
|
|
19
19
|
if [ -z "${LANES_TMP_OPTOUT+set}" ] && [ -d "${LANES_TMP_DIR:-/home/coder/.tmp}" ] \
|
|
20
20
|
&& [ -w "${LANES_TMP_DIR:-/home/coder/.tmp}" ] && [ -x "${LANES_TMP_DIR:-/home/coder/.tmp}" ]; then
|
|
@@ -38,7 +38,7 @@ ROOT = Path(os.environ.get('LANE_ROOT', '/home/coder/.lz'))
|
|
|
38
38
|
VERSION = os.environ.get('LANE_ZMX_VERSION', '0.8.1')
|
|
39
39
|
ZMX = ROOT / 'bin' / f'zmx-{VERSION}'
|
|
40
40
|
SOCKETS, META, STATE = ROOT / 's', ROOT / 'meta', ROOT / 'state'
|
|
41
|
-
HOOKS = STATE / 'hooks' # lane-signal's sidecars (never lane-run's phase records one directory up, DEF-
|
|
41
|
+
HOOKS = STATE / 'hooks' # lane-signal's sidecars (never lane-run's phase records one directory up, DEF-1572)
|
|
42
42
|
PROC = Path('/proc') # this container's pid namespace: the hook's agent pid is checked here
|
|
43
43
|
IDENT = re.compile(r'^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$')
|
|
44
44
|
STATUSES = ('working', 'needs_input', 'needs_approval', 'done', 'errored', 'shell', 'idle') # agent_state.STATUSES
|
|
@@ -167,7 +167,7 @@ IDENTITY = ('LANE_ID', 'LANE_WS', 'THROTTLE_TRACE') # + a lanes/ ANTHROPIC_CUS
|
|
|
167
167
|
def identity_free(environ, drop=('TMUX',)):
|
|
168
168
|
"""`environ` without any lane identity (lanes/docs/ARCHITECTURE.md "Lane identity"). Whatever tmux client STARTS
|
|
169
169
|
a socket's server hands its whole environment to the server's GLOBAL environment, and every later session on
|
|
170
|
-
that socket created without its own `-e` inherits it (MEASURED, tmux 3.4: DEF-
|
|
170
|
+
that socket created without its own `-e` inherits it (MEASURED, tmux 3.4: DEF-1559). Board sockets are shared by
|
|
171
171
|
many lanes, so a lane identity in the env of anything that may start one would be stamped on unrelated lanes.
|
|
172
172
|
Identity therefore reaches a lane's shell ONLY through `-e` in its create recipe (lanes_lib.canonical_recipe).
|
|
173
173
|
An operator's own ANTHROPIC_CUSTOM_HEADERS that is not a lane trace is theirs and stays."""
|
|
@@ -292,7 +292,7 @@ def cmd_open(args):
|
|
|
292
292
|
sys.exit(f"lane open: already inside tmux — refusing to nest. The lane is: {json.dumps(entry.get('tmux') or entry.get('zmx'))}")
|
|
293
293
|
# Nothing `lane open` execs carries a lane identity — neither this lane's nor one inherited from ANOTHER lane (an
|
|
294
294
|
# editor terminal started inside a lane): the recipe may be the first client on a shared board socket and would
|
|
295
|
-
# hand it to every later session there (DEF-
|
|
295
|
+
# hand it to every later session there (DEF-1559). The canonical recipe's own `-e` flags give the identity.
|
|
296
296
|
env = identity_free(os.environ, drop=('TMUX', 'TMUX_PANE'))
|
|
297
297
|
if entry.get('holder') == 'zmx':
|
|
298
298
|
name = entry['zmx']['name']
|
|
@@ -453,7 +453,7 @@ def cmd_status(args):
|
|
|
453
453
|
zname = zmx_name('status', name)
|
|
454
454
|
row = next((r for r in describe() if r['id'] == zname), None)
|
|
455
455
|
if row:
|
|
456
|
-
row['hook'] = hook_agent(zname) # lane-run's phase record stays `agent` (DEF-
|
|
456
|
+
row['hook'] = hook_agent(zname) # lane-run's phase record stays `agent` (DEF-1572)
|
|
457
457
|
print(json.dumps(row or {'id': name, 'running': False}, ensure_ascii=False, indent=1))
|
|
458
458
|
sys.exit(0 if row else 1)
|
|
459
459
|
|
|
@@ -519,7 +519,7 @@ def cmd_gc(args):
|
|
|
519
519
|
Refusal is read from the errno, never from tmux's prose: tmux 3.4 words ECONNREFUSED as `no server running on
|
|
520
520
|
<path>` and uses `error connecting to <path> (<strerror>)` only for OTHER errnos (measured 2026-10-04: EACCES →
|
|
521
521
|
`(Permission denied)`, ENOENT → `(No such file or directory)`), so a 'connection refused' text match never fired
|
|
522
|
-
(DEF-
|
|
522
|
+
(DEF-1540). When tmux fails, `socket_refused` connects to the same path once and only ConnectionRefusedError
|
|
523
523
|
counts."""
|
|
524
524
|
if args != ['--sockets']:
|
|
525
525
|
sys.exit(GC_USAGE)
|
|
@@ -5,7 +5,7 @@ lanes-statusd reads through /proc/<container init pid>/root and publishes as the
|
|
|
5
5
|
|
|
6
6
|
The sidecar has its own directory because `lane-run`, the root process of every zmx-held lane, writes its
|
|
7
7
|
`{phase, exit, at, pid}` record to ~/.lz/state/<zmx name>.json and exports LANE_ID=<zmx name> to the agent: a
|
|
8
|
-
sidecar at state/<lane id>.json would overwrite that record for the agent's whole life (DEF-
|
|
8
|
+
sidecar at state/<lane id>.json would overwrite that record for the agent's whole life (DEF-1572). Nothing but
|
|
9
9
|
this script writes state/hooks/, and lane-run never writes there.
|
|
10
10
|
|
|
11
11
|
{"v":1, "lane":"<id>", "harness":"claude|codex", "status":"<agent{} vocabulary>", "since":<epoch the status
|
|
@@ -16,10 +16,10 @@ It never fails the agent and never speaks: every path exits 0 and nothing reache
|
|
|
16
16
|
UserPromptSubmit/SessionStart hook's stdout becomes model context). An unresolvable lane, a status outside the
|
|
17
17
|
vocabulary or an unwritable state directory are silent no-ops.
|
|
18
18
|
|
|
19
|
-
Lane id: $LANE_ID — exported in a tmux lane only by a canonical/`--identity` recipe's `-e` flags (Task 7, DEF-
|
|
19
|
+
Lane id: $LANE_ID — exported in a tmux lane only by a canonical/`--identity` recipe's `-e` flags (Task 7, DEF-1559;
|
|
20
20
|
`lane open` never exports it), and in a zmx-held lane by `lane-run` as the zmx session name (which is NOT the
|
|
21
21
|
`<ws>--z-<name>` id lanes-statusd publishes for it, so statusd does not join that sidecar to the zmx lane yet:
|
|
22
|
-
DEF-
|
|
22
|
+
DEF-1573) — else the pane's tmux session (`tmux -S ${TMUX%%,*} display -p -t $TMUX_PANE '#S'`) looked up by (socket
|
|
23
23
|
name, session) in ~/.lz/registry.json, the projection `lanes sync` writes.
|
|
24
24
|
|
|
25
25
|
Agent pid: the parent process, or that parent's parent when the parent is a shell (Claude Code runs a hook command
|
|
@@ -141,7 +141,7 @@ def main(argv):
|
|
|
141
141
|
else 'codex')
|
|
142
142
|
pid, start = agent()
|
|
143
143
|
now = int(time.time())
|
|
144
|
-
state = os.path.join(root, 'state', 'hooks') # never state/<id>.json: lane-run's phase record (DEF-
|
|
144
|
+
state = os.path.join(root, 'state', 'hooks') # never state/<id>.json: lane-run's phase record (DEF-1572)
|
|
145
145
|
path = os.path.join(state, f'{lane}.json')
|
|
146
146
|
try:
|
|
147
147
|
old = json.load(open(path))
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Lanes has three parts: a Linux workspace host, long-running terminal sessions inside its containers, and
|
|
4
4
|
views on desktop or phone. The host owns views and derived registries. The container owns each agent's
|
|
5
|
-
terminal. Installing or closing a view does not end a lane.
|
|
5
|
+
terminal. Installing or closing a view does not end a lane. For how to use it, see the [user guide](USERGUIDE.md);
|
|
6
|
+
for operations, the [runbook](RUNBOOK.md).
|
|
6
7
|
|
|
7
8
|
## Source of truth and sync
|
|
8
9
|
|
|
@@ -44,9 +45,9 @@ terminal. Installing or closing a view does not end a lane.
|
|
|
44
45
|
whose agent pid is dead or recycled in the container's pid namespace. The sidecar has its own directory because
|
|
45
46
|
`lane-run` writes each zmx lane's `{phase, exit, at, pid}` record to `~/.lz/state/<zmx name>.json` and exports
|
|
46
47
|
that same name as `LANE_ID`: one shared file name would let a hook overwrite the record the zmx observer and the
|
|
47
|
-
container `lane ls` read (DEF-
|
|
48
|
+
container `lane ls` read (DEF-1572). A zmx lane's sidecar is therefore named by its zmx session, not by the
|
|
48
49
|
`<ws>--z-<name>` id statusd publishes, and does not reach that lane's `agent{}` yet — the zmx lane's `agent` key
|
|
49
|
-
stays lane-run's record (DEF-
|
|
50
|
+
stays lane-run's record (DEF-1573). A published sidecar adds `event` to `agent{}` and has `source: hook`. Hooks are
|
|
50
51
|
never installed implicitly: `lanes hooks install --agent claude|codex --container <name>` prints the merge and
|
|
51
52
|
writes only with `--write` (RUNBOOK "Installing agent hooks").
|
|
52
53
|
5. A lane's `source` is the container's spelling of its `terminals.json`. Every host-side reader of that file
|
|
@@ -71,7 +72,7 @@ to the server's GLOBAL environment (`show-environment -g`), and every later sess
|
|
|
71
72
|
unless its own `-e` overrides each key — a session created by a client carrying `LANE_ID=b` but no `-e` still got
|
|
72
73
|
the server starter's `LANE_ID=a`, and one created with `-e LANE_ID=c` alone kept `a`'s `THROTTLE_TRACE`. Board
|
|
73
74
|
sockets are shared (9 of the 10 bw-tools fixture lanes are on one socket), so `lane open` execs nothing — attach or
|
|
74
|
-
recipe — that carries a lane identity, this lane's or one inherited from another lane's editor terminal (DEF-
|
|
75
|
+
recipe — that carries a lane identity, this lane's or one inherited from another lane's editor terminal (DEF-1559);
|
|
75
76
|
a recipe run by it may start the server with an identity-free global environment only. An attach cannot change a
|
|
76
77
|
running session's env, so a running lane gains its identity only when it is next CREATED by a recipe carrying `-e`.
|
|
77
78
|
A legacy recipe without `-e` gives its lane no identity. `lanes cursor-sync --canonicalize` (dry-run unless
|
|
@@ -92,7 +93,7 @@ stamped `lanes/<ws>/<id>` therefore puts its main session, every subagent and an
|
|
|
92
93
|
into ONE fairness bucket where today each Claude session/agent is its own. `max_in_flight_per_trace` is configured
|
|
93
94
|
and reported but no enforcement site exists in `src/` (MEASURED by search; `pressure.rs` documents fairness as "not a
|
|
94
95
|
hard cap"). Whether a lane should be one bucket is an open decision for the plan owner before any live enablement
|
|
95
|
-
(DEF-
|
|
96
|
+
(DEF-1560); this tree does not change the header shape. Unit-proven only (`test_lib_probe` `LaneOpenIdentity`,
|
|
96
97
|
`CursorSyncCanonicalize`, `CursorSyncIdentity`, stopping at the exec boundary); the disposable-lane
|
|
97
98
|
`tests/cursor_lane.py` exercises the installed `/home/coder/.lz/bin/lane`, so it proves this only after `lanes
|
|
98
99
|
install-container`.
|
|
@@ -153,6 +154,10 @@ Explicit **New +** starts a detached tmux shell inside the workspace container o
|
|
|
153
154
|
rechecking its session ID and creation time and refusing registered lanes; affected viewers become empty.
|
|
154
155
|
The tmux close condition is checked inside the server, protecting a replacement created between observation
|
|
155
156
|
and the action. `user-sessions/` stores display metadata; `status.json` and `resolve.dynamic/` are projections.
|
|
157
|
+
`status.json` also carries `zmx_listed_at` — per container, the unix time statusd ISSUED the `zmx list` that last
|
|
158
|
+
answered (a container whose listing timed out or failed is absent; its `zmx` groups are then the previous ones
|
|
159
|
+
republished). It sits outside the change comparison, so the file is still rewritten only on change or the heartbeat;
|
|
160
|
+
the co-located Studio Host prunes a lane it started only on such a listing (docs/SHARING.md, design §8.10 C1).
|
|
156
161
|
The pre-persistence Docker-shell tracker (`legacy-sessions/`, `legacy_sessions.py`, `lanes verify-legacy`) was
|
|
157
162
|
retired by plan Task 19: statusd no longer reads that directory (a record still there is inert — kept on disk,
|
|
158
163
|
never observed, never closed), and a `legacy` row from an older status.json is refused by close. See
|
|
@@ -175,15 +180,15 @@ writes that last `;` as `\;`. Any other id is a zmx lane (a registry `holder: zm
|
|
|
175
180
|
keeps the zmx path; zmx `send` still writes the bytes as given, never an Enter of its own. `kill` now needs `--yes`
|
|
176
181
|
on both holders and refuses while the lane's hook sidecar says `working` unless `--force`; the container sees only
|
|
177
182
|
that sidecar (Claude session files and pane titles are judged on the host by `lanes-statusd`), so on a lane without
|
|
178
|
-
installed hooks the refusal cannot fire (DEF-
|
|
183
|
+
installed hooks the refusal cannot fire (DEF-1575). `ls --json` stays the zmx-only listing Throttle Studio reads —
|
|
179
184
|
its zmx holder takes EVERY row as a zmx lane, so tmux rows there would publish each tmux lane twice; `ls --all` adds
|
|
180
185
|
the registry's tmux lanes with `alive` from one `list-sessions` per socket (and still lists them in a root without the zmx binary, where the default listing keeps failing as before). zmx rows gain `alive`, and `status` of a
|
|
181
|
-
zmx lane gains `hook` (its sidecar); `agent` stays lane-run's phase record (DEF-
|
|
186
|
+
zmx lane gains `hook` (its sidecar); `agent` stays lane-run's phase record (DEF-1572).
|
|
182
187
|
|
|
183
188
|
The bw-tools agent launchers (`.bw-tools/bin/claude`, `codex`) exec the agent binary (shell `exec` → `agent.py`
|
|
184
189
|
`os.execvpe`), so a lane started through them shows the agent as the pane's foreground command. The operator's
|
|
185
190
|
`~/.local/bin/eval-claude` runs Claude as a child instead (it appends `session_end` to its trace file after the
|
|
186
|
-
exit), so a pane started through it keeps `bash` as its foreground command (DEF-
|
|
191
|
+
exit), so a pane started through it keeps `bash` as its foreground command (DEF-1576); on the launcher path the end
|
|
187
192
|
of a session is the `SessionEnd` hook's `lane-signal shell`, when hooks are installed. `.bw-tools/bin/claude` runs
|
|
188
193
|
`$EVAL_CLAUDE_REAL_BIN` when it is set (the binary eval-claude would run) and skips it when it names a `.bw-tools`
|
|
189
194
|
shim, so `EVAL_CLAUDE_REAL_BIN=<checkout>/.bw-tools/bin/claude eval-claude` composes the two without a loop.
|
|
@@ -236,6 +241,9 @@ It replaces old unit symlinks atomically and creates `~/.local/share/lanes/curre
|
|
|
236
241
|
No service requires the checkout to be named `~/lanes`. Re-run the server installer after moving it.
|
|
237
242
|
|
|
238
243
|
Core units are the status observer and sync, ensure and garbage-collection timers. The web units are opt-in.
|
|
244
|
+
The co-located Throttle Studio Host (`lanes-studio-host.service` and its enrolment retry timer) is opt-in too:
|
|
245
|
+
`install/enable-studio-host.sh` enables it, and its binary is selected by `lanes studio-host install` from an
|
|
246
|
+
`opt-in` line of `holders.lock` or a local build, sha256-verified either way (`libexec/studio_host.py`).
|
|
239
247
|
Temporary-file cleanup and process protection have explicit plan/apply or enable steps; installation does not
|
|
240
248
|
arm them. The root host guard is also separate and records rollback values. See [the runbook](RUNBOOK.md)
|
|
241
249
|
and [host protection](HOST-GUARD.md) for those operations.
|
|
@@ -128,7 +128,7 @@ frame is seven-bit. The `ANSI-16` column is what `LANES_COLOR=16` emits; truecol
|
|
|
128
128
|
| 2 | `AGENT_GLYPHS[agent.status]`; blank without `agent{}`; ink-dim when stale | its tone |
|
|
129
129
|
| 3 | › viewing (not on the cursor row) · ² / ³ another viewport shows it · else space | accent |
|
|
130
130
|
| 4–27 | name (24 cells) in `38;5;N` or ink, **bold when viewing**; viewer dots reserved first, name clipped to `24 − len(dots)` and ending in `…` when clipped; truncation detected by `len(clipped) < len(name)` (no second width walk) | identity colour |
|
|
131
|
-
| 28 | `+` register control on an unregistered row that can be added to a workspace (closable and `R.eligible`; CHA-positioned at `cols − 8`; hit box `cols − 8 ≤ x < cols − 4`), else space — amended by Task 23 (§10, DEF-
|
|
131
|
+
| 28 | `+` register control on an unregistered row that can be added to a workspace (closable and `R.eligible`; CHA-positioned at `cols − 8`; hit box `cols − 8 ≤ x < cols − 4`), else space — amended by Task 23 (§10, DEF-1509) | tone-done |
|
|
132
132
|
| 29–31 | age, right-aligned: `NNs` < 60 s, `NNm` < 60 min, `NNh` < 24 h, `NNd` ≤ 99 d; blank when nothing is known; source `agent.since` else `activity` | ink-dim; attention inside Needs you |
|
|
133
133
|
| 32 | space | |
|
|
134
134
|
| 33 | `GLYPHS[state]` — full tone without an agent, ink-dim with one (▸ + ○ is the long-tool-call case and stays readable) | its tone |
|
|
@@ -231,14 +231,14 @@ Each budget is pinned by a test; a budget that is not met is an open defect row,
|
|
|
231
231
|
|
|
232
232
|
| Budget | Pin |
|
|
233
233
|
|---|---|
|
|
234
|
-
| `render(35 lanes + 5 unregistered, 36×60)` median of 50 after 5 warm-ups ≤ 2.0 ms hard (CI, loose); design target ≤ 0.9× the same-run baseline cold (today 0.459 ms here, binary-safe read) and ≤ 0.10 ms warm (row cache) — MEASURED 2026-10-04 after Task 23: cold 0.64× met; warm 0.27–0.31 ms at load ~23–30, NOT met, DEF-
|
|
234
|
+
| `render(35 lanes + 5 unregistered, 36×60)` median of 50 after 5 warm-ups ≤ 2.0 ms hard (CI, loose); design target ≤ 0.9× the same-run baseline cold (today 0.459 ms here, binary-safe read) and ≤ 0.10 ms warm (row cache) — MEASURED 2026-10-04 after Task 23: cold 0.64× met; warm 0.27–0.31 ms at load ~23–30, NOT met, DEF-1510 open; the target is not raised | `test_sidebar_budget.test_desktop_render_time_and_frame` (time) + bench numbers in the commit message of every task touching `render()` |
|
|
235
235
|
| Frame ≤ 9216 B truecolor, ≤ 5632 B in `LANES_COLOR=16` at 36×60 (today 4330 B in both on the DEF-1417 fixture, 4297 B before it — `LANES_COLOR` is not read before Task 22; truecolor prefixes are the whole delta) | same test, `len(frame)` |
|
|
236
236
|
| Exactly `viewports` `open()` calls of target files per render (12 over 6 renders at 2 viewports today); `subprocess.Popen`/`run` never called | `test_render_opens_only_target_files_and_never_forks` (open counter on the view dir, Popen/run patched to raise) |
|
|
237
237
|
| Every emitted row ≤ 36 cells and column 36 blank on every row that is not made of the `rule` glyph alone. At Task 21 (before the one-row strip) the four upload-box rows are exempt as well — rows starting with `─ ┌ │ └`; Task 24 narrows the exemption to the rule when it removes the box | `test_no_row_exceeds_36_cells_and_column_36_is_blank` (ANSI- and OSC-stripped, binary-safe sink read, `cell_width`; and, because the strip hides positioned writes, every CHA/CUP column in the raw frame < 36 — today's maximum is 34; amendment 2026-10-04; and, because text positioned left of 36 can still run into it, every raw row walked cell by cell — `highest_cell` — writes no cell past 35, 36 on the exempt rows, over a fixture whose registrable ghosts carry both the `+` and the `×`; DEF-1417) with its detectors proven by `test_sidebar_budget_sabotage` |
|
|
238
238
|
| Per tick ≤ 7 `stat()` at one viewport — registry, status, target, health, `flags/sockets-lost` (today's five), economy (Task 12), `devices/<device>.json` (Task 22) — plus one per extra viewport; zero timers added; the OSC 11 query is one write at start | `test_tick_stats_at_most_seven_files` (one viewport); `test_theme_query_is_written_once` |
|
|
239
239
|
| Upload: select timeout stays 1.0 while uploading; `dirty` only on `.progress` mtime change | `test_upload_does_not_shorten_the_tick` (Task 24) |
|
|
240
240
|
| `render_mobile(35, 48×60)` ≤ 2.0 ms hard, frame ≤ 6144 B; target ≤ 0.45 ms cold / ≤ 0.10 ms warm (today 0.352 ms / 1663 B) | `test_mobile_render_time_and_frame` |
|
|
241
|
-
| `reload(force)` unchanged (0.14 ms at 35 / 0.39 ms at 183); fleet attention computed inside it from the loaded dict — NOT shown met: Task 24 measured 0.26–0.44 ms at 35 / 0.69–0.80 ms at 183 and Task 12 0.296 → 0.386 ms at 35 (load 30–62; the 0.14/0.39 figures are quiet-host figures), DEF-
|
|
241
|
+
| `reload(force)` unchanged (0.14 ms at 35 / 0.39 ms at 183); fleet attention computed inside it from the loaded dict — NOT shown met: Task 24 measured 0.26–0.44 ms at 35 / 0.69–0.80 ms at 183 and Task 12 0.296 → 0.386 ms at 35 (load 30–62; the 0.14/0.39 figures are quiet-host figures), DEF-1564 open; the budget is not raised | bench in Task 24's and Task 12's commit messages; DEF-1564 |
|
|
242
242
|
| Phone: exactly one `setInterval(`, zero `setTimeout(`, no `backdrop-filter`, a painted `box-shadow` only in `.lanes-sheet-panel` and `.zj-mobile-menu` (a `box-shadow: none` override, as on the security modal, is not paint work and is not counted); `fetch(` absent; mobile.css grows ≤ 2048 B over today's 16 934 B, mobile.js ≤ 3072 B over today's 19 102 B (measured 2026-10-03; the caps are baseline + allowance, quoted in the test); `index.html` keeps its 9 SRI-pinned upstream assets | `test_web.test_js_timers_unchanged`, `test_web.test_no_scroll_time_effects`, `test_web.test_bundle_growth`, `test_web.test_integrity_pins_untouched` |
|
|
243
243
|
| Zellij: config.kdl grows by the theme + ui block only (~4 KB), `load_plugins {}` empty, no `rename-tab` from the sidebar | `test_keybinds.test_theme_block_and_plugin_free`, `test_tokens.test_generated_blocks_match_committed_files` |
|
|
244
244
|
| Unit suite stays under 75 s wall (MEASURED 2026-10-04 after Task 21: 833 tests, 124–176 s at load ~30–60 — exceeded, DEF-1415 open; the budget is not raised); new pins are exact-string asserts, no sleeps; `tokens.py --check` ≤ 50 ms, contrast test ≤ 10 ms | observed in each task's run step |
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
The same `lanes/` directory installs either a desktop client or the workspace server. A deployment needs
|
|
4
4
|
one server and one client installation per desktop. Safari uses the optional web services on the server.
|
|
5
5
|
Configuration identifies your server and workspaces; credentials and generated runtime state stay outside Git.
|
|
6
|
+
For daily use after installation, see the [user guide](USERGUIDE.md).
|
|
6
7
|
|
|
7
8
|
## 1. Prepare a workspace host
|
|
8
9
|
|
|
@@ -194,7 +195,37 @@ There is no public fallback by design: views are reachable only through the tail
|
|
|
194
195
|
Tailscale name, `lanes web` prints the loopback URL `http://127.0.0.1:8083/<device>` and says that it opens only
|
|
195
196
|
on the host itself.
|
|
196
197
|
|
|
197
|
-
## 5.
|
|
198
|
+
## 5. Optional Throttle Studio Host on the server
|
|
199
|
+
|
|
200
|
+
One Throttle Studio Host can serve every board of this server from the workspace account, supervised as the
|
|
201
|
+
opt-in `lanes-studio-host.service` (Slice `lanes.slice`, after `lanes-statusd`). `./install.sh --server` renders its
|
|
202
|
+
unit but never enables it or downloads its binary. Install a sha256-verified `throttle-rc`, then enable it:
|
|
203
|
+
|
|
204
|
+
```sh
|
|
205
|
+
lanes studio-host install --from ./throttle-rc --sha256 <digest> # or, once holders.lock pins a release: lanes studio-host install
|
|
206
|
+
sh install/enable-studio-host.sh --server https://throttle.example.com --dry-run
|
|
207
|
+
sh install/enable-studio-host.sh --server https://throttle.example.com
|
|
208
|
+
lanes studio-host status
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Enrolment needs no browser on the server: the platform deploy adopts the machine's published public key and writes a
|
|
212
|
+
receipt that `lanes-studio-host-enrol.timer` installs, or the owner places a registration key file. Until then the
|
|
213
|
+
unit waits (exit 3, never a restart loop). The deploy-provisioned path needs the deploy to run on this machine as
|
|
214
|
+
this account (one filesystem, its own env file via `THROTTLE_RC_ENV_FILE`); otherwise use the registration key.
|
|
215
|
+
[Sharing](SHARING.md#co-located-studio-host-one-host-per-lanes-server) has the details, the deploy variables and the
|
|
216
|
+
migration from a hand-built `lanes-sharing-host.service`.
|
|
217
|
+
|
|
218
|
+
The installers write the lanes units where the user manager reads them: `~/.config/systemd/user`, or
|
|
219
|
+
`$XDG_CONFIG_HOME/systemd/user` when the MANAGER's environment sets `XDG_CONFIG_HOME` (`systemctl --user
|
|
220
|
+
show-environment`). Setting `XDG_CONFIG_HOME` in the manager's environment after installing (pam_env,
|
|
221
|
+
`/etc/environment`, `systemctl --user set-environment`) moves where it looks: re-run `./install.sh --server`. The
|
|
222
|
+
two studio units pin `XDG_CONFIG_HOME`/`XDG_DATA_HOME` themselves (the roots `install/enable-studio-host.sh` ran
|
|
223
|
+
with, recorded in `$LANES_STATE/studio-host/xdg-roots.json`, else `%h/.config` and `%h/.local/share`), so neither the
|
|
224
|
+
manager's own values nor a later `./install.sh --server` from a shell without those roots moves the Host's config or
|
|
225
|
+
credential; the server installer warns when its shell's roots differ from the record. To move them, re-run
|
|
226
|
+
`install/enable-studio-host.sh` from the shell whose roots should hold.
|
|
227
|
+
|
|
228
|
+
## 6. Updates, optional operations and removal
|
|
198
229
|
|
|
199
230
|
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.
|
|
200
231
|
|
|
@@ -205,6 +236,8 @@ use the same overrides when reinstalling. `LANES_WORKSPACES` is also carried int
|
|
|
205
236
|
when supplied; the standard configuration directory is the simplest default.
|
|
206
237
|
|
|
207
238
|
After updating server code, restart the status observer with `systemctl --user restart lanes-statusd.service`.
|
|
239
|
+
If you enabled the Studio Host and selected a new `throttle-rc` or edited its config, restart
|
|
240
|
+
`lanes-studio-host` (re-running `install/enable-studio-host.sh` does that, after `studio host --check`).
|
|
208
241
|
If web service definitions or the pinned Zellij binary changed, restart `lanes-web` and `lanes-mobile` too;
|
|
209
242
|
browser clients reconnect. Existing Zellij view sessions use their original layout until explicitly rebuilt.
|
|
210
243
|
`lanes rebuild DEVICE` replaces only that device's view, never the underlying agent sessions.
|
|
@@ -216,6 +249,8 @@ Normal paths:
|
|
|
216
249
|
| `~/.config/lanes/` | Site profile, client settings and server workspace configuration |
|
|
217
250
|
| `~/.local/share/lanes/` | Client copy, pinned binaries, download cache, installed checkout link and npm-staged trees (`pkg/<version>/`) |
|
|
218
251
|
| `~/.local/state/lanes/` | Host registry, device selections, status, upload handoff and housekeeping projections |
|
|
252
|
+
| `~/.local/state/lanes/studio-host/` | Optional Studio Host: published public key, deploy receipt, enrolment record, migrated unit text (`lanes studio-host re-key --yes` clears the key half after a revoke) |
|
|
253
|
+
| `~/.config/throttle-rc/`, `~/.local/share/throttle-rc/` | Optional Studio Host config, machine key and enrolment receipt (throttle-rc's own files) |
|
|
219
254
|
| `~/.local/state/lanes-client/` | Client helper heartbeat and logs |
|
|
220
255
|
| `/home/coder/.lz/` in each container | Lane helper, bindings and optional zmx metadata |
|
|
221
256
|
|
|
@@ -226,6 +261,7 @@ its root guard. These operations are included in the package but are not an inst
|
|
|
226
261
|
Clients uninstall with `./install.sh --uninstall`; SSH keys, `~/Lanes Uploads`, and any server workspace
|
|
227
262
|
configuration and state on the same account are kept.
|
|
228
263
|
To stop the host services, disable `lanes-statusd.service`, `lanes-sync.timer`, `lanes-ensure.timer`,
|
|
229
|
-
`lanes-gc.timer`, and any optional units you enabled.
|
|
264
|
+
`lanes-gc.timer`, and any optional units you enabled (`lanes-studio-host.service` and
|
|
265
|
+
`lanes-studio-host-enrol.timer` included; revoke the machine in the console if it should not come back). For the web endpoint, also run
|
|
230
266
|
`tailscale serve --https=8443 off`. Keep the state and container lane data until you have decided which
|
|
231
267
|
sessions to retain; no broad deletion command is part of uninstall.
|