@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.
- package/README.md +36 -5
- package/bin/lanes-install.mjs +166 -13
- package/lib/plan.mjs +113 -3
- package/package.json +1 -1
- package/payload/lanes/README.md +45 -22
- package/payload/lanes/VERSION +1 -1
- package/payload/lanes/bin/lanes +2459 -164
- package/payload/lanes/client/ghostty/lanes-palette-light.conf +19 -19
- package/payload/lanes/client/ghostty/lanes-palette.conf +19 -19
- package/payload/lanes/client/lanes-launch +289 -40
- package/payload/lanes/client/lanes-window +15 -3
- package/payload/lanes/container/hooks/claude-settings.json +1 -0
- package/payload/lanes/container/lane +106 -92
- package/payload/lanes/container/lane-edit +100 -0
- package/payload/lanes/container/lane-mcp +1091 -0
- package/payload/lanes/container/lane-signal +109 -29
- package/payload/lanes/container/lane_core.py +1140 -0
- package/payload/lanes/container/vimrc +31 -0
- package/payload/lanes/docs/ARCHITECTURE.md +618 -13
- package/payload/lanes/docs/DESIGN.md +122 -44
- package/payload/lanes/docs/INSTALL.md +157 -17
- package/payload/lanes/docs/RUNBOOK.md +603 -72
- package/payload/lanes/docs/SHARING.md +3 -2
- package/payload/lanes/docs/USERGUIDE.md +777 -136
- package/payload/lanes/install/enable-web.sh +2 -0
- package/payload/lanes/install/host-install.sh +163 -7
- package/payload/lanes/install/services.py +148 -18
- package/payload/lanes/install.sh +68 -7
- package/payload/lanes/libexec/agent_state.py +74 -1
- package/payload/lanes/libexec/cursor_sync.py +6 -0
- package/payload/lanes/libexec/device_sync.py +47 -10
- package/payload/lanes/libexec/economy_poll.py +1 -0
- package/payload/lanes/libexec/enrold.py +41 -22
- package/payload/lanes/libexec/keys.py +54 -23
- package/payload/lanes/libexec/lanes_lib.py +620 -22
- package/payload/lanes/libexec/members.py +16 -2
- package/payload/lanes/libexec/notify.py +104 -24
- package/payload/lanes/libexec/registrations.py +216 -18
- package/payload/lanes/libexec/session_actions.py +182 -14
- package/payload/lanes/libexec/sidebar +11 -3
- package/payload/lanes/libexec/sidebar.py +2135 -361
- package/payload/lanes/libexec/statusd.py +137 -10
- package/payload/lanes/libexec/studio_host.py +8 -1
- package/payload/lanes/libexec/tokens.py +121 -30
- package/payload/lanes/libexec/tokens.sh +86 -2
- package/payload/lanes/libexec/update.py +972 -0
- package/payload/lanes/libexec/users.py +614 -0
- package/payload/lanes/libexec/view.sh +34 -6
- package/payload/lanes/systemd/lanes-ensure.service +3 -0
- package/payload/lanes/systemd/lanes-gc.service +1 -1
- package/payload/lanes/systemd/lanes-update.service +17 -0
- package/payload/lanes/systemd/lanes-update.timer +17 -0
- package/payload/lanes/web/README.md +83 -24
- package/payload/lanes/web/browser.js +3 -5
- package/payload/lanes/web/mobile.css +17 -17
- package/payload/lanes/web/mobile.js +44 -32
- package/payload/lanes/workspaces.example/README.md +3 -1
- package/payload/lanes/zellij/config.kdl +106 -56
- 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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
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
|
-
|
|
39
|
-
and by `lanes-window --ascii|--light|--dark`
|
|
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
|
|
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,
|
|
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
|
-
|
|
|
59
|
-
|
|
|
60
|
-
|
|
|
61
|
-
|
|
|
62
|
-
|
|
|
63
|
-
|
|
|
64
|
-
|
|
|
65
|
-
|
|
|
66
|
-
|
|
|
67
|
-
|
|
|
68
|
-
|
|
|
69
|
-
|
|
|
70
|
-
|
|
|
71
|
-
|
|
|
72
|
-
|
|
|
73
|
-
|
|
|
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
|
|
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
|
|
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 ≤
|
|
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-
|
|
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
|
|
139
|
-
|
|
140
|
-
Each user's board
|
|
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
|
|
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
|
|
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,
|
|
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 |
|
|
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.
|