@vibelord/lanes 1.2.0 → 1.3.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/package.json +1 -1
- package/payload/lanes/README.md +4 -3
- package/payload/lanes/VERSION +1 -1
- package/payload/lanes/bin/lanes +301 -45
- package/payload/lanes/container/lane +42 -1
- package/payload/lanes/container/lane-mcp +65 -6
- package/payload/lanes/container/lane_core.py +178 -0
- package/payload/lanes/docs/ARCHITECTURE.md +192 -20
- package/payload/lanes/docs/DESIGN.md +6 -3
- package/payload/lanes/docs/INSTALL.md +25 -8
- package/payload/lanes/docs/RUNBOOK.md +369 -8
- package/payload/lanes/docs/USERGUIDE.md +201 -25
- package/payload/lanes/holders.lock +5 -0
- package/payload/lanes/install/enable-docs.sh +78 -0
- package/payload/lanes/install/host-install.sh +39 -5
- package/payload/lanes/libexec/docs_store.py +995 -0
- package/payload/lanes/libexec/docsd.py +2010 -0
- package/payload/lanes/libexec/mdlite.py +575 -0
- package/payload/lanes/libexec/members.py +5 -0
- package/payload/lanes/libexec/notify.py +65 -5
- package/payload/lanes/libexec/sidebar.py +106 -13
- package/payload/lanes/libexec/statusd.py +97 -5
- package/payload/lanes/libexec/tokens.py +31 -9
- package/payload/lanes/libexec/update.py +94 -22
- package/payload/lanes/libexec/uploads.py +538 -0
- package/payload/lanes/systemd/lanes-docsd.service +29 -0
- package/payload/lanes/web/Caddyfile +24 -1
- package/payload/lanes/web/README.md +86 -8
- package/payload/lanes/web/browser.js +2 -4
- package/payload/lanes/web/docs.css +52 -0
- package/payload/lanes/web/docs.js +36 -0
- package/payload/lanes/web/mobile.js +3 -2
- package/payload/lanes/web/upload.css +17 -0
- package/payload/lanes/web/upload.js +84 -0
- package/payload/manifest.json +4 -3
|
@@ -36,7 +36,8 @@ for operations, the [runbook](RUNBOOK.md).
|
|
|
36
36
|
Each workspace container that has the lane contract (`~/.lz/bin/lane`) also gets its own slice of the
|
|
37
37
|
observation as `~/.lz/status.json` (LNS-32, `statusd.write_container_status`), because nothing in a container
|
|
38
38
|
can read the host's `status.json`: `{v: 1, at, lanes{<id>: {state, agent?, attached?, viewers?, activity?, size?,
|
|
39
|
-
stale?}}, unregistered[{id, name, workspace, boards, tmux, identity}]}
|
|
39
|
+
stale?, docs?}}, unregistered[{id, name, workspace, boards, tmux, identity}]}` (`docs`, the host row's `{n, at}`
|
|
40
|
+
verbatim, since plan W10 W7). `lanes` holds the container's declared
|
|
40
41
|
and registered lanes (the ids its `.lz/registry.json` holds) and its unregistered sessions; a lane no cycle has
|
|
41
42
|
observed yet is `unknown`; the host-side details (cwd, pid, tty, command, probe outcome) stay on the host; a
|
|
42
43
|
session listed under several boards is one `unregistered` row naming them all. zmx lanes are not in it — `lane
|
|
@@ -286,6 +287,11 @@ while it ran); `status.json` and `resolve.dynamic/` are projections.
|
|
|
286
287
|
answered (a container whose listing timed out or failed is absent; its `zmx` groups are then the previous ones
|
|
287
288
|
republished). It sits outside the change comparison, so the file is still rewritten only on change or the heartbeat;
|
|
288
289
|
the co-located Studio Host prunes a lane it started only on such a listing (docs/SHARING.md, design §8.10 C1).
|
|
290
|
+
The docs cockpit (plan W10 W7) adds two keys, both absent when there is nothing to say: a lane row's `docs: {n, at}`
|
|
291
|
+
while `docs/counts.json` counts documents for that lane (statusd stats that file once per cycle and reads it only when
|
|
292
|
+
it moved), and the document's `web_origin`, `https://<name>:8443`, the name the enrolment service announces (site.conf's
|
|
293
|
+
`LANES_SERVER_NAME`, else this node's MagicDNS name), which statusd looks up between two cycles at most every 300 s
|
|
294
|
+
and never again under pressure once it has one. The sidebar builds its OSC 8 links to `/lanes-docs/` on it.
|
|
289
295
|
The pre-persistence Docker-shell tracker (`legacy-sessions/`, `legacy_sessions.py`, `lanes verify-legacy`) was
|
|
290
296
|
retired by plan Task 19: statusd no longer reads that directory (a record still there is inert — kept on disk,
|
|
291
297
|
never observed, never closed), and a `legacy` row from an older status.json is refused by close. See
|
|
@@ -328,10 +334,11 @@ shim, so `EVAL_CLAUDE_REAL_BIN=<checkout>/.bw-tools/bin/claude eval-claude` comp
|
|
|
328
334
|
|
|
329
335
|
`~/.lz/bin/lane-mcp` is the Agent API (the cockpit v3 plan's Track M, LNS-32…36). It is an MCP server inside each
|
|
330
336
|
workspace container: the agent in a lane uses it to see the lanes of its container and, within guardrails, to change
|
|
331
|
-
their declarations, type into a sibling lane
|
|
332
|
-
reads the two projections the host writes into `~/.lz` and the container's own tmux,
|
|
333
|
-
container: a repository's `.vscode/terminals.json`, a tmux session, the lane's hook
|
|
334
|
-
the boards through the host's ordinary readers (`lanes sync`,
|
|
337
|
+
their declarations, type into a sibling lane, report its own status and publish a file to the docs page. It is the
|
|
338
|
+
container's half of Lanes. It reads the two projections the host writes into `~/.lz` and the container's own tmux,
|
|
339
|
+
and it writes only inside the container: a repository's `.vscode/terminals.json`, a tmux session, the lane's hook
|
|
340
|
+
sidecar, a publish request. What it changes reaches the boards through the host's ordinary readers (`lanes sync`,
|
|
341
|
+
`lanes-statusd`, `lanes-docsd`'s pick-up); nothing on the host listens to it.
|
|
335
342
|
**[OPERATOR-GATED]** in a live container: `lanes install-container <container>` ships it and adds it to Claude Code
|
|
336
343
|
and Codex (the plan's operator gate 3), and new agent sessions load it. How an agent should use it:
|
|
337
344
|
[`skills/lanes-agent-tools`](../../skills/lanes-agent-tools/SKILL.md). Every argument and refusal, for people:
|
|
@@ -344,7 +351,7 @@ and Codex (the plan's operator gate 3), and new agent sessions load it. How an a
|
|
|
344
351
|
| --- | --- | --- | --- | --- |
|
|
345
352
|
| `lanes_whoami` | read | `$LANE_ID`, else the agent's own tmux pane, looked up in `~/.lz/registry.json` and `~/.lz/status.json` | — | now |
|
|
346
353
|
| `lanes_list` | read | `~/.lz/registry.json` and `~/.lz/status.json` only, no tmux | — | now; the observation is fresh while `status.fresh` (renewed within 60 s) |
|
|
347
|
-
| `lanes_status` | read | the two projections, one `has-session`, the lane's hook sidecar | — | now (`alive: null` when tmux does not answer in 5 s) |
|
|
354
|
+
| `lanes_status` | read | the two projections, one `has-session`, the lane's hook sidecar, its newest publish receipt (`published`) | — | now (`alive: null` when tmux does not answer in 5 s) |
|
|
348
355
|
| `lanes_capture` | read | one `capture-pane` of the lane | — | now |
|
|
349
356
|
| `lanes_register` | write | a repository's `terminals.json`: one terminal appended | `lanes sync`, started by `lanes-statusd`'s hot sync | 15 s |
|
|
350
357
|
| `lanes_unregister` | write | the same file: one terminal removed; its session keeps running | `lanes sync` | 15 s |
|
|
@@ -352,11 +359,13 @@ and Codex (the plan's operator gate 3), and new agent sessions load it. How an a
|
|
|
352
359
|
| `lanes_new` | exec | the same file: one terminal appended; and a new tmux session running `command` | `lanes sync` | 15 s (the session runs at once) |
|
|
353
360
|
| `lanes_send` | exec | keystrokes into a lane's active pane | the program in the lane's active pane | 0 s (its Enter half a second after the text) |
|
|
354
361
|
| `lanes_signal` | write | the agent's own hook sidecar, through `lane-signal` | `lanes-statusd` | 5 s |
|
|
362
|
+
| `lanes_publish` | write | a publish request `~/.lz/state/publish/<lane>/<id>.json` naming a file of the container (the file itself is read by the host) | `lanes-docsd` | 5 s (its pick-up cycle; the receipt comes back beside the request) |
|
|
355
363
|
|
|
356
364
|
"Seen within" is the `expected_within_s` each write answers (`test_docs.AgentApi` reads it from `lane-mcp` and holds
|
|
357
365
|
this table to it). A terminals.json edit: `lanes-statusd` checks every cycle (3 s, `LANES_STATUS_INTERVAL`) whether a
|
|
358
366
|
terminals.json its registry names has changed (`statusd.hot_sync`) and starts one `lanes sync` at most every 10 s;
|
|
359
|
-
the `lanes-sync.timer` sync every 2 minutes catches anything else. A sidecar is read on the next statusd cycle.
|
|
367
|
+
the `lanes-sync.timer` sync every 2 minutes catches anything else. A sidecar is read on the next statusd cycle. A
|
|
368
|
+
publish request is taken by the docs service's own pick-up cycle (5 s, "Docs service" below).
|
|
360
369
|
"Effect" is the tool gateway's classification (`read`, `write`, `exec`: arbitrary command execution). `lanes_new` runs
|
|
361
370
|
its `command`, and `lanes_send` types a line that a shell lane runs, so both are `exec`; the gateway recipe below
|
|
362
371
|
carries the same classes.
|
|
@@ -380,7 +389,7 @@ way: only when missing, `<file>.bak-<UTC>` first, re-read before the write, and
|
|
|
380
389
|
something else is never replaced.
|
|
381
390
|
|
|
382
391
|
The write tools (LNS-35) are `lanes_register`, `lanes_unregister`, `lanes_rename`, `lanes_new`, `lanes_send` and
|
|
383
|
-
`lanes_signal`. The first four change a lane's DECLARATION, a repository's terminals.json in the container, and
|
|
392
|
+
`lanes_signal`; plan W10 W4 (LNS-105) appended `lanes_publish`. The first four change a lane's DECLARATION, a repository's terminals.json in the container, and
|
|
384
393
|
never the host: `lanes-statusd`'s hot-sync notices the changed file and starts one `lanes sync`, which publishes it
|
|
385
394
|
(about 15 s). To answer with the id that sync will give, the container carries ports of the host's rules —
|
|
386
395
|
`slug`, `shown_key`, `lane_trace`, `canonical_recipe`, the id grammar — each pinned equal to its `lanes_lib`
|
|
@@ -400,7 +409,12 @@ reads the text and its CR as one chunk, which an agent's prompt takes as a paste
|
|
|
400
409
|
target's agent is `working` or `needs_approval` (the host's observation or the lane's own sidecar) unless forced.
|
|
401
410
|
`lanes_signal` runs `lane-signal` with `--agent-pid` (the server's parent, the agent), `--note` (the sidecar's
|
|
402
411
|
`name`, which `agent_state.read_sidecar_dict` already publishes as the agent's task line) and the agent's
|
|
403
|
-
`CLAUDE_CODE_SESSION_ID` when its environment carries one, so the report and the agent's hooks name the same agent.
|
|
412
|
+
`CLAUDE_CODE_SESSION_ID` when its environment carries one, so the report and the agent's hooks name the same agent.
|
|
413
|
+
`lanes_publish` (and the `lane publish` verb, the same `lane_core.publish`) resolves the file inside the container's
|
|
414
|
+
home, hashes it once and writes the request; it never copies the file. The host trusts nothing in the request: the
|
|
415
|
+
docs service resolves and hashes the file again and refuses a hash that differs. The tool reads its answer back as
|
|
416
|
+
`lanes_status`'s `published` (`lane_core.latest_receipt`), from the receipt in the container, never through
|
|
417
|
+
`status.json`. Each write answers what changed, what
|
|
404
418
|
picks it up and when; each refusal a machine code and what a person can do instead. On the host,
|
|
405
419
|
`session_actions.unregister_declared` runs the same `lane_core` editor (loaded once per process by
|
|
406
420
|
`lanes_lib.lane_core()`) through the container's home volume with `root=` set, so a symbolic link there is refused
|
|
@@ -420,7 +434,8 @@ No tool exists for these. An agent that needs one asks a person; a refusal's `hu
|
|
|
420
434
|
- **Answering an approval** prompt. `lanes_send` refuses a lane whose agent waits for an approval
|
|
421
435
|
(`agent_needs_approval`); `force` is for an answer the agent was asked to type.
|
|
422
436
|
- **Opening or attaching** a lane, or changing what a view shows.
|
|
423
|
-
- **Uploads
|
|
437
|
+
- **Uploads**: a person's file into a lane (the Docs page, the desktop drop). The reverse, an agent's own file onto
|
|
438
|
+
the docs page, is `lanes_publish`.
|
|
424
439
|
- The host's **`lanes sync`, `lanes gc`, `lanes ensure`** and every other host verb. The container cannot run them;
|
|
425
440
|
its changes reach the host only as files the host reads.
|
|
426
441
|
- **Renaming a tmux session** (`rename-session`). `lanes_rename` changes the name the boards show, never the id or the
|
|
@@ -439,9 +454,10 @@ the result and refusal shapes below. `lane-mcp` names it `WIRE = 1` (its `start`
|
|
|
439
454
|
MCP protocol versions 2025-06-18, 2025-03-26 and 2024-11-05: `initialize` answers the client's version when it is one
|
|
440
455
|
of these, else 2025-06-18. The data-model row is DM-LANES-LANE-MCP in `docs/master-datamodel.md`.
|
|
441
456
|
|
|
442
|
-
- **Frozen in v1:** the
|
|
443
|
-
|
|
444
|
-
readers and the hints of the
|
|
457
|
+
- **Frozen in v1:** the eleven tool names and their order (`lanes_publish`, plan W10 W4, appended after the other
|
|
458
|
+
ten); every input property's name, type and meaning; which arguments are required; the closed schemas
|
|
459
|
+
(`additionalProperties: false`); the read-only annotations of the four readers and the hints of the seven writers;
|
|
460
|
+
the result shapes below.
|
|
445
461
|
- **Additive only within v1:** a new tool appended after the others; a new optional argument whose default keeps
|
|
446
462
|
today's behaviour; a new key in a JSON result; a new refusal code. A reader ignores keys it does not know and treats
|
|
447
463
|
an unknown `refused` code as a refusal. A description may be clarified; the golden then changes visibly.
|
|
@@ -451,7 +467,11 @@ of these, else 2025-06-18. The data-model row is DM-LANES-LANE-MCP in `docs/mast
|
|
|
451
467
|
- **Results** are one `text` content block. The readers answer JSON (`lanes_whoami`, `lanes_list`, `lanes_status`)
|
|
452
468
|
or plain text (`lanes_capture`); a reader that cannot answer is `isError: true` with a sentence. The writers always
|
|
453
469
|
answer one JSON object. On success it holds what changed (`changed`), what picks the change up (`picked_up_by`) and
|
|
454
|
-
how soon (`expected_within_s`); `lanes_signal` adds `stands_until
|
|
470
|
+
how soon (`expected_within_s`); `lanes_signal` adds `stands_until`; `lanes_publish` the request's `id`, the file's
|
|
471
|
+
container `path`, `kind`, `bytes`, `sha256` and where the request and its receipt are (`request`, `receipt`).
|
|
472
|
+
`lanes_status` carries `published` (`{id, url, at, outcome, request, reason?}`, the newest receipt) when the lane
|
|
473
|
+
has one, and `docs` (`{n, at}`) when the docs page holds documents for it — both additive, absent otherwise. On a
|
|
474
|
+
refusal it is `isError: true` with the
|
|
455
475
|
machine code (`refused`), the reason (`why`) and what a person can do instead (`human`). A refusal changed nothing
|
|
456
476
|
unless it carries `changed`: `tmux_failed` after `lanes_new` declared its lane, or after `lanes_send` typed its text
|
|
457
477
|
but not its Enter. There is no `outputSchema` or `structuredContent`, so every protocol version reads the same thing.
|
|
@@ -471,7 +491,9 @@ The refusal codes (`refused`), each with the `human` sentence `lane-mcp`'s `HUMA
|
|
|
471
491
|
- which file: `no_source`, `ambiguous_source`, `ambiguous_workspace`, `source_missing`, `source_invalid`,
|
|
472
492
|
`source_unreadable`, `source_link`, `no_lanes_block`, `not_declared_here`, `unknown_prefix`;
|
|
473
493
|
- the edit: `edit_conflict`, `edit_refused`, `last_terminal`, `duplicate_name`, `duplicate_binding`, `slug_collision`;
|
|
474
|
-
- a value: `invalid_name`, `invalid_session`, `invalid_cwd`, `invalid_command`, `invalid_text`, `invalid_note
|
|
494
|
+
- a value: `invalid_name`, `invalid_session`, `invalid_cwd`, `invalid_command`, `invalid_text`, `invalid_note`,
|
|
495
|
+
`invalid_title`, `invalid_for`, `invalid_kind`;
|
|
496
|
+
- the file to publish: `outside_home`, `not_a_file`, `too_large`;
|
|
475
497
|
- the target's agent: `agent_working`, `agent_needs_approval`;
|
|
476
498
|
- tmux and the sidecar: `tmux_unanswered`, `tmux_failed`, `signal_not_written`.
|
|
477
499
|
|
|
@@ -513,7 +535,8 @@ at a pinned commit, never by hand:
|
|
|
513
535
|
{"name": "lanes_rename", "description": "<harvested>", "effect": "write", "state": "planned", "description_sha256": "<harvested>"},
|
|
514
536
|
{"name": "lanes_new", "description": "<harvested>", "effect": "exec", "state": "planned", "description_sha256": "<harvested>"},
|
|
515
537
|
{"name": "lanes_send", "description": "<harvested>", "effect": "exec", "state": "planned", "description_sha256": "<harvested>"},
|
|
516
|
-
{"name": "lanes_signal", "description": "<harvested>", "effect": "write", "state": "planned", "description_sha256": "<harvested>"}
|
|
538
|
+
{"name": "lanes_signal", "description": "<harvested>", "effect": "write", "state": "planned", "description_sha256": "<harvested>"},
|
|
539
|
+
{"name": "lanes_publish", "description": "<harvested>", "effect": "write", "state": "planned", "description_sha256": "<harvested>"}
|
|
517
540
|
]
|
|
518
541
|
}
|
|
519
542
|
```
|
|
@@ -561,6 +584,32 @@ workspace's `uploads/` folder without overwriting files; an optional paste place
|
|
|
561
584
|
selected lane. Active transfers have an idle timeout, not a short total-duration limit. Credentials, `.git`
|
|
562
585
|
metadata and disallowed folders cannot be uploaded through the helper.
|
|
563
586
|
|
|
587
|
+
**Uploads converge on the docs store** (docs cockpit W10 task W5, LNS-106). Both ways in — `lanes upload` over SSH and
|
|
588
|
+
the Docs page's `POST /up` (the phone and laptop drop area) — go through `libexec/uploads.py`: the file lands in
|
|
589
|
+
`<folder>/uploads/` exactly as before (first lane folder, else the workspace's working directory; `-1` on a
|
|
590
|
+
collision; the repository's `.git/info/exclude` line) and each chunk read from the upload is written there AND handed
|
|
591
|
+
to `docs_store.intake` in the same pass (a `Tee`), so the store's sha256 receipt is of exactly the bytes that arrived,
|
|
592
|
+
never of a file an agent could rewrite first. The store keeps a copy — a hard link would share an inode with a file
|
|
593
|
+
the container may write — for the uploader's login (`source` `ssh` or `web`, kind by extension), and the Docs page and
|
|
594
|
+
the sidebar's count show it. The folder is the container's, written by the host as the agents' own uid, so the walk
|
|
595
|
+
never follows a link (directories opened component by component, files `O_EXCL|O_NOFOLLOW`, the tar stream unpacked
|
|
596
|
+
member by member instead of `tarfile.extractall`) and no git process runs in the repository (its config's
|
|
597
|
+
`core.fsmonitor` would be a command the container chose). An `age` switch (`LANES_DOCS_AGE_RECIPIENT`, default off)
|
|
598
|
+
encrypts the store copy only; the docs service decrypts on the fly when it holds an identity, else answers 409.
|
|
599
|
+
|
|
600
|
+
**Docs store** (docs cockpit W10 task W1, LNS-102; read by the docs service, W3, and written by both upload paths
|
|
601
|
+
through `libexec/uploads.py`, W5, and by the publish pick-up, W4 — `docsd.Pickup`, `source` `publish`):
|
|
602
|
+
`libexec/docs_store.py` keeps documents per person in host state, under
|
|
603
|
+
`docs/<login, percent-encoded as for profiles>/<workspace>/<lane>/<id>/`: the file itself, `meta.json` and
|
|
604
|
+
`receipt.json`, both with the sha256 the host computed while streaming the bytes in (a claimed hash is never taken).
|
|
605
|
+
A byte cap refuses an oversize document and leaves nothing behind. Ids are base36 seconds plus six random
|
|
606
|
+
characters; a name keeps only its basename's letters, digits, `.`, `_` and `-`, and gains `-1`, `-2` within a lane as
|
|
607
|
+
an upload does in `uploads/`. Nothing is written or read through a symbolic link. A document is visible to the person
|
|
608
|
+
whose store holds it, to every member when it names nobody, otherwise to the logins it names; `docs/counts.json`
|
|
609
|
+
counts each lane's documents and the append-only `docs.log` records each intake and removal without content, titles
|
|
610
|
+
or names. The docs service, the publish verb, receipted uploads and the sidebar's count build on it (plan
|
|
611
|
+
`docs/superpowers/plans/2026-10-08-lanes-docs-cockpit.md`).
|
|
612
|
+
|
|
564
613
|
**Quick edit** (`Ctrl-Alt-e`, cockpit v3 E1–E3) is the opposite direction and moves no file at all. The floating
|
|
565
614
|
**Edit** pane runs `lanes edit --focused` on the server: it resolves the focused tmux lane as the preview does, finds
|
|
566
615
|
the lane's git checkout and, for a linked worktree, its main checkout (`lanes_lib.repo_roots`, walking up to `.git`, a
|
|
@@ -626,8 +675,26 @@ DM-LANES-INSTALL-HANDOVER).
|
|
|
626
675
|
|
|
627
676
|
Tailscale Serve terminates private HTTPS on 8443 and forwards to the dedicated Caddy user service on loopback
|
|
628
677
|
8083. Caddy serves `web/` and forwards native assets, authentication and WebSockets to Zellij on loopback 8082.
|
|
629
|
-
Zellij's existing token/cookie checks remain authoritative; the mobile layer adds no second login system.
|
|
630
|
-
|
|
678
|
+
Zellij's existing token/cookie checks remain authoritative; the mobile layer adds no second login system. Two other
|
|
679
|
+
routes go to services of their own, each bound to the tailnet login Serve injects and never to Zellij's cookie:
|
|
680
|
+
`/enrol/*` to the enrolment service on loopback 8184 (see Access) and `/lanes-docs/*` to the docs service on loopback
|
|
681
|
+
8185 (Docs service, below). Caddy's route list, in the order it is declared:
|
|
682
|
+
|
|
683
|
+
| Path | Goes to | Headers |
|
|
684
|
+
| --- | --- | --- |
|
|
685
|
+
| `/lanes-docs/*` (prefix stripped) | `lanes-docsd` 127.0.0.1:8185, keeping Serve's `X-Forwarded-For` | docsd's own, per route (the site-wide set skips it: `header @notdocs`) |
|
|
686
|
+
| `/lanes-assets/vendor/*` (prefix stripped) | `$LANES_SHARE/vendor` files: the pinned Mermaid `install/enable-docs.sh` installs (`Cache-Control: public, max-age=31536000, immutable`; a page names the versioned file, DEF-1870) | site-wide |
|
|
687
|
+
| `/lanes-assets/*` (prefix stripped) | `web/` files (`no-cache`) | site-wide |
|
|
688
|
+
| `/enrol/*` (prefix stripped) | `lanes-enrold` 127.0.0.1:8184, keeping Serve's `X-Forwarded-For` | site-wide |
|
|
689
|
+
| `/assets/*`, `/ws/*`, `/command/*`, `/info/*`, `/session`, `/session-list` | Zellij web 127.0.0.1:8082 | site-wide |
|
|
690
|
+
| `GET /<device>` (one path segment, or `/`) | `web/index.html` (`no-store`) | site-wide |
|
|
691
|
+
| anything else | Zellij web 127.0.0.1:8082 | site-wide |
|
|
692
|
+
|
|
693
|
+
A view session is served at `/<device>`, so `bin/lanes` `device_name` refuses the service roots as device names
|
|
694
|
+
(`enrol`, `lanes-docs`, `lanes-assets`, casefolded). Caddy sorts the routes before it runs them: a single-path `handle`
|
|
695
|
+
goes ahead of another only when that one has a single path too (DEF-1872), so the vendor route stays with the
|
|
696
|
+
single-path routes above the named-matcher ones; its order is proven on a model of that sort
|
|
697
|
+
(`test_docs_assets.VendorRoute`), not on a running Caddy (DEF-1871, open until the RUNBOOK's operator check).
|
|
631
698
|
|
|
632
699
|
The phone shell uses the pinned client's window hooks for workspaces, panes, input and sizing. A visible
|
|
633
700
|
**Type here** field holds the original terminal input, as a text field. Programmatic input focus is blocked on
|
|
@@ -665,6 +732,93 @@ reads the enrolment service's `GET /site` on the same origin (`/enrol/site`) and
|
|
|
665
732
|
enables the services through `install/enable-web.sh` and prints the private URL; without a flag a known device
|
|
666
733
|
keeps its layout.
|
|
667
734
|
|
|
735
|
+
**Docs cockpit (plan W10, LNS-102…110).** Agents put content where people open it, and people hand files to agents,
|
|
736
|
+
on one login-bound page per server, `/lanes-docs/`: `lane publish` / `lanes_publish` in the container write a
|
|
737
|
+
request that `lanes-docsd`'s own 5 s pick-up reads through `/proc/<pid>/root` (publish pick-up, below); both upload
|
|
738
|
+
paths land a file in the workspace's `uploads/` and the store in one pass ("Desktop and uploads"); the per-login store
|
|
739
|
+
`libexec/docs_store.py` keeps every document with its sha256 receipt; `libexec/mdlite.py` renders Markdown and the
|
|
740
|
+
pinned Mermaid draws its fences (docs rendering, below); `libexec/docsd.py` serves it all to the tailnet login Serve
|
|
741
|
+
injects, only after the operator pinned the Serve header check (docs service, below); `lanes-statusd` carries each
|
|
742
|
+
lane's count and the web origin into `status.json`, from which the sidebar's ` ▤ docs N` strip, its OSC 8 links and
|
|
743
|
+
the `d` key build ("Views and panes"), and the phone's **More → Docs** and the laptop bar's **Docs** link it from the
|
|
744
|
+
shell (above). `lanes doctor` reports the unit, the pin, the store, the Mermaid pin and the `age` switch. What
|
|
745
|
+
`lanes docs push <id>` would hand to the data service's publication surface is not built (it prints the receipt and
|
|
746
|
+
exits 2). Operations, limits and the operator's live checklist: RUNBOOK "Docs service"; the user's view: the user
|
|
747
|
+
guide's [Docs](USERGUIDE.md#docs).
|
|
748
|
+
|
|
749
|
+
**Docs rendering (W10 W2, LNS-103).** Markdown an agent publishes from a lane is turned into an
|
|
750
|
+
HTML fragment by `libexec/mdlite.py` `render(text)`, which the docs cockpit's `lanes-docsd` (W3) embeds: standard
|
|
751
|
+
library only, deterministic, at most 1 MiB of UTF-8 (`TooLarge` past it), every algorithm linear in its input. Agent
|
|
752
|
+
content is treated as hostile: every text node and attribute value is escaped exactly once, raw HTML stays text, a
|
|
753
|
+
link keeps its URL only for `http://`, `https://`, `mailto:`, `/lanes-docs/` or `#` (the web ones open in a new tab
|
|
754
|
+
with `rel="noopener noreferrer"`) and only without a backslash or a `.`/`..` path segment (`%2e` spellings included,
|
|
755
|
+
which a browser resolves before it requests; on this site no `%2f`/`%5c` either: security review 2026-10-08), an image only for a document's own bytes
|
|
756
|
+
(`/lanes-docs/d/<id>/raw` or `/artifact`) or a base64 `data:image/png|jpeg|gif|webp`; any other destination is dropped
|
|
757
|
+
and its text kept. A `mermaid` fence becomes `<pre class="mermaid">` for the pinned Mermaid (W6). The subset and the
|
|
758
|
+
output contract are DM-LANES-DOCS-RENDER in the master data-model ledger.
|
|
759
|
+
|
|
760
|
+
**Docs service (W10 W3, LNS-104).** `libexec/docsd.py` (`lanes-docsd.service`, loopback 127.0.0.1:8185, reached as
|
|
761
|
+
`/lanes-docs/*`) serves the docs store to the person it belongs to. A request is answered only when it came through
|
|
762
|
+
Tailscale Serve (its `X-Forwarded-For` names a tailnet address), carries Serve's identity headers (a tagged device has
|
|
763
|
+
none), names a valid login that owns a member record in an allowed domain, and the operator has pinned the Serve
|
|
764
|
+
header check (`lanes enrol mode request`), and — on every route since the security review of 2026-10-08 — its
|
|
765
|
+
`Sec-Fetch-Site` is `same-origin` or `none`, or it is a GET/HEAD top-level navigation (`Sec-Fetch-Mode: navigate`,
|
|
766
|
+
`Sec-Fetch-Dest: document`: a link followed from any site); a sandboxed artifact's `fetch()` and subresource loads are
|
|
767
|
+
`cross-site` and not navigations, another `*.ts.net` host's are `same-site`, and `POST /up` takes `same-origin` or
|
|
768
|
+
`none` only; otherwise 403. It uses enrold's own checks
|
|
769
|
+
(`through_serve`, `caller_of`, `member_logins`, `read_mode`, the domain rule), never Zellij's cookie, and listens on
|
|
770
|
+
loopback only (DEF-1645 stays the open risk; the pin gate is the mitigation). A person sees their own store, what
|
|
771
|
+
names them in `for`, and — for a document that names nobody — what lies in a workspace their member record grants; any
|
|
772
|
+
other document is a 404 like a missing one. Pages: the index (workspace → lane → newest first, with counts; filtered
|
|
773
|
+
by `?ws=<ws>&lane=<lane>`, the sidebar's deep link), a document page by kind (Markdown rendered by mdlite; an HTML or
|
|
774
|
+
SVG artifact only linked, never embedded; a file's facts and its download), the raw bytes as an attachment with
|
|
775
|
+
`X-Lanes-Sha256`, the artifact as a top-level document under `Content-Security-Policy: sandbox allow-scripts` (an
|
|
776
|
+
opaque origin: its scripts run but cannot read this site), the receipt, and `docs.css`/`docs.js` from `web/`. Every
|
|
777
|
+
reference a page emits is relative, because Caddy strips `/lanes-docs`. Pages carry `default-src 'none'; style-src
|
|
778
|
+
'self' 'unsafe-inline'; script-src 'self'; img-src 'self' data:; connect-src 'self'; form-action 'self'; base-uri
|
|
779
|
+
'none'` (W5 added `connect-src 'self'` for the drop area's own request), and the site-wide Caddy header set skips
|
|
780
|
+
`/lanes-docs/*` (`header @notdocs`), so these are the only policies a document gets. Per process it bounds what one
|
|
781
|
+
login can make it do: 300 requests a minute, two Markdown renders at once (a rendered document is kept in an 8 MiB
|
|
782
|
+
cache by its sha256, so a second view renders nothing), two 32 MiB buffers at once shared by uploads and decryptions
|
|
783
|
+
and at most one of them per login, and a deadline on each upload body; refusals are coalesced in `docs.log` and the
|
|
784
|
+
log rotates at 8 MiB (RUNBOOK "Docs service", Limits). `POST /up` takes a multipart form
|
|
785
|
+
from the Docs page (32 MiB, 30 uploads per login in 10 minutes, two at once, one per login) into a
|
|
786
|
+
lane of one of the person's workspaces through `uploads.receive` — the workspace's `uploads/` and `docs_store.intake`,
|
|
787
|
+
both or neither — and answers each file's receipt with the in-container path it landed at. A refused body is read away
|
|
788
|
+
before the answer (at most 64 MiB, then a short linger), so the client reads the refusal instead of a reset
|
|
789
|
+
(DEF-1854). The index loads the drop area's `upload.css`/`upload.js` (their own files: `docs.js` had no room). RUNBOOK
|
|
790
|
+
"Docs service" has the unit, the pin gate and the wire contract (DM-LANES-DOCS-WIRE). A member record's `workspaces`
|
|
791
|
+
grants the boards it lists; a record that lists none grants every workspace the host serves, as nowhere else in Lanes
|
|
792
|
+
narrows by that list (DEF-1852).
|
|
793
|
+
|
|
794
|
+
**Publish pick-up (W10 W4, LNS-105).** An agent publishes from a lane with `lane publish <file>` or `lanes_publish`:
|
|
795
|
+
the container writes a request, `~/.lz/state/publish/<lane>/<id>.json` (`{v, path, title, kind, claimed_sha256,
|
|
796
|
+
mime, bytes, for}`, DM-LANES-PUBLISH-REQUEST), and nothing else. The docs service runs its own pick-up cycle every
|
|
797
|
+
5 s on a thread beside the HTTP server (`docsd.Pickup`; statusd's tick is not involved). For each container
|
|
798
|
+
`containers.json` names whose init pid is alive — and whose `/proc/<pid>/root` is not the host's own root, so a
|
|
799
|
+
recycled pid never turns the host's home into a container's, and, where the account can see the home volume
|
|
800
|
+
`containers.json` records, whose `/home/coder` is that volume (another container that got the pid is skipped) — it
|
|
801
|
+
surveys `~/.lz/state/publish/` and each lane directory with `os.scandir` (opened by `lanes_lib.open_real_dir`, a link
|
|
802
|
+
is never followed), reading at most 4,096 entries of a container per cycle: a container that holds more is refused for
|
|
803
|
+
the cycle and said in the journal once an hour, so what one container plants never costs the service its memory or
|
|
804
|
+
the other containers their cycle. It claims a request by renaming it to `.<id>.taking` (a directory planted at that
|
|
805
|
+
name, or at the receipt's, is removed or moved aside; a request that still cannot be claimed is skipped for the next),
|
|
806
|
+
reads it with `read_regular_at` (no link, no FIFO, at most 16 KiB) and checks it. The file is
|
|
807
|
+
resolved inside the container (`container_realpath`: an absolute link restarts at the container's root) to a path
|
|
808
|
+
under `/home/coder`, its directories are opened without following a link and the file `O_NOFOLLOW|O_NONBLOCK`, a
|
|
809
|
+
regular file of at most 32 MiB; a link swapped in meanwhile, a FIFO, a device or a larger file is refused, so a
|
|
810
|
+
container can never make the host read outside its home. The bytes stream into `docs_store.intake` (`source`
|
|
811
|
+
`publish`, `by` the lane id) through a reader that hashes them and refuses, inside intake, a sha256 other than the
|
|
812
|
+
claimed one, so nothing is stored. The answer goes back beside the request as `<id>.receipt.json` (`{v, id,
|
|
813
|
+
request, sha256, bytes, url, at, outcome, reason?}`, DM-LANES-PUBLISH-RECEIPT), written by rename like statusd's
|
|
814
|
+
`~/.lz/status.json`; the claim is removed and the lane keeps its 16 newest receipts. The document is for the request's
|
|
815
|
+
`for` (a member login), else for the member whose profile last opened the lane, else for every member whose record
|
|
816
|
+
grants its workspace; a request whose `for` names no member, or that has nobody to go to, is answered
|
|
817
|
+
`not-delivered` either way, so a container cannot learn from the reason whether a login is a member (docs.log keeps
|
|
818
|
+
the precise word). One `published` push per lane per minute goes through `notify` (its config and desk-presence
|
|
819
|
+
rule; kind and size only, never a title). At most 16 requests per container per cycle and 30 per container per 10
|
|
820
|
+
minutes are taken; more are refused `rate-limited`.
|
|
821
|
+
|
|
668
822
|
## Installed files and services
|
|
669
823
|
|
|
670
824
|
`install/services.py` renders the service templates against the installed binary paths and the `current` link.
|
|
@@ -805,7 +959,7 @@ only the full server install enables it, and an update (`--update-only`) enables
|
|
|
805
959
|
an update has moved past. A tree is staged (`lanes_lib.staged_tree`) when it sits in the default `…/pkg/<version>`
|
|
806
960
|
root or carries the npm wrapper's `.lanes-package.json` stamp, so a `$LANES_PKG_ROOT` of any name is followed through
|
|
807
961
|
the link too (DEF-1757); only the link under the home counts for such a root. `tree_version` and `tree_stamp` read a
|
|
808
|
-
tree's `VERSION` and that stamp. Every long-running process — statusd, enrold, the economy poller, the notifier, the
|
|
962
|
+
tree's `VERSION` and that stamp. Every long-running process — statusd, enrold, docsd, the economy poller, the notifier, the
|
|
809
963
|
Studio Host enable step and each sidebar — writes the resolved tree it started from to
|
|
810
964
|
`~/.local/state/lanes/run/<name>.tree` (`lanes_lib.record_tree`), so what still runs an old tree is a file
|
|
811
965
|
comparison, not a process scan. `lanes version` makes it: each record against the resolved target of the `current`
|
|
@@ -824,7 +978,9 @@ layout's commands (`libexec/sidebar`, `libexec/view.sh`, `holders/ssh.sh`) resol
|
|
|
824
978
|
unescaped as `services.unit_value` escaped it) resolves to it.
|
|
825
979
|
No service requires the checkout to be named `~/lanes`. Re-run the server installer after moving it.
|
|
826
980
|
|
|
827
|
-
Core units are the status observer and sync, ensure and garbage-collection timers. The web units are opt-in.
|
|
981
|
+
Core units are the status observer and sync, ensure and garbage-collection timers. The web units are opt-in. The
|
|
982
|
+
enrolment and docs services (`lanes-enrold`, `lanes-docsd`) and `lanes-update.timer` are enabled by every full server
|
|
983
|
+
install; an update (`--update-only`) enables nothing and only restarts what runs (DEF-1900 for `lanes-docsd`).
|
|
828
984
|
The co-located Throttle Studio Host (`lanes-studio-host.service` and its enrolment retry timer) is opt-in too:
|
|
829
985
|
`install/enable-studio-host.sh` enables it, and its binary is selected by `lanes studio-host install` from an
|
|
830
986
|
`opt-in` line of `holders.lock` or a local build, sha256-verified either way (`libexec/studio_host.py`).
|
|
@@ -836,6 +992,22 @@ The package uses Python's standard library and POSIX shell. Host binaries are ve
|
|
|
836
992
|
in `holders.lock`; Zellij's web assets are served by that binary and integrity-pinned in `web/index.html`.
|
|
837
993
|
Deployment profiles, workspace catalogs and runtime state remain distinct from shared source code.
|
|
838
994
|
|
|
995
|
+
**What the docs cockpit installs (plan W10, W1–W8).** Nothing is vendored into the tree: Mermaid is downloaded and
|
|
996
|
+
checked against `holders.lock`'s pin, and every other piece is the standard library, POSIX shell or a file Caddy
|
|
997
|
+
serves:
|
|
998
|
+
|
|
999
|
+
| Piece | Where | Installed or run by |
|
|
1000
|
+
| --- | --- | --- |
|
|
1001
|
+
| `lanes-docsd.service` (`libexec/docsd.py`: the HTTP service on 127.0.0.1:8185 and its publish pick-up thread) | `~/.config/systemd/user/`, `lanes.slice` | `services.py` renders it; every full server install enables and restarts it after `lanes-enrold`; an update only restarts it when it runs (DEF-1900) |
|
|
1002
|
+
| `libexec/docs_store.py`, `libexec/mdlite.py`, `libexec/uploads.py` | the installed tree | the server install; no unit of their own (docsd, `lanes upload`, statusd and `lanes doctor` import them) |
|
|
1003
|
+
| `web/docs.css`, `web/docs.js`, `web/upload.css`, `web/upload.js` | the installed tree; docsd serves them at `/lanes-docs/docs.css` and so on | the server install |
|
|
1004
|
+
| The Caddy routes `/lanes-docs/*` and `/lanes-assets/vendor/*`, and `header @notdocs` | `web/Caddyfile`, run by `lanes-mobile.service` | the server install, which `try-restart`s `lanes-mobile` so Caddy reads the new file |
|
|
1005
|
+
| Mermaid 11.17.2, `holders.lock`'s opt-in `mermaid` line (DM-LANES-DOCS-VENDOR) | `~/.local/share/lanes/vendor/mermaid-11.17.2.min.js` and the link `mermaid.min.js` | `install/enable-docs.sh`: every full server install and every update (never fatal), and by hand |
|
|
1006
|
+
| The docs store, `docs/counts.json`, `docs.log` (DM-LANES-DOCS-STORE, -RECEIPT, -COUNTS, -LOG) | `~/.local/state/lanes/docs/`, `~/.local/state/lanes/docs.log` | written by docsd (uploads, publishes, reads and refusals) and `lanes upload`; read by statusd (counts) and `lanes doctor` |
|
|
1007
|
+
| `lane publish` and `lane-mcp`'s `lanes_publish` | each container's `~/.lz/bin/` (`lane`, `lane_core.py`, `lane-mcp`) | `lanes install-container <name>`, which every update runs again for each container `containers.json` records |
|
|
1008
|
+
| Publish requests and their receipts (DM-LANES-PUBLISH-REQUEST, -RECEIPT) | each container's `~/.lz/state/publish/<lane id>/` | the request by `lane publish` / `lanes_publish`, the receipt by docsd's pick-up |
|
|
1009
|
+
| `LANES_DOCS_PORT`, `LANES_DOCS_DOMAINS`, `LANES_DOCS_AGE_RECIPIENT`, `LANES_DOCS_AGE_IDENTITY` | lanes-docsd's drop-in (`systemctl --user edit lanes-docsd.service`) | the operator; `host-install.sh` `check_docs_age` refuses the recipient where no `age` binary is on PATH |
|
|
1010
|
+
|
|
839
1011
|
## Access
|
|
840
1012
|
|
|
841
1013
|
Clients log in to one Unix account over SSH (tailnet, or the `lanes-public` fallback). Who may do so is the
|
|
@@ -189,6 +189,7 @@ guessed variant on what may be a light background (`test_sidebar.ViewStatusScree
|
|
|
189
189
|
| `▲` | U+25B2 | `^` | accent | mark `pinned` |
|
|
190
190
|
| `✓` | U+2713 | `OK` | tone-done | mark `ok` |
|
|
191
191
|
| `✗` | U+2717 | `X` | tone-urgent | mark `fail` |
|
|
192
|
+
| `▤` | U+25A4 | `#` | ink-dim | mark `docs` |
|
|
192
193
|
| `⚠` | U+26A0 | `!` | tone-attention | economy `starving` |
|
|
193
194
|
| `↓` | U+2193 | `v` | tone-attention | economy `downgraded` |
|
|
194
195
|
| `$` | U+0024 | `$` | ink-dim | economy `paid` |
|
|
@@ -264,7 +265,9 @@ real list scrolls where `⋮` is:
|
|
|
264
265
|
```
|
|
265
266
|
|
|
266
267
|
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
|
|
267
|
-
viewport b (²) with two viewers (··).
|
|
268
|
+
viewport b (²) with two viewers (··). Once the workspace's lanes have published documents (plan W10 W7) the upload
|
|
269
|
+
strip reads ` ↑ uploads · ▤ docs 3 (U)`, one OSC 8 link to the docs page, and the cursor row's name links to its
|
|
270
|
+
lane's documents; `d` puts that link in the footer. A link changes no cell of the frame. The same frame in ASCII mode, first rows:
|
|
268
271
|
|
|
269
272
|
```
|
|
270
273
|
Sakio !1 ?2 >6 v1
|
|
@@ -310,14 +313,14 @@ Each budget is pinned by a test; a budget that is not met is an open defect row,
|
|
|
310
313
|
| Budget | Pin |
|
|
311
314
|
|---|---|
|
|
312
315
|
| `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()` |
|
|
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)` |
|
|
316
|
+
| 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). Docs cockpit (plan W10 W7, MEASURED 2026-10-08) with its OSC 8 links on the strip and the cursor row and a 53-character origin: truecolor 7483 B, 16 4791 B, 256 5994 B dark / 5978 B light; with `d`'s footer link 7576 / 4884 / 6087 / 6071 B — no budget raised | same test, `len(frame)`; `test_the_d_footer_link_keeps_every_frame_budget` |
|
|
314
317
|
| 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) |
|
|
315
318
|
| 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` |
|
|
316
319
|
| 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` |
|
|
317
320
|
| Upload: select timeout stays 1.0 while uploading; `dirty` only on `.progress` mtime change | `test_upload_does_not_shorten_the_tick` (Task 24) |
|
|
318
321
|
| `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` |
|
|
319
322
|
| `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 |
|
|
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
|
|
323
|
+
| 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 until W9 V3's one `/enrol/site`; 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), raised since by exactly what phone shell input v2, its review fixes, the cockpit v3 sync and the docs cockpit's **Docs** row (plan W10 W7, +135 B) MEASURED: the cap equals today's 24 747 B (spec §8 has the steps); `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` |
|
|
321
324
|
| 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` |
|
|
322
325
|
| 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 |
|
|
323
326
|
|
|
@@ -46,6 +46,12 @@ verifies archive checksums, installs the pinned binaries, registers this checkou
|
|
|
46
46
|
`lanes-statusd`, `lanes-sync.timer`, `lanes-ensure.timer`, `lanes-gc.timer` and `lanes-update.timer` (which updates an
|
|
47
47
|
npm-installed server by itself and leaves a checkout alone, section 6). It creates an empty workspace catalog
|
|
48
48
|
only if no configuration exists. Host protection, temporary-file deletion and web access remain opt-in.
|
|
49
|
+
It also enables and restarts the enrolment service (`lanes-enrold`) and the docs service (`lanes-docsd`), then
|
|
50
|
+
installs the docs page's pinned Mermaid with `install/enable-docs.sh` (a download from npm checked against both
|
|
51
|
+
digests in `holders.lock`, into `~/.local/share/lanes/vendor/`; a re-run finds it and downloads nothing). That step is
|
|
52
|
+
never fatal: the install's last lines say `Mermaid for the docs page: ok`, `skipped (<why>)` (no network, a missing
|
|
53
|
+
prerequisite: diagrams show as source until `sh ~/.local/share/lanes/current/install/enable-docs.sh` succeeds) or
|
|
54
|
+
`refused (<why>)` (a digest that is not the pin: nothing installed).
|
|
49
55
|
|
|
50
56
|
## 2. Configure workspaces and editor sync
|
|
51
57
|
|
|
@@ -400,7 +406,7 @@ refuse; the Debian 12 and Ubuntu 24.04 packages are 2.6) and Tailscale on the se
|
|
|
400
406
|
its tailnet. The web services use
|
|
401
407
|
loopback ports **8082** (native Zellij) and **8083** (Lanes frontend); private HTTPS uses **8443**. The enrolment
|
|
402
408
|
service every server install runs (`lanes-enrold`) listens on loopback **8184** and is reached as `/enrol/*` through
|
|
403
|
-
the frontend
|
|
409
|
+
the frontend; the docs service (`lanes-docsd`, W10) listens on loopback **8185** and is reached as `/lanes-docs/*`.
|
|
404
410
|
Reserve those ports for Lanes on this host. Caddy runs as a separate user service with its admin API disabled.
|
|
405
411
|
|
|
406
412
|
Once, a server administrator allows the workspace account to manage Tailscale Serve:
|
|
@@ -585,17 +591,21 @@ refused before anything is fetched. It runs in three phases. Pre-commit: the pre
|
|
|
585
591
|
Commit: `install/services.py --install` writes the units and moves `current` (below). Post-commit, from the release
|
|
586
592
|
just installed (`libexec/update.py`): `systemctl --user daemon-reload`; `lanes sync`; `systemctl --user restart
|
|
587
593
|
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
|
|
589
|
-
install-
|
|
590
|
-
|
|
594
|
+
`lanes-docsd`, `lanes-mobile`, `lanes-web` and `lanes-studio-host` (each restarted only when it runs; nothing is
|
|
595
|
+
enabled), with `sh install/enable-docs.sh` right after `lanes-docsd`'s (the docs page's Mermaid, step `enable-docs`);
|
|
596
|
+
`lanes install-container <name>` for each container `lanes sync` recorded with a home volume; `lanes
|
|
597
|
+
reload-views`; `lanes ensure --if-layout-changed` (each view whose tabs this release lays out differently is rebuilt when no client is
|
|
591
598
|
attached, and otherwise marked `update ready · reload (r)` in its sidebar footer until it is rebuilt — by `r`, by
|
|
592
599
|
`lanes rebuild <device>`, or by `lanes-ensure.timer` once its last client has left). Every post-commit step runs
|
|
593
600
|
whatever the one before it returned, and each is stopped after 300 s. `lanes
|
|
594
601
|
sync` exiting 1 — a board with an error, which it reports while keeping that board's previous lanes — is a warning
|
|
595
602
|
(`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
|
|
603
|
+
failure like any other step. The Mermaid step's exit 1 (a prerequisite or the download), a download stopped after
|
|
604
|
+
300 s, and exit 3 (refused: no valid pin, a digest mismatch, nothing installed) are warnings too, recorded with
|
|
605
|
+
`outcome` (`skipped`, `refused`) and `reason` (the script's last line), and the summary line names it (`mermaid ok`,
|
|
606
|
+
`mermaid skipped: <reason>`); any other status of it is a failure. The terminal or the journal gets one line per phase and step;
|
|
597
607
|
`~/.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
|
|
608
|
+
latest, installed, previous, result, steps: [{name, rc[, warning][, outcome, reason]}], log}`, `result` being `ok`, `partial` (installed, a step
|
|
599
609
|
after the switch failed: exit 4), `refused` (a prerequisite, no installed release or another install said no, nothing
|
|
600
610
|
changed: exit 3) or `failed` (nothing was installed: exit 1). `--update-only` takes neither `--check` nor
|
|
601
611
|
`--no-start`. A full install (no `--update-only`) writes no record; a failing `lanes sync` there is a warning too, and
|
|
@@ -625,10 +635,17 @@ use the same overrides when reinstalling. `LANES_WORKSPACES` is also carried int
|
|
|
625
635
|
when supplied; the standard configuration directory is the simplest default.
|
|
626
636
|
|
|
627
637
|
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
|
|
638
|
+
lanes-statusd.service` (`--update-only` does that and the restarts below). The installer itself restarts the enrolment service (`lanes-enrold`), the docs service (`lanes-docsd`, which answers nothing until that same pin is set — RUNBOOK "Docs service") and a running web frontend (`lanes-mobile`, so
|
|
629
639
|
Caddy reads the new tree's Caddyfile; browser clients reconnect). On the first server install with enrolment, run the
|
|
630
640
|
runbook's operator verification and pin its result with `lanes enrol mode request` (or `invite`) — see
|
|
631
641
|
[Approve a request](#approve-a-request); a pinned mode survives this restart and every later one.
|
|
642
|
+
The docs service answers only once that pin says `request` (an `invite` pin, or none, refuses every docs request).
|
|
643
|
+
On the first server install with the docs service (operator gate 1 of the docs plan: the full `npx
|
|
644
|
+
@vibelord/lanes@<release> --server`, which enables `lanes-docsd` and installs Mermaid), re-run the Serve header check
|
|
645
|
+
so the pin is `request`; until then `lanes doctor` says `⚠ docs: refuses until the Serve header check is pinned`, and
|
|
646
|
+
afterwards `✓ docs: pinned`, with `lanes-docsd` answering on its port and the Mermaid pin's state (RUNBOOK "Docs
|
|
647
|
+
service"). Then `lanes install-container <name>` per container gives its agents `lane publish` and the
|
|
648
|
+
`lanes_publish` MCP tool (an update does that for every recorded container).
|
|
632
649
|
If you enabled the Studio Host and selected a new `throttle-rc` or edited its config, restart
|
|
633
650
|
`lanes-studio-host` (re-running `install/enable-studio-host.sh` does that, after `studio host --check`).
|
|
634
651
|
If web service definitions or the pinned Zellij binary changed, restart `lanes-web` and `lanes-mobile` too;
|
|
@@ -640,7 +657,7 @@ Normal paths:
|
|
|
640
657
|
| Path | Contents |
|
|
641
658
|
| --- | --- |
|
|
642
659
|
| `~/.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) |
|
|
660
|
+
| `~/.local/share/lanes/` | Client copy, pinned binaries, download cache, the docs page's pinned Mermaid (`vendor/`, from `install/enable-docs.sh`, which every server install and update runs), 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
661
|
| `~/.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 |
|
|
645
662
|
| `~/.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) |
|
|
646
663
|
| `~/.config/throttle-rc/`, `~/.local/share/throttle-rc/` | Optional Studio Host config, machine key and enrolment receipt (throttle-rc's own files) |
|