@vibelord/lanes 1.1.1 → 1.2.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 (59) 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 +45 -22
  6. package/payload/lanes/VERSION +1 -1
  7. package/payload/lanes/bin/lanes +2459 -164
  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 +106 -92
  14. package/payload/lanes/container/lane-edit +100 -0
  15. package/payload/lanes/container/lane-mcp +1091 -0
  16. package/payload/lanes/container/lane-signal +109 -29
  17. package/payload/lanes/container/lane_core.py +1140 -0
  18. package/payload/lanes/container/vimrc +31 -0
  19. package/payload/lanes/docs/ARCHITECTURE.md +618 -13
  20. package/payload/lanes/docs/DESIGN.md +122 -44
  21. package/payload/lanes/docs/INSTALL.md +157 -17
  22. package/payload/lanes/docs/RUNBOOK.md +603 -72
  23. package/payload/lanes/docs/SHARING.md +3 -2
  24. package/payload/lanes/docs/USERGUIDE.md +777 -136
  25. package/payload/lanes/install/enable-web.sh +2 -0
  26. package/payload/lanes/install/host-install.sh +163 -7
  27. package/payload/lanes/install/services.py +148 -18
  28. package/payload/lanes/install.sh +68 -7
  29. package/payload/lanes/libexec/agent_state.py +74 -1
  30. package/payload/lanes/libexec/cursor_sync.py +6 -0
  31. package/payload/lanes/libexec/device_sync.py +47 -10
  32. package/payload/lanes/libexec/economy_poll.py +1 -0
  33. package/payload/lanes/libexec/enrold.py +41 -22
  34. package/payload/lanes/libexec/keys.py +54 -23
  35. package/payload/lanes/libexec/lanes_lib.py +620 -22
  36. package/payload/lanes/libexec/members.py +16 -2
  37. package/payload/lanes/libexec/notify.py +104 -24
  38. package/payload/lanes/libexec/registrations.py +216 -18
  39. package/payload/lanes/libexec/session_actions.py +182 -14
  40. package/payload/lanes/libexec/sidebar +11 -3
  41. package/payload/lanes/libexec/sidebar.py +2135 -361
  42. package/payload/lanes/libexec/statusd.py +137 -10
  43. package/payload/lanes/libexec/studio_host.py +8 -1
  44. package/payload/lanes/libexec/tokens.py +121 -30
  45. package/payload/lanes/libexec/tokens.sh +86 -2
  46. package/payload/lanes/libexec/update.py +972 -0
  47. package/payload/lanes/libexec/users.py +614 -0
  48. package/payload/lanes/libexec/view.sh +34 -6
  49. package/payload/lanes/systemd/lanes-ensure.service +3 -0
  50. package/payload/lanes/systemd/lanes-gc.service +1 -1
  51. package/payload/lanes/systemd/lanes-update.service +17 -0
  52. package/payload/lanes/systemd/lanes-update.timer +17 -0
  53. package/payload/lanes/web/README.md +83 -24
  54. package/payload/lanes/web/browser.js +3 -5
  55. package/payload/lanes/web/mobile.css +17 -17
  56. package/payload/lanes/web/mobile.js +44 -32
  57. package/payload/lanes/workspaces.example/README.md +3 -1
  58. package/payload/lanes/zellij/config.kdl +106 -56
  59. package/payload/manifest.json +5 -3
@@ -15,17 +15,28 @@ at the repository root; this page condenses it for daily use and never overrides
15
15
  2. **Never colour alone:** every state has a glyph, a tone and an ASCII twin, and a fixed column; `LANES_COLOR=none`
16
16
  leaves a fully readable sidebar.
17
17
  3. **One token set, many renderers:** `libexec/tokens.py` is the only place a colour or glyph is defined; every
18
- other surface is a generated projection pinned by a drift test.
19
- 4. **Theme-agnostic by construction:** the sidebar paints foreground only, learns dark/light once (OSC 11), falls
20
- back to dark, and keeps lane identity colours in your own 16-colour palette.
21
- 5. **Dim is a colour, not an attribute:** no SGR 2 in any frame; ink-dim in truecolor, bright black (`90`) in
22
- 16-colour mode.
18
+ other surface is a generated projection pinned by a drift test, and no colour sits outside a surface's generated
19
+ block (`test_tokens.GeneratedSurfaces`, cockpit v3 P5). The phone shell's `theme-color` metas and manifest colours
20
+ are hand-written copies of the `surface` tokens, held to them by the same test; the app icon is a brand asset
21
+ ([`web/README.md`](../web/README.md)).
22
+ 4. **Theme-agnostic by construction:** every painted role is a foreground but one. `selected`, the cursor row, is a
23
+ foreground-and-background role: the `accent-wash` value under the row's own foregrounds, in truecolor and 256
24
+ colours only and closed before column 36, so the terminal's own background shows everywhere else (cockpit v3 P2).
25
+ 16 colours and `none` paint no background at all; the cursor there is the bold `▌`. The sidebar learns dark/light
26
+ once (OSC 11), falls back to dark, and keeps lane identity colours in your own 16-colour palette.
27
+ 5. **Dim is a colour, not an attribute:** ink-dim is the table's value in truecolor and 256 colours and bright black
28
+ (`90`) in 16; a frame carries SGR 2 only when you choose `LANES_DIM=2` (16 colours only). The band is a surface
29
+ like any other: every text role, dim included, holds 4.5:1 on `selected` (`test_tokens.Contrast`; 4.2:1 for the
30
+ 256 approximations, `test_tokens.Xterm256`).
23
31
  6. **Restraint:** one accent, for selection, viewing marks and the brand only; no emoji in rows, one upload line, a
24
32
  one-line legend plus the `?` key sheet.
25
33
  7. **Every row says how long:** a 3-cell age on every row — the agent's `since`, else the last output.
26
34
  8. **Marks never lose to names:** viewer dots and viewport marks are reserved first; the name is clipped and ends
27
35
  in `…`.
