@vibelord/lanes 1.0.1 → 1.0.2
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/package.json +1 -1
- package/payload/lanes/README.md +37 -7
- package/payload/lanes/VERSION +1 -1
- package/payload/lanes/bin/lanes +651 -50
- package/payload/lanes/client/ghostty/common.conf +3 -0
- package/payload/lanes/client/ghostty/lanes-palette-light.conf +25 -0
- package/payload/lanes/client/ghostty/lanes-palette.conf +25 -0
- package/payload/lanes/client/ghostty/omarchy.conf +3 -2
- package/payload/lanes/client/lanes-launch +18 -3
- package/payload/lanes/client/lanes-window +17 -1
- package/payload/lanes/container/env.sh +1 -1
- package/payload/lanes/container/hooks/claude-settings.json +10 -0
- package/payload/lanes/container/hooks/codex-hooks.json +22 -0
- package/payload/lanes/container/lane +310 -26
- package/payload/lanes/container/lane-signal +173 -0
- package/payload/lanes/docs/ARCHITECTURE.md +80 -13
- package/payload/lanes/docs/DESIGN.md +182 -4
- package/payload/lanes/docs/RUNBOOK.md +429 -39
- package/payload/lanes/docs/SHARING.md +1 -1
- package/payload/lanes/holders/exec.sh +2 -5
- package/payload/lanes/holders.lock +3 -0
- package/payload/lanes/install/host-install.sh +10 -1
- package/payload/lanes/install/services.py +30 -3
- package/payload/lanes/install.sh +108 -3
- package/payload/lanes/libexec/agent_state.py +32 -0
- package/payload/lanes/libexec/cursor_sync.py +10 -2
- package/payload/lanes/libexec/economy_poll.py +685 -0
- package/payload/lanes/libexec/guard.py +27 -1
- package/payload/lanes/libexec/lanes_lib.py +285 -38
- package/payload/lanes/libexec/notify.py +317 -0
- package/payload/lanes/libexec/registrations.py +1 -1
- package/payload/lanes/libexec/session_actions.py +7 -8
- package/payload/lanes/libexec/sidebar.py +911 -218
- package/payload/lanes/libexec/statusd.py +238 -69
- package/payload/lanes/libexec/tokens.py +38 -7
- package/payload/lanes/libexec/view.sh +13 -3
- package/payload/lanes/systemd/lanes-economy.service +20 -0
- package/payload/lanes/systemd/lanes-notify.service +19 -0
- package/payload/lanes/systemd/lanes-sync.service +3 -0
- package/payload/lanes/web/README.md +83 -4
- package/payload/lanes/web/index.html +2 -1
- package/payload/lanes/web/mobile.css +53 -198
- package/payload/lanes/web/mobile.js +87 -55
- package/payload/lanes/zellij/config.kdl +67 -16
- package/payload/manifest.json +4 -3
- package/payload/lanes/libexec/legacy_sessions.py +0 -130
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""lane-signal <status> [--event NAME] [--harness claude|codex] [PAYLOAD] — the hook target of Claude Code and Codex
|
|
3
|
+
inside a workspace container. Writes ~/.lz/state/hooks/<lane id>.json (atomic replace), which the host's
|
|
4
|
+
lanes-statusd reads through /proc/<container init pid>/root and publishes as the lane's `agent{}` (source `hook`).
|
|
5
|
+
|
|
6
|
+
The sidecar has its own directory because `lane-run`, the root process of every zmx-held lane, writes its
|
|
7
|
+
`{phase, exit, at, pid}` record to ~/.lz/state/<zmx name>.json and exports LANE_ID=<zmx name> to the agent: a
|
|
8
|
+
sidecar at state/<lane id>.json would overwrite that record for the agent's whole life (DEF-1436). Nothing but
|
|
9
|
+
this script writes state/hooks/, and lane-run never writes there.
|
|
10
|
+
|
|
11
|
+
{"v":1, "lane":"<id>", "harness":"claude|codex", "status":"<agent{} vocabulary>", "since":<epoch the status
|
|
12
|
+
began>, "at":<epoch of this event>, "event":"<hook event>", "pid":<agent pid>, "start":"<its starttime>",
|
|
13
|
+
"session":"<session/thread id or ''>"}
|
|
14
|
+
|
|
15
|
+
It never fails the agent and never speaks: every path exits 0 and nothing reaches stdout or stderr (a Claude Code
|
|
16
|
+
UserPromptSubmit/SessionStart hook's stdout becomes model context). An unresolvable lane, a status outside the
|
|
17
|
+
vocabulary or an unwritable state directory are silent no-ops.
|
|
18
|
+
|
|
19
|
+
Lane id: $LANE_ID — exported in a tmux lane only by a canonical/`--identity` recipe's `-e` flags (Task 7, DEF-1478;
|
|
20
|
+
`lane open` never exports it), and in a zmx-held lane by `lane-run` as the zmx session name (which is NOT the
|
|
21
|
+
`<ws>--z-<name>` id lanes-statusd publishes for it, so statusd does not join that sidecar to the zmx lane yet:
|
|
22
|
+
DEF-1437) — else the pane's tmux session (`tmux -S ${TMUX%%,*} display -p -t $TMUX_PANE '#S'`) looked up by (socket
|
|
23
|
+
name, session) in ~/.lz/registry.json, the projection `lanes sync` writes.
|
|
24
|
+
|
|
25
|
+
Agent pid: the parent process, or that parent's parent when the parent is a shell (Claude Code runs a hook command
|
|
26
|
+
through a shell, which may fork it rather than exec it; a pid that dies with the shell would read as a dead agent).
|
|
27
|
+
Session id: `session_id` of the hook's JSON on stdin, `thread-id` of a JSON PAYLOAD argument (Codex `notify` passes
|
|
28
|
+
its JSON as the last argument), else $CLAUDE_SESSION_ID / $CODEX_THREAD_ID. Stdin is read only when it is not a
|
|
29
|
+
terminal and for at most STDIN_WAIT seconds: a stdin that never closes cannot hang the hook.
|
|
30
|
+
|
|
31
|
+
Same status as the file already holds (same session) keeps `since` and advances `at`; a new status starts `since`.
|
|
32
|
+
"""
|
|
33
|
+
import json
|
|
34
|
+
import os
|
|
35
|
+
import re
|
|
36
|
+
import select
|
|
37
|
+
import subprocess
|
|
38
|
+
import sys
|
|
39
|
+
import time
|
|
40
|
+
|
|
41
|
+
STATUSES = ('working', 'needs_input', 'needs_approval', 'done', 'errored', 'shell', 'idle') # agent_state.STATUSES
|
|
42
|
+
LANE_ID = re.compile(r'[A-Za-z0-9][A-Za-z0-9_.-]{0,127}')
|
|
43
|
+
SHELLS = {'sh', 'dash', 'bash', 'zsh', 'ksh', 'fish'}
|
|
44
|
+
STDIN_WAIT = 0.5
|
|
45
|
+
STDIN_MAX = 1 << 20
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def options(argv):
|
|
49
|
+
status, event, harness, payload = (argv[0] if argv else ''), '', '', ''
|
|
50
|
+
rest = argv[1:]
|
|
51
|
+
while rest:
|
|
52
|
+
word = rest.pop(0)
|
|
53
|
+
if word == '--event' and rest:
|
|
54
|
+
event = rest.pop(0)
|
|
55
|
+
elif word == '--harness' and rest:
|
|
56
|
+
harness = rest.pop(0)
|
|
57
|
+
elif word.startswith('{'):
|
|
58
|
+
payload = word
|
|
59
|
+
return status, event[:64], harness[:16], payload
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def lane_id(root):
|
|
63
|
+
found = os.environ.get('LANE_ID', '')
|
|
64
|
+
if not found and os.environ.get('TMUX') and os.environ.get('TMUX_PANE'):
|
|
65
|
+
sock = os.environ['TMUX'].split(',', 1)[0]
|
|
66
|
+
try:
|
|
67
|
+
done = subprocess.run(['tmux', '-S', sock, 'display', '-p', '-t', os.environ['TMUX_PANE'], '#S'],
|
|
68
|
+
stdin=subprocess.DEVNULL, capture_output=True, text=True, timeout=1)
|
|
69
|
+
session = done.stdout.strip() if done.returncode == 0 else ''
|
|
70
|
+
lanes = json.load(open(os.path.join(root, 'registry.json'))).get('lanes') or {}
|
|
71
|
+
except (OSError, ValueError, subprocess.SubprocessError, AttributeError):
|
|
72
|
+
return ''
|
|
73
|
+
name = os.path.basename(sock)
|
|
74
|
+
for key, lane in lanes.items():
|
|
75
|
+
tmux = (lane or {}).get('tmux') or {}
|
|
76
|
+
if session and tmux.get('socket') == name and tmux.get('session') == session:
|
|
77
|
+
found = key
|
|
78
|
+
break
|
|
79
|
+
return found if LANE_ID.fullmatch(found or '') else ''
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def stat_fields(pid):
|
|
83
|
+
"""Fields after `comm` of /proc/<pid>/stat (state is [0], ppid [1], starttime [19]) and the comm itself."""
|
|
84
|
+
with open(f'/proc/{pid}/stat') as handle:
|
|
85
|
+
text = handle.read()
|
|
86
|
+
comm = text[text.index('(') + 1:text.rindex(')')]
|
|
87
|
+
return comm, text.rsplit(')', 1)[1].split()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def agent():
|
|
91
|
+
pid = os.getppid()
|
|
92
|
+
try:
|
|
93
|
+
comm, fields = stat_fields(pid)
|
|
94
|
+
if comm in SHELLS and int(fields[1]) > 1:
|
|
95
|
+
pid = int(fields[1])
|
|
96
|
+
comm, fields = stat_fields(pid)
|
|
97
|
+
return pid, fields[19]
|
|
98
|
+
except (OSError, ValueError, IndexError):
|
|
99
|
+
return pid, None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def stdin_session():
|
|
103
|
+
try:
|
|
104
|
+
if os.isatty(0):
|
|
105
|
+
return ''
|
|
106
|
+
except OSError:
|
|
107
|
+
return ''
|
|
108
|
+
data, deadline = b'', time.monotonic() + STDIN_WAIT
|
|
109
|
+
try:
|
|
110
|
+
while len(data) < STDIN_MAX:
|
|
111
|
+
left = deadline - time.monotonic()
|
|
112
|
+
if left <= 0 or not select.select([0], [], [], left)[0]:
|
|
113
|
+
break
|
|
114
|
+
chunk = os.read(0, 65536)
|
|
115
|
+
if not chunk:
|
|
116
|
+
break
|
|
117
|
+
data += chunk
|
|
118
|
+
doc = json.loads(data.decode('utf-8', 'replace')) if data.strip() else {}
|
|
119
|
+
except (OSError, ValueError):
|
|
120
|
+
return ''
|
|
121
|
+
return str(doc.get('session_id') or '') if isinstance(doc, dict) else ''
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def main(argv):
|
|
125
|
+
status, event, harness, payload = options(argv)
|
|
126
|
+
if status not in STATUSES:
|
|
127
|
+
return
|
|
128
|
+
root = os.environ.get('LANE_ROOT') or os.path.join(os.path.expanduser('~'), '.lz')
|
|
129
|
+
lane = lane_id(root)
|
|
130
|
+
if not lane:
|
|
131
|
+
return
|
|
132
|
+
session = stdin_session()
|
|
133
|
+
if not session and payload:
|
|
134
|
+
try:
|
|
135
|
+
doc = json.loads(payload)
|
|
136
|
+
session = str(doc.get('thread-id') or doc.get('session_id') or '') if isinstance(doc, dict) else ''
|
|
137
|
+
except ValueError:
|
|
138
|
+
session = ''
|
|
139
|
+
session = session or os.environ.get('CLAUDE_SESSION_ID') or os.environ.get('CODEX_THREAD_ID') or ''
|
|
140
|
+
harness = harness or os.environ.get('LANE_HARNESS') or ('claude' if os.environ.get('CLAUDE_CODE_ENTRYPOINT')
|
|
141
|
+
else 'codex')
|
|
142
|
+
pid, start = agent()
|
|
143
|
+
now = int(time.time())
|
|
144
|
+
state = os.path.join(root, 'state', 'hooks') # never state/<id>.json: lane-run's phase record (DEF-1436)
|
|
145
|
+
path = os.path.join(state, f'{lane}.json')
|
|
146
|
+
try:
|
|
147
|
+
old = json.load(open(path))
|
|
148
|
+
except (OSError, ValueError):
|
|
149
|
+
old = None
|
|
150
|
+
session, since = session[:128], now
|
|
151
|
+
if (isinstance(old, dict) and old.get('v') == 1 and old.get('status') == status
|
|
152
|
+
and (not session or old.get('session') == session) and isinstance(old.get('since'), int)):
|
|
153
|
+
since, session = old['since'], session or str(old.get('session') or '')
|
|
154
|
+
doc = {'v': 1, 'lane': lane, 'harness': harness, 'status': status, 'since': since, 'at': now, 'event': event,
|
|
155
|
+
'pid': pid, 'start': start, 'session': session}
|
|
156
|
+
os.makedirs(state, exist_ok=True)
|
|
157
|
+
tmp = os.path.join(state, f'.{lane}.json.{os.getpid()}.tmp')
|
|
158
|
+
try:
|
|
159
|
+
with open(tmp, 'w') as handle:
|
|
160
|
+
handle.write(json.dumps(doc, separators=(',', ':')) + '\n')
|
|
161
|
+
os.replace(tmp, path)
|
|
162
|
+
finally:
|
|
163
|
+
if os.path.exists(tmp):
|
|
164
|
+
os.unlink(tmp)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
if __name__ == '__main__':
|
|
168
|
+
try:
|
|
169
|
+
sys.stdout = sys.stderr = open(os.devnull, 'w')
|
|
170
|
+
main(sys.argv[1:])
|
|
171
|
+
except BaseException: # noqa: BLE001 - a hook must never fail the agent: every outcome is exit 0
|
|
172
|
+
pass
|
|
173
|
+
os._exit(0)
|
|
@@ -10,7 +10,10 @@ terminal. Installing or closing a view does not end a lane.
|
|
|
10
10
|
2. Each manifest identifies container names and repository `.vscode/terminals.json` paths.
|
|
11
11
|
3. `lanes sync` reads those files through the container's mounted home, includes the site's optional
|
|
12
12
|
`registered-sessions.json`, and emits the host registry,
|
|
13
|
-
exact socket/session resolve files, and the container's `.lz/registry.json` projection.
|
|
13
|
+
exact socket/session resolve files, and the container's `.lz/registry.json` projection. It asks docker about
|
|
14
|
+
every board container in ONE `docker inspect` and rewrites a projection only when its content changed
|
|
15
|
+
(`atomic_write_if_changed`; the two registry documents compare without `generated_at`), so a timer sync with
|
|
16
|
+
nothing new wakes no reader that watches an mtime. The sync time itself is kept in `registry.synced`.
|
|
14
17
|
4. The status daemon observes tmux sessions and publishes status/health snapshots. Failed or timed-out
|
|
15
18
|
probes mean unknown; they do not prove a missing session. A bad terminal registry retains the last good board.
|
|
16
19
|
|
|
@@ -19,7 +22,7 @@ terminal. Installing or closing a view does not end a lane.
|
|
|
19
22
|
`working | needs_input | needs_approval | done | errored | shell | idle`. An absent key means no signal names
|
|
20
23
|
an agent there. `statusd` builds it with `agent_state.merge` from signals the harnesses already emit, read once
|
|
21
24
|
per container per cycle through `/proc/<container init pid>/root` without spawning anything. Precedence: a
|
|
22
|
-
hook sidecar
|
|
25
|
+
hook sidecar > a Claude `~/.claude/sessions/<pid>.json` naming the pane
|
|
23
26
|
`<session>:@<win>.%<pane>` (live pids only, checked in the container's pid namespace, recycled pids rejected
|
|
24
27
|
by `procStart`) > the Codex pane title (`[ ! ] Action Required` → `needs_approval`, braille spinner →
|
|
25
28
|
`working`, a task title → `idle`) > the window bell (an idle title plus a bell is `done`; an approval title
|
|
@@ -28,11 +31,29 @@ terminal. Installing or closing a view does not end a lane.
|
|
|
28
31
|
title. Unknown liveness leaves that key unset and never demotes. The tmux columns this needs
|
|
29
32
|
(`window_bell_flag`, `window_id`, `pane_id`, `pane_title`) are appended to the listing format, the free-text
|
|
30
33
|
title last so a tab inside it cannot shift another column.
|
|
34
|
+
|
|
35
|
+
The hook sidecar is `~/.lz/state/hooks/<lane id>.json` in the container, written by `~/.lz/bin/lane-signal`
|
|
36
|
+
(Python 3 stdlib, installed by `lanes install-container`) when an opt-in Claude Code or Codex hook fires:
|
|
37
|
+
`{v: 1, lane, harness, status, since, at, event, pid, start, session}` — `since` when the status began (kept
|
|
38
|
+
while the same status is re-signalled), `at` the last event, `pid`/`start` the agent process and its
|
|
39
|
+
starttime. The lane id is `$LANE_ID` (in a tmux lane only through a canonical recipe's `-e` flags; in a zmx lane
|
|
40
|
+
`lane-run` exports it as the zmx session name) or the pane's tmux session looked up in `~/.lz/registry.json`; an unresolvable lane, a word outside the vocabulary or an
|
|
41
|
+
unwritable directory is a silent exit 0, and nothing is ever printed (a Claude prompt/session hook's stdout
|
|
42
|
+
becomes model context). `statusd` lists each container's `state/hooks/` once per cycle (`agent_sidecars`) and
|
|
43
|
+
`agent_state.read_sidecar_dict` drops a sidecar whose last event is older than `SIDECAR_MAX_AGE` (3600 s) or
|
|
44
|
+
whose agent pid is dead or recycled in the container's pid namespace. The sidecar has its own directory because
|
|
45
|
+
`lane-run` writes each zmx lane's `{phase, exit, at, pid}` record to `~/.lz/state/<zmx name>.json` and exports
|
|
46
|
+
that same name as `LANE_ID`: one shared file name would let a hook overwrite the record the zmx observer and the
|
|
47
|
+
container `lane ls` read (DEF-1436). A zmx lane's sidecar is therefore named by its zmx session, not by the
|
|
48
|
+
`<ws>--z-<name>` id statusd publishes, and does not reach that lane's `agent{}` yet — the zmx lane's `agent` key
|
|
49
|
+
stays lane-run's record (DEF-1437). A published sidecar adds `event` to `agent{}` and has `source: hook`. Hooks are
|
|
50
|
+
never installed implicitly: `lanes hooks install --agent claude|codex --container <name>` prints the merge and
|
|
51
|
+
writes only with `--write` (RUNBOOK "Installing agent hooks").
|
|
31
52
|
5. A lane's `source` is the container's spelling of its `terminals.json`. Every host-side reader of that file
|
|
32
53
|
maps it to the container's home volume first (`statusd.registry_source_paths`, deduplicated by that host path:
|
|
33
54
|
one repository cloned in two containers is two files): the status daemon's hot-sync
|
|
34
55
|
starts one `lanes sync` per edit (at most one per 10 s), `lanes doctor` reports "registry up to date" from the
|
|
35
|
-
same host-side mtimes, and promoting an unregistered session writes the binding through the volume.
|
|
56
|
+
same host-side mtimes (against the later of `generated_at` and `registry.synced`), and promoting an unregistered session writes the binding through the volume.
|
|
36
57
|
|
|
37
58
|
`lanes cursor-sync` is a separate, explicit writer. Its default is a diff; `--write` takes backups and changes
|
|
38
59
|
editor commands to `lane open <id>`. The original create recipe and exact tmux binding remain in the registry's
|
|
@@ -50,12 +71,14 @@ to the server's GLOBAL environment (`show-environment -g`), and every later sess
|
|
|
50
71
|
unless its own `-e` overrides each key — a session created by a client carrying `LANE_ID=b` but no `-e` still got
|
|
51
72
|
the server starter's `LANE_ID=a`, and one created with `-e LANE_ID=c` alone kept `a`'s `THROTTLE_TRACE`. Board
|
|
52
73
|
sockets are shared (9 of the 10 bw-tools fixture lanes are on one socket), so `lane open` execs nothing — attach or
|
|
53
|
-
recipe — that carries a lane identity, this lane's or one inherited from another lane's editor terminal (DEF-
|
|
74
|
+
recipe — that carries a lane identity, this lane's or one inherited from another lane's editor terminal (DEF-1478);
|
|
54
75
|
a recipe run by it may start the server with an identity-free global environment only. An attach cannot change a
|
|
55
76
|
running session's env, so a running lane gains its identity only when it is next CREATED by a recipe carrying `-e`.
|
|
56
77
|
A legacy recipe without `-e` gives its lane no identity. `lanes cursor-sync --canonicalize` (dry-run unless
|
|
57
78
|
`--write`) gives every tmux lane whose recorded `create` cannot create it — null, attach-only (`attach-session`
|
|
58
|
-
without `new`),
|
|
79
|
+
without `new`), naming the retired in-container `~/.config/coder-launch` path (these three are
|
|
80
|
+
`lanes_lib.classify_recipe`'s classes, the ones `lanes doctor` warns about), or naming an absolute `.sh`/`.py`/…
|
|
81
|
+
script missing in the container — the canonical recipe with
|
|
59
82
|
`bash -l`; `--identity` gives a WORKING recipe of the plain shape `[exec] tmux -L <socket> new -ADs <session> [-c
|
|
60
83
|
<dir>] <command…>` the same `-e` flags, re-quoting the command's words one for one so tmux receives the same argv
|
|
61
84
|
(any expansion, glob, operator or other shape is left alone and stays identity-less). Both keep the old value as
|
|
@@ -69,7 +92,7 @@ stamped `lanes/<ws>/<id>` therefore puts its main session, every subagent and an
|
|
|
69
92
|
into ONE fairness bucket where today each Claude session/agent is its own. `max_in_flight_per_trace` is configured
|
|
70
93
|
and reported but no enforcement site exists in `src/` (MEASURED by search; `pressure.rs` documents fairness as "not a
|
|
71
94
|
hard cap"). Whether a lane should be one bucket is an open decision for the plan owner before any live enablement
|
|
72
|
-
(DEF-
|
|
95
|
+
(DEF-1479); this tree does not change the header shape. Unit-proven only (`test_lib_probe` `LaneOpenIdentity`,
|
|
73
96
|
`CursorSyncCanonicalize`, `CursorSyncIdentity`, stopping at the exec boundary); the disposable-lane
|
|
74
97
|
`tests/cursor_lane.py` exercises the installed `/home/coder/.lz/bin/lane`, so it proves this only after `lanes
|
|
75
98
|
install-container`.
|
|
@@ -80,6 +103,22 @@ install-container`.
|
|
|
80
103
|
a lane sidebar, and one to three viewports. The mobile layout uses separate picker and full-width view tabs.
|
|
81
104
|
Its hidden viewports do not attach at placeholder dimensions; they wait until a real client displays them.
|
|
82
105
|
|
|
106
|
+
**Visual system:** one token table (`libexec/tokens.py`) is rendered into the sidebar's escapes, the Zellij themes,
|
|
107
|
+
the phone CSS (and its xterm.js palette in `mobile.js`), the viewer's shell (`libexec/tokens.sh`), the optional Ghostty
|
|
108
|
+
palette files and the docs table; `tokens.py --check` fails on drift in any of them. `docs/DESIGN.md` is the legend:
|
|
109
|
+
glyphs with their ASCII twins, modes (`LANES_COLOR`, `LANES_THEME`, `LANES_GLYPHS`, `LANES_DIM`, `LANES_NOTIFY`), row
|
|
110
|
+
anatomy, mockups and budgets; `tests/unit/test_docs.py` keeps the README legend complete against the glyph tables.
|
|
111
|
+
|
|
112
|
+
Frames and bars take their colours from the `lanes-dark`/`lanes-light` themes generated by `libexec/tokens.py`; the
|
|
113
|
+
compact bar is the only plugin; the mobile picker's sidebar pane has no layout name so its own title (attention
|
|
114
|
+
counts) reaches the phone. `zellij/config.kdl` adds, after the generated token block, one `ui` block whose
|
|
115
|
+
`pane_frames` set `rounded_corners false` and `hide_session_name true` (straight frames to match the sidebar's `─`
|
|
116
|
+
rules, no session name in the bar) and `simplified_ui true` (no Private-Use-Area chevrons, so no Nerd Font is needed);
|
|
117
|
+
`load_plugins` stays empty. The desktop sidebar keeps `pane size=36 borderless=true name="Lanes"` (Throttle Studio's view rule reads
|
|
118
|
+
that name); the view pane is borderless at one viewport and framed at two or three. `tests/unit/test_layout.py` pins
|
|
119
|
+
the generated layout and `test_keybinds.Chrome` the config; `zellij --config zellij/config.kdl setup --check` is the
|
|
120
|
+
parse check (0.45.1).
|
|
121
|
+
|
|
83
122
|
The tmux holder reaches the container's socket through `/proc/<init-pid>/root`, scrubs the viewer environment,
|
|
84
123
|
and uses `tmux -N` so an attach cannot create a server accidentally. Guarded geometry and client identification
|
|
85
124
|
keep the visible view and editor attachments coordinated. Zmx is a separately pinned alternative holder;
|
|
@@ -114,15 +153,41 @@ Explicit **New +** starts a detached tmux shell inside the workspace container o
|
|
|
114
153
|
rechecking its session ID and creation time and refusing registered lanes; affected viewers become empty.
|
|
115
154
|
The tmux close condition is checked inside the server, protecting a replacement created between observation
|
|
116
155
|
and the action. `user-sessions/` stores display metadata; `status.json` and `resolve.dynamic/` are projections.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
156
|
+
The pre-persistence Docker-shell tracker (`legacy-sessions/`, `legacy_sessions.py`, `lanes verify-legacy`) was
|
|
157
|
+
retired by plan Task 19: statusd no longer reads that directory (a record still there is inert — kept on disk,
|
|
158
|
+
never observed, never closed), and a `legacy` row from an older status.json is refused by close. See
|
|
159
|
+
[state contracts](../../docs/master-datamodel.md).
|
|
121
160
|
|
|
122
161
|
The `lane` command lives in `/home/coder/.lz/bin`. `open` attaches or creates using its saved recipe;
|
|
123
162
|
`attach` never creates; `send`, `capture`, `ls` and `status` are explicit operations. Only `kill` ends a lane.
|
|
124
163
|
These commands operate inside the container, independently of the Zellij viewer's lifetime.
|
|
125
164
|
|
|
165
|
+
Every verb dispatches on the lane's holder. An id that `~/.lz/registry.json` holds as `holder: tmux` is driven
|
|
166
|
+
through its exact socket and session, by a tmux client that carries no lane identity (`identity_free`, as `open`):
|
|
167
|
+
`send` types the text literally and then Enter in one client (`send-keys -t =<session>: -l -- TEXT ; send-keys
|
|
168
|
+
Enter`; `--no-enter` omits it), `capture` is `capture-pane -p -J -S -N|- -t =<session>:` (`--vt` adds `-e`) trimmed
|
|
169
|
+
to the last N non-blank lines, `status` prints `{id, holder, ws, title, tmux, alive, agent}` from `has-session` and
|
|
170
|
+
the lane's `lane-signal` hook sidecar (exit 1 when not alive), `attach` refuses to nest and never creates, and
|
|
171
|
+
`kill` is `kill-session`. Two tmux 3.4 facts shape these argv (MEASURED on a disposable `lanes_test` server): a bare
|
|
172
|
+
`-t =<session>` is refused by `send-keys` and `capture-pane` ("can't find pane") while `=<session>:` resolves, and
|
|
173
|
+
tmux's argv parser ends a command at a trailing `;` of ANY argument, so `echo a;` would type `echo a` — `send`
|
|
174
|
+
writes that last `;` as `\;`. Any other id is a zmx lane (a registry `holder: zmx` entry maps to its `zmx.name`) and
|
|
175
|
+
keeps the zmx path; zmx `send` still writes the bytes as given, never an Enter of its own. `kill` now needs `--yes`
|
|
176
|
+
on both holders and refuses while the lane's hook sidecar says `working` unless `--force`; the container sees only
|
|
177
|
+
that sidecar (Claude session files and pane titles are judged on the host by `lanes-statusd`), so on a lane without
|
|
178
|
+
installed hooks the refusal cannot fire (DEF-1439). `ls --json` stays the zmx-only listing Throttle Studio reads —
|
|
179
|
+
its zmx holder takes EVERY row as a zmx lane, so tmux rows there would publish each tmux lane twice; `ls --all` adds
|
|
180
|
+
the registry's tmux lanes with `alive` from one `list-sessions` per socket (and still lists them in a root without the zmx binary, where the default listing keeps failing as before). zmx rows gain `alive`, and `status` of a
|
|
181
|
+
zmx lane gains `hook` (its sidecar); `agent` stays lane-run's phase record (DEF-1436).
|
|
182
|
+
|
|
183
|
+
The bw-tools agent launchers (`.bw-tools/bin/claude`, `codex`) exec the agent binary (shell `exec` → `agent.py`
|
|
184
|
+
`os.execvpe`), so a lane started through them shows the agent as the pane's foreground command. The operator's
|
|
185
|
+
`~/.local/bin/eval-claude` runs Claude as a child instead (it appends `session_end` to its trace file after the
|
|
186
|
+
exit), so a pane started through it keeps `bash` as its foreground command (DEF-1440); on the launcher path the end
|
|
187
|
+
of a session is the `SessionEnd` hook's `lane-signal shell`, when hooks are installed. `.bw-tools/bin/claude` runs
|
|
188
|
+
`$EVAL_CLAUDE_REAL_BIN` when it is set (the binary eval-claude would run) and skips it when it names a `.bw-tools`
|
|
189
|
+
shim, so `EVAL_CLAUDE_REAL_BIN=<checkout>/.bw-tools/bin/claude eval-claude` composes the two without a loop.
|
|
190
|
+
|
|
126
191
|
## Desktop and uploads
|
|
127
192
|
|
|
128
193
|
The client installer creates an SSH alias and dedicated terminal configuration, using the selected profile's
|
|
@@ -158,9 +223,11 @@ workspace and lane pickers. The bar reads the `MobileState` that Zellij sends to
|
|
|
158
223
|
phone picker's `FocusPane`. It overlays the terminal, so the terminal's columns and rows do not change, and a
|
|
159
224
|
click on the chip leaves the keyboard focus in the terminal. The tablet and desktop code and styles live in
|
|
160
225
|
`web/browser.js` and `web/browser.css`, which `mobile.js` appends only in those two modes, so a phone never
|
|
161
|
-
downloads them
|
|
162
|
-
|
|
163
|
-
|
|
226
|
+
downloads them: a phone fetches only the two files spec §8 budgets, MEASURED 2026-10-04 after the fan-in with
|
|
227
|
+
plan Tasks 13/28 at 21 964 B of 22 174 B (`mobile.js`, whose explanations live in `web/README.md`, Code notes)
|
|
228
|
+
and 18 945 B of 18 982 B (`mobile.css`). `lanes web <device> [--desktop|--mobile]` prepares a browser view,
|
|
229
|
+
enables the services through `install/enable-web.sh` and prints the private URL; without a flag a known device
|
|
230
|
+
keeps its layout.
|
|
164
231
|
|
|
165
232
|
## Installed files and services
|
|
166
233
|
|
|
@@ -1,9 +1,56 @@
|
|
|
1
1
|
# Lanes design system
|
|
2
2
|
|
|
3
|
-
The operator-facing legend for the Lanes cockpit
|
|
4
|
-
|
|
5
|
-
`
|
|
6
|
-
|
|
3
|
+
The operator-facing legend for the Lanes cockpit: what every glyph, colour and switch means, how a sidebar row is
|
|
4
|
+
laid out, and the budgets each surface is held to. The token and glyph tables below are generated from
|
|
5
|
+
`libexec/tokens.py` (`python3 libexec/tokens.py --write`; `--check` fails on drift), the one source the sidebar's
|
|
6
|
+
escapes, the Zellij themes, the phone CSS, the viewer's shell (`libexec/tokens.sh`), the optional Ghostty palette and
|
|
7
|
+
this page are projected from. The design of record is
|
|
8
|
+
[`docs/superpowers/specs/2026-10-03-lanes-cockpit-design.md`](../../docs/superpowers/specs/2026-10-03-lanes-cockpit-design.md)
|
|
9
|
+
at the repository root; this page condenses it for daily use and never overrides it.
|
|
10
|
+
|
|
11
|
+
## Principles
|
|
12
|
+
|
|
13
|
+
1. **Attention before identity before telemetry:** column 2 says who needs you, columns 4–27 which lane, columns
|
|
14
|
+
29–34 how long and doing what.
|
|
15
|
+
2. **Never colour alone:** every state has a glyph, a tone and an ASCII twin, and a fixed column; `LANES_COLOR=none`
|
|
16
|
+
leaves a fully readable sidebar.
|
|
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.
|
|
23
|
+
6. **Restraint:** one accent, for selection, viewing marks and the brand only; no emoji in rows, one upload line, a
|
|
24
|
+
one-line legend plus the `?` key sheet.
|
|
25
|
+
7. **Every row says how long:** a 3-cell age on every row — the agent's `since`, else the last output.
|
|
26
|
+
8. **Marks never lose to names:** viewer dots and viewport marks are reserved first; the name is clipped and ends
|
|
27
|
+
in `…`.
|
|
28
|
+
9. **The cursor follows the lane, not the index:** re-sorts never move your focus in a list that scrolls.
|
|
29
|
+
10. **Performance is a design property:** cached row bytes, one width walk per name, frames written only when dirty,
|
|
30
|
+
OSC 2/9/11 in the same write; budgets below.
|
|
31
|
+
11. **Pinned bytes change deliberately:** a moved byte assertion is rewritten with a role name, never loosened.
|
|
32
|
+
12. **The phone is a list, not a terminal:** the system-font shell carries identity, counts and actions.
|
|
33
|
+
13. **Docs are the fourth surface:** this page, the README legend and the RUNBOOK column table are kept with the
|
|
34
|
+
code; the screenshots are user-supplied and never regenerated.
|
|
35
|
+
|
|
36
|
+
## Modes
|
|
37
|
+
|
|
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.
|
|
40
|
+
|
|
41
|
+
| Switch | Values | Default | Where read |
|
|
42
|
+
|---|---|---|---|
|
|
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) |
|
|
48
|
+
|
|
49
|
+
## Tokens and glyphs
|
|
50
|
+
|
|
51
|
+
Every glyph table is `key -> (unicode, ascii, tone)`: agent glyphs are drawn only in column 2, stream glyphs only in
|
|
52
|
+
column 33, marks only where the row anatomy puts them. `LANES_GLYPHS=ascii` swaps in the ASCII column, so an ASCII
|
|
53
|
+
frame is seven-bit. The `ANSI-16` column is what `LANES_COLOR=16` emits; truecolor uses the Dark or Light value.
|
|
7
54
|
|
|
8
55
|
<!-- BEGIN lanes tokens (generated by libexec/tokens.py --md; do not edit by hand) -->
|
|
9
56
|
| Role | Dark | Light | ANSI-16 | CSS variable |
|
|
@@ -70,3 +117,134 @@ mockups land with the docs task of the cockpit v2 plan.
|
|
|
70
117
|
| `↓` | U+2193 | `v` | tone-attention | economy `downgraded` |
|
|
71
118
|
| `$` | U+0024 | `$` | ink-dim | economy `paid` |
|
|
72
119
|
<!-- END lanes tokens -->
|
|
120
|
+
|
|
121
|
+
## Row anatomy
|
|
122
|
+
|
|
123
|
+
1-based columns; every lane row is exactly 36 cells by `cell_width`, and column 36 is never written.
|
|
124
|
+
|
|
125
|
+
| col | content | tone |
|
|
126
|
+
|---|---|---|
|
|
127
|
+
| 1 | ▌ cursor or space | accent |
|
|
128
|
+
| 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 |
|
|
131
|
+
| 28 | `+` register control on an unregistered row that can be added to a workspace (closable and `R.eligible`; CHA-positioned at `cols − 8`; hit box `cols − 8 ≤ x < cols − 4`), else space — amended by Task 23 (§10, DEF-1428) | 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 |
|
|
133
|
+
| 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 |
|
|
135
|
+
| 34 | tail: stale `?` > starving ⚠ > downgraded ↓ > paid `$` > space | ink-dim / attention |
|
|
136
|
+
| 35 | × on closable rows, else space | urgent |
|
|
137
|
+
| 36 | never written | |
|
|
138
|
+
|
|
139
|
+
Budget: 3 + 24 + 1 + 3 + 1 + 1 + 1 + 1 + 1 = 36. The close hit box stays `x ≥ cols − 4` (a click at `cols − 2` closes,
|
|
140
|
+
at `x = 5` opens — the pinned test keeps both). The `×` moved from `cols − 2` to `cols − 1`.
|
|
141
|
+
|
|
142
|
+
## Mockups
|
|
143
|
+
|
|
144
|
+
Desktop, a 35-lane workspace at 36 × 40 in `unicode` and truecolor (spec §4.8). Every line is exactly 36 cells; the
|
|
145
|
+
real list scrolls where `⋮` is:
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
Sakio ◆1 ◇2 ▸6 ✓1
|
|
149
|
+
── ◆ Needs you (3) ─────────────────
|
|
150
|
+
▌◆ Compiler - Orchestrator 25m ○
|
|
151
|
+
◇›Runtime: supervisor 12m ○
|
|
152
|
+
◇ granite_games 4m ○ ×
|
|
153
|
+
── ▸ Working (6) ───────────────────
|
|
154
|
+
▸²Runtime - Orchestrator·· 3m ●$
|
|
155
|
+
▸ Runtime: rendering 41s ●
|
|
156
|
+
▸ Runtime: contracts 2m ●
|
|
157
|
+
▸ Runtime: rules 18m ○$
|
|
158
|
+
▸ Runtime: events 9m ●
|
|
159
|
+
▸ Runtime: value-hunter-r… 1h ○↓
|
|
160
|
+
── ✓ Done (1) ──────────────────────
|
|
161
|
+
✓ Sakio CLI 7m ○
|
|
162
|
+
── Sakio (25) ──────────────────────
|
|
163
|
+
Granite 2h ○
|
|
164
|
+
Sakio rename 3d –
|
|
165
|
+
_ Runtime: waits-effects 12h ○
|
|
166
|
+
· Runtime: trace-replay 1d ○
|
|
167
|
+
Runtime: source-assemb…· 5h !
|
|
168
|
+
Runtime: conformance 2d ○
|
|
169
|
+
Runtime: integration 6h ✗
|
|
170
|
+
Runtime: wave-b-critic-… 9h ○
|
|
171
|
+
Design review 3h ○?
|
|
172
|
+
Docs 11h ○
|
|
173
|
+
Release 4d –
|
|
174
|
+
Bench 2d ○
|
|
175
|
+
Fixtures 8h ○
|
|
176
|
+
Harness 1d ○
|
|
177
|
+
Site 5d –
|
|
178
|
+
Infra 9h ○
|
|
179
|
+
⋮ 9 more
|
|
180
|
+
── Unregistered (5) ────────────────
|
|
181
|
+
+ New shell
|
|
182
|
+
granite_brand_migration 2h ○ ×
|
|
183
|
+
refinery-workblocks 6h ? ×
|
|
184
|
+
↑ uploads · click, then drop (U)
|
|
185
|
+
$8.5k/16k 53% · spill 76% · free 3
|
|
186
|
+
────────────────────────────────────
|
|
187
|
+
⏎ open · / find · x close · ? keys
|
|
188
|
+
```
|
|
189
|
+
|
|
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
|
|
191
|
+
viewport b (²) with two viewers (··). The same frame in ASCII mode, first rows:
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
Sakio !1 ?2 >6 v1
|
|
195
|
+
-- ! Needs you (3) -----------------
|
|
196
|
+
|! Compiler - Orchestrator 25m o
|
|
197
|
+
?=Runtime: supervisor 12m o
|
|
198
|
+
>2Runtime - Orchestrator.. 3m *$
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
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.
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
◆1 need approval · ◇2 need you · ▸6 working
|
|
206
|
+
|
|
207
|
+
── ◆ Needs you (3) ─────────────────────────────
|
|
208
|
+
›◆ Compiler - Orchestrator
|
|
209
|
+
Needs approval · 25m · claude
|
|
210
|
+
Fix buildctl bugs
|
|
211
|
+
◇ Runtime: supervisor··
|
|
212
|
+
Needs you · 12m · codex
|
|
213
|
+
Verify fleet plan mapping
|
|
214
|
+
◇ granite_games ×
|
|
215
|
+
Needs you · 4m · claude
|
|
216
|
+
Port the brand migration
|
|
217
|
+
── ▸ Working (6) ───────────────────────────────
|
|
218
|
+
▸ Runtime: rendering
|
|
219
|
+
Working · 41s · claude
|
|
220
|
+
Renderer contracts wave B
|
|
221
|
+
── Sakio (25) ──────────────────────────────────
|
|
222
|
+
Granite
|
|
223
|
+
Quiet · 2h
|
|
224
|
+
|
|
225
|
+
Swipe to scroll · Tap to open
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Budgets
|
|
229
|
+
|
|
230
|
+
Each budget is pinned by a test; a budget that is not met is an open defect row, never a raised number.
|
|
231
|
+
|
|
232
|
+
| Budget | Pin |
|
|
233
|
+
|---|---|
|
|
234
|
+
| `render(35 lanes + 5 unregistered, 36×60)` median of 50 after 5 warm-ups ≤ 2.0 ms hard (CI, loose); design target ≤ 0.9× the same-run baseline cold (today 0.459 ms here, binary-safe read) and ≤ 0.10 ms warm (row cache) — MEASURED 2026-10-04 after Task 23: cold 0.64× met; warm 0.27–0.31 ms at load ~23–30, NOT met, DEF-1429 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)` |
|
|
236
|
+
| Exactly `viewports` `open()` calls of target files per render (12 over 6 renders at 2 viewports today); `subprocess.Popen`/`run` never called | `test_render_opens_only_target_files_and_never_forks` (open counter on the view dir, Popen/run patched to raise) |
|
|
237
|
+
| 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` |
|
|
239
|
+
| Upload: select timeout stays 1.0 while uploading; `dirty` only on `.progress` mtime change | `test_upload_does_not_shorten_the_tick` (Task 24) |
|
|
240
|
+
| `render_mobile(35, 48×60)` ≤ 2.0 ms hard, frame ≤ 6144 B; target ≤ 0.45 ms cold / ≤ 0.10 ms warm (today 0.352 ms / 1663 B) | `test_mobile_render_time_and_frame` |
|
|
241
|
+
| `reload(force)` unchanged (0.14 ms at 35 / 0.39 ms at 183); fleet attention computed inside it from the loaded dict — NOT shown met: Task 24 measured 0.26–0.44 ms at 35 / 0.69–0.80 ms at 183 and Task 12 0.296 → 0.386 ms at 35 (load 30–62; the 0.14/0.39 figures are quiet-host figures), DEF-1483 open; the budget is not raised | bench in Task 24's and Task 12's commit messages; DEF-1483 |
|
|
242
|
+
| Phone: exactly one `setInterval(`, zero `setTimeout(`, no `backdrop-filter`, a painted `box-shadow` only in `.lanes-sheet-panel` and `.zj-mobile-menu` (a `box-shadow: none` override, as on the security modal, is not paint work and is not counted); `fetch(` absent; mobile.css grows ≤ 2048 B over today's 16 934 B, mobile.js ≤ 3072 B over today's 19 102 B (measured 2026-10-03; the caps are baseline + allowance, quoted in the test); `index.html` keeps its 9 SRI-pinned upstream assets | `test_web.test_js_timers_unchanged`, `test_web.test_no_scroll_time_effects`, `test_web.test_bundle_growth`, `test_web.test_integrity_pins_untouched` |
|
|
243
|
+
| Zellij: config.kdl grows by the theme + ui block only (~4 KB), `load_plugins {}` empty, no `rename-tab` from the sidebar | `test_keybinds.test_theme_block_and_plugin_free`, `test_tokens.test_generated_blocks_match_committed_files` |
|
|
244
|
+
| Unit suite stays under 75 s wall (MEASURED 2026-10-04 after Task 21: 833 tests, 124–176 s at load ~30–60 — exceeded, DEF-1415 open; the budget is not raised); new pins are exact-string asserts, no sleeps; `tokens.py --check` ≤ 50 ms, contrast test ≤ 10 ms | observed in each task's run step |
|
|
245
|
+
|
|
246
|
+
## Spec
|
|
247
|
+
|
|
248
|
+
The full design system — token values and contrast, the glyph rules, header, sections, footer ladder, title and
|
|
249
|
+
notification bytes, Zellij chrome, phone shell and client alignment, and the decisions log — is
|
|
250
|
+
[`docs/superpowers/specs/2026-10-03-lanes-cockpit-design.md`](../../docs/superpowers/specs/2026-10-03-lanes-cockpit-design.md).
|