28
- 9. **The cursor follows the lane, not the index:** re-sorts never move your focus in a list that scrolls.
36
+ 9. **The cursor follows the lane, not the index, and the list holds still:** a lane changing section moves the cursor
37
+ with it, never the list (`top` is pinned; it scrolls only to keep the cursor's lane on screen). Sections keep entry
38
+ order (`stable_since`), and the Needs you / Working headers stay drawn once a workspace has agents, `(0)` with a
39
+ dimmed glyph — so a lane entering or leaving one never shifts the rows below it (cockpit v3 S2).
29
40
  10. **Performance is a design property:** cached row bytes, one width walk per name, frames written only when dirty,
30
41
  OSC 2/9/11 in the same write; budgets below.
31
42
  11. **Pinned bytes change deliberately:** a moved byte assertion is rewritten with a role name, never loosened.
@@ -35,16 +46,24 @@ at the repository root; this page condenses it for daily use and never overrides
35
46
 
36
47
  ## Modes
37
48
 
38
- Four switches pick the look; `devices/<device>.json` (written by `lanes attach DEVICE --ascii|--theme|--glyphs|--color`
39
- and by `lanes-window --ascii|--light|--dark`) wins over the environment and reaches a running sidebar within one tick.
49
+ Five switches pick the look and its announcements; `devices/<device>.json` (written by `lanes attach DEVICE
50
+ --ascii|--theme|--glyphs|--color|--notify|--dim` and by `lanes-window --ascii|--light|--dark` — on a device with an
51
+ owner, theme, colour and glyphs only with `--device-only`, which names them in the file's `device_only` list, else
52
+ they go to the owner's profile `users/<login>.json`, which every sidebar of that person's devices layers over its
53
+ device file — cockpit v3 R3, `sidebar.layered_look`: the device's `--device-only` keys, then the profile, then a key
54
+ from before the profile (`lanes device claim` folds it in), then environment, then default)
55
+ wins over the environment and reaches a running sidebar within one tick. The variables reach a sidebar through the
56
+ view server's
57
+ environment: `bin/lanes` `clean_env()` forwards the five, when set, from the shell that creates the session (cockpit
58
+ v3 P4, DEF-1577); a running session keeps the environment it was created with.
40
59
 
41
60
  | Switch | Values | Default | Where read |
42
61
  |---|---|---|---|
43
- | `LANES_COLOR` | `truecolor` · `16` · `none` | `truecolor` when `COLORTERM` ∈ {`truecolor`, `24bit`} (config.kdl's `env` block forces it for every pane), else `16`; `NO_COLOR` set ⇒ `none` | `sidebar.py` at start **and on every tick**: `devices/<device>.json` is part of `reload()`'s mtime stamp (one `stat()`), its `color` key wins over env, and a changed file re-runs `resolve_modes()` → `sgr_table()`/`glyph_set()` and clears the row cache (`apply_look()`). Zellij panes inherit the *server's* environment at session creation, so an environment variable can never reach a running sidebar; the device file is the one channel that does |
44
- | `LANES_THEME` | `dark` · `light` · `auto` | `auto`: one OSC 11 query `\033]11;?\033\\` written at start; the reply (`\033]11;rgb:RRRR/GGGG/BBBB\033\\` or BEL-terminated, 8- or 16-bit components) is parsed in `handle()` and **swallowed** — it never cancels a filter or prompt; relative luminance < 0.5 ⇒ dark; no reply within 300 ms ⇒ dark; no re-ask | same; `devices/<device>.json` key `theme` (per tick, as above); `STATE/clients/<device>.theme` is written (`dark`/`light`, only on change) for `glow` |
45
- | `LANES_GLYPHS` | `unicode` · `ascii` · `emoji` | `unicode`; `ascii` automatically when none of `LC_ALL`/`LC_CTYPE`/`LANG` contains `UTF-8` | same; `devices/<device>.json` key `glyphs` (per tick, as above); `lanes attach DEVICE --ascii` / `--theme light` write them and every running sidebar of that device picks them up within one tick; `lanes-window --ascii`/`--light`/`--dark` pass them through. The sidebar publishes the resolved mode as `STATE/clients/<device>.glyphs` (`unicode`/`ascii`/`emoji` + newline, written on change or when absent) — the file `view.sh` reads with a builtin `read` for its status screens (§7) |
46
- | `LANES_DIM` | `90` · `2` | `90` | 16-colour mode only: `2` is the documented escape hatch for palettes whose colour 8 is near the background (classic Solarized dark) |
47
- | `LANES_NOTIFY` | `1` · `0` | `1` on desktop devices, `0` on mobile | OSC 9 emission (§4.9) |
62
+ | `LANES_COLOR` | `truecolor` · `256` · `16` · `none` | `truecolor` when `COLORTERM` ∈ {`truecolor`, `24bit`} (config.kdl's `env` block forces it for every pane), else `256` when `TERM` contains `256color` (cockpit v3 P2), else `16`; `NO_COLOR` set ⇒ `none`. Inside a view 256 colours are chosen, never detected (cockpit v3 P5): every pane has `COLORTERM=truecolor`, read before `TERM`, so the `TERM` rule applies only outside Zellij (a `libexec/sidebar` started by hand), as does `NO_COLOR`, which `clean_env()` does not forward. A terminal without 24-bit colour chooses `lanes attach DEVICE --color 256` (live) or `LANES_COLOR=256` in the shell that creates the session | `sidebar.py` at start **and on every tick**: `devices/<device>.json` is part of `reload()`'s mtime stamp (one `stat()`), its `color` key wins over env, and a changed file re-runs `resolve_modes()` → `sgr_table()`/`glyph_set()` and clears the row cache (`apply_look()`). Zellij panes inherit the *server's* environment at session creation, so an environment variable can never reach a running sidebar; the device file is the one channel that does. The sidebar publishes the resolved mode as `STATE/clients/<device>.color` (`truecolor`/`256`/`16`/`none` + newline, compared by content on every resolution: a stamp an earlier session left is replaced, an equal one is not rewritten), beside `.theme` and `.glyphs`: `view.sh` picks its status screens' colour set by it and `.theme` (cockpit v3 P3, below) |
63
+ | `LANES_THEME` | `dark` · `light` · `auto` | `auto`: one OSC 11 query `\033]11;?\033\\` written at start; the reply (`\033]11;rgb:RRRR/GGGG/BBBB\033\\` or BEL-terminated, 8- or 16-bit components) is parsed in `handle()` and **swallowed** — it never cancels a filter or prompt; relative luminance < 0.5 ⇒ dark; no reply within 300 ms ⇒ dark; no re-ask | same; `devices/<device>.json` key `theme` (per tick, as above); `STATE/clients/<device>.theme` is written (`dark`/`light`) for `glow` and `view.sh` on every resolution — an explicit theme, the answer, or the grace (run()'s tick, no frame needed) — compared by content, not by whether the variant changed in memory (DEF-1775): a later attach whose answer is the pending dark still replaces an earlier light terminal's `light`; until a sidebar resolves, the earlier stamp stands |
64
+ | `LANES_GLYPHS` | `unicode` · `ascii` · `emoji` | `unicode`; `ascii` automatically when none of `LC_ALL`/`LC_CTYPE`/`LANG` contains `UTF-8` | same; `devices/<device>.json` key `glyphs` (per tick, as above); `lanes attach DEVICE --ascii` / `--theme light` write them and every running sidebar of that device picks them up within one tick; `lanes-window --ascii`/`--light`/`--dark` pass them through. The sidebar publishes the resolved mode as `STATE/clients/<device>.glyphs` (`unicode`/`ascii`/`emoji` + newline, compared by content on every resolution) — the file `view.sh` reads with a builtin `read` for its status screens (§7) |
65
+ | `LANES_DIM` | `90` · `2` | `90` | 16-colour mode only: `2` is the documented escape hatch for palettes whose colour 8 is near the background (classic Solarized dark). `sidebar.py` `resolve_dim_notify()` in `apply_look()`: `devices/<device>.json` key `dim` (`lanes attach DEVICE --dim 90\|2`, per tick) wins over env, and the resolved SGR is handed to `sgr_table(dim16=…)` for every table the sidebar builds (the OSC 11 answer's included); `view.sh`'s status screens keep 90 (the generated `tokens.sh` pins it) |
66
+ | `LANES_NOTIFY` | `1` · `0` | `1` on desktop devices, `0` on mobile | OSC 9 emission (§4.9). Same resolution: `devices/<device>.json` key `notify` (`lanes attach DEVICE --notify 1\|0`, per tick) wins over env, `osc_bytes()` reads the resolved `Sidebar.notify`; a mobile device never notifies, whatever the key says |
48
67
 
49
68
  ## Tokens and glyphs
50
69
 
@@ -52,28 +71,81 @@ Every glyph table is `key -> (unicode, ascii, tone)`: agent glyphs are drawn onl
52
71
  column 33, marks only where the row anatomy puts them. `LANES_GLYPHS=ascii` swaps in the ASCII column, so an ASCII
53
72
  frame is seven-bit. The `ANSI-16` column is what `LANES_COLOR=16` emits; truecolor uses the Dark or Light value.
54
73
 
74
+ The palette is v2 (cockpit v3 P1, LNS-69): vivid tones in both variants, every text role ≥ 4.5:1 on `surface`,
75
+ `surface-raised`, `accent-wash` and `selected` (the tightest pair is light `on-accent-urgent` on `accent`, 4.54:1).
76
+ `selected` is the cursor row's background — the `accent-wash` value with ink on it, so it has no CSS variable of its
77
+ own. `256 dark` / `256 light` are the xterm-256 indices `sgr_table('256', …)` emits (`38;5;N`; `48;5;N` for
78
+ `selected`): always 16–255, never the user's 0–15, and each the nearest entry in its token's hue family that keeps
79
+ 4.2:1 on the surfaces and the 256 `selected` band (`test_tokens.Xterm256` re-derives all eighteen). In 16 colours
80
+ `selected` paints nothing and the cursor stays the bold `▌`. `libexec/tokens.sh` carries the 16-colour set when
81
+ sourced and `lz_tones MODE VARIANT` for the others: `lz_tones none` takes one argument, and any other call —
82
+ `lz_tones 16`, a bare `lz_tones` — restores 16; every spelling is safe under view.sh's `set -u`, and every set says
83
+ `LZ_VIVID` (`1` in `truecolor` and `256`, `tokens.VIVID`, the sidebar's `VIVID`; empty in 16 and none). The slot
84
+ table is the lane palette Lanes defines for the phone's web client and the optional Ghostty file: slots 1–4 are the
85
+ tones, 7 and 15 the ink pair, 0 and 8 greys, and the colour slots 5, 6 and 9–14 are re-saturated in v2 (more chroma
86
+ than v1 in each, pinned by `test_tokens.PaletteV2`, which holds the tones and the accent to the same rule).
87
+
88
+ How the sidebar spends it (cockpit v3 P2, LNS-70). In `truecolor` and `256` — the colours Lanes defines — a section
89
+ header says its words in the section's tone (Needs you attention, Working working, Done done; an empty `(0)` section
90
+ stays the dim placeholder) with the `(n)` ink-dim; a lane name is ink unless the lane has an identity colour; the age
91
+ takes its section's tone and is ink-dim only outside the three attention sections; the stream glyph keeps its own tone
92
+ beside an agent and is ink-dim only on a stale row (the shell `_`, idle `·` and quiet `○` glyphs are ink-dim by their
93
+ token); the cursor row lies on `selected` (`48;2;…` / `48;5;N`), re-opened after every reset in the row and closed by a
94
+ plain reset before the row's `\033[K`, so column 36 stays the terminal's own; the `▌` stays in column 1. `16` (your
95
+ terminal's palette — its yellow may be unreadable on a light background) and `none` keep v1's bytes exactly
96
+ (`ColourUsage.test_sixteen_colour_rows_are_unchanged`); the phone's section headers follow the desktop's, and the
97
+ phone has no cursor row to band.
98
+
99
+ How the viewer spends it (cockpit v3 P3, LNS-71). `view.sh`'s status screens read the device's
100
+ `clients/<device>.color` and `.theme` stamps on every screen with the builtin `read` (beside `.glyphs`; no fork) and
101
+ call `lz_tones "$color" "$theme"`. In `truecolor` and `256` of a known variant the hint line is that set's ink-dim
102
+ and the lead (`⚠ THIS VIEWER DISCONNECTED — …`) takes the `warning` mark's tone, attention, as the sidebar's health
103
+ banner does; `none` keeps bold alone (no SGR 90 on the hint); `16`, no stamp, an unknown word, or a vivid mode
104
+ whose `.theme` is missing or not `dark`/`light` is the 16-colour screen byte for byte — the user's palette, never a
105
+ guessed variant on what may be a light background (`test_sidebar.ViewStatusScreen`).
106
+
55
107
  <!-- BEGIN lanes tokens (generated by libexec/tokens.py --md; do not edit by hand) -->
56
- | Role | Dark | Light | ANSI-16 | CSS variable |
108
+ | Role | Dark | Light | 256 dark | 256 light | ANSI-16 | CSS variable |
109
+ |---|---|---|---|---|---|---|
110
+ | surface | `#101318` | `#f3f5f2` | `—` | `—` | `—` | `--lanes-surface` |
111
+ | surface-raised | `#191e25` | `#ffffff` | `—` | `—` | `—` | `--lanes-surface-raised` |
112
+ | line | `#2c333d` | `#d5ded6` | `—` | `—` | `—` | `--lanes-line` |
113
+ | accent-wash | `#1c3326` | `#e5f0e6` | `—` | `—` | `—` | `--lanes-accent-wash` |
114
+ | selected | `#1c3326` | `#e5f0e6` | `235` | `254` | `—` | `--lanes-accent-wash` |
115
+ | accent-line | `#4b6955` | `#acc9b4` | `—` | `—` | `—` | `--lanes-accent-line` |
116
+ | press | `#26332c` | `#d8e9db` | `—` | `—` | `—` | `--lanes-press` |
117
+ | chip | `#ffffff0f` | `#edf3ec` | `—` | `—` | `—` | `--lanes-chip` |
118
+ | ink | `#f2f5f8` | `#111a14` | `255` | `233` | `39` | `--lanes-ink` |
119
+ | ink-dim | `#a6b1be` | `#4a5661` | `249` | `240` | `90` | `--lanes-ink-dim` |
120
+ | ink-faint | `#6f7a87` | `#6f7b87` | `243` | `243` | `90` | `--lanes-ink-faint` |
121
+ | accent | `#8fe3a6` | `#1f6f3f` | `115` | `22` | `1;39` | `--lanes-accent` |
122
+ | on-accent | `#101318` | `#ffffff` | `—` | `—` | `—` | `--lanes-on-accent` |
123
+ | on-accent-urgent | `#7a1a12` | `#ffd3cc` | `—` | `—` | `—` | `--lanes-on-accent-urgent` |
124
+ | tone-urgent | `#ff7b6e` | `#c41c1c` | `209` | `160` | `31` | `--lanes-tone-urgent` |
125
+ | tone-attention | `#ffc233` | `#a04d00` | `215` | `94` | `33` | `--lanes-tone-attention` |
126
+ | tone-working | `#6fb6ff` | `#1a56c4` | `75` | `26` | `34` | `--lanes-tone-working` |
127
+ | tone-done | `#5fdc8a` | `#0d7338` | `78` | `22` | `32` | `--lanes-tone-done` |
128
+ | scrim | `#000000a6` | `#1a222b80` | `—` | `—` | `—` | `--lanes-scrim` |
129
+ | shadow | `0 -16px 80px #0007` | `0 -16px 80px #0003` | `—` | `—` | `—` | `--lanes-shadow` |
130
+
131
+ | Slot | ANSI | Dark | Light | Role |
57
132
  |---|---|---|---|---|
58
- | surface | `#101318` | `#f3f5f2` | `—` | `--lanes-surface` |
59
- | surface-raised | `#191e25` | `#ffffff` | `—` | `--lanes-surface-raised` |
60
- | line | `#2c333d` | `#d5ded6` | `—` | `--lanes-line` |
61
- | accent-wash | `#1a2721` | `#e5f0e6` | `—` | `--lanes-accent-wash` |
62
- | accent-line | `#4b6955` | `#acc9b4` | `—` | `--lanes-accent-line` |
63
- | press | `#26332c` | `#d8e9db` | `—` | `--lanes-press` |
64
- | chip | `#ffffff0f` | `#edf3ec` | `—` | `--lanes-chip` |
65
- | ink | `#edf1f5` | `#17241b` | `39` | `--lanes-ink` |
66
- | ink-dim | `#9ca7b5` | `#56616d` | `90` | `--lanes-ink-dim` |
67
- | ink-faint | `#6b7683` | `#7f8a96` | `90` | `--lanes-ink-faint` |
68
- | accent | `#b4e3bf` | `#2f6b45` | `1;39` | `--lanes-accent` |
69
- | on-accent | `#101318` | `#ffffff` | `—` | `--lanes-on-accent` |
70
- | on-accent-urgent | `#7a1a12` | `#ffd3cc` | `—` | `--lanes-on-accent-urgent` |
71
- | tone-urgent | `#f28b7d` | `#b7322a` | `31` | `--lanes-tone-urgent` |
72
- | tone-attention | `#e5b84a` | `#8a5a00` | `33` | `--lanes-tone-attention` |
73
- | tone-working | `#7fb3f2` | `#2456a8` | `34` | `--lanes-tone-working` |
74
- | tone-done | `#6fd39a` | `#1b7142` | `32` | `--lanes-tone-done` |
75
- | scrim | `#000000a6` | `#1a222b80` | `—` | `--lanes-scrim` |
76
- | shadow | `0 -16px 80px #0007` | `0 -16px 80px #0003` | `—` | `--lanes-shadow` |
133
+ | 0 | black | `#8792a0` | `#2f3b34` | — |
134
+ | 1 | red | `#ff7b6e` | `#c41c1c` | tone-urgent |
135
+ | 2 | green | `#5fdc8a` | `#0d7338` | tone-done |
136
+ | 3 | yellow | `#ffc233` | `#a04d00` | tone-attention |
137
+ | 4 | blue | `#6fb6ff` | `#1a56c4` | tone-working |
138
+ | 5 | magenta | `#c58bff` | `#8228c4` | — |
139
+ | 6 | cyan | `#3fd9cf` | `#006f73` | — |
140
+ | 7 | white | `#a6b1be` | `#4a5661` | ink-dim |
141
+ | 8 | bright black | `#97a3b1` | `#55625b` | — |
142
+ | 9 | bright red | `#ff9a8e` | `#d0221a` | — |
143
+ | 10 | bright green | `#86f0ad` | `#0e7a3a` | — |
144
+ | 11 | bright yellow | `#ffd866` | `#a85200` | — |
145
+ | 12 | bright blue | `#98cbff` | `#1f5fd8` | — |
146
+ | 13 | bright magenta | `#dcb2ff` | `#9133d0` | — |
147
+ | 14 | bright cyan | `#7deee6` | `#00797c` | — |
148
+ | 15 | bright white | `#f2f5f8` | `#111a14` | ink |
77
149
 
78
150
  | Glyph | Codepoint | ASCII | Tone | Meaning |
79
151
  |---|---|---|---|---|
@@ -111,6 +183,10 @@ frame is seven-bit. The `ANSI-16` column is what `LANES_COLOR=16` emits; truecol
111
183
  | `·` | U+00B7 | `-` | ink-dim | mark `sep` |
112
184
  | `‘` | U+2018 | `'` | ink | mark `quote-open` |
113
185
  | `’` | U+2019 | `'` | ink | mark `quote-close` |
186
+ | `—` | U+2014 | `-` | ink-dim | mark `dash` |
187
+ | `→` | U+2192 | `->` | accent | mark `arrow` |
188
+ | `…` | U+2026 | `~` | ink-dim | mark `pending` |
189
+ | `▲` | U+25B2 | `^` | accent | mark `pinned` |
114
190
  | `✓` | U+2713 | `OK` | tone-done | mark `ok` |
115
191
  | `✗` | U+2717 | `X` | tone-urgent | mark `fail` |
116
192
  | `⚠` | U+26A0 | `!` | tone-attention | economy `starving` |
@@ -124,14 +200,14 @@ frame is seven-bit. The `ANSI-16` column is what `LANES_COLOR=16` emits; truecol
124
200
 
125
201
  | col | content | tone |
126
202
  |---|---|---|
127
- | 1 | ▌ cursor or space | accent |
203
+ | 1 | ▌ cursor or space; the cursor row lies on the `selected` band (truecolor / 256) up to column 35 | accent |
128
204
  | 2 | `AGENT_GLYPHS[agent.status]`; blank without `agent{}`; ink-dim when stale | its tone |
129
- | 3 | › viewing (not on the cursor row) · ² / ³ another viewport shows it · else space | accent |
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 |
205
+ | 3 | `pending` … (ASCII `~`) while an action is on its way: `x` closing the session, `P` registering it until the registry lists its lane (cockpit v3 S8), and on the New shell row a shell being created (cockpit v3 S9; the empty workspace's New shell row carries it in column 2) · › viewing (not on the cursor row) · ² / ³ another viewport shows it · else space | ink-dim for `pending`; else accent |
206
+ | 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, else ink (painted in truecolor / 256) |
131
207
  | 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
- | 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 |
208
+ | 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; truecolor / 256: its section's tone in Working and Done too |
133
209
  | 32 | space | |
134
- | 33 | `GLYPHS[state]` — full tone without an agent, ink-dim with one (▸ + ○ is the long-tool-call case and stays readable) | its tone |
210
+ | 33 | `GLYPHS[state]` — full tone without an agent; with one ink-dim in 16 colours, its own tone in truecolor / 256 (▸ + ○ is the long-tool-call case and stays readable); ink-dim when stale | its tone |
135
211
  | 34 | tail: stale `?` > starving ⚠ > downgraded ↓ > paid `$` > space | ink-dim / attention |
136
212
  | 35 | × on closable rows, else space | urgent |
137
213
  | 36 | never written | |
@@ -187,7 +263,7 @@ real list scrolls where `⋮` is:
187
263
  ⏎ open · / find · x close · ? keys
188
264
  ```
189
265
 
190
- Row 3 is the cursor row (▌, no ›, bold name); row 4 is the viewing lane in this slot (›, bold); row 7 is shown in
266
+ Row 3 is the cursor row (▌, no ›, bold name; on the `selected` band in truecolor and 256); row 4 is the viewing lane in this slot (›, bold); row 7 is shown in
191
267
  viewport b (²) with two viewers (··). The same frame in ASCII mode, first rows:
192
268
 
193
269
  ```
@@ -199,7 +275,9 @@ viewport b (²) with two viewers (··). The same frame in ASCII mode, first row
199
275
  ```
200
276
 
201
277
  Phone, the `render_mobile` picker at 48+ columns with three rows per lane (spec §4.10): row A is the viewing mark,
202
- the agent glyph and the name; row B the status word, age and harness; row C the agent's task.
278
+ the agent glyph and the name; row B the status word, age and harness; row C the agent's task. Row A draws `+` and `×`
279
+ at the desktop row's columns, `cols − 8` and `cols − 1` (cockpit v3 S5; `cols − 6` and `cols − 2` until then), inside
280
+ the same hit boxes, and never writes column `cols`.
203
281
 
204
282
  ```
205
283
  ◆1 need approval · ◇2 need you · ▸6 working
@@ -211,7 +289,7 @@ the agent glyph and the name; row B the status word, age and harness; row C the
211
289
  ◇ Runtime: supervisor··
212
290
  Needs you · 12m · codex
213
291
  Verify fleet plan mapping
214
- ◇ granite_games ×
292
+ ◇ granite_games ×
215
293
  Needs you · 4m · claude
216
294
  Port the brand migration
217
295
  ── ▸ Working (6) ───────────────────────────────
@@ -232,12 +310,12 @@ Each budget is pinned by a test; a budget that is not met is an open defect row,
232
310
  | Budget | Pin |
233
311
  |---|---|
234
312
  | `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
- | 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)` |
313
+ | Frame ≤ 9216 B truecolor, ≤ 5632 B in `LANES_COLOR=16`, ≤ 6144 B (16 + 512) in `LANES_COLOR=256` at 36×60, the cursor on a registrable ghost row (today, cockpit v3 P2, MEASURED 2026-10-07: truecolor 7310 B dark / 6890 B light, 16 4618 B, 256 5821 B dark / 5805 B light; 7034 / 6598 / 4618 B before P2 with the same cursor; 4330 B in both on the DEF-1417 fixture, 4297 B before it — `LANES_COLOR` is not read before Task 22) | same test, `len(frame)` |
236
314
  | 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
315
  | 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
- | 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` |
316
+ | Per tick ≤ 8 `stat()` at one viewport — registry, status, target, health, `flags/sockets-lost` (today's five), economy (Task 12), `devices/<device>.json` (Task 22), `views/<device>/<ws>.sidebar.cmd` (cockpit v3 S4: 7 → 8, a deliberate amendment — `Ctrl-Alt-/` and `Ctrl-Alt-u` hand their key over as that file instead of typing it; measured 8 on 2026-10-06) — plus one per extra viewport; zero timers added; the OSC 11 query and the focus-report request (`?1004h`) are one write at start; the sidebar.cmd file is stat()ed once more per read of keys (never per tick) | `test_tick_stats_at_most_eight_files` (one viewport); `test_theme_query_is_written_once` |
239
317
  | Upload: select timeout stays 1.0 while uploading; `dirty` only on `.progress` mtime change | `test_upload_does_not_shorten_the_tick` (Task 24) |
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` |
318
+ | `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; 4058 B truecolor after cockpit v3 P2, 3991 B before) | `test_mobile_render_time_and_frame` |
241
319
  | `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
320
  | 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
321
  | 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` |
@@ -22,7 +22,9 @@ and writes nothing (exit 1), `lanes hooks install --write` into a container refu
22
22
  `lanes doctor` warns about each container. Run them from a host account with UID 1000.
23
23
  Containers need Bash, Python 3 and tmux. Use compatible tmux versions on the host and containers (tested with
24
24
  tmux 3.4): the host client attaches directly to the container server. Zmx is installed as an optional holder,
25
- not a migration of existing lanes.
25
+ not a migration of existing lanes. Quick edit (`Ctrl-Alt-e`) runs an editor the container already has:
26
+ `$LANES_EDITOR`, else the first of micro, hx, nvim, vim and vi on its PATH. Lanes installs no editor, so vi (or vim)
27
+ is the floor; a container with none of them cannot quick-edit (`lane-edit: no editor on PATH`).
26
28
 
27
29
  For services to stay available after logout, an administrator enables the account's user manager:
28
30
 
@@ -41,7 +43,8 @@ cd bw-tools/lanes
41
43
 
42
44
  `--server --no-start` installs files and units without contacting Docker or starting services. The normal install
43
45
  verifies archive checksums, installs the pinned binaries, registers this checkout's absolute paths, and enables
44
- `lanes-statusd`, `lanes-sync.timer`, `lanes-ensure.timer` and `lanes-gc.timer`. It creates an empty workspace catalog
46
+ `lanes-statusd`, `lanes-sync.timer`, `lanes-ensure.timer`, `lanes-gc.timer` and `lanes-update.timer` (which updates an
47
+ npm-installed server by itself and leaves a checkout alone, section 6). It creates an empty workspace catalog
45
48
  only if no configuration exists. Host protection, temporary-file deletion and web access remain opt-in.
46
49
 
47
50
  ## 2. Configure workspaces and editor sync
@@ -108,7 +111,9 @@ account with UID 1000. With `--write` it then applies these steps in order:
108
111
  1. **Board.** Runs `lanes workspaces add`, when `--workspace` and `--repo` are given. `--title` defaults to the id
109
112
  or to the title the board already has. A board that already disagrees stops the run before the container is
110
113
  touched.
111
- 2. **Contract.** Runs `lanes install-container`.
114
+ 2. **Contract.** Runs `lanes install-container`: the `lane` contract, and the Lanes MCP server (`lane-mcp`) in
115
+ Claude Code's `~/.claude.json` and Codex's `~/.codex/config.toml` (only where it is missing; the old file is kept
116
+ as `<file>.bak-<UTC time>`). The `lane-mcp` row of the table then says whether that changed either file.
112
117
  3. **Agent hooks.** Runs `lanes hooks install --write`, first for Claude and then for Codex. Your own hooks and
113
118
  keys are kept, with a backup of each changed file. A Lanes hook group that is already installed stays where
114
119
  it is, even when Throttle's setup has added its own group around it.
@@ -135,9 +140,51 @@ The run ends with a `key: value` table and a receipt in `~/.local/state/lanes/on
135
140
  `changed`, `unchanged`, `ok`, `skipped`, `manual` (a step left for a person, with its command), `warn`, `failed` or
136
141
  `not-run`. When a step stops the run, every step after it is still listed, as `not-run`, in the table and the receipt.
137
142
  The exit code is 1 only when a step failed. Running the command again changes nothing that is already done.
138
- `--no-mcp` leaves out `lane-mcp`, the in-container MCP server, which has not shipped yet. Until it does, the
139
- receipt records that step as skipped.
140
- Each user's board still shows on every device's view. A per-device subset of boards is a separate follow-up.
143
+ `--no-mcp` leaves out `lane-mcp` and its two entries; the receipt then records that step as skipped, and nothing an
144
+ earlier run wrote is removed ([user guide](USERGUIDE.md#agent-tools-mcp)).
145
+ Each user's board shows on every device's view. A person who owns a device can hide boards or reorder them on all of
146
+ their devices at once (`lanes workspaces hide|show|order`, kept in their profile; [user guide](USERGUIDE.md#your-profile)).
147
+
148
+ ### Agent tools
149
+
150
+ What `lanes install-container <name>` puts into the container's home volume, `/home/coder/.lz/` (additive: it
151
+ replaces only its own files, by rename, and never a `zmx-*` binary a running lane uses):
152
+
153
+ | Path | What it is |
154
+ | --- | --- |
155
+ | `bin/lane`, `bin/lane_core.py`, `bin/lane-run`, `bin/lane-signal` | The in-container `lane` contract, its shared reader core, the zmx lane root process and the hook sidecar writer |
156
+ | `bin/lane-edit`, `vimrc` | What quick edit (`Ctrl-Alt-e`) runs there, and the settings it gives vim and nvim; no editor is installed |
157
+ | `bin/lane-mcp` | The Lanes MCP server for the agents (left out with `--no-mcp`) |
158
+ | `bin/zmx-*` | The pinned zmx holder (optional lanes only) |
159
+ | `VERSION`, `hooks/`, `env.sh` | The release that installed them, the hook definitions `lanes hooks install` merges, and the shell settings new shells source |
160
+
161
+ It installs no agent hook (that is `lanes hooks install`, opt-in) and, unless `--no-mcp`, adds the agents' MCP entries
162
+ below. A re-run after a server update (the update does it itself for every container `lanes sync` knows) brings every
163
+ file to the installed release.
164
+
165
+ `lanes install-container <name>` (and so `lanes onboard`, step 2 above) also gives the container's agents the Lanes
166
+ MCP server, `~/.lz/bin/lane-mcp`. With it, the agent in a lane can do these things in its own container:
167
+
168
+ - see the lanes there: `lanes_whoami`, `lanes_list`, `lanes_status`, `lanes_capture`;
169
+ - declare, rename or remove a lane's terminal, or start a new lane;
170
+ - type into a sibling lane;
171
+ - report its own status on the boards.
172
+
173
+ Ending, interrupting or attaching a lane stays with people
174
+ ([what agents may and may not do](ARCHITECTURE.md#agent-api)).
175
+
176
+ - It is on by default. The entry is added to Claude Code's `~/.claude.json` and Codex's `~/.codex/config.toml` only
177
+ where it is missing, and each file is kept first as `<file>.bak-<UTC time>`. `--no-mcp` leaves both out and
178
+ removes nothing.
179
+ - Check each container once with the [protocol smoke](RUNBOOK.md#protocol-smoke): two frames in, two out. Then start
180
+ a new agent session and look at `/mcp` in Claude Code or `codex mcp list`. Sessions that were already running keep
181
+ the tools they started with.
182
+ - Point your agents at the [`lanes-agent-tools` skill](../../skills/lanes-agent-tools/SKILL.md). It says which tool
183
+ to call when, never to try to end a lane, and how to hand work to a sibling lane. Lanes bootstrapped from this
184
+ repository's `.bw-tools/` are told about it in `.bw-tools/context.md`.
185
+ - Uninstall and troubleshooting: [runbook](RUNBOOK.md#install-check-uninstall).
186
+ - It is not a Throttle tool-gateway package: agents start it directly, and the catalog does not list it
187
+ ([why](ARCHITECTURE.md#gateway-adoption-documented-not-registered)).
141
188
 
142
189
  ### Laptop agents on the Throttle pool (operator-gated)
143
190
 
@@ -219,7 +266,13 @@ request that a member approves (the installer waits up to 10 minutes; `--resume`
219
266
  `https://<LANES_SERVER_NAME>:8443` (with curl, or python3 without it; `--resolve` to the tailnet address when MagicDNS is
220
267
  off). What the client reads of lanes-enrold's wire contract is in [the architecture](ARCHITECTURE.md#joining-a-site).
221
268
  The code is never shown, stored, or put on a command line. A malformed code, or one with another site's prefix, is refused before anything is installed
222
- (exit 1).
269
+ (exit 1). The code is redeemed by the server's own release (W9 K3): with an invite, the npm installer first reads the
270
+ server's release from `GET /enrol/site` (`version`; on a tailnet site only when the machine is already connected) and,
271
+ when it is newer than the package, ends before anything is installed or redeemed (`install.sh` exit 5 to the wrapper)
272
+ while the wrapper runs `npx --yes @vibelord/lanes@<release>` with the same options and `LANES_NO_SELF_UPDATE=1`; no
273
+ npx, a release npm does not have yet, or npm not answering: one line, and the package that ran installs as before
274
+ ([user guide](USERGUIDE.md#join-with-an-invite)). A server whose release is not on npm yet therefore costs a joining
275
+ machine nothing but that line: publish before telling members to join with a new release's features.
223
276
 
224
277
  Without the service, after a refused code, or with no code and no terminal, authorize the printed public key on the
225
278
  server, as the workspace account: `lanes keys add <device> <file.pub>` (or `lanes keys add <device> -` and paste the
@@ -298,7 +351,9 @@ lanes enrol list --all # also the approved, denied and ex
298
351
  On a phone or laptop on the tailnet, `https://<server>:8443/enrol/pending` lists the same requests with **Approve**
299
352
  and **Deny**. It admits a tailnet login that owns a member record, and keys added by hand (`lanes keys add`, `lanes
300
353
  keys adopt`) record no owner, so name yourself once on one of your own devices: `lanes keys owner <your-device>
301
- <your tailnet login>`. With the opt-in push notifier (`lanes-notify.service`), each new request arrives as one push
354
+ <your tailnet login>` (or adopt your own keys with `lanes keys adopt --login <your tailnet login>`). Naming an owner
355
+ also folds those devices' views into that person's profile; a view no member record speaks for gets its owner with
356
+ `lanes device claim <device> --login <login>`. With the opt-in push notifier (`lanes-notify.service`), each new request arrives as one push
302
357
  naming the requester, the device and the approve command. The CLI records where a decision came from
303
358
  (`cli[:<device>]@<address>`), not who made it (everyone over SSH is the one workspace account); the web page records
304
359
  the real tailnet login. Requests need request mode, which needs Tailscale Serve to hand the service a tailnet
@@ -476,16 +531,101 @@ credential; the server installer warns when its shell's roots differ from the re
476
531
 
477
532
  ## 6. Updates, optional operations and removal
478
533
 
479
- 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.
534
+ 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 --update-only`, and the previous versions a service may still run from are kept.
535
+
536
+ Old staged trees on a server are pruned by `lanes gc` (the daily `lanes-gc.timer`; W9 U4), never by the npm installer,
537
+ which prunes only a machine without `~/.local/share/lanes/current` (a client). Each folder of staged trees keeps its 3
538
+ newest and every older tree something still uses: the one `current` names, the rollback target `current.previous`
539
+ names, the one the running `lanes` came from, one a view layout names (`~/.local/state/lanes/layouts/<device>.kdl`: a
540
+ view built before 1.2.0 runs its `pkg/<version>` tree until `lanes rebuild <device>`), one a running daemon's
541
+ `run/<name>.tree` names (a service started through `current` before an update whose restart failed), and one any
542
+ process of this account uses — read from `/proc` (its argv, working directory, executable and open files), so an
543
+ installer running from a tree or a viewport started before the update keeps it. When `/proc` cannot be read, no tree
544
+ is pruned. `lanes gc` prints `pruned <tree>`, and `kept <tree>: <why>` for each older tree it keeps.
545
+
546
+ That update runs by itself (W9 U3): the full install enables `lanes-update.timer`, which runs `lanes update --timer`
547
+ 15 minutes after a boot and then every 30 minutes (up to 5 minutes later, at random; the unit is stopped after an
548
+ hour). One `lanes update` runs at a time (`~/.local/state/lanes/locks/update.lock`). On an npm-installed server it first
549
+ runs the post-commit of the installed release again when the last update ended `partial` (once per release from the
550
+ timer), then — unless a check answered less than 20 minutes ago; `lanes update --now` asks anyway — asks the npm
551
+ registry (`curl -fsS --max-time 20 https://registry.npmjs.org/@vibelord/lanes`) which release the dist-tag `latest`
552
+ names, or `lanes-stable` with `channel=stable` in `~/.config/lanes/update.conf` (publish a release to the stable channel
553
+ with `npm dist-tag add @vibelord/lanes@<release> lanes-stable`). A release newer than the installed one (major.minor.patch
554
+ as numbers; never a downgrade) that no rollback marked bad is installed with the npx the npm installer recorded when it
555
+ installed the server, `~/.local/state/lanes/update/npx`: `npx --yes @vibelord/lanes@<release> --server --update-only`.
556
+ Publishing a release is therefore deploying it to every npm-installed server within about 30 minutes; `channel=stable`
557
+ and a `lanes-stable` tag that trails `latest` are the soak valve. The settings the installed units carry — `LANES_WORKSPACES`,
558
+ `LANES_THROTTLE_*`, `LANES_NOTIFY_CONF`, which an install renders from its own environment — are given to that install
559
+ again, so a timer's update renders them as the install that set them did. Every check is recorded in
560
+ `update/last.json` as `check`, beside the installer's record; `lanes update --status` prints the file and `lanes
561
+ doctor` says how long ago the last check was and how the last update ended. When the recorded npx is gone (an nvm or
562
+ Node.js upgrade moved it) the update fails and says so: run `npx @vibelord/lanes --server --update-only` once from a
563
+ shell whose npx works, which records it again. A server run from a checkout is left alone by the timer: update it with
564
+ `git pull` and `./install.sh --server --update-only`.
565
+
566
+ Only the full install enables `lanes-update.timer`: `--update-only` enables nothing, the timer included. A server set
567
+ up before 1.2.0 (the first release with `lanes update`) and updated since with `npx @vibelord/lanes@latest --server
568
+ --update-only` has it off and never updates by itself: run the full `npx @vibelord/lanes@latest --server` once, or
569
+ `systemctl --user enable --now lanes-update.timer`. `lanes doctor` warns (`lanes-update.timer is not enabled`) until
570
+ then.
571
+
572
+ To go back one release: `lanes update --rollback`. It needs no network: this release's `install/services.py` renders the
573
+ units of the tree `~/.local/share/lanes/current.previous` names and moves `current` back to it (the same commit as an
574
+ install, under the same locks), the release left is marked `bad` in `update/last.json`, so no update installs it
575
+ again (the next newer release is installed as usual), and the post-commit of that release runs. The mark is written
576
+ in the same record as the flip, the result `partial` until the post-commit has run (Ctrl-C and hangups wait for that
577
+ write): a rollback stopped during its post-commit keeps the mark, and the next timer tick runs the post-commit again
578
+ instead of installing the release left. The record of the rollback lists the step `rollback` first. A second
579
+ rollback, which would put the bad release back, is refused.
580
+
581
+ `--update-only` (W9 U2) is the update path of a server a full `--server` install set up: it restarts what that install
582
+ enabled and enables nothing, so where no release is installed (`~/.local/share/lanes/current` names no tree) it is
583
+ refused before anything is fetched. It runs in three phases. Pre-commit: the prerequisite checks, the pinned binaries
584
+ (sha256-verified), terminfo and the catalog — a failure here leaves the installed release running as it was.
585
+ Commit: `install/services.py --install` writes the units and moves `current` (below). Post-commit, from the release
586
+ just installed (`libexec/update.py`): `systemctl --user daemon-reload`; `lanes sync`; `systemctl --user restart
587
+ lanes-statusd.service`; `systemctl --user try-restart` of `lanes-economy`, `lanes-notify`, `lanes-enrold`,
588
+ `lanes-mobile`, `lanes-web` and `lanes-studio-host` (each restarted only when it runs; nothing is enabled); `lanes
589
+ install-container <name>` for each container `lanes sync` recorded with a home volume; `lanes reload-views`; `lanes
590
+ ensure --if-layout-changed` (each view whose tabs this release lays out differently is rebuilt when no client is
591
+ attached, and otherwise marked `update ready · reload (r)` in its sidebar footer until it is rebuilt — by `r`, by
592
+ `lanes rebuild <device>`, or by `lanes-ensure.timer` once its last client has left). Every post-commit step runs
593
+ whatever the one before it returned, and each is stopped after 300 s. `lanes
594
+ sync` exiting 1 — a board with an error, which it reports while keeping that board's previous lanes — is a warning
595
+ (`WARN`, `"warning": true` in the record) that leaves the result `ok`; a sync that does not answer or cannot start is a
596
+ failure like any other step. The terminal or the journal gets one line per phase and step;
597
+ `~/.local/state/lanes/update/last.log` holds the full output and `update/last.json` the outcome: `{checked_at,
598
+ latest, installed, previous, result, steps: [{name, rc}], log}`, `result` being `ok`, `partial` (installed, a step
599
+ after the switch failed: exit 4), `refused` (a prerequisite, no installed release or another install said no, nothing
600
+ changed: exit 3) or `failed` (nothing was installed: exit 1). `--update-only` takes neither `--check` nor
601
+ `--no-start`. A full install (no `--update-only`) writes no record; a failing `lanes sync` there is a warning too, and
602
+ the services are still enabled.
603
+
604
+ One install or update runs at a time. Every server install and update except `--check` first takes the run lock
605
+ `~/.local/state/lanes/update/.lock` and holds it until it ends (a crash releases it); a second run started meanwhile —
606
+ the update timer and a hand-run `npx`, two shells — changes nothing, writes neither the log nor the record of the run
607
+ that holds it, prints one line naming that run (its pid, whether it is an update or an install, and since when) and
608
+ exits 3. Run it again once the other has ended.
480
609
 
481
610
  Keep the server checkout at a stable location. To move it, run `./install.sh --server` from the new location:
482
611
  service definitions and `~/.local/share/lanes/current` are regenerated. Existing workspace configuration is preserved.
612
+
613
+ The units, `~/.local/bin/lanes` and the view layouts name `~/.local/share/lanes/current`, never the tree itself, and
614
+ the installer moves that link last, in one step — the commit point of an install or update. Every unit is written
615
+ before it (a failure there leaves the running release installed), and `~/.local/share/lanes/current.previous` then
616
+ names the tree `current` named before (unless it is the same tree or no longer exists). Only the server installer
617
+ moves the link: `install/enable-web.sh` (`lanes web <device>`) and `install/enable-studio-host.sh` re-render the units of
618
+ the release `current` names and leave every link as it is, so running them from a tree an update has moved past (a
619
+ shell or a view that still has the old tree on its PATH) never puts that release back; they say so on stderr. Installs
620
+ and re-renders take turns on `~/.local/share/lanes/.install.lock` (an install that finds it held for 60 s stops
621
+ without changing anything).
622
+
483
623
  Services get the explicit `LANES_SHARE` and `LANES_STATE` paths supplied at installation, or the defaults below;
484
624
  use the same overrides when reinstalling. `LANES_WORKSPACES` is also carried into the generated user services
485
625
  when supplied; the standard configuration directory is the simplest default.
486
626
 
487
- After updating server code, restart the status observer with `systemctl --user restart lanes-statusd.service`.
488
- The installer itself restarts the enrolment service (`lanes-enrold`) and a running web frontend (`lanes-mobile`, so
627
+ After updating server code with a full install, restart the status observer with `systemctl --user restart
628
+ lanes-statusd.service` (`--update-only` does that and the restarts below). The installer itself restarts the enrolment service (`lanes-enrold`) and a running web frontend (`lanes-mobile`, so
489
629
  Caddy reads the new tree's Caddyfile; browser clients reconnect). On the first server install with enrolment, run the
490
630
  runbook's operator verification and pin its result with `lanes enrol mode request` (or `invite`) — see
491
631
  [Approve a request](#approve-a-request); a pinned mode survives this restart and every later one.
@@ -499,13 +639,13 @@ Normal paths:
499
639
 
500
640
  | Path | Contents |
501
641
  | --- | --- |
502
- | `~/.config/lanes/` | Site profile, client settings and server workspace configuration |
503
- | `~/.local/share/lanes/` | Client copy, pinned binaries, download cache, installed checkout link and npm-staged trees (`pkg/<version>/`) |
504
- | `~/.local/state/lanes/` | Host registry, device selections, status, upload handoff and housekeeping projections; `members/`, `invites/`, `enrol-requests/` and the append-only `enrol.log` of the enrolment service |
642
+ | `~/.config/lanes/` | Site profile, client settings and server workspace configuration; `update.conf` (`channel=stable`: `lanes update` follows the `lanes-stable` dist-tag) |
643
+ | `~/.local/share/lanes/` | Client copy, pinned binaries, download cache, npm-staged trees (`pkg/<version>/`; on a server `lanes gc` prunes them), the `current` link to the installed tree (the one path the units, `~/.local/bin/lanes` and the view layouts name), `current.previous` (the tree `current` named before the last install that moved it) and `.install.lock` (the lock installs and unit re-renders take turns on) |
644
+ | `~/.local/state/lanes/` | Host registry, device selections, status, upload handoff and housekeeping projections; `members/`, `invites/`, `enrol-requests/` and the append-only `enrol.log` of the enrolment service; `run/<name>.tree`, the tree each service started from (`lanes version` compares them with `current`; `lanes doctor` flags each one whose process runs older code); `update/last.json` and `update/last.log`, the outcome and output of the last `--update-only` run (with `lanes update`'s last `check`, a rollback's `bad`), and `update/.lock`, the run lock every server install and update holds while it runs; `update/npx`, the npx the npm installer recorded for `lanes update`, and `locks/update.lock`, held by the one `lanes update` that runs |
505
645
  | `~/.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) |
506
646
  | `~/.config/throttle-rc/`, `~/.local/share/throttle-rc/` | Optional Studio Host config, machine key and enrolment receipt (throttle-rc's own files) |
507
- | `~/.local/state/lanes-client/` | Client helper heartbeat and logs |
508
- | `/home/coder/.lz/` in each container | Lane helper, bindings and optional zmx metadata |
647
+ | `~/.local/state/lanes-client/` | Client helper heartbeat and logs; `checked`, the time of a window's last update question (W9 K2: at most one every 6 h); `checking/`, while one window asks, its claim with the server's and npm's answers |
648
+ | `/home/coder/.lz/` in each container | What `lanes install-container` installs ([table](#agent-tools)), the registry and status projections `lanes sync` and the status daemon write there, hook sidecars and optional zmx metadata |
509
649
 
510
650
  `lanes pressure`, `lanes sockets check` and `lanes tmp plan` are inspection tools. Review
511
651
  [the runbook](RUNBOOK.md) before applying cleanup/repair; read [host protection](HOST-GUARD.md) before enabling
@@ -514,7 +654,7 @@ its root guard. These operations are included in the package but are not an inst
514
654
  Clients uninstall with `./install.sh --uninstall`; SSH keys, `~/Lanes Uploads`, and any server workspace
515
655
  configuration and state on the same account are kept.
516
656
  To stop the host services, disable `lanes-statusd.service`, `lanes-sync.timer`, `lanes-ensure.timer`,
517
- `lanes-gc.timer`, and any optional units you enabled (`lanes-studio-host.service` and
657
+ `lanes-gc.timer`, `lanes-update.timer`, and any optional units you enabled (`lanes-studio-host.service` and
518
658
  `lanes-studio-host-enrol.timer` included; revoke the machine in the console if it should not come back). For the web endpoint, also run
519
659
  `tailscale serve --https=8443 off`. Keep the state and container lane data until you have decided which
520
660
  sessions to retain; no broad deletion command is part of uninstall.